Repository: incubator-zeppelin
Updated Branches:
  refs/heads/master ff99ecba1 -> 89c592489


Shiro security v2

Added Authentication.
Once authenticated, a user has access to all notes.
HTTP & Websocket channels are secured and require auth.
This PR is based  on #53 which also implements user ownership on notes.

Author: Hayssam Saleh <[email protected]>

Closes #586 from hayssams/shiro-security-v2 and squashes the following commits:

47421b8 [Hayssam Saleh] Rollback classpath change since zeppelin conf dir 
already in classpath
5485dcd [Hayssam Saleh] Updates licences for shiro-core and shiro-web 
introduced in this PR
7200e77 [Hayssam Saleh] Default ticket / principal to anonymous in websocket 
message
30736a0 [Hayssam Saleh] Add support for cross site requests with credentials
1372231 [Hayssam Saleh] Test mode requires to user baseUrlSrv to connect to the 
REST API
96ec240 [Hayssam Saleh] use standard HTML tags for SECURITY-README.md
01ba543 [Hayssam Saleh] get ticket before Angular is bootstrapped
2a9e275 [Hayssam Saleh] Add implementation notes
96d1fac [Hayssam Saleh] correct comment in SECURITY-README and keep anonymous 
policy by default in zeppelin-site.xml.template
6fd9982 [Hayssam Saleh] Add minimal shiro.ini file for test phase
8eee51d [Hayssam Saleh] Remove cache optimization in shiro since it references 
stormpath and comes from there.
2017925 [Hayssam Saleh] exclude SECURITY-README from rat check
f9b1952 [Hayssam Saleh] The Websocket channel is now as secure as the HTTP 
channel.
e2affca [Hayssam Saleh] Securing the HTTP channel only. Websocket security is 
done in the next commit


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

Branch: refs/heads/master
Commit: 89c592489fab38e087604159fa0ed33ccef16991
Parents: ff99ecb
Author: Hayssam Saleh <[email protected]>
Authored: Fri Jan 8 08:37:24 2016 +0100
Committer: Lee moon soo <[email protected]>
Committed: Sun Jan 10 08:14:30 2016 -0800

----------------------------------------------------------------------
 SECURITY-README.md                              |  43 ++++++
 conf/shiro.ini                                  |  33 ++++
 conf/zeppelin-site.xml.template                 |   6 +
 pom.xml                                         |  12 ++
 zeppelin-distribution/src/bin_license/LICENSE   |   2 +
 zeppelin-server/pom.xml                         |  10 ++
 .../apache/zeppelin/rest/SecurityRestApi.java   |  74 +++++++++
 .../apache/zeppelin/server/ZeppelinServer.java  |  10 ++
 .../org/apache/zeppelin/socket/Message.java     |   2 +
 .../apache/zeppelin/socket/NotebookServer.java  |  14 ++
 .../apache/zeppelin/ticket/TicketContainer.java |  82 ++++++++++
 .../apache/zeppelin/utils/SecurityUtils.java    |  17 +++
 zeppelin-server/src/main/resources/shiro.ini    |  31 ++++
 .../zeppelin/rest/SecurityRestApiTest.java      |  58 ++++++++
 .../zeppelin/ticket/TicketContainerTest.java    |  62 ++++++++
 zeppelin-web/src/app/app.js                     | 149 +++++++++++--------
 zeppelin-web/src/app/home/home.controller.js    |   1 -
 .../src/components/navbar/navbar.controller.js  |   3 +
 zeppelin-web/src/components/navbar/navbar.html  |   4 +-
 .../websocketEvents/websocketEvents.factory.js  |   4 +-
 zeppelin-web/src/index.html                     |   2 +-
 .../zeppelin/conf/ZeppelinConfiguration.java    |   3 +-
 22 files changed, 557 insertions(+), 65 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/SECURITY-README.md
