Repository: zeppelin
Updated Branches:
  refs/heads/master b2545641a -> e6f51e718


[ZEPPELIN-1164] ZeppelinHub Realm

### What is this PR for?
Add a new Shiro Realm that lets users authenticate in Apache Zeppelin using 
their ZeppelinHub credentials.

### What type of PR is it?
`Feature`

### Todos
* [x] - Create ZeppelinHub Realm that extends `AuthorizingRealm`.
* [x] - Update `shiro.ini` file and add ZeppelinHub configuration template.
* [x] - Add documentation

### What is the Jira issue?
[Issue 1164](https://issues.apache.org/jira/browse/ZEPPELIN-1164)

### How should this be tested?
 - Assuming that you already have an account 
[Zeppelinhub](https://www.zeppelinhub.com/register), edit `conf/shiro.ini` file 
and uncomment ZeppelinHub related configuration.
 - Start your Zeppelin
 - Login with your ZeppelinHub credentials.

### Screenshots (if appropriate)

### Questions:
* Does the licenses files need update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? YES

Author: Anthony Corbacho <[email protected]>

Closes #1173 from anthonycorbacho/feat/ZeppelinHubAuthentication and squashes 
the following commits:

63b06c6 [Anthony Corbacho] Fix rebase mistake in documentation
0f16344 [Anthony Corbacho] Fix typo in documentation
5a27871 [Anthony Corbacho] Add Documentation about ZeppelinHub Realm 
configuration
8347fa9 [Anthony Corbacho] Handle long line > 100 char
9bf96ba [Anthony Corbacho] Remove author tag...
64154d4 [Anthony Corbacho] Add more method comments.
c207b5e [Anthony Corbacho] Change check of token.getUsername() in 
doGetAuthenticationInfo by using StringUtils::isBlank instead of checking only 
null.
38683e1 [Anthony Corbacho] Add new setting in Shiri.ini to handle ZeppelinHub 
realm.
34a8e5e [Anthony Corbacho] Create new Apache Shiro Realm for ZeppelinHub


Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo
Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/e6f51e71
Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/e6f51e71
Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/e6f51e71

Branch: refs/heads/master
Commit: e6f51e7183bf44778c9eb7948a12dcb196c4d31b
Parents: b254564
Author: Anthony Corbacho <[email protected]>
Authored: Fri Jul 22 15:55:50 2016 +0900
Committer: Jongyoul Lee <[email protected]>
Committed: Thu Aug 4 02:43:32 2016 +0900

----------------------------------------------------------------------
 conf/shiro.ini                                  |   5 +
 docs/security/shiroauthentication.md            |  28 +++
 .../apache/zeppelin/realm/ZeppelinHubRealm.java | 199 +++++++++++++++++++
 3 files changed, 232 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e6f51e71/conf/shiro.ini
----------------------------------------------------------------------
diff --git a/conf/shiro.ini b/conf/shiro.ini
index ca5afe3..75a3fb9 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/e6f51e71/docs/security/shiroauthentication.md
----------------------------------------------------------------------
diff --git a/docs/security/shiroauthentication.md 
b/docs/security/shiroauthentication.md
index a5c9c31..2f3590a 100644
--- a/docs/security/shiroauthentication.md
+++ b/docs/security/shiroauthentication.md
@@ -105,6 +105,33 @@ finance = *
 group1 = *
 ```
 
+## 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. 
@@ -123,3 +150,4 @@ If you want to grant this permission to other users, you 
can change **roles[ ]**
 
 <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).
+

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e6f51e71/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;
+  }
+}

Reply via email to