From 6e85730343399c174090d2cf329bc3407d12e142 Mon Sep 17 00:00:00 2001 From: Prasad Wagle Date: Sun, 31 Jan 2016 22:30:51 -0800 Subject: [PATCH 01/14] Notebook Authorization --- .../apache/zeppelin/rest/NotebookRestApi.java | 44 +++++++++ .../org/apache/zeppelin/socket/Message.java | 1 + .../zeppelin/socket/NotebookServer.java | 95 +++++++++++++++++++ .../src/app/notebook/notebook-actionBar.html | 5 + .../src/app/notebook/notebook.controller.js | 72 ++++++++++++++ zeppelin-web/src/app/notebook/notebook.html | 23 +++++ .../websocketEvents.factory.js | 4 +- .../org/apache/zeppelin/notebook/Note.java | 48 ++++++++++ 8 files changed, 291 insertions(+), 1 deletion(-) 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..19de0790d84 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; @@ -75,6 +77,48 @@ 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(); + } + + /** + * 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); + LOG.debug("Set permissions {} {} {}", permMap.get("owners"), + permMap.get("readers"), + permMap.get("writers")); + // TODO(prasadwagle) Authenticate and check authorization +// if (!note.isOwner(userAndGroups())) { +// return new JsonResponse<>(Status.FORBIDDEN, ownerPermissionError(userAndGroups, +// note)).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/socket/Message.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/Message.java index 2a8e06d7687..4f6bf842527 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 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 9a4a378aa99..80c8e86a51d 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 @@ -358,8 +358,32 @@ public void broadcastReloadedNoteList() { broadcastAll(new Message(OP.NOTES_INFO).put("notes", notesInfo)); } + void readPermissionEror(NotebookSocket conn, String principal, Note note) throws IOException { + LOG.info("Cannot read. Connection readers {}. Allowed readers {}", + principal, note.getReaders()); + conn.send(serializeMessage(new Message(OP.AUTH_INFO).put("info", + "Insufficient privileges to read note.\n" + + "Allowed readers: " + note.getReaders().toString() + "\n" + + "User belongs to: " + principal))); + } + + void writePermissionError(NotebookSocket conn, String principal, Note note) throws IOException { + LOG.info("Cannot write. Connection writers {}. Allowed writers {}", + principal, note.getWriters()); + conn.send(serializeMessage(new Message(OP.AUTH_INFO).put("info", + "Insufficient privileges to write note.\n" + + "Allowed writers: " + note.getWriters().toString() + "\n" + + "User belongs to: " + principal))); + } + private void sendNote(NotebookSocket conn, 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,6 +391,14 @@ private void sendNote(NotebookSocket conn, Notebook notebook, Note note = notebook.getNote(noteId); if (note != null) { + HashSet usersAndGroups = new HashSet(); + usersAndGroups.add(fromMessage.principal); + // TODO(prasadwagle) add groups to usersAndGroups + if (!note.isReader(usersAndGroups)) { + readPermissionEror(conn, fromMessage.principal, note); + broadcastNoteList(); + return; + } addConnectionToNote(note.id(), conn); conn.send(serializeMessage(new Message(OP.NOTE).put("note", note))); sendAllAngularObjects(note, conn); @@ -476,6 +508,15 @@ private void updateParagraph(NotebookSocket conn, Notebook notebook, Map config = (Map) fromMessage .get("config"); final Note note = notebook.getNote(getOpenNoteId(conn)); + + HashSet usersAndGroups = new HashSet(); + usersAndGroups.add(fromMessage.principal); + // TODO(prasadwagle) add groups to usersAndGroups + if (!note.isWriter(usersAndGroups)) { + writePermissionError(conn, fromMessage.principal, note); + return; + } + Paragraph p = note.getParagraph(paragraphId); p.settings.setParams(params); p.setConfig(config); @@ -517,6 +558,15 @@ private void removeParagraph(NotebookSocket conn, Notebook notebook, } final Note note = notebook.getNote(getOpenNoteId(conn)); + + HashSet usersAndGroups = new HashSet(); + usersAndGroups.add(fromMessage.principal); + // TODO(prasadwagle) add groups to usersAndGroups + if (!note.isWriter(usersAndGroups)) { + writePermissionError(conn, fromMessage.principal, note); + return; + } + /** We dont want to remove the last paragraph */ if (!note.isLastParagraph(paragraphId)) { note.removeParagraph(paragraphId); @@ -533,6 +583,15 @@ private void clearParagraphOutput(NotebookSocket conn, Notebook notebook, } final Note note = notebook.getNote(getOpenNoteId(conn)); + + HashSet usersAndGroups = new HashSet(); + usersAndGroups.add(fromMessage.principal); + // TODO(prasadwagle) add groups to usersAndGroups + if (!note.isWriter(usersAndGroups)) { + writePermissionError(conn, fromMessage.principal, note); + return; + } + note.clearParagraphOutput(paragraphId); broadcastNote(note); } @@ -654,6 +713,15 @@ private void moveParagraph(NotebookSocket conn, Notebook notebook, final int newIndex = (int) Double.parseDouble(fromMessage.get("index") .toString()); final Note note = notebook.getNote(getOpenNoteId(conn)); + + HashSet usersAndGroups = new HashSet(); + usersAndGroups.add(fromMessage.principal); + // TODO(prasadwagle) add groups to usersAndGroups + if (!note.isWriter(usersAndGroups)) { + writePermissionError(conn, fromMessage.principal, note); + return; + } + note.moveParagraph(paragraphId, newIndex); note.persist(); broadcastNote(note); @@ -664,6 +732,15 @@ private void insertParagraph(NotebookSocket conn, Notebook notebook, final int index = (int) Double.parseDouble(fromMessage.get("index") .toString()); final Note note = notebook.getNote(getOpenNoteId(conn)); + + HashSet usersAndGroups = new HashSet(); + usersAndGroups.add(fromMessage.principal); + // TODO(prasadwagle) add groups to usersAndGroups + if (!note.isWriter(usersAndGroups)) { + writePermissionError(conn, fromMessage.principal, note); + return; + } + note.insertParagraph(index); note.persist(); broadcastNote(note); @@ -677,6 +754,15 @@ private void cancelParagraph(NotebookSocket conn, Notebook notebook, } final Note note = notebook.getNote(getOpenNoteId(conn)); + + HashSet usersAndGroups = new HashSet(); + usersAndGroups.add(fromMessage.principal); + // TODO(prasadwagle) add groups to usersAndGroups + if (!note.isWriter(usersAndGroups)) { + writePermissionError(conn, fromMessage.principal, note); + return; + } + Paragraph p = note.getParagraph(paragraphId); p.abort(); } @@ -689,6 +775,15 @@ private void runParagraph(NotebookSocket conn, Notebook notebook, } final Note note = notebook.getNote(getOpenNoteId(conn)); + + HashSet usersAndGroups = new HashSet(); + usersAndGroups.add(fromMessage.principal); + // TODO(prasadwagle) add groups to usersAndGroups + if (!note.isWriter(usersAndGroups)) { + writePermissionError(conn, fromMessage.principal, note); + return; + } + Paragraph p = note.getParagraph(paragraphId); String text = (String) fromMessage.get("paragraph"); p.setText(text); diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index 0340e28d689..0b4a1be5d30 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -129,6 +129,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..39d785cb985 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js +++ b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js @@ -52,12 +52,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 27e2f77cee2..abab2ae8d27 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 @@ -58,6 +58,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<>(); @@ -114,6 +117,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; } From a8d0ecbb8f973a10a60e17f0b27b2c63c6623e78 Mon Sep 17 00:00:00 2001 From: Prasad Wagle Date: Thu, 4 Feb 2016 17:57:19 -0800 Subject: [PATCH 02/14] Add security documentation --- docs/security/authentication.md | 31 ++++++++++ docs/security/interpreter_authorization.md | 28 +++++++++ docs/security/notebook_authorization.md | 66 ++++++++++++++++++++++ docs/security/overview.md | 29 ++++++++++ 4 files changed, 154 insertions(+) create mode 100644 docs/security/authentication.md create mode 100644 docs/security/interpreter_authorization.md create mode 100644 docs/security/notebook_authorization.md create mode 100644 docs/security/overview.md diff --git a/docs/security/authentication.md b/docs/security/authentication.md new file mode 100644 index 00000000000..b71d28ad2a4 --- /dev/null +++ b/docs/security/authentication.md @@ -0,0 +1,31 @@ +--- +layout: page +title: "Authentication" +description: "Authentication" +group: security +--- + +# Zeppelin Authentication + +Authentication is company-specific. + +One option is to use [Basic Access Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) + +Anoteher 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..371ba4a35d2 --- /dev/null +++ b/docs/security/interpreter_authorization.md @@ -0,0 +1,28 @@ +--- +layout: page +title: "Notebook Authorization" +description: "Notebook Authorization" +group: security +--- + +# Zeppelin Interpreter Authorization + +The Interpreter authorization problem is more complex. Different interpreters require different strategies. + +For the Hive interpreter, we need to maintain per-user connection pools. +The interpreter 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 would 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..6bbaf45218f --- /dev/null +++ b/docs/security/notebook_authorization.md @@ -0,0 +1,66 @@ +--- +layout: page +title: "Notebook Authorization" +description: "Notebook Authorization" +group: security +--- + +# Zeppelin Notebook Authorization + +## Overview +There are different aspects to Zeppelin security: + +* Authentication: is the user who they say they are? +* Notebook authorization: does the user have permissions to read or write to a note? +* Interpreter authorization: does the user have permissions to perform interpreter operations e.g. access data source objects? + + +## Authentication + +Authentication is company-specific. One 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. + + +## 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. + +## Interpreter Authorization +The Interpreter authorization problem is more complex. Different interpreters require different strategies. + +For the Hive interpreter, we need to maintain per-user connection pools. +The interpreter 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 would have to store password information for users. diff --git a/docs/security/overview.md b/docs/security/overview.md new file mode 100644 index 00000000000..7c2631144e3 --- /dev/null +++ b/docs/security/overview.md @@ -0,0 +1,29 @@ +--- +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](overview.html) +* Notebook authorization: does the user have permissions to read or write to a note? [More](notebook_authorization.html) +* Interpreter authorization: does the user have permissions to perform interpreter operations e.g. access data source objects? [More](interpreter_authorization.html) + From 6b9e27447ddc2b7f58be9bad16cf527521405ce1 Mon Sep 17 00:00:00 2001 From: Prasad Wagle Date: Thu, 4 Feb 2016 18:01:53 -0800 Subject: [PATCH 03/14] Add unit test for note permissions --- .../zeppelin/notebook/NotebookTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 506b682b293..ba9175d69be 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; @@ -362,6 +364,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 { From 06c5e07c23cdc5f2e95dd8368faf5780f0f0665a Mon Sep 17 00:00:00 2001 From: Prasad Wagle Date: Thu, 4 Feb 2016 18:02:47 -0800 Subject: [PATCH 04/14] Update navigation.html for security docs --- docs/_includes/themes/zeppelin/_navigation.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/_includes/themes/zeppelin/_navigation.html b/docs/_includes/themes/zeppelin/_navigation.html index d0581b14549..00c6938e457 100644 --- a/docs/_includes/themes/zeppelin/_navigation.html +++ b/docs/_includes/themes/zeppelin/_navigation.html @@ -55,6 +55,15 @@
  • Tajo
  • +
  • + Security + +
  • Display System