----------------------------------------------------------------------
diff --git a/SECURITY-README.md b/SECURITY-README.md
new file mode 100644
index 0000000..2eb1fd6
--- /dev/null
+++ b/SECURITY-README.md
@@ -0,0 +1,43 @@
+<!--
+Licensed 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.
+-->
+
+# Shiro Authentication
+To connect to Zeppelin, users will be asked to enter their credentials. Once 
logged, a user has access to all notes including other users notes.
+This a a first step toward full security as implemented by this pull request 
(https://github.com/apache/incubator-zeppelin/pull/53).
+
+# Security setup
+1. Secure the HTTP channel: Comment the line "/** = anon" and uncomment the 
line "/** = authcBasic" in the file conf/shiro.ini. Read more about he 
shiro.ini file format at the following URL 
http://shiro.apache.org/configuration.html#Configuration-INISections.  
+2. Secure the Websocket channel : Set to property "zeppelin.anonymous.allowed" 
to "false" in the file conf/zeppelin-site.xml. You can start by renaming 
conf/zeppelin-site.xml.template to conf/zeppelin-site.xml
+3. Start Zeppelin : bin/zeppelin.sh
+4. point your browser to http://localhost:8080
+5. Login using one of the user/password combinations defined in the 
conf/shiro.ini file.
+
+# Implementation notes
+## Vocabulary
+username, owner and principal are used interchangeably to designate the 
currently authenticated user
+## What are we securing ?
+Zeppelin is basically a web application that spawn remote interpreters to run 
commands and return HTML fragments to be displayed on the user browser.
+The scope of this PR is to require credentials to access Zeppelin. To achieve 
this, we use Apache Shiro. 
+## HTTP Endpoint security
+Apache Shiro sits as a servlet filter between the browser and the exposed 
services and handles the required authentication without any programming 
required. (See Apache Shiro for more info).
+## Websocket security
+Securing the HTTP endpoints is not enough, since Zeppelin also communicates 
with the browser through websockets. To secure this channel, we take the 
following approach:
+1. The browser on startup requests a ticket through HTTP
+2. The Apache Shiro Servlet filter handles the user auth
+3. Once the user is authenticated, a ticket is assigned to this user and the 
ticket is returned to the browser
+
+All websockets communications require the username and ticket  to be submitted 
by the browser. Upon receiving a websocket message, the server checks that the 
ticket received is the one assigned to the username through the HTTP request 
(step 3 above).
+
+
+ 

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/conf/shiro.ini
----------------------------------------------------------------------
diff --git a/conf/shiro.ini b/conf/shiro.ini
new file mode 100644
index 0000000..a592b43
--- /dev/null
+++ b/conf/shiro.ini
@@ -0,0 +1,33 @@
+#
+# 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.
+#
+
+[users]
+# List of users with their password allowed to access Zeppelin.
+# To use a different strategy (LDAP / Database / ...) check the shiro doc at 
http://shiro.apache.org/configuration.html#Configuration-INISections
+admin = password1
+user1 = password2
+user2 = password3
+
+
+[urls]
+
+# anon means the access is anonymous.
+# authcBasic means Basic Auth Security
+# To enfore security, comment the line below and uncomment the next one
+/** = anon
+#/** = authcBasic
+

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/conf/zeppelin-site.xml.template
----------------------------------------------------------------------
diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template
index 74fa2e7..a51c732 100755
--- a/conf/zeppelin-site.xml.template
+++ b/conf/zeppelin-site.xml.template
@@ -180,5 +180,11 @@
   <description>Allowed sources for REST and WebSocket requests (i.e. 
http://onehost:8080,http://otherhost.com). If you leave * you are vulnerable to 
https://issues.apache.org/jira/browse/ZEPPELIN-173</description>
 </property>
 
+<property>
+  <name>zeppelin.anonymous.allowed</name>
+  <value>true</value>
+  <description>Anonymous user allowed by default</description>
+</property>
+
 </configuration>
 

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 9d5b572..ead9637 100755
--- a/pom.xml
+++ b/pom.xml
@@ -208,6 +208,18 @@
         <version>4.11</version>
         <scope>test</scope>
       </dependency>
+
+      <!-- Apache Shiro -->
+      <dependency>
+        <groupId>org.apache.shiro</groupId>
+        <artifactId>shiro-core</artifactId>
+        <version>1.2.3</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.shiro</groupId>
+        <artifactId>shiro-web</artifactId>
+        <version>1.2.3</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-distribution/src/bin_license/LICENSE
----------------------------------------------------------------------
diff --git a/zeppelin-distribution/src/bin_license/LICENSE 
b/zeppelin-distribution/src/bin_license/LICENSE
index 5bc5bef..a5324d0 100644
--- a/zeppelin-distribution/src/bin_license/LICENSE
+++ b/zeppelin-distribution/src/bin_license/LICENSE
@@ -91,6 +91,8 @@ The following components are provided under Apache License.
     (Apache 2.0) Lucene Suggest (org.apache.lucene:lucene-suggest:5.3.1 - 
http://lucene.apache.org/lucene-parent/lucene-suggest)
     (Apache 2.0) Elasticsearch: Core (org.elasticsearch:elasticsearch:2.1.0 - 
http://nexus.sonatype.org/oss-repository-hosting.html/parent/elasticsearch)
     (Apache 2.0) Joda convert (org.joda:joda-convert:1.2 - 
http://joda-convert.sourceforge.net)
+    (Apache 2.0) Shiro Core (org.apache.shiro:shiro-core:1.2.3 - 
https://shiro.apache.org)
+    (Apache 2.0) Shiro Web (org.apache.shiro:shiro-web:1.2.3 - 
https://shiro.apache.org)
     (Apache 2.0) SnakeYAML (org.yaml:snakeyaml:1.15 - http://www.snakeyaml.org)
 
 

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-server/pom.xml
----------------------------------------------------------------------
diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml
index e77ee6c..73e878a 100644
--- a/zeppelin-server/pom.xml
+++ b/zeppelin-server/pom.xml
@@ -269,6 +269,16 @@
       <version>1.9.0</version>
       <scope>test</scope>
     </dependency>
+
+    <!-- Apache Shiro -->
+    <dependency>
+      <groupId>org.apache.shiro</groupId>
+      <artifactId>shiro-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shiro</groupId>
+      <artifactId>shiro-web</artifactId>
+    </dependency>
   </dependencies>
 
   <build>

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java 
b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java
new file mode 100644
index 0000000..d6f3dec
--- /dev/null
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java
@@ -0,0 +1,74 @@
+/*
+ * 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.rest;
+
+import org.apache.zeppelin.conf.ZeppelinConfiguration;
+import org.apache.zeppelin.server.JsonResponse;
+import org.apache.zeppelin.ticket.TicketContainer;
+import org.apache.zeppelin.utils.SecurityUtils;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Zeppelin security rest api endpoint.
+ *
+ */
+@Path("/security")
+@Produces("application/json")
+public class SecurityRestApi {
+  /**
+   * Required by Swagger.
+   */
+  public SecurityRestApi() {
+    super();
+  }
+
+  /**
+   * Get ticket
+   * Returns username & ticket
+   * for anonymous access, username is always anonymous.
+   * After getting this ticket, access through websockets become safe
+   *
+   * @return 200 response
+   */
+  @GET
+  @Path("ticket")
+  public Response ticket() {
+    ZeppelinConfiguration conf = ZeppelinConfiguration.create();
+    String principal = SecurityUtils.getPrincipal();
+    JsonResponse response;
+    // ticket set to anonymous for anonymous user. Simplify testing.
+    String ticket;
+    if ("anonymous".equals(principal))
+      ticket = "anonymous";
+    else
+      ticket = TicketContainer.instance.getTicket(principal);
+
+    Map<String, String> data = new HashMap<>();
+    data.put("principal", principal);
+    data.put("ticket", ticket);
+
+    response = new JsonResponse(Response.Status.OK, "", data);
+    return response.build();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java 
b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
index 07aac08..7ad2b71 100644
--- 
a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
@@ -36,6 +36,7 @@ import org.apache.zeppelin.notebook.repo.NotebookRepo;
 import org.apache.zeppelin.notebook.repo.NotebookRepoSync;
 import org.apache.zeppelin.rest.InterpreterRestApi;
 import org.apache.zeppelin.rest.NotebookRestApi;
+import org.apache.zeppelin.rest.SecurityRestApi;
 import org.apache.zeppelin.rest.ZeppelinRestApi;
 import org.apache.zeppelin.scheduler.SchedulerFactory;
 import org.apache.zeppelin.search.SearchService;
@@ -227,6 +228,12 @@ public class ZeppelinServer extends Application {
 
     cxfContext.addFilter(new FilterHolder(CorsFilter.class), "/*",
         EnumSet.allOf(DispatcherType.class));
+
+    cxfContext.addFilter(org.apache.shiro.web.servlet.ShiroFilter.class, "/*",
+        EnumSet.allOf(DispatcherType.class));
+
+    cxfContext.addEventListener(new 
org.apache.shiro.web.env.EnvironmentLoaderListener());
+
     return cxfContext;
   }
 
@@ -274,6 +281,9 @@ public class ZeppelinServer extends Application {
     InterpreterRestApi interpreterApi = new InterpreterRestApi(replFactory);
     singletons.add(interpreterApi);
 
+    SecurityRestApi securityApi = new SecurityRestApi();
+    singletons.add(securityApi);
+
     return singletons;
   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-server/src/main/java/org/apache/zeppelin/socket/Message.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/Message.java 
b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/Message.java
index 7640b10..0142df2 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/Message.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/Message.java
@@ -103,6 +103,8 @@ public class Message {
 
   public OP op;
   public Map<String, Object> data = new HashMap<String, Object>();
+  public String ticket = "anonymous";
+  public String principal = "anonymous";
 
   public Message(OP op) {
     this.op = op;

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java 
b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
index 038aff1..3dfdca3 100644
--- 
a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
@@ -41,6 +41,7 @@ import org.apache.zeppelin.scheduler.Job.Status;
 import org.apache.zeppelin.scheduler.JobListener;
 import org.apache.zeppelin.server.ZeppelinServer;
 import org.apache.zeppelin.socket.Message.OP;
+import org.apache.zeppelin.ticket.TicketContainer;
 import org.apache.zeppelin.utils.SecurityUtils;
 import org.eclipse.jetty.websocket.WebSocket;
 import org.eclipse.jetty.websocket.WebSocketServlet;
@@ -96,6 +97,19 @@ public class NotebookServer extends WebSocketServlet 
implements
     try {
       Message messagereceived = deserializeMessage(msg);
       LOG.debug("RECEIVE << " + messagereceived.op);
+      LOG.debug("RECEIVE PRINCIPAL << " + messagereceived.principal);
+      LOG.debug("RECEIVE TICKET << " + messagereceived.ticket);
+      String ticket = 
TicketContainer.instance.getTicket(messagereceived.principal);
+      if (ticket != null && !ticket.equals(messagereceived.ticket))
+        throw new Exception("Invalid ticket " + messagereceived.ticket + " != 
" + ticket);
+
+      ZeppelinConfiguration conf = ZeppelinConfiguration.create();
+      boolean allowAnonymous = conf.
+          
getBoolean(ZeppelinConfiguration.ConfVars.ZEPPELIN_ANONYMOUS_ALLOWED);
+      if (!allowAnonymous && messagereceived.principal.equals("anonymous")) {
+        throw new Exception("Anonymous access not allowed ");
+      }
+
       /** Lets be elegant here */
       switch (messagereceived.op) {
           case LIST_NOTES:

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-server/src/main/java/org/apache/zeppelin/ticket/TicketContainer.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/ticket/TicketContainer.java 
b/zeppelin-server/src/main/java/org/apache/zeppelin/ticket/TicketContainer.java
new file mode 100644
index 0000000..513bb4a
--- /dev/null
+++ 
b/zeppelin-server/src/main/java/org/apache/zeppelin/ticket/TicketContainer.java
@@ -0,0 +1,82 @@
+/*
+ * 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.ticket;
+
+import java.util.Calendar;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Very simple ticket container
+ * No cleanup is done, since the same user accross different devices share the 
same ticket
+ * The Map size is at most the number of different user names having access to 
a Zeppelin instance
+ */
+
+
+public class TicketContainer {
+  private static class Entry {
+    public final String ticket;
+    // lastAccessTime still unused
+    public final long lastAccessTime;
+
+    Entry(String ticket) {
+      this.ticket = ticket;
+      this.lastAccessTime = Calendar.getInstance().getTimeInMillis();
+    }
+  }
+
+  private Map<String, Entry> sessions = new ConcurrentHashMap<>();
+
+  public static final TicketContainer instance = new TicketContainer();
+
+  /**
+   * For test use
+   * @param principal
+   * @param ticket
+   * @return true if ticket assigned to principal.
+   */
+  public boolean isValid(String principal, String ticket) {
+    if ("anonymous".equals(principal) && "anonymous".equals(ticket))
+      return true;
+    Entry entry = sessions.get(principal);
+    return entry != null && entry.ticket.equals(ticket);
+  }
+
+  /**
+   * get or create ticket for Websocket authentication assigned to 
authenticated shiro user
+   * For unathenticated user (anonymous), always return ticket value 
"anonymous"
+   * @param principal
+   * @return
+   */
+  public synchronized String getTicket(String principal) {
+    Entry entry = sessions.get(principal);
+    String ticket;
+    if (entry == null) {
+      if (principal.equals("anonymous"))
+        ticket = "anonymous";
+      else
+        ticket = UUID.randomUUID().toString();
+    } else {
+      ticket = entry.ticket;
+    }
+    entry = new Entry(ticket);
+    sessions.put(principal, entry);
+    return ticket;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java 
b/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java
index 732c7c8..1d06e3a 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java
@@ -16,6 +16,7 @@
  */
 package org.apache.zeppelin.utils;
 
+import org.apache.shiro.subject.Subject;
 import org.apache.zeppelin.conf.ZeppelinConfiguration;
 
 import java.net.InetAddress;
@@ -44,4 +45,20 @@ public class SecurityUtils {
             "localhost".equals(sourceUriHost) ||
             conf.getAllowedOrigins().contains(sourceHost);
   }
+
+  /**
+   * Return the authenticated user if any otherwise returns "anonymous"
+   * @return shiro principal
+   */
+  public static String getPrincipal() {
+    Subject subject = org.apache.shiro.SecurityUtils.getSubject();
+    String principal;
+    if (subject.isAuthenticated()) {
+      principal = subject.getPrincipal().toString();
+    }
+    else {
+      principal = "anonymous";
+    }
+    return principal;
+  }
 }

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-server/src/main/resources/shiro.ini
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/resources/shiro.ini 
b/zeppelin-server/src/main/resources/shiro.ini
new file mode 100644
index 0000000..371a44e
--- /dev/null
+++ b/zeppelin-server/src/main/resources/shiro.ini
@@ -0,0 +1,31 @@
+#
+# 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.
+#
+
+[users]
+# List of users with their password allowed to access Zeppelin.
+# To use a different strategy (LDAP / Database / ...) check the shiro doc at 
http://shiro.apache.org/configuration.html#Configuration-INISections
+admin = password
+
+
+[urls]
+
+# anon means the access is anonymous.
+# authcBasic means Basic Auth Security
+# To enfore security, comment the line below and uncomment the next one
+/** = anon
+#/** = authcBasic
+

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java
 
b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java
new file mode 100644
index 0000000..b496f99
--- /dev/null
+++ 
b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.rest;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+public class SecurityRestApiTest extends AbstractTestRestApi {
+  Gson gson = new Gson();
+
+  @BeforeClass
+  public static void init() throws Exception {
+    AbstractTestRestApi.startUp();
+  }
+
+  @AfterClass
+  public static void destroy() throws Exception {
+    AbstractTestRestApi.shutDown();
+  }
+
+  @Test
+  public void testTicket() throws IOException {
+    GetMethod get = httpGet("/security/ticket");
+    get.addRequestHeader("Origin", "http://localhost";);
+    Map<String, Object> resp = gson.fromJson(get.getResponseBodyAsString(),
+        new TypeToken<Map<String, Object>>(){}.getType());
+    Map<String, String> body = (Map<String, String>) resp.get("body");
+    assertEquals("anonymous", body.get("principal"));
+    assertEquals("anonymous", body.get("ticket"));
+    get.releaseConnection();
+  }
+
+}
+

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-server/src/test/java/org/apache/zeppelin/ticket/TicketContainerTest.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-server/src/test/java/org/apache/zeppelin/ticket/TicketContainerTest.java
 
b/zeppelin-server/src/test/java/org/apache/zeppelin/ticket/TicketContainerTest.java
new file mode 100644
index 0000000..91d2cb3
--- /dev/null
+++ 
b/zeppelin-server/src/test/java/org/apache/zeppelin/ticket/TicketContainerTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.ticket;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.net.UnknownHostException;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class TicketContainerTest {
+  private TicketContainer container;
+
+  @Before
+  public void setUp() throws Exception {
+    container = TicketContainer.instance;
+  }
+
+  @Test
+  public void isValidAnonymous() throws UnknownHostException {
+    boolean ok = container.isValid("anonymous", "anonymous");
+    assertTrue(ok);
+  }
+
+  @Test
+  public void isValidExistingPrincipal() throws UnknownHostException {
+    String ticket = container.getTicket("someuser1");
+    boolean ok = container.isValid("someuser1", ticket);
+    assertTrue(ok);
+  }
+
+  @Test
+  public void isValidNonExistingPrincipal() throws UnknownHostException {
+    boolean ok = container.isValid("unknownuser", "someticket");
+    assertFalse(ok);
+  }
+
+  @Test
+  public void isValidunkownTicket() throws UnknownHostException {
+    String ticket = container.getTicket("someuser2");
+    boolean ok = container.isValid("someuser2", ticket+"makeitinvalid");
+    assertFalse(ok);
+  }
+}
+

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-web/src/app/app.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/app.js b/zeppelin-web/src/app/app.js
index 92e7345..364ede7 100644
--- a/zeppelin-web/src/app/app.js
+++ b/zeppelin-web/src/app/app.js
@@ -15,65 +15,96 @@
  * limitations under the License.
  */
 'use strict';
+(function() {
+    var zeppelinWebApp = angular.module('zeppelinWebApp', [
+            'ngAnimate',
+            'ngCookies',
+            'ngRoute',
+            'ngSanitize',
+            'angular-websocket',
+            'ui.ace',
+            'ui.bootstrap',
+            'ui.sortable',
+            'ngTouch',
+            'ngDragDrop',
+            'angular.filter',
+            'monospaced.elastic',
+            'puElasticInput',
+            'xeditable',
+            'ngToast',
+            'focus-if',
+            'ngResource'
+        ])
+        .filter('breakFilter', function() {
+            return function (text) {
+                if (!!text) {
+                    return text.replace(/\n/g, '<br />');
+                }
+            };
+        })
+        .config(function ($httpProvider, $routeProvider, ngToastProvider) {
+            // withCredentials when running locally via grunt
+            $httpProvider.defaults.withCredentials = true;
 
-angular.module('zeppelinWebApp', [
-    'ngAnimate',
-    'ngCookies',
-    'ngRoute',
-    'ngSanitize',
-    'angular-websocket',
-    'ui.ace',
-    'ui.bootstrap',
-    'ui.sortable',
-    'ngTouch',
-    'ngDragDrop',
-    'angular.filter',
-    'monospaced.elastic',
-    'puElasticInput',
-    'xeditable',
-    'ngToast',
-    'focus-if',
-    'ngResource'
-  ])
-  .filter('breakFilter', function() {
-    return function (text) {
-      if (!!text) {
-        return text.replace(/\n/g, '<br />');
-      }
-    };
-  })
-  .config(function ($routeProvider, ngToastProvider) {
-    $routeProvider
-      .when('/', {
-        templateUrl: 'app/home/home.html'
-      })
-      .when('/notebook/:noteId', {
-        templateUrl: 'app/notebook/notebook.html',
-        controller: 'NotebookCtrl'
-      })
-      .when('/notebook/:noteId/paragraph?=:paragraphId', {
-        templateUrl: 'app/notebook/notebook.html',
-        controller: 'NotebookCtrl'
-      })
-      .when('/notebook/:noteId/paragraph/:paragraphId?', {
-        templateUrl: 'app/notebook/notebook.html',
-        controller: 'NotebookCtrl'
-      })
-      .when('/interpreter', {
-        templateUrl: 'app/interpreter/interpreter.html',
-        controller: 'InterpreterCtrl'
-      })
-      .when('/search/:searchTerm', {
-        templateUrl: 'app/search/result-list.html',
-        controller: 'SearchResultCtrl'
-      })
-      .otherwise({
-        redirectTo: '/'
-      });
+            $routeProvider
+                .when('/', {
+                    templateUrl: 'app/home/home.html'
+                })
+                .when('/notebook/:noteId', {
+                    templateUrl: 'app/notebook/notebook.html',
+                    controller: 'NotebookCtrl'
+                })
+                .when('/notebook/:noteId/paragraph?=:paragraphId', {
+                    templateUrl: 'app/notebook/notebook.html',
+                    controller: 'NotebookCtrl'
+                })
+                .when('/notebook/:noteId/paragraph/:paragraphId?', {
+                    templateUrl: 'app/notebook/notebook.html',
+                    controller: 'NotebookCtrl'
+                })
+                .when('/interpreter', {
+                    templateUrl: 'app/interpreter/interpreter.html',
+                    controller: 'InterpreterCtrl'
+                })
+                .when('/search/:searchTerm', {
+                    templateUrl: 'app/search/result-list.html',
+                    controller: 'SearchResultCtrl'
+                })
+                .otherwise({
+                    redirectTo: '/'
+                });
 
-    ngToastProvider.configure({
-      dismissButton: true,
-      dismissOnClick: false,
-      timeout: 6000
+            ngToastProvider.configure({
+                dismissButton: true,
+                dismissOnClick: false,
+                timeout: 6000
+            });
+        });
+
+
+    function auth() {
+        var $http = angular.injector(['ng']).get('$http');
+        var baseUrlSrv = 
angular.injector(['zeppelinWebApp']).get('baseUrlSrv');
+        // withCredentials when running locally via grunt
+        $http.defaults.withCredentials = true;
+
+        return 
$http.get(baseUrlSrv.getRestApiBase()+'/security/ticket').then(function(response)
 {
+            zeppelinWebApp.run(function($rootScope) {
+                $rootScope.ticket = angular.fromJson(response.data).body;
+            });
+        }, function(errorResponse) {
+            // Handle error case
+        });
+    }
+
+    function bootstrapApplication() {
+        angular.bootstrap(document, ['zeppelinWebApp']);
+    }
+
+
+    angular.element(document).ready(function() {
+        auth().then(bootstrapApplication);
     });
-  });
+
+}());
+

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-web/src/app/home/home.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/home/home.controller.js 
b/zeppelin-web/src/app/home/home.controller.js
index f1b2ab3..6f7f909 100644
--- a/zeppelin-web/src/app/home/home.controller.js
+++ b/zeppelin-web/src/app/home/home.controller.js
@@ -14,7 +14,6 @@
 'use strict';
 
 angular.module('zeppelinWebApp').controller('HomeCtrl', function($scope, 
notebookListDataFactory, websocketMsgSrv, $rootScope, arrayOrderingSrv) {
-  
   var vm = this;
   vm.notes = notebookListDataFactory;
   vm.websocketMsgSrv = websocketMsgSrv;

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-web/src/components/navbar/navbar.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/navbar/navbar.controller.js 
b/zeppelin-web/src/components/navbar/navbar.controller.js
index 30e6ac2..2f03e1a 100644
--- a/zeppelin-web/src/components/navbar/navbar.controller.js
+++ b/zeppelin-web/src/components/navbar/navbar.controller.js
@@ -23,6 +23,7 @@ angular.module('zeppelinWebApp').controller('NavCtrl', 
function($scope, $rootSco
   vm.connected = websocketMsgSrv.isConnected();
   vm.websocketMsgSrv = websocketMsgSrv;
   vm.arrayOrderingSrv = arrayOrderingSrv;
+  vm.authenticated = $rootScope.ticket.principal !== 'anonymous';
 
   angular.element('#notebook-list').perfectScrollbar({suppressScrollX: true});
 
@@ -51,6 +52,8 @@ angular.module('zeppelinWebApp').controller('NavCtrl', 
function($scope, $rootSco
     websocketMsgSrv.getNotebookList();
   }
 
+  vm.authenticated = $rootScope.ticket.principal !== 'anonymous';
+
   function isActive(noteId) {
     return ($routeParams.noteId === noteId);
   }

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-web/src/components/navbar/navbar.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/navbar/navbar.html 
b/zeppelin-web/src/components/navbar/navbar.html
index 86a8512..20ee024 100644
--- a/zeppelin-web/src/components/navbar/navbar.html
+++ b/zeppelin-web/src/components/navbar/navbar.html
@@ -73,8 +73,8 @@ limitations under the License.
         </li>
         <li class="server-status">
           <i class="fa fa-circle" 
ng-class="{'server-connected':navbar.connected, 
'server-disconnected':!navbar.connected}"></i>
-          <span ng-show="navbar.connected">Connected</span>
-          <span ng-show="!navbar.connected">Disconnected</span>
+          <span ng-show="navbar.authenticated">{{ticket.principal}} 
connected</span>
+          <span ng-show="!navbar.authenticated">Disconnected</span>
         </li>
       </ul>
     </div>

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js
----------------------------------------------------------------------
diff --git 
a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js 
b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js
index 7bd8a63..bb99d56 100644
--- a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js
+++ b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js
@@ -28,7 +28,9 @@ angular.module('zeppelinWebApp').factory('websocketEvents', 
function($rootScope,
   });
 
   websocketCalls.sendNewEvent = function(data) {
-    console.log('Send >> %o, %o', data.op, data);
+    data.principal = $rootScope.ticket.principal;
+    data.ticket = $rootScope.ticket.ticket;
+    console.log('Send >> %o, %o, %o, %o', data.op, data.principal, 
data.ticket, data);
     websocketCalls.ws.send(JSON.stringify(data));
   };
 

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-web/src/index.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/index.html b/zeppelin-web/src/index.html
index 4ef4056..8a2c0f7 100644
--- a/zeppelin-web/src/index.html
+++ b/zeppelin-web/src/index.html
@@ -12,7 +12,7 @@ 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.
 -->
-<html ng-app="zeppelinWebApp" ng-controller="MainCtrl" class="no-js">
+<html ng-controller="MainCtrl" class="no-js">
   <head>
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta charset="utf-8">

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/89c59248/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
 
b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
index 6efc2a3..ca63eef 100755
--- 
a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
+++ 
b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
@@ -430,7 +430,8 @@ public class ZeppelinConfiguration extends XMLConfiguration 
{
     ZEPPELIN_CONF_DIR("zeppelin.conf.dir", "conf"),
     // Allows a way to specify a ',' separated list of allowed origins for 
rest and websockets
     // i.e. http://localhost:8080
-    ZEPPELIN_ALLOWED_ORIGINS("zeppelin.server.allowed.origins", "*");
+    ZEPPELIN_ALLOWED_ORIGINS("zeppelin.server.allowed.origins", "*"),
+    ZEPPELIN_ANONYMOUS_ALLOWED("zeppelin.anonymous.allowed", true);
 
     private String varName;
     @SuppressWarnings("rawtypes")


Reply via email to