From 89c592489fab38e087604159fa0ed33ccef16991 Mon Sep 17 00:00:00 2001 From: Hayssam Saleh Date: Fri, 8 Jan 2016 08:37:24 +0100 Subject: [PATCH] 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 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 --- 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 +++++++++ .../zeppelin/server/ZeppelinServer.java | 10 ++ .../org/apache/zeppelin/socket/Message.java | 2 + .../zeppelin/socket/NotebookServer.java | 14 ++ .../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 - .../components/navbar/navbar.controller.js | 3 + .../src/components/navbar/navbar.html | 4 +- .../websocketEvents.factory.js | 4 +- zeppelin-web/src/index.html | 2 +- .../zeppelin/conf/ZeppelinConfiguration.java | 3 +- 22 files changed, 557 insertions(+), 65 deletions(-) create mode 100644 SECURITY-README.md create mode 100644 conf/shiro.ini create mode 100644 zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java create mode 100644 zeppelin-server/src/main/java/org/apache/zeppelin/ticket/TicketContainer.java create mode 100644 zeppelin-server/src/main/resources/shiro.ini create mode 100644 zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java create mode 100644 zeppelin-server/src/test/java/org/apache/zeppelin/ticket/TicketContainerTest.java diff --git a/SECURITY-README.md b/SECURITY-README.md new file mode 100644 index 00000000000..2eb1fd64128 --- /dev/null +++ b/SECURITY-README.md @@ -0,0 +1,43 @@ + + +# 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). + + + diff --git a/conf/shiro.ini b/conf/shiro.ini new file mode 100644 index 00000000000..a592b4317dc --- /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 + diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index 74fa2e76053..a51c732717e 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -180,5 +180,11 @@ 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 + + zeppelin.anonymous.allowed + true + Anonymous user allowed by default + + diff --git a/pom.xml b/pom.xml index 9d5b572aef5..ead96373cf7 100755 --- a/pom.xml +++ b/pom.xml @@ -208,6 +208,18 @@ 4.11 test + + + + org.apache.shiro + shiro-core + 1.2.3 + + + org.apache.shiro + shiro-web + 1.2.3 + diff --git a/zeppelin-distribution/src/bin_license/LICENSE b/zeppelin-distribution/src/bin_license/LICENSE index 5bc5befc419..a5324d0c486 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) diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index e77ee6ca38d..73e878a58f5 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -269,6 +269,16 @@ 1.9.0 test + + + + org.apache.shiro + shiro-core + + + org.apache.shiro + shiro-web + 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 00000000000..d6f3dec05d4 --- /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 data = new HashMap<>(); + data.put("principal", principal); + data.put("ticket", ticket); + + response = new JsonResponse(Response.Status.OK, "", data); + return response.build(); + } +} 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 07aac08ffa7..7ad2b713037 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.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 @@ private static ServletContextHandler setupRestApiContextHandler(ZeppelinConfigur 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 Set getSingletons() { InterpreterRestApi interpreterApi = new InterpreterRestApi(replFactory); singletons.add(interpreterApi); + SecurityRestApi securityApi = new SecurityRestApi(); + singletons.add(securityApi); + return singletons; } } 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 7640b10f8e2..0142df2c6f7 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 static enum OP { public OP op; public Map data = new HashMap(); + public String ticket = "anonymous"; + public String principal = "anonymous"; public Message(OP op) { this.op = op; 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 038aff11a5b..3dfdca3cd8d 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.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 void onMessage(NotebookSocket conn, String msg) { 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: 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 00000000000..513bb4a9337 --- /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 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; + } +} 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 732c7c8b4e6..1d06e3a5ebf 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 static Boolean isValidOrigin(String sourceHost, ZeppelinConfiguration con "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; + } } diff --git a/zeppelin-server/src/main/resources/shiro.ini b/zeppelin-server/src/main/resources/shiro.ini new file mode 100644 index 00000000000..371a44e11e1 --- /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 + 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 00000000000..b496f99a117 --- /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 resp = gson.fromJson(get.getResponseBodyAsString(), + new TypeToken>(){}.getType()); + Map body = (Map) resp.get("body"); + assertEquals("anonymous", body.get("principal")); + assertEquals("anonymous", body.get("ticket")); + get.releaseConnection(); + } + +} + 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 00000000000..91d2cb3af20 --- /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); + } +} + diff --git a/zeppelin-web/src/app/app.js b/zeppelin-web/src/app/app.js index 92e7345db05..364ede76b51 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, '
'); + } + }; + }) + .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, '
'); - } - }; - }) - .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); }); - }); + +}()); + diff --git a/zeppelin-web/src/app/home/home.controller.js b/zeppelin-web/src/app/home/home.controller.js index f1b2ab382b7..6f7f909e0be 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; diff --git a/zeppelin-web/src/components/navbar/navbar.controller.js b/zeppelin-web/src/components/navbar/navbar.controller.js index 30e6ac27892..2f03e1a7b46 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); } diff --git a/zeppelin-web/src/components/navbar/navbar.html b/zeppelin-web/src/components/navbar/navbar.html index 86a85122add..20ee0241c90 100644 --- a/zeppelin-web/src/components/navbar/navbar.html +++ b/zeppelin-web/src/components/navbar/navbar.html @@ -73,8 +73,8 @@
  • - Connected - Disconnected + {{ticket.principal}} connected + Disconnected
  • diff --git a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js index 7bd8a63be36..bb99d562709 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)); }; diff --git a/zeppelin-web/src/index.html b/zeppelin-web/src/index.html index 4ef405690f7..8a2c0f72303 100644 --- a/zeppelin-web/src/index.html +++ b/zeppelin-web/src/index.html @@ -12,7 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - + 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 6efc2a38fc0..ca63eefe2d0 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 static enum ConfVars { 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")