/* * 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.zeppelin.realm; import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.HashSet; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.PutMethod; import org.apache.commons.httpclient.methods.StringRequestEntity; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.authc.AccountException; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.zeppelin.notebook.repo.zeppelinhub.model.UserSessionContainer; import org.apache.zeppelin.notebook.repo.zeppelinhub.websocket.utils.ZeppelinhubUtils; import org.apache.zeppelin.server.ZeppelinServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Joiner; import com.google.gson.Gson; import com.google.gson.JsonParseException; /** * A {@code Realm} implementation that uses the ZeppelinHub to authenticate users. * */ public class ZeppelinHubRealm extends AuthorizingRealm { private static final Logger LOG = LoggerFactory.getLogger(ZeppelinHubRealm.class); private static final String DEFAULT_ZEPPELINHUB_URL = "https://www.zeppelinhub.com"; private static final String USER_LOGIN_API_ENDPOINT = "api/v1/users/login"; private static final String JSON_CONTENT_TYPE = "application/json"; private static final String UTF_8_ENCODING = "UTF-8"; private static final String USER_SESSION_HEADER = "X-session"; private static final AtomicInteger INSTANCE_COUNT = new AtomicInteger(); private final HttpClient httpClient; private final Gson gson; private String zeppelinhubUrl; private String name; public ZeppelinHubRealm() { super(); LOG.debug("Init ZeppelinhubRealm"); //TODO(anthonyc): think about more setting for this HTTP client. // eg: if user uses proxy etcetc... httpClient = new HttpClient(); gson = new Gson(); name = getClass().getName() + "_" + INSTANCE_COUNT.getAndIncrement(); } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authToken; if (StringUtils.isBlank(token.getUsername())) { throw new AccountException("Empty usernames are not allowed by this realm."); } String loginPayload = createLoginPayload(token.getUsername(), token.getPassword()); User user = authenticateUser(loginPayload); LOG.debug("{} successfully login via ZeppelinHub", user.login); return new SimpleAuthenticationInfo(user.login, token.getPassword(), name); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // TODO(xxx): future work will be done here. return null; } protected void onInit() { super.onInit(); } /** * Setter of ZeppelinHub URL, this will be called by Shiro based on zeppelinhubUrl property * in shiro.ini file.</p> * It will also perform a check of ZeppelinHub url {@link #isZeppelinHubUrlValid}, * if the url is not valid, the default zeppelinhub url will be used. * * @param url */ public void setZeppelinhubUrl(String url) { if (StringUtils.isBlank(url)) { LOG.warn("Zeppelinhub url is empty, setting up default url {}", DEFAULT_ZEPPELINHUB_URL); zeppelinhubUrl = DEFAULT_ZEPPELINHUB_URL; } else { zeppelinhubUrl = (isZeppelinHubUrlValid(url) ? url : DEFAULT_ZEPPELINHUB_URL); LOG.info("Setting up Zeppelinhub url to {}", zeppelinhubUrl); } } /** * Send to ZeppelinHub a login request based on the request body which is a JSON that contains 2 * fields "login" and "password". * * @param requestBody JSON string of ZeppelinHub payload. * @return Account object with login, name (if set in ZeppelinHub), and mail. * @throws AuthenticationException if fail to login. */ protected User authenticateUser(String requestBody) { PutMethod put = new PutMethod(Joiner.on("/").join(zeppelinhubUrl, USER_LOGIN_API_ENDPOINT)); String responseBody = StringUtils.EMPTY; String userSession = StringUtils.EMPTY; try { put.setRequestEntity(new StringRequestEntity(requestBody, JSON_CONTENT_TYPE, UTF_8_ENCODING)); int statusCode = httpClient.executeMethod(put); if (statusCode != HttpStatus.SC_OK) { LOG.error("Cannot login user, HTTP status code is {} instead on 200 (OK)", statusCode); put.releaseConnection(); throw new AuthenticationException("Couldnt login to ZeppelinHub. " + "Login or password incorrect"); } responseBody = put.getResponseBodyAsString(); userSession = put.getResponseHeader(USER_SESSION_HEADER).getValue(); put.releaseConnection(); } catch (IOException e) { LOG.error("Cannot login user", e); throw new AuthenticationException(e.getMessage()); } User account = null; try { account = gson.fromJson(responseBody, User.class); } catch (JsonParseException e) { LOG.error("Cannot deserialize ZeppelinHub response to User instance", e); throw new AuthenticationException("Cannot login to ZeppelinHub"); } onLoginSuccess(account.login, userSession); return account; } /** * Create a JSON String that represent login payload.</p> * Payload will look like: * <code> * { * 'login': 'userLogin', * 'password': 'userpassword' * } * </code> * @param login * @param pwd * @return */ protected String createLoginPayload(String login, char[] pwd) { StringBuilder sb = new StringBuilder("{\"login\":\""); return sb.append(login).append("\", \"password\":\"").append(pwd).append("\"}").toString(); } /** * Perform a Simple URL check by using <code>URI(url).toURL()</code>. * If the url is not valid, the try-catch condition will catch the exceptions and return false, * otherwise true will be returned. * * @param url * @return */ protected boolean isZeppelinHubUrlValid(String url) { boolean valid; try { new URI(url).toURL(); valid = true; } catch (URISyntaxException | MalformedURLException e) { LOG.error("Zeppelinhub url is not valid, default ZeppelinHub url will be used.", e); valid = false; } return valid; } /** * Helper class that will be use to deserialize ZeppelinHub response. */ protected class User { public String login; public String email; public String name; } public void onLoginSuccess(String username, String session) { UserSessionContainer.instance.setSession(username, session); /* TODO(xxx): add proper roles */ HashSet<String> userAndRoles = new HashSet<String>(); userAndRoles.add(username); ZeppelinServer.notebookWsServer.broadcastReloadedNoteList( new org.apache.zeppelin.user.AuthenticationInfo(username), userAndRoles); ZeppelinhubUtils.userLoginRoutine(username); } @Override public void onLogout(PrincipalCollection principals) { ZeppelinhubUtils.userLogoutRoutine((String) principals.getPrimaryPrincipal()); } }