From 34a8e5e3a895b54f9170c67067b1791d86c0083f Mon Sep 17 00:00:00 2001
From: Anthony Corbacho
Date: Wed, 13 Jul 2016 12:23:15 +0900
Subject: [PATCH 1/9] Create new Apache Shiro Realm for ZeppelinHub
This Realm will let users login with they ZeppelinHub credentials.
---
.../zeppelin/realm/ZeppelinHubRealm.java | 192 ++++++++++++++++++
1 file changed, 192 insertions(+)
create mode 100644 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 00000000000..cdc097f49c2
--- /dev/null
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java
@@ -0,0 +1,192 @@
+/*
+ * 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.
+ *
+ * @author anthonyc
+ *
+ */
+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 use 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 (token.getUsername() == null) {
+ throw new AccountException("Null 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.
+ * It will also perform a check of ZeppelinHub url.
+ *
+ * @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
+ * field "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.
+ * Payload will look like:
+ *
+ * {
+ * 'login': 'userLogin',
+ * 'password': 'userpassword'
+ * }
+ *
+ * @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();
+ }
+
+ 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 used to deserialise ZeppelinHub response.
+ */
+ protected class User {
+ public String login;
+ public String email;
+ public String name;
+ }
+}
From 38683e17a399cc5946a9e4993a238634862bbaef Mon Sep 17 00:00:00 2001
From: Anthony Corbacho
Date: Wed, 13 Jul 2016 12:24:29 +0900
Subject: [PATCH 2/9] Add new setting in Shiri.ini to handle ZeppelinHub realm.
---
conf/shiro.ini | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/conf/shiro.ini b/conf/shiro.ini
index ca5afe3471b..75a3fb9bd91 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
From c207b5e4022b64f46a2669850e3e3995cd1b43db Mon Sep 17 00:00:00 2001
From: Anthony Corbacho
Date: Wed, 13 Jul 2016 12:39:33 +0900
Subject: [PATCH 3/9] Change check of token.getUsername() in
doGetAuthenticationInfo by using StringUtils::isBlank instead of checking
only null.
---
.../main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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
index cdc097f49c2..534df21b891 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java
@@ -77,8 +77,8 @@ public ZeppelinHubRealm() {
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken)
throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authToken;
- if (token.getUsername() == null) {
- throw new AccountException("Null usernames are not allowed by this realm.");
+ 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);
From 64154d42922766c3066c70137e336b1041c041eb Mon Sep 17 00:00:00 2001
From: Anthony Corbacho
Date: Wed, 13 Jul 2016 12:52:43 +0900
Subject: [PATCH 4/9] Add more method comments.
---
.../apache/zeppelin/realm/ZeppelinHubRealm.java | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
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
index 534df21b891..57552b58767 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java
@@ -99,7 +99,8 @@ protected void onInit() {
/**
* Setter of ZeppelinHub URL, this will be called by Shiro based on zeppelinhubUrl property
* in shiro.ini file.
- * It will also perform a check of ZeppelinHub url.
+ * 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
*/
@@ -115,8 +116,7 @@ public void setZeppelinhubUrl(String url) {
/**
* Send to ZeppelinHub a login request based on the request body which is a JSON that contains 2
- * field "login" and password.
- *
+ * fields "login" and "password".
*
* @param requestBody JSON string of ZeppelinHub payload.
* @return Account object with login, name (if set in ZeppelinHub), and mail.
@@ -169,6 +169,14 @@ protected String createLoginPayload(String login, char[] pwd) {
return sb.append(login).append("\", \"password\":\"").append(pwd).append("\"}").toString();
}
+ /**
+ * Perform a Simple URL check by using URI(url).toURL().
+ * 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 {
From 9bf96ba461c35cfd59a2ca3b84e2e3050fe22a9b Mon Sep 17 00:00:00 2001
From: Anthony Corbacho
Date: Wed, 13 Jul 2016 12:55:22 +0900
Subject: [PATCH 5/9] Remove author tag...
---
.../main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java | 2 --
1 file changed, 2 deletions(-)
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
index 57552b58767..86577362205 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java
@@ -45,8 +45,6 @@
/**
* A {@code Realm} implementation that uses the ZeppelinHub to authenticate users.
- *
- * @author anthonyc
*
*/
public class ZeppelinHubRealm extends AuthorizingRealm {
From 8347fa9965a1703d1514dde893a273df5d6ac07e Mon Sep 17 00:00:00 2001
From: Anthony Corbacho
Date: Wed, 13 Jul 2016 14:31:31 +0900
Subject: [PATCH 6/9] Handle long line > 100 char
---
.../java/org/apache/zeppelin/realm/ZeppelinHubRealm.java | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
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
index 86577362205..cbe490d8de5 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinHubRealm.java
@@ -65,7 +65,8 @@ public class ZeppelinHubRealm extends AuthorizingRealm {
public ZeppelinHubRealm() {
super();
LOG.debug("Init ZeppelinhubRealm");
- //TODO(anthonyc): think about more setting for this HTTP client. eg: if user use proxy etcetc...
+ //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();
@@ -188,7 +189,7 @@ protected boolean isZeppelinHubUrlValid(String url) {
}
/**
- * Helper class that will be used to deserialise ZeppelinHub response.
+ * Helper class that will be use to deserialize ZeppelinHub response.
*/
protected class User {
public String login;
From 5a27871f5a9e436653c8862e6d2ecc8913fdd496 Mon Sep 17 00:00:00 2001
From: Anthony Corbacho
Date: Fri, 22 Jul 2016 15:13:40 +0900
Subject: [PATCH 7/9] Add Documentation about ZeppelinHub Realm configuration
---
docs/security/shiroauthentication.md | 31 ++++++++++++++++++++++++++--
1 file changed, 29 insertions(+), 2 deletions(-)
diff --git a/docs/security/shiroauthentication.md b/docs/security/shiroauthentication.md
index a5c9c317b36..44eb67d1f13 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 easely.
+
+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.
@@ -121,5 +148,5 @@ Since Shiro provides **url-based security**, you can hide the information by com
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.
-
-> **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).
+
From 0f16344ce8a7183a34b30bcf0dfc27271da20200 Mon Sep 17 00:00:00 2001
From: Anthony Corbacho
Date: Fri, 22 Jul 2016 15:49:35 +0900
Subject: [PATCH 8/9] Fix typo in documentation
---
docs/security/shiroauthentication.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/security/shiroauthentication.md b/docs/security/shiroauthentication.md
index 44eb67d1f13..c5a2c0b38ad 100644
--- a/docs/security/shiroauthentication.md
+++ b/docs/security/shiroauthentication.md
@@ -107,7 +107,7 @@ 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)
+To learn more about Apache Shiro Realm, please check [this documentation](http://shiro.apache.org/realm.html).
We also provide community custom Realms.
@@ -118,7 +118,7 @@ TBD
TBD
### ZeppelinHub
-[ZeppelinHub](https://www.zeppelinhub.com) is a service that synchronize your Apache Zeppelin notebooks and enables you to collaborate easely.
+[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.
From 63b06c644b4ee5fe4ac471f2bcaf2806557dd8b2 Mon Sep 17 00:00:00 2001
From: Anthony Corbacho
Date: Fri, 22 Jul 2016 15:55:50 +0900
Subject: [PATCH 9/9] Fix rebase mistake in documentation
---
docs/security/shiroauthentication.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/security/shiroauthentication.md b/docs/security/shiroauthentication.md
index c5a2c0b38ad..2f3590a2332 100644
--- a/docs/security/shiroauthentication.md
+++ b/docs/security/shiroauthentication.md
@@ -148,5 +148,6 @@ Since Shiro provides **url-based security**, you can hide the information by com
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.
-> **NOTE :** This documentation is originally from [SECURITY-README.md](https://github.com/apache/zeppelin/blob/master/SECURITY-README.md).
+
+> **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).