package org.apache.zeppelin.notebook.repo.zeppelinhub.security;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.util.Collections;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.lang3.StringUtils;
import org.apache.zeppelin.conf.ZeppelinConfiguration;
import org.apache.zeppelin.notebook.socket.Message;
import org.apache.zeppelin.notebook.socket.Message.OP;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
/**
* Authentication module.
*
*/
public class Authentication implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(Authentication.class);
private String principal = "anonymous";
private String ticket = "anonymous";
private String roles = StringUtils.EMPTY;
private final HttpClient client;
private String loginEndpoint;
// Cipher is an AES in CBC mode
private static final String CIPHER_ALGORITHM = "AES";
private static final String CIPHER_MODE = "AES/CBC/PKCS5PADDING";
private static final String KEY = "AbtEr99DxsWWbJkP";
private static final int ivSize = 16;
private static final String ZEPPELIN_CONF_ANONYMOUS_ALLOWED = "zeppelin.anonymous.allowed";
private static final String ZEPPELINHUB_USER_KEY = "zeppelinhub.user.key";
private String token;
private boolean authEnabled;
private boolean authenticated;
String userKey;
private Gson gson = new Gson();
private static Authentication instance = null;
public static Authentication initialize(String token, ZeppelinConfiguration conf) {
if (instance == null && conf != null) {
instance = new Authentication(token, conf);
}
return instance;
}
public static Authentication getInstance() {
return instance;
}
private Authentication(String token, ZeppelinConfiguration conf) {
MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
client = new HttpClient(connectionManager);
this.token = token;
authEnabled = !conf.getBoolean("ZEPPELIN_ALLOW_ANONYMOUS",
ZEPPELIN_CONF_ANONYMOUS_ALLOWED, true);
userKey = conf.getString("ZEPPELINHUB_USER_KEY",
ZEPPELINHUB_USER_KEY, "");
loginEndpoint = getLoginEndpoint(conf);
}
public String getPrincipal() {
return this.principal;
}
public String getTicket() {
return this.ticket;
}
public String getRoles() {
return this.roles;
}
public boolean isAuthenticated() {
return authenticated;
}
private String getLoginEndpoint(ZeppelinConfiguration conf) {
int port = conf.getInt("ZEPPELIN_PORT", "zeppelin.server.port" , 8080);
if (port <= 0) {
port = 8080;
}
String scheme = "http";
if (conf.useSsl()) {
scheme = "https";
}
String endpoint = scheme + "://localhost:" + port + "/api/login";
return endpoint;
}
public boolean authenticate() {
if (authEnabled) {
if (!StringUtils.isEmpty(userKey)) {
String authKey = getAuthKey(userKey);
Map<String, String> authCredentials = login(authKey, loginEndpoint);
if (isEmptyMap(authCredentials)) {
return false;
}
principal = authCredentials.containsKey("principal") ? authCredentials.get("principal")
: principal;
ticket = authCredentials.containsKey("ticket") ? authCredentials.get("ticket") : ticket;
roles = authCredentials.containsKey("roles") ? authCredentials.get("roles") : roles;
LOG.info("Authenticated into Zeppelin as {} and roles {}", principal, roles);
return true;
} else {
LOG.warn("ZEPPELINHUB_USER_KEY isn't provided. Please provide your credentials"
+ "for your instance in ZeppelinHub website and generate your key.");
}
}
return false;
}
// returns login:password
private String getAuthKey(String userKey) {
LOG.debug("Encrypted user key is {}", userKey);
if (StringUtils.isBlank(userKey)) {
LOG.warn("ZEPPELINHUB_USER_KEY is blank");
return StringUtils.EMPTY;
}
//use hashed token as a salt
String hashedToken = Integer.toString(token.hashCode());
return decrypt(userKey, hashedToken);
}
private String decrypt(String value, String initVector) {
LOG.debug("IV is {}, IV length is {}", initVector, initVector.length());
if (StringUtils.isBlank(value) || StringUtils.isBlank(initVector)) {
LOG.error("String to decode or salt is not provided");
return StringUtils.EMPTY;
}
try {
IvParameterSpec iv = generateIV(initVector);
Key key = generateKey();
Cipher cipher = Cipher.getInstance(CIPHER_MODE);
cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] decryptedString = Base64.decodeBase64(toBytes(value));
decryptedString = cipher.doFinal(decryptedString);
return new String(decryptedString);
} catch (GeneralSecurityException e) {
LOG.error("Error when decrypting", e);
return StringUtils.EMPTY;
}
}
@SuppressWarnings("unchecked")
private Map<String, String> login(String authKey, String endpoint) {
String[] credentials = authKey.split(":");
if (credentials.length != 2) {
return Collections.emptyMap();
}
PostMethod post = new PostMethod(endpoint);
post.addRequestHeader("Origin", "http://localhost");
post.addParameter(new NameValuePair("userName", credentials[0]));
post.addParameter(new NameValuePair("password", credentials[1]));
try {
int code = client.executeMethod(post);
if (code == HttpStatus.SC_OK) {
String content = post.getResponseBodyAsString();
Map<String, Object> resp = gson.fromJson(content,
new TypeToken<Map<String, Object>>() {}.getType());
LOG.info("Received from Zeppelin LoginRestApi : " + content);
return (Map<String, String>) resp.get("body");
} else {
LOG.error("Failed Zeppelin login {}, status code {}", endpoint, code);
return Collections.emptyMap();
}
} catch (IOException e) {
LOG.error("Cannot login into Zeppelin", e);
return Collections.emptyMap();
}
}
private Key generateKey() {
return new SecretKeySpec(toBytes(KEY), CIPHER_ALGORITHM);
}
private byte[] toBytes(String value) {
byte[] bytes;
try {
bytes = value.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
LOG.warn("UTF-8 isn't supported ", e);
bytes = value.getBytes();
}
return bytes;
}
private IvParameterSpec generateIV(String ivString) {
byte[] ivFromBytes = toBytes(ivString);
byte[] iv16ToBytes = new byte[ivSize];
System.arraycopy(ivFromBytes, 0, iv16ToBytes, 0, Math.min(ivFromBytes.length, ivSize));
return new IvParameterSpec(iv16ToBytes);
}
private boolean isEmptyMap(Map<String, String> map) {
return map == null || map.isEmpty();
}
@Override
public void run() {
authenticated = authenticate();
LOG.info("Scheduled authentication status is {}", authenticated);
}
}