diff --git a/conf/shiro.ini b/conf/shiro.ini index 1cc3cf809ac..8d803d009b5 100644 --- a/conf/shiro.ini +++ b/conf/shiro.ini @@ -19,8 +19,9 @@ # 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 +user1 = password2, role1, role2 +user2 = password3, role3 +user3 = password4, role2 # Sample LDAP configuration, for user Authentication, currently tested for single Realm [main] diff --git a/docs/_includes/themes/zeppelin/_navigation.html b/docs/_includes/themes/zeppelin/_navigation.html index 9eddbf9590d..2bc4f38eb4c 100644 --- a/docs/_includes/themes/zeppelin/_navigation.html +++ b/docs/_includes/themes/zeppelin/_navigation.html @@ -84,6 +84,12 @@
  • Notebook API
  • Configuration API
  • + +
  • Security Overview
  • +
  • Authentication
  • +
  • Notebook Authorization
  • +
  • Interpreter Authorization
  • +
  • Writing Zeppelin Interpreter
  • How to contribute (code)
  • diff --git a/docs/security/authentication.md b/docs/security/authentication.md new file mode 100644 index 00000000000..081d41915c4 --- /dev/null +++ b/docs/security/authentication.md @@ -0,0 +1,31 @@ +--- +layout: page +title: "Authentication" +description: "Authentication" +group: security +--- + +# Authentication + +Authentication is company-specific. + +One option is to use [Basic Access Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) + +Another option is to have an authentication server that can verify user credentials in an LDAP server. +If an incoming request to the Zeppelin server does not have a cookie with user information encrypted with the authentication server public key, the user +is redirected to the authentication server. Once the user is verified, the authentication server redirects the browser to a specific +URL in the Zeppelin server which sets the authentication cookie in the browser. +The end result is that all requests to the Zeppelin +web server have the authentication cookie which contains user and groups information. diff --git a/docs/security/interpreter_authorization.md b/docs/security/interpreter_authorization.md new file mode 100644 index 00000000000..d1c792d113f --- /dev/null +++ b/docs/security/interpreter_authorization.md @@ -0,0 +1,34 @@ +--- +layout: page +title: "Notebook Authorization" +description: "Notebook Authorization" +group: security +--- + +# Interpreter and Data Source Authorization + +## Interpreter Authorization + +Interpreter authorization involves permissions like creating an interpreter and execution queries using it. + +## Data Source Authorization + +Data source authorization involves authenticating to the data source like a Mysql database and letting it determine user permissions. + +For the Hive interpreter, we need to maintain per-user connection pools. +The interpret method takes the user string as parameter and executes the jdbc call using a connection in the user's connection pool. + +In case of Presto, we don't need password if the Presto DB server runs backend code using HDFS authorization for the user. +For databases like Vertica and Mysql we have to store password information for users. diff --git a/docs/security/notebook_authorization.md b/docs/security/notebook_authorization.md new file mode 100644 index 00000000000..b9521b14577 --- /dev/null +++ b/docs/security/notebook_authorization.md @@ -0,0 +1,37 @@ +--- +layout: page +title: "Notebook Authorization" +description: "Notebook Authorization" +group: security +--- + +# Notebook Authorization + +We assume that there is an authentication component that associates a user string and a set of group strings with every NotebookSocket. + +Each note has the following: +* set of owner entities (users or groups) +* set of reader entities (users or groups) +* set of writer entities (users or groups) + +If a set is empty, it means that any user can perform that operation. + +The NotebookServer classifies every Note operation into three categories: read, write, manage. +Before executing a Note operation, it checks if the user and the groups associated with the NotebookSocket have permissions. For example, before executing an read +operation, it checks if the user and the groups have at least one entity that belongs to the reader entities. + +To initialize and modify note permissions, we provide UI like "Interpreter binding". The user inputs comma separated entities for owners, readers and writers. +We execute a rest api call with this information. In the backend we get the user information for the connection and allow the operation if the user and groups +associated with the current user have at least one entity that belongs to owner entities for the note. diff --git a/docs/security/overview.md b/docs/security/overview.md new file mode 100644 index 00000000000..e76410d13cc --- /dev/null +++ b/docs/security/overview.md @@ -0,0 +1,28 @@ +--- +layout: page +title: "Security Overview" +description: "Security Overview" +group: security +--- + +{% include JB/setup %} + +# Security Overview + +There are three aspects to Zeppelin security: + +* Authentication: is the user who they say they are? [More](authentication.html) +* Notebook authorization: does the user have permissions to read or write to a note? [More](notebook_authorization.html) +* Interpreter and data source authorization: does the user have permissions to perform interpreter operations or access data source objects? [More](interpreter_authorization.html) diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java index 486e5b122c7..be46f13b0a2 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java @@ -18,6 +18,8 @@ package org.apache.zeppelin.rest; import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -46,6 +48,7 @@ import org.apache.zeppelin.search.SearchService; import org.apache.zeppelin.server.JsonResponse; import org.apache.zeppelin.socket.NotebookServer; +import org.apache.zeppelin.utils.SecurityUtils; import org.quartz.CronExpression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -75,6 +78,66 @@ public NotebookRestApi(Notebook notebook, NotebookServer notebookServer, SearchS this.notebookIndex = search; } + /** + * list note owners + */ + @GET + @Path("{noteId}/permissions") + public Response getNotePermissions(@PathParam("noteId") String noteId) { + Note note = notebook.getNote(noteId); + HashMap permissionsMap = new HashMap(); + permissionsMap.put("owners", note.getOwners()); + permissionsMap.put("readers", note.getReaders()); + permissionsMap.put("writers", note.getWriters()); + return new JsonResponse<>(Status.OK, "", permissionsMap).build(); + } + + String ownerPermissionError(HashSet current, + HashSet allowed) throws IOException { + LOG.info("Cannot change permissions. Connection owners {}. Allowed owners {}", + current.toString(), allowed.toString()); + return "Insufficient privileges to change permissions.\n\n" + + "Allowed owners: " + allowed.toString() + "\n\n" + + "User belongs to: " + current.toString(); + } + + /** + * Set note owners + */ + @PUT + @Path("{noteId}/permissions") + public Response putNotePermissions(@PathParam("noteId") String noteId, String req) + throws IOException { + HashMap permMap = gson.fromJson(req, + new TypeToken>(){}.getType()); + Note note = notebook.getNote(noteId); + String principal = SecurityUtils.getPrincipal(); + HashSet roles = SecurityUtils.getRoles(); + LOG.info("Set permissions {} {} {} {} {}", + noteId, + principal, + permMap.get("owners"), + permMap.get("readers"), + permMap.get("writers") + ); + + HashSet userAndRoles = new HashSet(); + userAndRoles.add(principal); + userAndRoles.addAll(roles); + if (!note.isOwner(userAndRoles)) { + return new JsonResponse<>(Status.FORBIDDEN, ownerPermissionError(userAndRoles, + note.getOwners())).build(); + } + note.setOwners(permMap.get("owners")); + note.setReaders(permMap.get("readers")); + note.setWriters(permMap.get("writers")); + LOG.debug("After set permissions {} {} {}", note.getOwners(), note.getReaders(), + note.getWriters()); + note.persist(); + notebookServer.broadcastNote(note); + return new JsonResponse<>(Status.OK).build(); + } + /** * bind a setting to note * @throws IOException 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 index d6f3dec05d4..905eb2bdd9a 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java @@ -21,12 +21,15 @@ import org.apache.zeppelin.server.JsonResponse; import org.apache.zeppelin.ticket.TicketContainer; import org.apache.zeppelin.utils.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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.HashSet; import java.util.Map; /** @@ -36,6 +39,8 @@ @Path("/security") @Produces("application/json") public class SecurityRestApi { + private static final Logger LOG = LoggerFactory.getLogger(SecurityRestApi.class); + /** * Required by Swagger. */ @@ -56,6 +61,7 @@ public SecurityRestApi() { public Response ticket() { ZeppelinConfiguration conf = ZeppelinConfiguration.create(); String principal = SecurityUtils.getPrincipal(); + HashSet roles = SecurityUtils.getRoles(); JsonResponse response; // ticket set to anonymous for anonymous user. Simplify testing. String ticket; @@ -66,9 +72,11 @@ public Response ticket() { Map data = new HashMap<>(); data.put("principal", principal); + data.put("roles", roles.toString()); data.put("ticket", ticket); response = new JsonResponse(Response.Status.OK, "", data); + LOG.warn(response.toString()); return response.build(); } } 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 54d9ab21356..7da23ec3526 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 @@ -96,6 +96,7 @@ public static enum OP { PARAGRAPH_APPEND_OUTPUT, // [s-c] append output PARAGRAPH_UPDATE_OUTPUT, // [s-c] update (replace) output PING, + AUTH_INFO, ANGULAR_OBJECT_UPDATE, // [s-c] add/update angular object ANGULAR_OBJECT_REMOVE, // [s-c] add angular object del @@ -116,6 +117,7 @@ public static enum OP { public Map data = new HashMap(); public String ticket = "anonymous"; public String principal = "anonymous"; + public String roles = ""; 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 00e48583644..3b7da732171 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 @@ -18,6 +18,8 @@ import com.google.common.base.Strings; import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; import org.apache.zeppelin.display.AngularObject; @@ -96,6 +98,7 @@ public void onMessage(NotebookSocket conn, String msg) { LOG.debug("RECEIVE << " + messagereceived.op); LOG.debug("RECEIVE PRINCIPAL << " + messagereceived.principal); LOG.debug("RECEIVE TICKET << " + messagereceived.ticket); + LOG.debug("RECEIVE ROLES << " + messagereceived.roles); String ticket = TicketContainer.instance.getTicket(messagereceived.principal); if (ticket != null && !ticket.equals(messagereceived.ticket)) throw new Exception("Invalid ticket " + messagereceived.ticket + " != " + ticket); @@ -107,6 +110,16 @@ public void onMessage(NotebookSocket conn, String msg) { throw new Exception("Anonymous access not allowed "); } + HashSet userAndRoles = new HashSet(); + userAndRoles.add(messagereceived.principal); + if (!messagereceived.roles.equals("")) { + HashSet roles = gson.fromJson(messagereceived.roles, + new TypeToken>(){}.getType()); + if (roles != null) { + userAndRoles.addAll(roles); + } + } + /** Lets be elegant here */ switch (messagereceived.op) { case LIST_NOTES: @@ -116,57 +129,57 @@ public void onMessage(NotebookSocket conn, String msg) { broadcastReloadedNoteList(); break; case GET_HOME_NOTE: - sendHomeNote(conn, notebook); + sendHomeNote(conn, userAndRoles, notebook); break; case GET_NOTE: - sendNote(conn, notebook, messagereceived); + sendNote(conn, userAndRoles, notebook, messagereceived); break; case NEW_NOTE: - createNote(conn, notebook, messagereceived); + createNote(conn, userAndRoles, notebook, messagereceived); break; case DEL_NOTE: - removeNote(conn, notebook, messagereceived); + removeNote(conn, userAndRoles, notebook, messagereceived); break; case CLONE_NOTE: - cloneNote(conn, notebook, messagereceived); + cloneNote(conn, userAndRoles, notebook, messagereceived); break; case IMPORT_NOTE: - importNote(conn, notebook, messagereceived); + importNote(conn, userAndRoles, notebook, messagereceived); break; case COMMIT_PARAGRAPH: - updateParagraph(conn, notebook, messagereceived); + updateParagraph(conn, userAndRoles, notebook, messagereceived); break; case RUN_PARAGRAPH: - runParagraph(conn, notebook, messagereceived); + runParagraph(conn, userAndRoles, notebook, messagereceived); break; case CANCEL_PARAGRAPH: - cancelParagraph(conn, notebook, messagereceived); + cancelParagraph(conn, userAndRoles, notebook, messagereceived); break; case MOVE_PARAGRAPH: - moveParagraph(conn, notebook, messagereceived); + moveParagraph(conn, userAndRoles, notebook, messagereceived); break; case INSERT_PARAGRAPH: - insertParagraph(conn, notebook, messagereceived); + insertParagraph(conn, userAndRoles, notebook, messagereceived); break; case PARAGRAPH_REMOVE: - removeParagraph(conn, notebook, messagereceived); + removeParagraph(conn, userAndRoles, notebook, messagereceived); break; case PARAGRAPH_CLEAR_OUTPUT: - clearParagraphOutput(conn, notebook, messagereceived); + clearParagraphOutput(conn, userAndRoles, notebook, messagereceived); break; case NOTE_UPDATE: - updateNote(conn, notebook, messagereceived); + updateNote(conn, userAndRoles, notebook, messagereceived); break; case COMPLETION: - completion(conn, notebook, messagereceived); + completion(conn, userAndRoles, notebook, messagereceived); break; case PING: break; //do nothing case ANGULAR_OBJECT_UPDATED: - angularObjectUpdated(conn, notebook, messagereceived); + angularObjectUpdated(conn, userAndRoles, notebook, messagereceived); break; case LIST_CONFIGURATIONS: - sendAllConfigurations(conn, notebook); + sendAllConfigurations(conn, userAndRoles, notebook); break; case CHECKPOINT_NOTEBOOK: checkpointNotebook(conn, notebook, messagereceived); @@ -358,8 +371,24 @@ public void broadcastReloadedNoteList() { broadcastAll(new Message(OP.NOTES_INFO).put("notes", notesInfo)); } - private void sendNote(NotebookSocket conn, Notebook notebook, + void permissionError(NotebookSocket conn, String op, HashSet current, + HashSet allowed) throws IOException { + LOG.info("Cannot {}. Connection readers {}. Allowed readers {}", + op, current, allowed); + conn.send(serializeMessage(new Message(OP.AUTH_INFO).put("info", + "Insufficient privileges to " + op + " note.\n\n" + + "Allowed users or roles: " + allowed.toString() + "\n\n" + + "User belongs to: " + current.toString()))); + } + + private void sendNote(NotebookSocket conn, HashSet userAndRoles, Notebook notebook, Message fromMessage) throws IOException { + + LOG.info("New operation from {} : {} : {} : {} : {}", conn.getRequest().getRemoteAddr(), + conn.getRequest().getRemotePort(), + fromMessage.principal, fromMessage.op, fromMessage.get("id") + ); + String noteId = (String) fromMessage.get("id"); if (noteId == null) { return; @@ -367,13 +396,19 @@ private void sendNote(NotebookSocket conn, Notebook notebook, Note note = notebook.getNote(noteId); if (note != null) { + if (!note.isReader(userAndRoles)) { + permissionError(conn, "read", userAndRoles, note.getReaders()); + broadcastNoteList(); + return; + } addConnectionToNote(note.id(), conn); conn.send(serializeMessage(new Message(OP.NOTE).put("note", note))); sendAllAngularObjects(note, conn); } } - private void sendHomeNote(NotebookSocket conn, Notebook notebook) throws IOException { + private void sendHomeNote(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook) throws IOException { String noteId = notebook.getConf().getString(ConfVars.ZEPPELIN_NOTEBOOK_HOMESCREEN); Note note = null; @@ -382,6 +417,11 @@ private void sendHomeNote(NotebookSocket conn, Notebook notebook) throws IOExcep } if (note != null) { + if (!note.isReader(userAndRoles)) { + permissionError(conn, "read", userAndRoles, note.getReaders()); + broadcastNoteList(); + return; + } addConnectionToNote(note.id(), conn); conn.send(serializeMessage(new Message(OP.NOTE).put("note", note))); sendAllAngularObjects(note, conn); @@ -391,7 +431,8 @@ private void sendHomeNote(NotebookSocket conn, Notebook notebook) throws IOExcep } } - private void updateNote(WebSocket conn, Notebook notebook, Message fromMessage) + private void updateNote(WebSocket conn, HashSet userAndRoles, + Notebook notebook, Message fromMessage) throws SchedulerException, IOException { String noteId = (String) fromMessage.get("id"); String name = (String) fromMessage.get("name"); @@ -433,7 +474,8 @@ private boolean isCronUpdated(Map configA, return cronUpdated; } - private void createNote(NotebookSocket conn, Notebook notebook, Message message) + private void createNote(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook, Message message) throws IOException { Note note = notebook.createNote(); note.addParagraph(); // it's an empty note. so add one paragraph @@ -451,7 +493,8 @@ private void createNote(NotebookSocket conn, Notebook notebook, Message message) broadcastNoteList(); } - private void removeNote(WebSocket conn, Notebook notebook, Message fromMessage) + private void removeNote(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook, Message fromMessage) throws IOException { String noteId = (String) fromMessage.get("id"); if (noteId == null) { @@ -459,13 +502,19 @@ private void removeNote(WebSocket conn, Notebook notebook, Message fromMessage) } Note note = notebook.getNote(noteId); + + if (!note.isOwner(userAndRoles)) { + permissionError(conn, "remove", userAndRoles, note.getOwners()); + return; + } + notebook.removeNote(noteId); removeNote(noteId); broadcastNoteList(); } - private void updateParagraph(NotebookSocket conn, Notebook notebook, - Message fromMessage) throws IOException { + private void updateParagraph(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook, Message fromMessage) throws IOException { String paragraphId = (String) fromMessage.get("id"); if (paragraphId == null) { return; @@ -476,6 +525,12 @@ private void updateParagraph(NotebookSocket conn, Notebook notebook, Map config = (Map) fromMessage .get("config"); final Note note = notebook.getNote(getOpenNoteId(conn)); + + if (!note.isWriter(userAndRoles)) { + permissionError(conn, "write", userAndRoles, note.getWriters()); + return; + } + Paragraph p = note.getParagraph(paragraphId); p.settings.setParams(params); p.setConfig(config); @@ -485,7 +540,8 @@ private void updateParagraph(NotebookSocket conn, Notebook notebook, broadcast(note.id(), new Message(OP.PARAGRAPH).put("paragraph", p)); } - private void cloneNote(NotebookSocket conn, Notebook notebook, Message fromMessage) + private void cloneNote(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook, Message fromMessage) throws IOException, CloneNotSupportedException { String noteId = getOpenNoteId(conn); String name = (String) fromMessage.get("name"); @@ -495,7 +551,8 @@ private void cloneNote(NotebookSocket conn, Notebook notebook, Message fromMessa broadcastNoteList(); } - protected Note importNote(NotebookSocket conn, Notebook notebook, Message fromMessage) + protected Note importNote(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook, Message fromMessage) throws IOException { Note note = null; if (fromMessage != null) { @@ -509,14 +566,20 @@ protected Note importNote(NotebookSocket conn, Notebook notebook, Message fromMe return note; } - private void removeParagraph(NotebookSocket conn, Notebook notebook, - Message fromMessage) throws IOException { + private void removeParagraph(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook, Message fromMessage) throws IOException { final String paragraphId = (String) fromMessage.get("id"); if (paragraphId == null) { return; } final Note note = notebook.getNote(getOpenNoteId(conn)); + + if (!note.isWriter(userAndRoles)) { + permissionError(conn, "write", userAndRoles, note.getWriters()); + return; + } + /** We dont want to remove the last paragraph */ if (!note.isLastParagraph(paragraphId)) { note.removeParagraph(paragraphId); @@ -525,19 +588,25 @@ private void removeParagraph(NotebookSocket conn, Notebook notebook, } } - private void clearParagraphOutput(NotebookSocket conn, Notebook notebook, - Message fromMessage) throws IOException { + private void clearParagraphOutput(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook, Message fromMessage) throws IOException { final String paragraphId = (String) fromMessage.get("id"); if (paragraphId == null) { return; } final Note note = notebook.getNote(getOpenNoteId(conn)); + + if (!note.isWriter(userAndRoles)) { + permissionError(conn, "write", userAndRoles, note.getWriters()); + return; + } + note.clearParagraphOutput(paragraphId); broadcastNote(note); } - private void completion(NotebookSocket conn, Notebook notebook, + private void completion(NotebookSocket conn, HashSet userAndRoles, Notebook notebook, Message fromMessage) throws IOException { String paragraphId = (String) fromMessage.get("id"); String buffer = (String) fromMessage.get("buf"); @@ -561,8 +630,8 @@ private void completion(NotebookSocket conn, Notebook notebook, * @param notebook the notebook. * @param fromMessage the message. */ - private void angularObjectUpdated(NotebookSocket conn, Notebook notebook, - Message fromMessage) { + private void angularObjectUpdated(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook, Message fromMessage) { String noteId = (String) fromMessage.get("noteId"); String paragraphId = (String) fromMessage.get("paragraphId"); String interpreterGroupId = (String) fromMessage.get("interpreterGroupId"); @@ -644,7 +713,7 @@ private void angularObjectUpdated(NotebookSocket conn, Notebook notebook, } } - private void moveParagraph(NotebookSocket conn, Notebook notebook, + private void moveParagraph(NotebookSocket conn, HashSet userAndRoles, Notebook notebook, Message fromMessage) throws IOException { final String paragraphId = (String) fromMessage.get("id"); if (paragraphId == null) { @@ -654,22 +723,34 @@ private void moveParagraph(NotebookSocket conn, Notebook notebook, final int newIndex = (int) Double.parseDouble(fromMessage.get("index") .toString()); final Note note = notebook.getNote(getOpenNoteId(conn)); + + if (!note.isWriter(userAndRoles)) { + permissionError(conn, "write", userAndRoles, note.getWriters()); + return; + } + note.moveParagraph(paragraphId, newIndex); note.persist(); broadcastNote(note); } - private void insertParagraph(NotebookSocket conn, Notebook notebook, - Message fromMessage) throws IOException { + private void insertParagraph(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook, Message fromMessage) throws IOException { final int index = (int) Double.parseDouble(fromMessage.get("index") .toString()); final Note note = notebook.getNote(getOpenNoteId(conn)); + + if (!note.isWriter(userAndRoles)) { + permissionError(conn, "write", userAndRoles, note.getWriters()); + return; + } + note.insertParagraph(index); note.persist(); broadcastNote(note); } - private void cancelParagraph(NotebookSocket conn, Notebook notebook, + private void cancelParagraph(NotebookSocket conn, HashSet userAndRoles, Notebook notebook, Message fromMessage) throws IOException { final String paragraphId = (String) fromMessage.get("id"); if (paragraphId == null) { @@ -677,11 +758,17 @@ private void cancelParagraph(NotebookSocket conn, Notebook notebook, } final Note note = notebook.getNote(getOpenNoteId(conn)); + + if (!note.isWriter(userAndRoles)) { + permissionError(conn, "write", userAndRoles, note.getWriters()); + return; + } + Paragraph p = note.getParagraph(paragraphId); p.abort(); } - private void runParagraph(NotebookSocket conn, Notebook notebook, + private void runParagraph(NotebookSocket conn, HashSet userAndRoles, Notebook notebook, Message fromMessage) throws IOException { final String paragraphId = (String) fromMessage.get("id"); if (paragraphId == null) { @@ -689,6 +776,12 @@ private void runParagraph(NotebookSocket conn, Notebook notebook, } final Note note = notebook.getNote(getOpenNoteId(conn)); + + if (!note.isWriter(userAndRoles)) { + permissionError(conn, "write", userAndRoles, note.getWriters()); + return; + } + Paragraph p = note.getParagraph(paragraphId); String text = (String) fromMessage.get("paragraph"); p.setText(text); @@ -729,8 +822,8 @@ private void runParagraph(NotebookSocket conn, Notebook notebook, } } - private void sendAllConfigurations(NotebookSocket conn, Notebook notebook) - throws IOException { + private void sendAllConfigurations(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook) throws IOException { ZeppelinConfiguration conf = notebook.getConf(); Map configurations = conf.dumpConfigurations(conf, 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 1d06e3a5ebf..e7e39f223a6 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 @@ -23,6 +23,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.HashSet; /** * Tools for securing Zeppelin @@ -52,6 +54,7 @@ public static Boolean isValidOrigin(String sourceHost, ZeppelinConfiguration con */ public static String getPrincipal() { Subject subject = org.apache.shiro.SecurityUtils.getSubject(); + String principal; if (subject.isAuthenticated()) { principal = subject.getPrincipal().toString(); @@ -61,4 +64,24 @@ public static String getPrincipal() { } return principal; } + + /** + * Return the roles associated with the authenticated user if any otherwise returns empty set + * TODO(prasadwagle) Find correct way to get user roles (see SHIRO-492) + * @return shiro roles + */ + public static HashSet getRoles() { + Subject subject = org.apache.shiro.SecurityUtils.getSubject(); + HashSet roles = new HashSet<>(); + + if (subject.isAuthenticated()) { + for (String role : Arrays.asList("role1", "role2", "role3")) { + if (subject.hasRole(role)) { + roles.add(role); + } + } + } + return roles; + } + } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java index 7d8f3cf710f..774f30ac108 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java @@ -140,7 +140,7 @@ public void testImportNotebook() throws IOException { Message messageReceived = notebookServer.deserializeMessage(msg); Note note = null; try { - note = notebookServer.importNote(null, notebook, messageReceived); + note = notebookServer.importNote(null, null, notebook, messageReceived); } catch (NullPointerException e) { //broadcastNoteList(); failed nothing to worry. LOG.error("Exception in NotebookServerTest while testImportNotebook, failed nothing to " + diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index f69a0873180..66e497b2523 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -155,6 +155,11 @@

    tooltip-placement="bottom" tooltip="Interpreter binding"> + + + + + +
    diff --git a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js index 800d450d50d..f40c47f3079 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js +++ b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js @@ -30,7 +30,8 @@ angular.module('zeppelinWebApp').factory('websocketEvents', function($rootScope, websocketCalls.sendNewEvent = function(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); + data.roles = $rootScope.ticket.roles; + console.log('Send >> %o, %o, %o, %o, %o', data.op, data.principal, data.ticket, data.roles, data); websocketCalls.ws.send(JSON.stringify(data)); }; @@ -52,12 +53,14 @@ angular.module('zeppelinWebApp').factory('websocketEvents', function($rootScope, $location.path('notebook/' + data.note.id); } else if (op === 'NOTES_INFO') { $rootScope.$broadcast('setNoteMenu', data.notes); + } else if (op === 'AUTH_INFO') { + alert(data.info.toString()); } else if (op === 'PARAGRAPH') { $rootScope.$broadcast('updateParagraph', data); } else if (op === 'PARAGRAPH_APPEND_OUTPUT') { $rootScope.$broadcast('appendParagraphOutput', data); } else if (op === 'PARAGRAPH_UPDATE_OUTPUT') { - $rootScope.$broadcast('updateParagraphOutput', data); + $rootScope.$broadcast('updateParagraphOutput', data); } else if (op === 'PROGRESS') { $rootScope.$broadcast('updateProgress', data); } else if (op === 'COMPLETION_LIST') { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java index b0470c82d5d..3765b3266e0 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java @@ -59,6 +59,9 @@ public class Note implements Serializable, JobListener { private String name = ""; private String id; + private HashSet owners = new HashSet(); + private HashSet readers = new HashSet(); + private HashSet writers = new HashSet(); @SuppressWarnings("rawtypes") Map> angularObjects = new HashMap<>(); @@ -115,6 +118,51 @@ public void setName(String name) { this.name = name; } + public HashSet getOwners() { + return (new HashSet(owners)); + } + + public void setOwners(HashSet owners) { + this.owners = new HashSet(owners); + } + + public HashSet getReaders() { + return (new HashSet(readers)); + } + + public void setReaders(HashSet readers) { + this.readers = new HashSet(readers); + } + + public HashSet getWriters() { + return (new HashSet(writers)); + } + + public void setWriters(HashSet writers) { + this.writers = new HashSet(writers); + } + + public boolean isOwner(HashSet entities) { + return isMember(entities, this.owners); + } + + public boolean isWriter(HashSet entities) { + return isMember(entities, this.writers) || isMember(entities, this.owners); + } + + public boolean isReader(HashSet entities) { + return isMember(entities, this.readers) || + isMember(entities, this.owners) || + isMember(entities, this.writers); + } + + // return true if b is empty or if (a intersection b) is non-empty + private boolean isMember(HashSet a, HashSet b) { + Set intersection = new HashSet(b); + intersection.retainAll(a); + return (b.isEmpty() || (intersection.size() > 0)); + } + public NoteInterpreterLoader getNoteReplLoader() { return replLoader; } diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java index d96f7a90a0e..0b5af2ca3fb 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java @@ -27,10 +27,12 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.HashSet; import org.apache.commons.io.FileUtils; import org.apache.zeppelin.conf.ZeppelinConfiguration; @@ -425,6 +427,31 @@ public void testAngularObjectRemovalOnInterpreterRestart() throws InterruptedExc notebook.removeNote(note.id()); } + @Test + public void testPermissions() throws IOException { + // create a note and a paragraph + Note note = notebook.createNote(); + // empty owners, readers and writers means note is public + assertEquals(note.isOwner(new HashSet(Arrays.asList("user2"))), true); + assertEquals(note.isReader(new HashSet(Arrays.asList("user2"))), true); + assertEquals(note.isWriter(new HashSet(Arrays.asList("user2"))), true); + + note.setOwners(new HashSet(Arrays.asList("user1"))); + note.setReaders(new HashSet(Arrays.asList("user1", "user2"))); + note.setWriters(new HashSet(Arrays.asList("user1"))); + + assertEquals(note.isOwner(new HashSet(Arrays.asList("user2"))), false); + assertEquals(note.isOwner(new HashSet(Arrays.asList("user1"))), true); + + assertEquals(note.isReader(new HashSet(Arrays.asList("user3"))), false); + assertEquals(note.isReader(new HashSet(Arrays.asList("user2"))), true); + + assertEquals(note.isWriter(new HashSet(Arrays.asList("user2"))), false); + assertEquals(note.isWriter(new HashSet(Arrays.asList("user1"))), true); + + notebook.removeNote(note.id()); + } + @Test public void testAbortParagraphStatusOnInterpreterRestart() throws InterruptedException, IOException {