Repository: zeppelin Updated Branches: refs/heads/master 2a3791020 -> e47b30a88
[ZEPPELIN-2848] Added new type of user to only run notebook ### What is this PR for? The idea of this PR is to provide a new kind of user : Runner. Basically, what it does is that it just removes write authorization and allow user to read and run note. ### What type of PR is it? [Feature] ### Todos * [ ] - Task ### What is the Jira issue? [ZEPPELIN-2848] https://issues.apache.org/jira/browse/ZEPPELIN-2848 ### How should this be tested? - Log in as admin - Create new notebook and create a paragraph with the interpreter you want - Assign runner right to user1 - Log in as user1 - Try to run the paragraph (should work) - Try to modify the paragraph (should fail) - Log in as user2 - Try to run the paragraph (should fail) ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? Yes * Does this needs documentation? No Author: Paolo Genissel <paolo.genissel-monsall...@1000mercis.com> Author: gfalcone <paologenis...@gmail.com> Author: Paolo Genissel <paologenis...@gmail.com> Closes #2526 from gfalcone/new_type_runner and squashes the following commits: 96bba66 [gfalcone] Fix typo on notebook_authorization.md 8ab4512 [gfalcone] Update notebook_authorization.md 22a1eb3 [Paolo Genissel] Fixed typo d621792 [Paolo Genissel] Fix NotebookSecurityRestApiTest a67af0f [Paolo Genissel] Fix test 5c43ca9 [Paolo Genissel] Added new type of user Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/e47b30a8 Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/e47b30a8 Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/e47b30a8 Branch: refs/heads/master Commit: e47b30a88f09434cd266946d3858cf34275be6a6 Parents: 2a37910 Author: Paolo Genissel <paolo.genissel-monsall...@1000mercis.com> Authored: Sun Aug 20 09:28:46 2017 +0200 Committer: Felix Cheung <felixche...@apache.org> Committed: Tue Aug 29 10:05:55 2017 -0700 ---------------------------------------------------------------------- docs/setup/security/notebook_authorization.md | 10 +++- docs/usage/rest_api/notebook.md | 6 ++ .../apache/zeppelin/rest/NotebookRestApi.java | 58 ++++++++++++++---- .../apache/zeppelin/socket/NotebookServer.java | 27 +++++++-- .../zeppelin/integration/AuthenticationIT.java | 2 + .../rest/NotebookSecurityRestApiTest.java | 11 ++-- .../src/app/notebook/notebook.controller.js | 6 +- zeppelin-web/src/app/notebook/notebook.css | 5 ++ zeppelin-web/src/app/notebook/notebook.html | 10 +++- .../notebook/NotebookAuthorization.java | 62 +++++++++++++++++++- .../notebook/repo/NotebookRepoSync.java | 4 ++ .../apache/zeppelin/notebook/NotebookTest.java | 33 +++++++++-- .../notebook/repo/NotebookRepoSyncTest.java | 5 ++ 13 files changed, 203 insertions(+), 36 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/docs/setup/security/notebook_authorization.md ---------------------------------------------------------------------- diff --git a/docs/setup/security/notebook_authorization.md b/docs/setup/security/notebook_authorization.md index 0d55104..fe0e27a 100644 --- a/docs/setup/security/notebook_authorization.md +++ b/docs/setup/security/notebook_authorization.md @@ -36,13 +36,17 @@ As you can see, each Zeppelin notebooks has 3 entities : * Owners ( users or groups ) * Readers ( users or groups ) * Writers ( users or groups ) +* Runners ( users or groups ) <center><img src="{{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/permission_setting.png"></center> Fill out the each forms with comma seperated **users** and **groups** configured in `conf/shiro.ini` file. If the form is empty (*), it means that any users can perform that operation. -If someone who doesn't have **read** permission is trying to access the notebook or someone who doesn't have **write** permission is trying to edit the notebook, Zeppelin will ask to login or block the user. +If someone who doesn't have **read** permission is trying to access the notebook or someone who doesn't have **write** permission is trying to edit the notebook, +or someone who doesn't have **run** permission is trying to run a paragraph Zeppelin will ask to login or block the user. + +By default, owners and writers have **write** permission, owners, writers and runners have **run** permission, owners, writers, runners and readers have **read** permission <center><img src="{{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/insufficient_privileges.png"></center> @@ -63,13 +67,13 @@ or set `zeppelin.notebook.public` property to `false` in `conf/zeppelin-site.xml </property> ``` -Behind the scenes, when you create a new note only the `owners` field is filled with current user, leaving `readers` and `writers` fields empty. All the notes with at least one empty authorization field are considered to be in `public` workspace. Thus when setting `zeppelin.notebook.public` (or corresponding `ZEPPELIN_NOTEBOOK_PUBLIC`) to false, newly created notes have `readers` and `writers` fields filled with current user, making note appear as in `private` workspace. +Behind the scenes, when you create a new note only the `owners` field is filled with current user, leaving `readers`, `runners` and `writers` fields empty. All the notes with at least one empty authorization field are considered to be in `public` workspace. Thus when setting `zeppelin.notebook.public` (or corresponding `ZEPPELIN_NOTEBOOK_PUBLIC`) to false, newly created notes have `readers`, `runners`, `writers` fields filled with current user, making note appear as in `private` workspace. ## How it works In this section, we will explain the detail about how the notebook authorization works in backend side. ### NotebookServer -The [NotebookServer](https://github.com/apache/zeppelin/blob/master/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java) classifies every notebook operations into three categories: **Read**, **Write**, **Manage**. +The [NotebookServer](https://github.com/apache/zeppelin/blob/master/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java) classifies every notebook operations into three categories: **Read**, **Run**, **Write**, **Manage**. Before executing a notebook operation, it checks if the user and the groups associated with the `NotebookSocket` have permissions. For example, before executing a **Read** operation, it checks if the user and the groups have at least one entity that belongs to the **Reader** entities. http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/docs/usage/rest_api/notebook.md ---------------------------------------------------------------------- diff --git a/docs/usage/rest_api/notebook.md b/docs/usage/rest_api/notebook.md index dfb491a..ff93553 100644 --- a/docs/usage/rest_api/notebook.md +++ b/docs/usage/rest_api/notebook.md @@ -1215,6 +1215,9 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, "owners":[ "user1" ], + "runners":[ + "user2" + ], "writers":[ "user2" ] @@ -1259,6 +1262,9 @@ Notebooks REST API supports the following operations: List, Create, Get, Delete, "owners": [ "user2" ], + "runners":[ + "user2" + ], "writers": [ "user1" ] http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java ---------------------------------------------------------------------- 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 a343879..c170a09 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 @@ -98,6 +98,7 @@ public class NotebookRestApi { permissionsMap.put("owners", notebookAuthorization.getOwners(noteId)); permissionsMap.put("readers", notebookAuthorization.getReaders(noteId)); permissionsMap.put("writers", notebookAuthorization.getWriters(noteId)); + permissionsMap.put("runners", notebookAuthorization.getRunners(noteId)); return new JsonResponse<>(Status.OK, "", permissionsMap).build(); } @@ -165,6 +166,18 @@ public class NotebookRestApi { throw new ForbiddenException(errorMsg); } } + + /** + * Check if the current user can run the given note. + */ + private void checkIfUserCanRun(String noteId, String errorMsg) { + Set<String> userAndRoles = Sets.newHashSet(); + userAndRoles.add(SecurityUtils.getPrincipal()); + userAndRoles.addAll(SecurityUtils.getRoles()); + if (!notebookAuthorization.hasRunAuthorization(userAndRoles, noteId)) { + throw new ForbiddenException(errorMsg); + } + } private void checkIfNoteIsNotNull(Note note) { if (note == null) { @@ -199,15 +212,31 @@ public class NotebookRestApi { HashMap<String, HashSet<String>> permMap = gson.fromJson(req, new TypeToken<HashMap<String, HashSet<String>>>() {}.getType()); Note note = notebook.getNote(noteId); - - LOG.info("Set permissions {} {} {} {} {}", noteId, principal, permMap.get("owners"), - permMap.get("readers"), permMap.get("writers")); + + LOG.info("Set permissions {} {} {} {} {} {}", noteId, principal, permMap.get("owners"), + permMap.get("readers"), permMap.get("runners"), permMap.get("writers")); HashSet<String> readers = permMap.get("readers"); + HashSet<String> runners = permMap.get("runners"); HashSet<String> owners = permMap.get("owners"); HashSet<String> writers = permMap.get("writers"); - // Set readers, if writers and owners is empty -> set to user requesting the change + // Set readers, if runners, writers and owners is empty -> set to user requesting the change if (readers != null && !readers.isEmpty()) { + if (runners.isEmpty()) { + runners = Sets.newHashSet(SecurityUtils.getPrincipal()); + } + if (writers.isEmpty()) { + writers = Sets.newHashSet(SecurityUtils.getPrincipal()); + } + if (owners.isEmpty()) { + owners = Sets.newHashSet(SecurityUtils.getPrincipal()); + } + } + // Set runners, if writers and owners is empty -> set to user requesting the change + if (runners != null && !runners.isEmpty()) { + if (writers.isEmpty()) { + writers = Sets.newHashSet(SecurityUtils.getPrincipal()); + } if (owners.isEmpty()) { owners = Sets.newHashSet(SecurityUtils.getPrincipal()); } @@ -220,10 +249,12 @@ public class NotebookRestApi { } notebookAuthorization.setReaders(noteId, readers); + notebookAuthorization.setRunners(noteId, runners); notebookAuthorization.setWriters(noteId, writers); notebookAuthorization.setOwners(noteId, owners); - LOG.debug("After set permissions {} {} {}", notebookAuthorization.getOwners(noteId), - notebookAuthorization.getReaders(noteId), notebookAuthorization.getWriters(noteId)); + LOG.debug("After set permissions {} {} {} {}", notebookAuthorization.getOwners(noteId), + notebookAuthorization.getReaders(noteId), notebookAuthorization.getRunners(noteId), + notebookAuthorization.getWriters(noteId)); AuthenticationInfo subject = new AuthenticationInfo(SecurityUtils.getPrincipal()); note.persist(subject); notebookServer.broadcastNote(note); @@ -589,7 +620,7 @@ public class NotebookRestApi { Note note = notebook.getNote(noteId); AuthenticationInfo subject = new AuthenticationInfo(SecurityUtils.getPrincipal()); checkIfNoteIsNotNull(note); - checkIfUserCanWrite(noteId, "Insufficient privileges you cannot run job for this note"); + checkIfUserCanRun(noteId, "Insufficient privileges you cannot run job for this note"); try { note.runAll(subject); @@ -616,7 +647,7 @@ public class NotebookRestApi { LOG.info("stop note jobs {} ", noteId); Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); - checkIfUserCanWrite(noteId, "Insufficient privileges you cannot stop this job for this note"); + checkIfUserCanRun(noteId, "Insufficient privileges you cannot stop this job for this note"); for (Paragraph p : note.getParagraphs()) { if (!p.isTerminated()) { @@ -690,7 +721,7 @@ public class NotebookRestApi { Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); - checkIfUserCanWrite(noteId, "Insufficient privileges you cannot run job for this note"); + checkIfUserCanRun(noteId, "Insufficient privileges you cannot run job for this note"); Paragraph paragraph = note.getParagraph(paragraphId); checkIfParagraphIsNotNull(paragraph); @@ -728,7 +759,7 @@ public class NotebookRestApi { Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); - checkIfUserCanWrite(noteId, "Insufficient privileges you cannot run paragraph"); + checkIfUserCanRun(noteId, "Insufficient privileges you cannot run paragraph"); Paragraph paragraph = note.getParagraph(paragraphId); checkIfParagraphIsNotNull(paragraph); @@ -766,7 +797,7 @@ public class NotebookRestApi { LOG.info("stop paragraph job {} ", noteId); Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); - checkIfUserCanWrite(noteId, "Insufficient privileges you cannot stop paragraph"); + checkIfUserCanRun(noteId, "Insufficient privileges you cannot stop paragraph"); Paragraph p = note.getParagraph(paragraphId); checkIfParagraphIsNotNull(p); p.abort(); @@ -791,7 +822,7 @@ public class NotebookRestApi { Note note = notebook.getNote(noteId); checkIfNoteIsNotNull(note); - checkIfUserCanWrite(noteId, "Insufficient privileges you cannot set a cron job for this note"); + checkIfUserCanRun(noteId, "Insufficient privileges you cannot set a cron job for this note"); if (!CronExpression.isValidExpression(request.getCronString())) { return new JsonResponse<>(Status.BAD_REQUEST, "wrong cron expressions.").build(); @@ -922,7 +953,8 @@ public class NotebookRestApi { String noteId = Id[0]; if (!notebookAuthorization.isOwner(noteId, userAndRoles) && !notebookAuthorization.isReader(noteId, userAndRoles) && - !notebookAuthorization.isWriter(noteId, userAndRoles)) { + !notebookAuthorization.isWriter(noteId, userAndRoles) && + !notebookAuthorization.isRunner(noteId, userAndRoles)) { notesFound.remove(i); i--; } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java index 3ddeec0..f0e0bb2 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 @@ -752,6 +752,25 @@ public class NotebookServer extends WebSocketServlet } /** + * @return false if user doesn't have runner permission for this paragraph + */ + private boolean hasParagraphRunnerPermission(NotebookSocket conn, + Notebook notebook, String noteId, + HashSet<String> userAndRoles, + String principal, String op) + throws IOException { + + NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization(); + if (!notebookAuthorization.isRunner(noteId, userAndRoles)) { + permissionError(conn, op, principal, userAndRoles, + notebookAuthorization.getOwners(noteId)); + return false; + } + + return true; + } + + /** * @return false if user doesn't have writer permission for this paragraph */ private boolean hasParagraphWriterPermission(NotebookSocket conn, @@ -1621,7 +1640,7 @@ public class NotebookServer extends WebSocketServlet String noteId = getOpenNoteId(conn); - if (!hasParagraphWriterPermission(conn, notebook, noteId, + if (!hasParagraphRunnerPermission(conn, notebook, noteId, userAndRoles, fromMessage.principal, "write")) { return; } @@ -1639,7 +1658,7 @@ public class NotebookServer extends WebSocketServlet return; } - if (!hasParagraphWriterPermission(conn, notebook, noteId, + if (!hasParagraphRunnerPermission(conn, notebook, noteId, userAndRoles, fromMessage.principal, "run all paragraphs")) { return; } @@ -1732,7 +1751,7 @@ public class NotebookServer extends WebSocketServlet String noteId = getOpenNoteId(conn); - if (!hasParagraphWriterPermission(conn, notebook, noteId, + if (!hasParagraphRunnerPermission(conn, notebook, noteId, userAndRoles, fromMessage.principal, "write")) { return; } @@ -2060,7 +2079,7 @@ public class NotebookServer extends WebSocketServlet Set<String> userAndRoles = Sets.newHashSet(); userAndRoles.add(SecurityUtils.getPrincipal()); userAndRoles.addAll(SecurityUtils.getRoles()); - if (!notebookIns.getNotebookAuthorization().hasWriteAuthorization(userAndRoles, noteId)) { + if (!notebookIns.getNotebookAuthorization().hasRunAuthorization(userAndRoles, noteId)) { throw new ForbiddenException(String.format("can't execute note %s", noteId)); } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java index 38fe574..7debf1b 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java @@ -258,6 +258,8 @@ public class AuthenticationIT extends AbstractZeppelinIT { MAX_BROWSER_TIMEOUT_SEC).sendKeys("finance "); pollingWait(By.xpath(".//*[@id='selectReaders']/following::span//input"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("finance "); + pollingWait(By.xpath(".//*[@id='selectRunners']/following::span//input"), + MAX_BROWSER_TIMEOUT_SEC).sendKeys("finance "); pollingWait(By.xpath(".//*[@id='selectWriters']/following::span//input"), MAX_BROWSER_TIMEOUT_SEC).sendKeys("finance "); pollingWait(By.xpath("//button[@ng-click='savePermissions()']"), MAX_BROWSER_TIMEOUT_SEC) http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookSecurityRestApiTest.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookSecurityRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookSecurityRestApiTest.java index 367a199..c3b0977 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookSecurityRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookSecurityRestApiTest.java @@ -81,7 +81,7 @@ public class NotebookSecurityRestApiTest extends AbstractTestRestApi { String noteId = createNoteForUser("test", "admin", "password1"); //set permission - String payload = "{ \"owners\": [\"admin\"], \"readers\": [\"user2\"], \"writers\": [\"user2\"] }"; + String payload = "{ \"owners\": [\"admin\"], \"readers\": [\"user2\"], \"runners\": [\"user2\"], \"writers\": [\"user2\"] }"; PutMethod put = httpPut("/notebook/" + noteId + "/permissions", payload , "admin", "password1"); assertThat("test set note permission method:", put, isAllowed()); put.releaseConnection(); @@ -98,7 +98,7 @@ public class NotebookSecurityRestApiTest extends AbstractTestRestApi { String noteId = createNoteForUser("test", "admin", "password1"); //set permission - String payload = "{ \"owners\": [\"admin\", \"user1\"], \"readers\": [\"user2\"], \"writers\": [\"user2\"] }"; + String payload = "{ \"owners\": [\"admin\", \"user1\"], \"readers\": [\"user2\"], \"runners\": [\"user2\"], \"writers\": [\"user2\"] }"; PutMethod put = httpPut("/notebook/" + noteId + "/permissions", payload , "admin", "password1"); assertThat("test set note permission method:", put, isAllowed()); put.releaseConnection(); @@ -180,7 +180,7 @@ public class NotebookSecurityRestApiTest extends AbstractTestRestApi { } private void setPermissionForNote(String noteId, String user, String pwd) throws IOException { - String payload = "{\"owners\":[\"" + user + "\"],\"readers\":[\"" + user + "\"],\"writers\":[\"" + user + "\"]}"; + String payload = "{\"owners\":[\"" + user + "\"],\"readers\":[\"" + user + "\"],\"runners\":[\"" + user + "\"],\"writers\":[\"" + user + "\"]}"; PutMethod put = httpPut(("/notebook/" + noteId + "/permissions"), payload, user, pwd); put.releaseConnection(); } @@ -206,10 +206,11 @@ public class NotebookSecurityRestApiTest extends AbstractTestRestApi { ArrayList owners = permissions.get("owners"); ArrayList readers = permissions.get("readers"); ArrayList writers = permissions.get("writers"); + ArrayList runners = permissions.get("runners"); - if (owners.size() != 0 && readers.size() != 0 && writers.size() != 0) { + if (owners.size() != 0 && readers.size() != 0 && writers.size() != 0 && runners.size() != 0) { assertEquals("User has permissions ", true, (owners.contains(user) || readers.contains(user) || - writers.contains(user))); + writers.contains(user) || runners.contains(user))); } getPermission.releaseConnection(); } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/zeppelin-web/src/app/notebook/notebook.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 4b8b23f..bdd47c2 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -696,6 +696,7 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, $scope.setIamOwner() angular.element('#selectOwners').select2(selectJson) angular.element('#selectReaders').select2(selectJson) + angular.element('#selectRunners').select2(selectJson) angular.element('#selectWriters').select2(selectJson) if (callback) { callback() @@ -735,6 +736,7 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, function convertPermissionsToArray () { $scope.permissions.owners = angular.element('#selectOwners').val() $scope.permissions.readers = angular.element('#selectReaders').val() + $scope.permissions.runners = angular.element('#selectRunners').val() $scope.permissions.writers = angular.element('#selectWriters').val() angular.element('.permissionsForm select').find('option:not([is-select2="false"])').remove() } @@ -1013,7 +1015,8 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, closable: true, title: 'Permissions Saved Successfully', message: 'Owners : ' + $scope.permissions.owners + '\n\n' + 'Readers : ' + - $scope.permissions.readers + '\n\n' + 'Writers : ' + $scope.permissions.writers + $scope.permissions.readers + '\n\n' + 'Runners : ' + $scope.permissions.runners + + '\n\n' + 'Writers : ' + $scope.permissions.writers }) $scope.showPermissions = false }) @@ -1058,6 +1061,7 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, $scope.closePermissions() angular.element('#selectOwners').select2({}) angular.element('#selectReaders').select2({}) + angular.element('#selectRunners').select2({}) angular.element('#selectWriters').select2({}) } else { $scope.openPermissions() http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/zeppelin-web/src/app/notebook/notebook.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/notebook.css b/zeppelin-web/src/app/notebook/notebook.css index a7b7a7f..3a09647 100644 --- a/zeppelin-web/src/app/notebook/notebook.css +++ b/zeppelin-web/src/app/notebook/notebook.css @@ -219,6 +219,11 @@ display: inline-block; } +.permissions .runners { + width:60px; + display: inline-block; +} + .permissions .writers { width:60px; display: inline-block; http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/zeppelin-web/src/app/notebook/notebook.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/notebook.html b/zeppelin-web/src/app/notebook/notebook.html index b96d08a..98b66fe 100644 --- a/zeppelin-web/src/app/notebook/notebook.html +++ b/zeppelin-web/src/app/notebook/notebook.html @@ -81,13 +81,19 @@ limitations under the License. <select id="selectOwners" multiple="multiple"> <option is-select2="false" ng-repeat="owner in permissions.owners" selected="selected">{{owner}}</option> </select> - Owners can change permissions,read and write the note. + Owners can change permissions,read, run and write the note. </p> <p><span class="writers">Writers </span> <select id="selectWriters" multiple="multiple"> <option is-select2="false" ng-repeat="writers in permissions.writers" selected="selected">{{writers}}</option> </select> - Writers can read and write the note. + Writers can read, run and write the note. + </p> + <p><span class="runners">Runners </span> + <select id="selectRunners" multiple="multiple"> + <option is-select2="false" ng-repeat="runners in permissions.runners" selected="selected">{{runners}}</option> + </select> + Runners can read and run the note. </p> <p><span class="readers">Readers </span> <select id="selectReaders" multiple="multiple"> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java ---------------------------------------------------------------------- diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java index 500f068..69ba891 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java @@ -52,7 +52,8 @@ public class NotebookAuthorization { private static final Logger LOG = LoggerFactory.getLogger(NotebookAuthorization.class); private static NotebookAuthorization instance = null; /* - * { "note1": { "owners": ["u1"], "readers": ["u1", "u2"], "writers": ["u1"] }, "note2": ... } } + * { "note1": { "owners": ["u1"], "readers": ["u1", "u2"], "runners": ["u2"], + * "writers": ["u1"] }, "note2": ... } } */ private static Map<String, Map<String, Set<String>>> authInfo = new HashMap<>(); /* @@ -177,6 +178,7 @@ public class NotebookAuthorization { noteAuthInfo = new LinkedHashMap(); noteAuthInfo.put("owners", new LinkedHashSet(entities)); noteAuthInfo.put("readers", new LinkedHashSet()); + noteAuthInfo.put("runners", new LinkedHashSet()); noteAuthInfo.put("writers", new LinkedHashSet()); } else { noteAuthInfo.put("owners", new LinkedHashSet(entities)); @@ -192,6 +194,7 @@ public class NotebookAuthorization { noteAuthInfo = new LinkedHashMap(); noteAuthInfo.put("owners", new LinkedHashSet()); noteAuthInfo.put("readers", new LinkedHashSet(entities)); + noteAuthInfo.put("runners", new LinkedHashSet()); noteAuthInfo.put("writers", new LinkedHashSet()); } else { noteAuthInfo.put("readers", new LinkedHashSet(entities)); @@ -200,6 +203,23 @@ public class NotebookAuthorization { saveToFile(); } + public void setRunners(String noteId, Set<String> entities) { + Map<String, Set<String>> noteAuthInfo = authInfo.get(noteId); + entities = validateUser(entities); + if (noteAuthInfo == null) { + noteAuthInfo = new LinkedHashMap(); + noteAuthInfo.put("owners", new LinkedHashSet()); + noteAuthInfo.put("readers", new LinkedHashSet()); + noteAuthInfo.put("runners", new LinkedHashSet(entities)); + noteAuthInfo.put("writers", new LinkedHashSet()); + } else { + noteAuthInfo.put("runners", new LinkedHashSet(entities)); + } + authInfo.put(noteId, noteAuthInfo); + saveToFile(); + } + + public void setWriters(String noteId, Set<String> entities) { Map<String, Set<String>> noteAuthInfo = authInfo.get(noteId); entities = validateUser(entities); @@ -207,6 +227,7 @@ public class NotebookAuthorization { noteAuthInfo = new LinkedHashMap(); noteAuthInfo.put("owners", new LinkedHashSet()); noteAuthInfo.put("readers", new LinkedHashSet()); + noteAuthInfo.put("runners", new LinkedHashSet()); noteAuthInfo.put("writers", new LinkedHashSet(entities)); } else { noteAuthInfo.put("writers", new LinkedHashSet(entities)); @@ -243,6 +264,20 @@ public class NotebookAuthorization { return entities; } + public Set<String> getRunners(String noteId) { + Map<String, Set<String>> noteAuthInfo = authInfo.get(noteId); + Set<String> entities = null; + if (noteAuthInfo == null) { + entities = new HashSet<>(); + } else { + entities = noteAuthInfo.get("runners"); + if (entities == null) { + entities = new HashSet<>(); + } + } + return entities; + } + public Set<String> getWriters(String noteId) { Map<String, Set<String>> noteAuthInfo = authInfo.get(noteId); Set<String> entities = null; @@ -268,7 +303,14 @@ public class NotebookAuthorization { public boolean isReader(String noteId, Set<String> entities) { return isMember(entities, getReaders(noteId)) || isMember(entities, getOwners(noteId)) || - isMember(entities, getWriters(noteId)); + isMember(entities, getWriters(noteId)) || + isMember(entities, getRunners(noteId)); + } + + public boolean isRunner(String noteId, Set<String> entities) { + return isMember(entities, getRunners(noteId)) || + isMember(entities, getWriters(noteId)) || + isMember(entities, getOwners(noteId)); } // return true if b is empty or if (a intersection b) is non-empty @@ -311,6 +353,17 @@ public class NotebookAuthorization { return isReader(noteId, userAndRoles); } + public boolean hasRunAuthorization(Set<String> userAndRoles, String noteId) { + if (conf.isAnonymousAllowed()) { + LOG.debug("Zeppelin runs in anonymous mode, everybody is runner"); + return true; + } + if (userAndRoles == null) { + return false; + } + return isRunner(noteId, userAndRoles); + } + public void removeNote(String noteId) { authInfo.remove(noteId); saveToFile(); @@ -337,13 +390,16 @@ public class NotebookAuthorization { owners.add(subject.getUser()); setOwners(noteId, owners); } else { - // add current user to owners, readers, writers - private note + // add current user to owners, readers, runners, writers - private note Set<String> entities = getOwners(noteId); entities.add(subject.getUser()); setOwners(noteId, entities); entities = getReaders(noteId); entities.add(subject.getUser()); setReaders(noteId, entities); + entities = getRunners(noteId); + entities.add(subject.getUser()); + setRunners(noteId, entities); entities = getWriters(noteId); entities.add(subject.getUser()); setWriters(noteId, entities); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java ---------------------------------------------------------------------- diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java index 28de7c8..30af83f 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java @@ -285,6 +285,7 @@ public class NotebookRepoSync implements NotebookRepo { NotebookAuthorization notebookAuthorization = NotebookAuthorization.getInstance(); return notebookAuthorization.getOwners(noteId).isEmpty() && notebookAuthorization.getReaders(noteId).isEmpty() + && notebookAuthorization.getRunners(noteId).isEmpty() && notebookAuthorization.getWriters(noteId).isEmpty(); } @@ -300,6 +301,9 @@ public class NotebookRepoSync implements NotebookRepo { users = notebookAuthorization.getReaders(noteId); users.add(subject.getUser()); notebookAuthorization.setReaders(noteId, users); + users = notebookAuthorization.getRunners(noteId); + users.add(subject.getUser()); + notebookAuthorization.setRunners(noteId, users); users = notebookAuthorization.getWriters(noteId); users.add(subject.getUser()); notebookAuthorization.setWriters(noteId, users); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java ---------------------------------------------------------------------- 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 e1a20b5..634ac30 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 @@ -721,6 +721,8 @@ public class NotebookTest implements JobListenerFactory{ new HashSet<>(Arrays.asList("user2"))), true); assertEquals(notebookAuthorization.isReader(note.getId(), new HashSet<>(Arrays.asList("user2"))), true); + assertEquals(notebookAuthorization.isRunner(note.getId(), + new HashSet<>(Arrays.asList("user2"))), true); assertEquals(notebookAuthorization.isWriter(note.getId(), new HashSet<>(Arrays.asList("user2"))), true); @@ -728,6 +730,8 @@ public class NotebookTest implements JobListenerFactory{ new HashSet<>(Arrays.asList("user1"))); notebookAuthorization.setReaders(note.getId(), new HashSet<>(Arrays.asList("user1", "user2"))); + notebookAuthorization.setRunners(note.getId(), + new HashSet<>(Arrays.asList("user3"))); notebookAuthorization.setWriters(note.getId(), new HashSet<>(Arrays.asList("user1"))); @@ -737,21 +741,26 @@ public class NotebookTest implements JobListenerFactory{ new HashSet<>(Arrays.asList("user1"))), true); assertEquals(notebookAuthorization.isReader(note.getId(), - new HashSet<>(Arrays.asList("user3"))), false); + new HashSet<>(Arrays.asList("user4"))), false); assertEquals(notebookAuthorization.isReader(note.getId(), new HashSet<>(Arrays.asList("user2"))), true); + assertEquals(notebookAuthorization.isRunner(note.getId(), + new HashSet<>(Arrays.asList("user3"))), true); + assertEquals(notebookAuthorization.isRunner(note.getId(), + new HashSet<>(Arrays.asList("user2"))), false); + assertEquals(notebookAuthorization.isWriter(note.getId(), new HashSet<>(Arrays.asList("user2"))), false); assertEquals(notebookAuthorization.isWriter(note.getId(), new HashSet<>(Arrays.asList("user1"))), true); - // Test clearing of permssions + // Test clearing of permissions notebookAuthorization.setReaders(note.getId(), Sets.<String>newHashSet()); assertEquals(notebookAuthorization.isReader(note.getId(), new HashSet<>(Arrays.asList("user2"))), true); assertEquals(notebookAuthorization.isReader(note.getId(), - new HashSet<>(Arrays.asList("user3"))), true); + new HashSet<>(Arrays.asList("user4"))), true); notebook.removeNote(note.getId(), anonymous); } @@ -767,11 +776,13 @@ public class NotebookTest implements JobListenerFactory{ Note note = notebook.createNote(new AuthenticationInfo(user1)); - // check that user1 is owner, reader and writer + // check that user1 is owner, reader, runner and writer assertEquals(notebookAuthorization.isOwner(note.getId(), Sets.newHashSet(user1)), true); assertEquals(notebookAuthorization.isReader(note.getId(), Sets.newHashSet(user1)), true); + assertEquals(notebookAuthorization.isRunner(note.getId(), + Sets.newHashSet(user2)), true); assertEquals(notebookAuthorization.isWriter(note.getId(), Sets.newHashSet(user1)), true); @@ -780,6 +791,8 @@ public class NotebookTest implements JobListenerFactory{ Sets.newHashSet(user2)), false); assertEquals(notebookAuthorization.isReader(note.getId(), Sets.newHashSet(user2)), true); + assertEquals(notebookAuthorization.isRunner(note.getId(), + Sets.newHashSet(user2)), true); assertEquals(notebookAuthorization.isWriter(note.getId(), Sets.newHashSet(user2)), true); @@ -790,7 +803,7 @@ public class NotebookTest implements JobListenerFactory{ assertEquals(user1Notes.size(), 1); assertEquals(user1Notes.get(0).getId(), note.getId()); - // check that user2 has note listed in his workbech because of admin role + // check that user2 has note listed in his workbench because of admin role Set<String> user2AndRoles = notebookAuthorization.getRoles(user2); user2AndRoles.add(user2); List<Note> user2Notes = notebook.getAllNotes(user2AndRoles); @@ -1094,6 +1107,7 @@ public class NotebookTest implements JobListenerFactory{ notebook.getNotebookAuthorization().setOwners(note1.getId(), Sets.newHashSet("user1")); notebook.getNotebookAuthorization().setWriters(note1.getId(), Sets.newHashSet("user1")); + notebook.getNotebookAuthorization().setRunners(note1.getId(), Sets.newHashSet("user1")); notebook.getNotebookAuthorization().setReaders(note1.getId(), Sets.newHashSet("user1")); assertEquals(1, notebook.getAllNotes(Sets.newHashSet("anonymous")).size()); assertEquals(2, notebook.getAllNotes(Sets.newHashSet("user1")).size()); @@ -1101,6 +1115,7 @@ public class NotebookTest implements JobListenerFactory{ notebook.getNotebookAuthorization().setOwners(note2.getId(), Sets.newHashSet("user2")); notebook.getNotebookAuthorization().setWriters(note2.getId(), Sets.newHashSet("user2")); notebook.getNotebookAuthorization().setReaders(note2.getId(), Sets.newHashSet("user2")); + notebook.getNotebookAuthorization().setRunners(note2.getId(), Sets.newHashSet("user2")); assertEquals(0, notebook.getAllNotes(Sets.newHashSet("anonymous")).size()); assertEquals(1, notebook.getAllNotes(Sets.newHashSet("user1")).size()); assertEquals(1, notebook.getAllNotes(Sets.newHashSet("user2")).size()); @@ -1133,6 +1148,12 @@ public class NotebookTest implements JobListenerFactory{ notes2 = notebook.getAllNotes(user2); assertEquals(notes1.size(), 1); assertEquals(notes2.size(), 1); + + notebook.getNotebookAuthorization().setRunners(note.getId(), Sets.newHashSet("user1")); + notes1 = notebook.getAllNotes(user1); + notes2 = notebook.getAllNotes(user2); + assertEquals(notes1.size(), 1); + assertEquals(notes2.size(), 1); notebook.getNotebookAuthorization().setWriters(note.getId(), Sets.newHashSet("user1")); notes1 = notebook.getAllNotes(user1); @@ -1169,6 +1190,7 @@ public class NotebookTest implements JobListenerFactory{ // user1 is only owner assertEquals(notebookAuthorization.getOwners(notePublic.getId()).size(), 1); assertEquals(notebookAuthorization.getReaders(notePublic.getId()).size(), 0); + assertEquals(notebookAuthorization.getRunners(notePublic.getId()).size(), 0); assertEquals(notebookAuthorization.getWriters(notePublic.getId()).size(), 0); // case of private note @@ -1197,6 +1219,7 @@ public class NotebookTest implements JobListenerFactory{ // user1 have all rights assertEquals(notebookAuthorization.getOwners(notePrivate.getId()).size(), 1); assertEquals(notebookAuthorization.getReaders(notePrivate.getId()).size(), 1); + assertEquals(notebookAuthorization.getRunners(notePrivate.getId()).size(), 1); assertEquals(notebookAuthorization.getWriters(notePrivate.getId()).size(), 1); //set back public to true http://git-wip-us.apache.org/repos/asf/zeppelin/blob/e47b30a8/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java ---------------------------------------------------------------------- diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java index 803912e..a6c9393 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/NotebookRepoSyncTest.java @@ -330,6 +330,7 @@ public class NotebookRepoSyncTest implements JobListenerFactory { assertEquals(true, authInfo.isOwner(note.getId(), entity)); assertEquals(1, authInfo.getOwners(note.getId()).size()); assertEquals(0, authInfo.getReaders(note.getId()).size()); + assertEquals(0, authInfo.getRunners(note.getId()).size()); assertEquals(0, authInfo.getWriters(note.getId()).size()); /* update note and save on secondary storage */ @@ -354,6 +355,7 @@ public class NotebookRepoSyncTest implements JobListenerFactory { assertEquals(true, authInfo.isOwner(note.getId(), entity)); assertEquals(1, authInfo.getOwners(note.getId()).size()); assertEquals(0, authInfo.getReaders(note.getId()).size()); + assertEquals(0, authInfo.getRunners(note.getId()).size()); assertEquals(0, authInfo.getWriters(note.getId()).size()); /* scenario 2 - note doesn't exist on main storage */ @@ -364,6 +366,7 @@ public class NotebookRepoSyncTest implements JobListenerFactory { authInfo.removeNote(note.getId()); assertEquals(0, authInfo.getOwners(note.getId()).size()); assertEquals(0, authInfo.getReaders(note.getId()).size()); + assertEquals(0, authInfo.getRunners(note.getId()).size()); assertEquals(0, authInfo.getWriters(note.getId()).size()); /* now sync - should bring note from secondary storage with added acl */ @@ -372,9 +375,11 @@ public class NotebookRepoSyncTest implements JobListenerFactory { assertEquals(1, notebookRepoSync.list(1, null).size()); assertEquals(1, authInfo.getOwners(note.getId()).size()); assertEquals(1, authInfo.getReaders(note.getId()).size()); + assertEquals(1, authInfo.getRunners(note.getId()).size()); assertEquals(1, authInfo.getWriters(note.getId()).size()); assertEquals(true, authInfo.isOwner(note.getId(), entity)); assertEquals(true, authInfo.isReader(note.getId(), entity)); + assertEquals(true, authInfo.isRunner(note.getId(), entity)); assertEquals(true, authInfo.isWriter(note.getId(), entity)); }