Skip to content

Commit

Permalink
Securing the HTTP channel only.
Browse files Browse the repository at this point in the history
Websocket security is done in the next commit
  • Loading branch information
hayssams committed Dec 31, 2015
1 parent 45ce8a2 commit e2affca
Show file tree
Hide file tree
Showing 13 changed files with 390 additions and 2 deletions.
2 changes: 1 addition & 1 deletion bin/zeppelin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,4 @@ if [[ ! -d "${ZEPPELIN_NOTEBOOK_DIR}" ]]; then
$(mkdir -p "${ZEPPELIN_NOTEBOOK_DIR}")
fi

$(exec $ZEPPELIN_RUNNER $JAVA_OPTS -cp $ZEPPELIN_CLASSPATH_OVERRIDES:$CLASSPATH $ZEPPELIN_SERVER "$@")
$(exec $ZEPPELIN_RUNNER $JAVA_OPTS -cp $ZEPPELIN_CONF_DIR/shiro.ini:$ZEPPELIN_CLASSPATH_OVERRIDES:$CLASSPATH $ZEPPELIN_SERVER "$@")
43 changes: 43 additions & 0 deletions conf/shiro.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#
# 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

[main]

# Let's use some in-memory caching to reduce the number of runtime lookups against Stormpath.
# A real application might want to use a more robust caching solution (e.g. ehcache or a
# distributed cache). When using such caches, be aware of your cache TTL settings: too high
# a TTL and the cache won't reflect any potential changes in Stormpath fast enough. Too low
# and the cache could evict too often, reducing performance.
cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
securityManager.cacheManager = $cacheManager


[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

6 changes: 6 additions & 0 deletions conf/zeppelin-site.xml.template
Original file line number Diff line number Diff line change
Expand Up @@ -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>false</value>
<description>Anonymous user allowed by default</description>
</property>

</configuration>

12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
13 changes: 13 additions & 0 deletions security-readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#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 "true" 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.



10 changes: 10 additions & 0 deletions zeppelin-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -226,6 +227,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;
}

Expand Down Expand Up @@ -273,6 +280,9 @@ public Set<Object> getSingletons() {
InterpreterRestApi interpreterApi = new InterpreterRestApi(replFactory);
singletons.add(interpreterApi);

SecurityRestApi securityApi = new SecurityRestApi();
singletons.add(securityApi);

return singletons;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}

}

Loading

0 comments on commit e2affca

Please sign in to comment.