Repository: zeppelin Updated Branches: refs/heads/branch-0.6 c1bddddef -> 9c5ae39cc
ZEPPELIN-1164: backport new Shiro realm to 0.6 branch Just a backport of #1173 Author: Anthony Corbacho <[email protected]> Closes #1344 from bzz/branch-0.6-add-realm and squashes the following commits: fbf8615 [Anthony Corbacho] [ZEPPELIN-1164] ZeppelinHub Realm Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/9c5ae39c Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/9c5ae39c Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/9c5ae39c Branch: refs/heads/branch-0.6 Commit: 9c5ae39ccc154beac92c08c6a82e032e153e80d1 Parents: c1bdddd Author: Anthony Corbacho <[email protected]> Authored: Fri Jul 22 15:55:50 2016 +0900 Committer: Alexander Bezzubov <[email protected]> Committed: Fri Aug 19 18:21:58 2016 +0900 ---------------------------------------------------------------------- conf/shiro.ini | 5 + docs/security/shiroauthentication.md | 47 ++++- .../apache/zeppelin/realm/ZeppelinHubRealm.java | 199 +++++++++++++++++++ 3 files changed, 249 insertions(+), 2 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9c5ae39c/conf/shiro.ini ---------------------------------------------------------------------- diff --git a/conf/shiro.ini b/conf/shiro.ini index ced9776..e774624 100644 --- a/conf/shiro.ini +++ b/conf/shiro.ini @@ -42,6 +42,11 @@ user3 = password4, role2 #ldapRealm.userDnTemplate = uid={0},ou=Users,dc=COMPANY,dc=COM #ldapRealm.contextFactory.authenticationMechanism = SIMPLE +### A sample for configuring ZeppelinHub Realm +#zeppelinHubRealm = org.apache.zeppelin.realm.ZeppelinHubRealm +## Url of ZeppelinHub +#zeppelinHubRealm.zeppelinhubUrl = https://www.zeppelinhub.com +#securityManager.realms = $zeppelinHubRealm sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9c5ae39c/docs/security/shiroauthentication.md ---------------------------------------------------------------------- diff --git a/docs/security/shiroauthentication.md b/docs/security/shiroauthentication.md index 733ff11..f698a0a 100644 --- a/docs/security/shiroauthentication.md +++ b/docs/security/shiroauthentication.md @@ -101,6 +101,49 @@ finance = * group1 = * ``` -All of above configurations are defined in the `conf/shiro.ini` file. +## Configure Realm (optional) +Realms are responsible for authentication and authorization in Apache Zeppelin. By default, Apache Zeppelin uses [IniRealm](https://shiro.apache.org/static/latest/apidocs/org/apache/shiro/realm/text/IniRealm.html) (users and groups are configurable in `conf/shiro.ini` file under `[user]` and `[group]` section). You can also leverage Shiro Realms like [JndiLdapRealm](https://shiro.apache.org/static/latest/apidocs/org/apache/shiro/realm/ldap/JndiLdapRealm.html), [JdbcRealm](https://shiro.apache.org/static/latest/apidocs/org/apache/shiro/realm/jdbc/JdbcRealm.html) or create [our own](https://shiro.apache.org/static/latest/apidocs/org/apache/shiro/realm/AuthorizingRealm.html). +To learn more about Apache Shiro Realm, please check [this documentation](http://shiro.apache.org/realm.html). + +We also provide community custom Realms. + +### Active Directory +TBD + +### LDAP +TBD + +### ZeppelinHub +[ZeppelinHub](https://www.zeppelinhub.com) is a service that synchronize your Apache Zeppelin notebooks and enables you to collaborate easily. + +To enable login with your ZeppelinHub credential, apply the following change in `conf/shiro.ini` under `[main]` section. + +``` +### A sample for configuring ZeppelinHub Realm +zeppelinHubRealm = org.apache.zeppelin.realm.ZeppelinHubRealm +## Url of ZeppelinHub +zeppelinHubRealm.zeppelinhubUrl = https://www.zeppelinhub.com +securityManager.realms = $zeppelinHubRealm +``` + +> Note: ZeppelinHub is not releated to apache Zeppelin project. + +## Secure your Zeppelin information (optional) +By default, anyone who defined in `[users]` can share **Interpreter Setting**, **Credential** and **Configuration** information in Apache Zeppelin. +Sometimes you might want to hide these information for your use case. +Since Shiro provides **url-based security**, you can hide the information by commenting or uncommenting these below lines in `conf/shiro.ini`. + +``` +[urls] + +/api/interpreter/** = authc, roles[admin] +/api/configurations/** = authc, roles[admin] +/api/credential/** = authc, roles[admin] +``` + +In this case, only who have `admin` role can see **Interpreter Setting**, **Credential** and **Configuration** information. +If you want to grant this permission to other users, you can change **roles[ ]** as you defined at `[users]` section. + +<br/> +> **NOTE :** All of the above configurations are defined in the `conf/shiro.ini` file. This documentation is originally from [SECURITY-README.md](https://github.com/apache/zeppelin/blob/master/SECURITY-README.md). -> **NOTE :** This documentation is originally from [SECURITY-README.md](https://github.com/apache/zeppelin/blob/master/SECURITY-README.md). http://git-wip-us.apache.org/repos/asf/zeppelin/blob/9c5ae39c/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java new file mode 100644 index 0000000..cbe490d --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java @@ -0,0 +1,199 @@ +/* + * 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.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.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 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; + 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(); + 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"); + } + 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; + } +}
