/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.mapred; import java.security.SignatureException; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.BufferedReader; import java.io.FileReader; import java.io.File; import java.net.URLDecoder; import java.util.HashMap; import org.apache.hadoop.conf.Configuration; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.commons.codec.binary.Base64; /** * This class implements symmetric key HMAC/SHA1 signature * based authorization of users and admins. */ public class PriorityAuthorization { public static final int USER = 0; public static final int ADMIN = 1; public static final int NO_ACCESS = 2; private HashMap<String,UserACL> acl = new HashMap<String,UserACL>(); private long lastSuccessfulReload = 0; public static final long START_TIME = System.currentTimeMillis(); private String aclFile; private static final Log LOG = LogFactory.getLog(PriorityAuthorization.class); private static final boolean debug = LOG.isDebugEnabled(); private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1"; /** * Initializes authorization configuration * @param conf MapReduce configuration handle */ public void init(Configuration conf) { aclFile = conf.get("mapred.priority-scheduler.acl-file","/etc/hadoop.acl"); } /** * Adapted from AWS Query Authentication cookbook: * Computes RFC 2104-compliant HMAC signature. * * @param data * The data to be signed. * @param key * The signing key. * @return * The base64-encoded RFC 2104-compliant HMAC signature. * @throws * java.security.SignatureException when signature generation fails */ public static String hmac(String data, String key) throws java.security.SignatureException { String result; try { // get an hmac_sha1 key from the raw key bytes SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM); // get an hmac_sha1 Mac instance and initialize with the signing key Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); mac.init(signingKey); // compute the hmac on input data bytes byte[] rawHmac = mac.doFinal(data.getBytes()); // base64-encode the hmac result = new String(Base64.encodeBase64(rawHmac)); } catch (Exception e) { throw new SignatureException("Failed to generate HMAC : " + e, e); } return result; } class UserACL { String user; String role; String key; // for replay detection long lastTimestamp = START_TIME; UserACL(String user, String role, String key) { this.user = user; this.role = role; this.key = key; } } private void reloadACL() { BufferedReader in = null; try { in = new BufferedReader(new FileReader(aclFile)); String line = in.readLine(); while (line != null) { String[] nameValue = line.split(" "); if (nameValue.length != 3) { continue; } acl.put(nameValue[0], new UserACL(nameValue[0], nameValue[1], nameValue[2])); if (debug) { LOG.debug("Loading " + line); } line = in.readLine(); } } catch (Exception e) { LOG.error(e); } try { in.close(); } catch (Exception e) { LOG.error(e); } } private void loadACL() { long time = System.currentTimeMillis(); try { File file = new File(aclFile); long lastModified = file.lastModified(); if (lastModified > lastSuccessfulReload) { reloadACL(); lastSuccessfulReload = time; } } catch (Exception e) { LOG.error("Failed to reload acl file", e); } } private boolean isReplay(String timestamp, String signature, UserACL userACL) { long signatureTime = Long.parseLong(timestamp); if (debug) { LOG.debug("signaturetime: " + Long.toString(signatureTime)); LOG.debug("lasttime: " + Long.toString(userACL.lastTimestamp)); } if (signatureTime <= userACL.lastTimestamp) { return true; } userACL.lastTimestamp = signatureTime; return false; } /** * Returns authorized role for user. * Checks whether signature obtained by user was made by key stored in local acl. * Also checks for replay attacks. * @param data data that was signed by user * @param signature user-provided signature * @param user-provided nonce/timestamp of signature * @return the authorized role of the user: * ADMIN, USER or NO_ACCESS */ public int authorize(String data, String signature, String user, String timestamp) { try { signature = URLDecoder.decode(signature, "UTF-8"); } catch (Exception e) { LOG.error("Authorization exception:",e); return NO_ACCESS; } if (debug) { LOG.debug(data + " sig: " + signature + " user: " + user + " time: " + timestamp); } try { loadACL(); UserACL userACL = acl.get(user); if (userACL == null) { return NO_ACCESS; } String signatureTest = hmac(data, userACL.key); if (debug) { LOG.debug("SignatureTest " + signatureTest); LOG.debug("Signature " + signature); } if (signatureTest.equals(signature) && !isReplay(timestamp, signature, userACL)) { return (userACL.role.equals("admin")) ? ADMIN : USER; } } catch (Exception e) { LOG.error("Athorization exception:", e); } return NO_ACCESS; } }