diff --git a/Security-README.md b/Security-README.md new file mode 100644 index 00000000000..985c8c4ce35 --- /dev/null +++ b/Security-README.md @@ -0,0 +1,63 @@ +# Vocabulary +username, owner and principal are used interchangeably to designate the currently authenticated user +# What are we securing ? +Zeppelin is basically a web application that spawn remote interpreters to run commands and return HTML fragments to be displayed on the user browser. +The scope of this PR is to secure the access to the notebooks. A user has access to his notebooks and only his notebooks. To achieve this, we use Apache Shiro. +## HTTP Endpoint security +Apache Shiro sits as a servlet filter between the browser and the exposed services and handles the required authentication without any programming required. (See Apache Shiro for more info). +## Websocket security +Securing the HTTP endpoints is not enough, since Zeppelin also communicates with the browser through websockets. To secure this channel, we take the following approach: +1. The browser on startup requests a ticket through HTTP +2. The Apache Shiro Servlet filter handles the user auth +3. Once the user is authenticated, a ticket is assigned to this user and the ticket is returned to the browser + +All websockets communications require the username and ticket to be submitted by the browser. Upon receiving a websocket message, the server checks that the ticket received is the one assigned to the username through the HTTP request (step 3 above). + +# Strategies to access the principal +Apache Shiro expose the principal through the following call + + org.apache.shiro.SecurityUtils.getSubject().getPrincipal() + +Apache Shiro stores the `principal` and the `subject` in a thread local variable. + +That makes it possible to get the principal from wherever we need it in the application as long as we are in the context of a HTTP synchronous request. + +Two strategies are possible : (1) Rely on the Thread local (anti ?)pattern to get the principal whenever we need it or (2) modify the interfaces of the NotebookRepo and SearchService classes to require the owner to be passed as a parameter. + +## Relying on ThreadLocal +The Shiro ThreadLocal subject is available only for HTTP request which is not enough since we need to access notes through websockets (NotebookServer class) where the Shiro filter is not involved. + +We could however explicitly create the Shiro `subject` on each websocket request using the provided username and ticket in the web socket request. + +The drawback of this approach is that it makes it impossible to write async services and/or multithreaded code involving access to the Shiro principal (The subject is available in the current thread only). + +On the other side, the main benefit of this approach is that it does not require any change to the NotebookRepo and the SearchService interfaces. + +## Updating the service interfaces +Coming from an actor based concurrent & distributed programming background I have decided to go for this approach. This required me to add the owner parameter to a couple of method. + + trait NotebookRepo { + ... + public List list(String owner) throws IOException; + public Note get(String noteId, String owner) throws IOException; + ... + } + trait SearchService { + public List<~> query(String queryStr, String owner); + } + + +As you can guess the has a significant impact on the existing code since the owner parameter had to be made available explicitly along the code that is executed to handle the request. + +# How Notes are stored +TODO +# Future : How Permissions could be implemented (note sharing, accessible features in iframes …) +TODO + + + + + +\`\`\` +\` +\`\`\`\` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5e492fab3ae..47d49278e9c 100755 --- a/pom.xml +++ b/pom.xml @@ -208,6 +208,19 @@ 4.11 test + + + + + org.apache.shiro + shiro-core + 1.2.3 + + + org.apache.shiro + shiro-web + 1.2.3 + diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObject.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObject.java index cebe4cc4911..db6a740191d 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObject.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObject.java @@ -33,7 +33,7 @@ public class AngularObject { private String name; private T object; - + private String principal; private transient AngularObjectListener listener; private transient List watchers = new LinkedList(); @@ -64,6 +64,14 @@ public boolean isGlobal() { return noteId == null; } + public String getPrincipal() { + return principal; + } + + public void setPrincipal(String principal) { + this.principal = principal; + } + @Override public boolean equals(Object o) { if (o instanceof AngularObject) { diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObjectRegistry.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObjectRegistry.java index d6bab7b732c..77980397138 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObjectRegistry.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObjectRegistry.java @@ -119,7 +119,7 @@ public AngularObject remove(String name, String noteId, boolean emit) { Map r = getRegistryForKey(noteId); AngularObject o = r.remove(name); if (listener != null && emit) { - listener.onRemove(interpreterId, name, noteId);; + listener.onRemove(interpreterId, o); } return o; } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObjectRegistryListener.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObjectRegistryListener.java index 3ba57d7b1af..3f08efae4a2 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObjectRegistryListener.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObjectRegistryListener.java @@ -24,5 +24,5 @@ public interface AngularObjectRegistryListener { public void onAdd(String interpreterGroupId, AngularObject object); public void onUpdate(String interpreterGroupId, AngularObject object); - public void onRemove(String interpreterGroupId, String name, String noteId); + public void onRemove(String interpreterGroupId, AngularObject object); } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java index 46a07083468..03d0898844c 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java @@ -432,10 +432,10 @@ public void onUpdate(String interpreterGroupId, AngularObject object) { } @Override - public void onRemove(String interpreterGroupId, String name, String noteId) { + public void onRemove(String interpreterGroupId, AngularObject object) { Map removeObject = new HashMap(); - removeObject.put("name", name); - removeObject.put("noteId", noteId); + removeObject.put("name", object.getName()); + removeObject.put("noteId", object.getNoteId()); sendEvent(new RemoteInterpreterEvent( RemoteInterpreterEventType.ANGULAR_OBJECT_REMOVE, gson.toJson(removeObject))); @@ -468,7 +468,6 @@ public RemoteInterpreterEvent getEvent() throws TException { /** * called when object is updated in client (web) side. - * @param className * @param name * @param noteId noteId where the update issues * @param object diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/AngularObjectRegistryTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/AngularObjectRegistryTest.java index 43aca62c497..b693e6a4f42 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/AngularObjectRegistryTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/AngularObjectRegistryTest.java @@ -45,7 +45,7 @@ public void onUpdate(String interpreterGroupId, AngularObject object) { } @Override - public void onRemove(String interpreterGroupId, String name, String noteId) { + public void onRemove(String interpreterGroupId, AngularObject object) { onRemove.incrementAndGet(); } }); diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java index 29a1fb11972..fcc2f6c1fe9 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/remote/RemoteAngularObjectTest.java @@ -184,7 +184,7 @@ public void onUpdate(String interpreterGroupId, AngularObject object) { } @Override - public void onRemove(String interpreterGroupId, String name, String noteId) { + public void onRemove(String interpreterGroupId, AngularObject object) { onRemove.incrementAndGet(); } diff --git a/zeppelin-server/pom.xml b/zeppelin-server/pom.xml index e77ee6ca38d..73e878a58f5 100644 --- a/zeppelin-server/pom.xml +++ b/zeppelin-server/pom.xml @@ -269,6 +269,16 @@ 1.9.0 test + + + + org.apache.shiro + shiro-core + + + org.apache.shiro + shiro-web + diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java index aefa2c1d739..fa8d82b9132 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 @@ -46,6 +46,7 @@ import org.apache.zeppelin.search.SearchService; import org.apache.zeppelin.server.JsonResponse; import org.apache.zeppelin.socket.NotebookServer; +import org.apache.zeppelin.ticket.SecurityUtils; import org.quartz.CronExpression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -80,9 +81,11 @@ public NotebookRestApi(Notebook notebook, NotebookServer notebookServer, SearchS @PUT @Path("interpreter/bind/{noteId}") public Response bind(@PathParam("noteId") String noteId, String req) throws IOException { + String principal = SecurityUtils.getPrincipal(); + List settingIdList = gson.fromJson(req, new TypeToken>(){}.getType()); - notebook.bindInterpretersToNote(noteId, settingIdList); - return new JsonResponse<>(Status.OK).build(); + notebook.bindInterpretersToNote(noteId, settingIdList, principal); + return new JsonResponse(Status.OK).build(); } /** @@ -91,10 +94,13 @@ public Response bind(@PathParam("noteId") String noteId, String req) throws IOEx @GET @Path("interpreter/bind/{noteId}") public Response bind(@PathParam("noteId") String noteId) { - List settingList - = new LinkedList(); + String principal = SecurityUtils.getPrincipal(); + + List settingList = + new LinkedList(); - List selectedSettings = notebook.getBindedInterpreterSettings(noteId); + List selectedSettings = + notebook.getBindedInterpreterSettings(noteId, principal); for (InterpreterSetting setting : selectedSettings) { settingList.add(new InterpreterSettingListForNoteBind( setting.id(), @@ -131,14 +137,15 @@ public Response bind(@PathParam("noteId") String noteId) { @GET @Path("/") public Response getNotebookList() throws IOException { - List> notesInfo = notebookServer.generateNotebooksInfo(); - return new JsonResponse<>(Status.OK, "", notesInfo ).build(); + List> notesInfo = notebookServer. + generateNotebooksInfo(SecurityUtils.getPrincipal()); + return new JsonResponse(Status.OK, "", notesInfo ).build(); } @GET @Path("{notebookId}") public Response getNotebook(@PathParam("notebookId") String notebookId) throws IOException { - Note note = notebook.getNote(notebookId); + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } @@ -155,10 +162,11 @@ public Response getNotebook(@PathParam("notebookId") String notebookId) throws I @POST @Path("/") public Response createNote(String message) throws IOException { + String principal = SecurityUtils.getPrincipal(); LOG.info("Create new notebook by JSON {}" , message); NewNotebookRequest request = gson.fromJson(message, NewNotebookRequest.class); - Note note = notebook.createNote(); + Note note = notebook.createNote(principal); List initialParagraphs = request.getParagraphs(); if (initialParagraphs != null) { for (NewParagraphRequest paragraphRequest : initialParagraphs) { @@ -175,8 +183,8 @@ public Response createNote(String message) throws IOException { note.setName(noteName); note.persist(); notebookServer.broadcastNote(note); - notebookServer.broadcastNoteList(); - return new JsonResponse<>(Status.CREATED, "", note.getId() ).build(); + notebookServer.broadcastNoteList(principal); + return new JsonResponse(Status.CREATED, "", note.getId() ).build(); } /** @@ -188,15 +196,16 @@ public Response createNote(String message) throws IOException { @DELETE @Path("{notebookId}") public Response deleteNote(@PathParam("notebookId") String notebookId) throws IOException { + String principal = SecurityUtils.getPrincipal(); LOG.info("Delete notebook {} ", notebookId); if (!(notebookId.isEmpty())) { - Note note = notebook.getNote(notebookId); + Note note = notebook.getNote(notebookId, principal); if (note != null) { - notebook.removeNote(notebookId); + notebook.removeNote(notebookId, principal); } } - notebookServer.broadcastNoteList(); - return new JsonResponse<>(Status.OK, "").build(); + notebookServer.broadcastNoteList(principal); + return new JsonResponse(Status.OK, "").build(); } /** @@ -209,14 +218,15 @@ public Response deleteNote(@PathParam("notebookId") String notebookId) throws IO @Path("{notebookId}") public Response cloneNote(@PathParam("notebookId") String notebookId, String message) throws IOException, CloneNotSupportedException, IllegalArgumentException { + String principal = SecurityUtils.getPrincipal(); LOG.info("clone notebook by JSON {}" , message); NewNotebookRequest request = gson.fromJson(message, NewNotebookRequest.class); String newNoteName = request.getName(); - Note newNote = notebook.cloneNote(notebookId, newNoteName); + Note newNote = notebook.cloneNote(notebookId, newNoteName, principal); notebookServer.broadcastNote(newNote); - notebookServer.broadcastNoteList(); - return new JsonResponse<>(Status.CREATED, "", newNote.getId()).build(); + notebookServer.broadcastNoteList(principal); + return new JsonResponse(Status.CREATED, "", newNote.getId()).build(); } /** @@ -231,7 +241,7 @@ public Response insertParagraph(@PathParam("notebookId") String notebookId, Stri throws IOException { LOG.info("insert paragraph {} {}", notebookId, message); - Note note = notebook.getNote(notebookId); + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); if (note == null) { return new JsonResponse(Status.NOT_FOUND, "note not found.").build(); } @@ -265,7 +275,7 @@ public Response getParagraph(@PathParam("notebookId") String notebookId, @PathParam("paragraphId") String paragraphId) throws IOException { LOG.info("get paragraph {} {}", notebookId, paragraphId); - Note note = notebook.getNote(notebookId); + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); if (note == null) { return new JsonResponse(Status.NOT_FOUND, "note not found.").build(); } @@ -291,7 +301,7 @@ public Response moveParagraph(@PathParam("notebookId") String notebookId, @PathParam("newIndex") String newIndex) throws IOException { LOG.info("move paragraph {} {} {}", notebookId, paragraphId, newIndex); - Note note = notebook.getNote(notebookId); + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); if (note == null) { return new JsonResponse(Status.NOT_FOUND, "note not found.").build(); } @@ -324,7 +334,7 @@ public Response deleteParagraph(@PathParam("notebookId") String notebookId, @PathParam("paragraphId") String paragraphId) throws IOException { LOG.info("delete paragraph {} {}", notebookId, paragraphId); - Note note = notebook.getNote(notebookId); + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); if (note == null) { return new JsonResponse(Status.NOT_FOUND, "note not found.").build(); } @@ -351,8 +361,8 @@ public Response deleteParagraph(@PathParam("notebookId") String notebookId, @Path("job/{notebookId}") public Response runNoteJobs(@PathParam("notebookId") String notebookId) throws IOException, IllegalArgumentException { + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); LOG.info("run notebook jobs {} ", notebookId); - Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } @@ -371,8 +381,7 @@ public Response runNoteJobs(@PathParam("notebookId") String notebookId) throws @Path("job/{notebookId}") public Response stopNoteJobs(@PathParam("notebookId") String notebookId) throws IOException, IllegalArgumentException { - LOG.info("stop notebook jobs {} ", notebookId); - Note note = notebook.getNote(notebookId); + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } @@ -396,7 +405,7 @@ public Response stopNoteJobs(@PathParam("notebookId") String notebookId) throws public Response getNoteJobStatus(@PathParam("notebookId") String notebookId) throws IOException, IllegalArgumentException { LOG.info("get notebook job status."); - Note note = notebook.getNote(notebookId); + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } @@ -419,9 +428,8 @@ public Response runParagraph(@PathParam("notebookId") String notebookId, @PathParam("paragraphId") String paragraphId, String message) throws IOException, IllegalArgumentException { + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); LOG.info("run paragraph job {} {} {}", notebookId, paragraphId, message); - - Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } @@ -457,8 +465,8 @@ public Response runParagraph(@PathParam("notebookId") String notebookId, public Response stopParagraph(@PathParam("notebookId") String notebookId, @PathParam("paragraphId") String paragraphId) throws IOException, IllegalArgumentException { + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); LOG.info("stop paragraph job {} ", notebookId); - Note note = notebook.getNote(notebookId); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } @@ -486,7 +494,7 @@ public Response registerCronJob(@PathParam("notebookId") String notebookId, Stri CronRequest request = gson.fromJson(message, CronRequest.class); - Note note = notebook.getNote(notebookId); + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } @@ -498,7 +506,8 @@ public Response registerCronJob(@PathParam("notebookId") String notebookId, Stri Map config = note.getConfig(); config.put("cron", request.getCronString()); note.setConfig(config); - notebook.refreshCron(note.id()); + notebook.refreshCron(note.id(), + SecurityUtils.getPrincipal()); return new JsonResponse<>(Status.OK).build(); } @@ -515,7 +524,7 @@ public Response removeCronJob(@PathParam("notebookId") String notebookId) throws IOException, IllegalArgumentException { LOG.info("Remove cron job note {}", notebookId); - Note note = notebook.getNote(notebookId); + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } @@ -523,7 +532,7 @@ public Response removeCronJob(@PathParam("notebookId") String notebookId) throws Map config = note.getConfig(); config.put("cron", null); note.setConfig(config); - notebook.refreshCron(note.id()); + notebook.refreshCron(note.id(), SecurityUtils.getPrincipal()); return new JsonResponse<>(Status.OK).build(); } @@ -540,7 +549,7 @@ public Response getCronJob(@PathParam("notebookId") String notebookId) throws IOException, IllegalArgumentException { LOG.info("Get cron job note {}", notebookId); - Note note = notebook.getNote(notebookId); + Note note = notebook.getNote(notebookId, SecurityUtils.getPrincipal()); if (note == null) { return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build(); } @@ -555,7 +564,8 @@ public Response getCronJob(@PathParam("notebookId") String notebookId) throws @Path("search") public Response search(@QueryParam("q") String queryTerm) { LOG.info("Searching notebooks for: {}", queryTerm); - List> notebooksFound = notebookIndex.query(queryTerm); + List> notebooksFound = notebookIndex.query(queryTerm, + SecurityUtils.getPrincipal()); LOG.info("{} notbooks found", notebooksFound.size()); return new JsonResponse<>(Status.OK, notebooksFound).build(); } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java new file mode 100644 index 00000000000..cd7168e5740 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zeppelin.rest; + +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.server.JsonResponse; +import org.apache.zeppelin.ticket.SecurityUtils; +import org.apache.zeppelin.ticket.TicketContainer; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; + +/** + * Zeppelin security rest api endpoint. + * + */ +@Path("/security") +@Produces("application/json") +public class SecurityRestApi { + /** + * Required by Swagger. + */ + public SecurityRestApi() { + super(); + } + + /** + * Get ticket + * Returns username & ticket + * for anonymous access, username is always anonymous. + * After getting this ticket, access through websockets become safe + * + * @return 200 response + */ + @GET + @Path("ticket") + public Response ticket() { + ZeppelinConfiguration conf = ZeppelinConfiguration.create(); + String principal = SecurityUtils.getPrincipal(); + JsonResponse response; + // ticket set to anonymous for anonymous user. Simplify testing. + String ticket; + if ("anonymous".equals(principal)) + ticket = "anonymous"; + else + ticket = TicketContainer.instance.getTicket(principal); + + Map data = new HashMap<>(); + data.put("principal", principal); + data.put("ticket", ticket); + + response = new JsonResponse(Response.Status.OK, "", data); + return response.build(); + } +} diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/server/CorsFilter.java b/zeppelin-server/src/main/java/org/apache/zeppelin/server/CorsFilter.java index 0e39242acd8..c520094814c 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/server/CorsFilter.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/server/CorsFilter.java @@ -18,13 +18,11 @@ package org.apache.zeppelin.server; import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.utils.SecurityUtils; +import org.apache.zeppelin.ticket.SecurityUtils; import java.io.IOException; -import java.net.URI; import java.net.URISyntaxException; import java.text.DateFormat; -import java.util.Arrays; import java.util.Date; import java.util.Locale; diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java index fd115ee18cb..c297f9d834c 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java @@ -41,6 +41,7 @@ import org.apache.zeppelin.search.SearchService; import org.apache.zeppelin.search.LuceneSearch; import org.apache.zeppelin.socket.NotebookServer; +import org.apache.zeppelin.rest.SecurityRestApi; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; @@ -226,6 +227,12 @@ private static ServletContextHandler setupRestApiContextHandler(ZeppelinConfigur cxfContext.addFilter(new FilterHolder(CorsFilter.class), "/*", EnumSet.allOf(DispatcherType.class)); + + cxfContext.addFilter(org.apache.shiro.web.servlet.ShiroFilter.class, "/*", + EnumSet.allOf(DispatcherType.class)); + + cxfContext.addEventListener(new org.apache.shiro.web.env.EnvironmentLoaderListener()); + return cxfContext; } @@ -273,6 +280,9 @@ public Set getSingletons() { InterpreterRestApi interpreterApi = new InterpreterRestApi(replFactory); singletons.add(interpreterApi); + SecurityRestApi securityApi = new SecurityRestApi(); + singletons.add(securityApi); + return singletons; } } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/Message.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/Message.java index dc657bdda06..27432453ca9 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 @@ -101,6 +101,8 @@ public static enum OP { } public OP op; + public String ticket; + public String principal; public Map data = new HashMap(); public Message(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 554f68cf073..54aad80f9f9 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java @@ -41,7 +41,8 @@ import org.apache.zeppelin.scheduler.JobListener; import org.apache.zeppelin.server.ZeppelinServer; import org.apache.zeppelin.socket.Message.OP; -import org.apache.zeppelin.utils.SecurityUtils; +import org.apache.zeppelin.ticket.TicketContainer; +import org.apache.zeppelin.ticket.SecurityUtils; import org.eclipse.jetty.websocket.WebSocket; import org.eclipse.jetty.websocket.WebSocketServlet; import org.quartz.SchedulerException; @@ -56,9 +57,10 @@ * */ public class NotebookServer extends WebSocketServlet implements - NotebookSocketListener, JobListenerFactory, AngularObjectRegistryListener { + NotebookSocketListener, JobListenerFactory, AngularObjectRegistryListener { private static final Logger LOG = LoggerFactory.getLogger(NotebookServer.class); Gson gson = new Gson(); + Map> userSocketMap = new HashMap<>(); final Map> noteSocketMap = new HashMap<>(); final Queue connectedSockets = new ConcurrentLinkedQueue<>(); @@ -96,13 +98,28 @@ public void onMessage(NotebookSocket conn, String msg) { try { Message messagereceived = deserializeMessage(msg); LOG.debug("RECEIVE << " + messagereceived.op); + LOG.debug("RECEIVE PRINCIPAL << " + messagereceived.principal); + LOG.debug("RECEIVE TICKET << " + messagereceived.ticket); + String ticket = TicketContainer.instance.getTicket(messagereceived.principal); + if (ticket != null && !ticket.equals(messagereceived.ticket)) + throw new Exception("Invalid ticket " + messagereceived.ticket + " != " + ticket); + + ZeppelinConfiguration conf = ZeppelinConfiguration.create(); + boolean allowAnonymous = conf. + getBoolean(ZeppelinConfiguration.ConfVars.ZEPPELIN_ANONYMOUS_ALLOWED); + if (!allowAnonymous && messagereceived.principal.equals("anonymous")) { + throw new Exception("Anonymous access not allowed "); + } + + addConnectionToUserSocketMap(conn, messagereceived); + /** Lets be elegant here */ switch (messagereceived.op) { case LIST_NOTES: - broadcastNoteList(); + broadcastNoteList(messagereceived.principal); break; case GET_HOME_NOTE: - sendHomeNote(conn, notebook); + sendHomeNote(conn, notebook, messagereceived); break; case GET_NOTE: sendNote(conn, notebook, messagereceived); @@ -152,7 +169,7 @@ public void onMessage(NotebookSocket conn, String msg) { angularObjectUpdated(conn, notebook, messagereceived); break; default: - broadcastNoteList(); + broadcastNoteList(messagereceived.principal); break; } } catch (Exception e) { @@ -160,11 +177,46 @@ public void onMessage(NotebookSocket conn, String msg) { } } + private void addConnectionToUserSocketMap(NotebookSocket conn, Message messagereceived) { + List conns = userSocketMap.get(messagereceived.principal); + + if (conns == null) { + synchronized (userSocketMap) { + conns = userSocketMap.get(messagereceived.principal); + if (conns == null) { + conns = new LinkedList<>(); + userSocketMap.put(messagereceived.principal, conns); + } + } + } + + if (!conns.contains(conn)) { + conns.add(conn); + } + } + @Override public void onClose(NotebookSocket conn, int code, String reason) { LOG.info("Closed connection to {} : {}. ({}) {}", conn.getRequest() .getRemoteAddr(), conn.getRequest().getRemotePort(), code, reason); removeConnectionFromAllNote(conn); + synchronized (userSocketMap) { + Collection> allSockets = userSocketMap.values(); + for (List userList : allSockets) { + userList.remove(conn); + } + } + } + + @Override + public void onError(NotebookSocket conn, Exception message) { + removeConnectionFromAllNote(conn); + synchronized (userSocketMap) { + Collection> allSockets = userSocketMap.values(); + for (List userList : allSockets) { + userList.remove(conn); + } + } connectedSockets.remove(conn); } @@ -231,9 +283,9 @@ private String getOpenNoteId(NotebookSocket socket) { } private void broadcastToNoteBindedInterpreter(String interpreterGroupId, - Message m) { + Message m) { Notebook notebook = notebook(); - List notes = notebook.getAllNotes(); + List notes = notebook.getAllNotes(m.principal); for (Note note : notes) { List ids = note.getNoteReplLoader().getInterpreters(); for (String id : ids) { @@ -282,24 +334,34 @@ private void broadcastExcept(String noteId, Message m, NotebookSocket exclude) { } private void broadcastAll(Message m) { - for (NotebookSocket conn : connectedSockets) { - try { - conn.send(serializeMessage(m)); - } catch (IOException e) { - LOG.error("socket error", e); + synchronized (userSocketMap) { + List> notesInfo = (List>) m.get("notes"); + String principal = m.principal; + List conns = userSocketMap.get(principal); + if (conns == null) { + conns = new LinkedList<>(); + userSocketMap.put(principal, conns); + } + + for (NotebookSocket theconn : conns) { + try { + theconn.send(serializeMessage(m)); + } catch (IOException e) { + LOG.error("socket error", e); + } } } } - public List> generateNotebooksInfo (){ + public List> generateNotebooksInfo (String principal){ Notebook notebook = notebook(); ZeppelinConfiguration conf = notebook.getConf(); String homescreenNotebookId = conf.getString(ConfVars.ZEPPELIN_NOTEBOOK_HOMESCREEN); - boolean hideHomeScreenNotebookFromList = conf - .getBoolean(ConfVars.ZEPPELIN_NOTEBOOK_HOMESCREEN_HIDE); + boolean hideHomeScreenNotebookFromList = conf. + getBoolean(ConfVars.ZEPPELIN_NOTEBOOK_HOMESCREEN_HIDE); - List notes = notebook.getAllNotes(); + List notes = notebook.getAllNotes(principal); List> notesInfo = new LinkedList<>(); for (Note note : notes) { Map info = new HashMap<>(); @@ -310,6 +372,7 @@ public List> generateNotebooksInfo (){ info.put("id", note.id()); info.put("name", note.getName()); + info.put("principal", principal); notesInfo.add(info); } @@ -320,20 +383,21 @@ public void broadcastNote(Note note) { broadcast(note.id(), new Message(OP.NOTE).put("note", note)); } - public void broadcastNoteList() { + public void broadcastNoteList(String principal) { - List> notesInfo = generateNotebooksInfo(); - broadcastAll(new Message(OP.NOTES_INFO).put("notes", notesInfo)); + List> notesInfo = generateNotebooksInfo(principal); + Message message = new Message(OP.NOTES_INFO).put("notes", notesInfo); + message.principal = principal; + broadcastAll(message); } private void sendNote(NotebookSocket conn, Notebook notebook, - Message fromMessage) throws IOException { + Message fromMessage) throws IOException { String noteId = (String) fromMessage.get("id"); if (noteId == null) { return; } - - Note note = notebook.getNote(noteId); + Note note = notebook.getNote(noteId, fromMessage.principal); if (note != null) { addConnectionToNote(note.id(), conn); conn.send(serializeMessage(new Message(OP.NOTE).put("note", note))); @@ -341,12 +405,13 @@ private void sendNote(NotebookSocket conn, Notebook notebook, } } - private void sendHomeNote(NotebookSocket conn, Notebook notebook) throws IOException { + private void sendHomeNote(NotebookSocket conn, Notebook notebook, + Message fromMessage) throws IOException { String noteId = notebook.getConf().getString(ConfVars.ZEPPELIN_NOTEBOOK_HOMESCREEN); Note note = null; if (noteId != null) { - note = notebook.getNote(noteId); + note = notebook.getNote(noteId, fromMessage.principal); } if (note != null) { @@ -359,36 +424,34 @@ private void sendHomeNote(NotebookSocket conn, Notebook notebook) throws IOExcep } } - private void updateNote(WebSocket conn, Notebook notebook, Message fromMessage) + private void updateNote(NotebookSocket conn, Notebook notebook, Message fromMessage) throws SchedulerException, IOException { String noteId = (String) fromMessage.get("id"); String name = (String) fromMessage.get("name"); - Map config = (Map) fromMessage - .get("config"); + Map config = (Map) fromMessage.get("config"); if (noteId == null) { return; } if (config == null) { return; } - - Note note = notebook.getNote(noteId); + Note note = notebook.getNote(noteId, fromMessage.principal); if (note != null) { boolean cronUpdated = isCronUpdated(config, note.getConfig()); note.setName(name); note.setConfig(config); if (cronUpdated) { - notebook.refreshCron(note.id()); + notebook.refreshCron(note.id(), fromMessage.principal); } note.persist(); broadcastNote(note); - broadcastNoteList(); + broadcastNoteList(fromMessage.principal); } } private boolean isCronUpdated(Map configA, - Map configB) { + Map configB) { boolean cronUpdated = false; if (configA.get("cron") != null && configB.get("cron") != null && configA.get("cron").equals(configB.get("cron"))) { @@ -401,12 +464,14 @@ private boolean isCronUpdated(Map configA, return cronUpdated; } - private void createNote(WebSocket conn, Notebook notebook, Message message) throws IOException { - Note note = notebook.createNote(); + + private void createNote(NotebookSocket conn, Notebook notebook, Message fromMsg) + throws IOException { + Note note = notebook.createNote(fromMsg.principal); note.addParagraph(); // it's an empty note. so add one paragraph - if (message != null) { - String noteName = (String) message.get("name"); - if (noteName == null || noteName.isEmpty()){ + if (fromMsg != null) { + String noteName = (String) fromMsg.get("name"); + if (noteName == null || noteName.isEmpty()) { noteName = "Note " + note.getId(); } note.setName(noteName); @@ -415,34 +480,31 @@ private void createNote(WebSocket conn, Notebook notebook, Message message) thro note.persist(); addConnectionToNote(note.id(), (NotebookSocket) conn); broadcastNote(note); - broadcastNoteList(); + broadcastNoteList(fromMsg.principal); } - private void removeNote(WebSocket conn, Notebook notebook, Message fromMessage) + private void removeNote(NotebookSocket conn, Notebook notebook, Message fromMessage) throws IOException { String noteId = (String) fromMessage.get("id"); if (noteId == null) { return; } - - Note note = notebook.getNote(noteId); - notebook.removeNote(noteId); + Note note = notebook.getNote(noteId, fromMessage.principal); + note.unpersist(); + notebook.removeNote(noteId, fromMessage.principal); removeNote(noteId); - broadcastNoteList(); + broadcastNoteList(fromMessage.principal); } private void updateParagraph(NotebookSocket conn, Notebook notebook, - Message fromMessage) throws IOException { + Message fromMessage) throws IOException { String paragraphId = (String) fromMessage.get("id"); if (paragraphId == null) { return; } - - Map params = (Map) fromMessage - .get("params"); - Map config = (Map) fromMessage - .get("config"); - final Note note = notebook.getNote(getOpenNoteId(conn)); + Map params = (Map) fromMessage.get("params"); + Map config = (Map) fromMessage.get("config"); + final Note note = notebook.getNote(getOpenNoteId(conn), fromMessage.principal); Paragraph p = note.getParagraph(paragraphId); p.settings.setParams(params); p.setConfig(config); @@ -456,16 +518,16 @@ private void cloneNote(NotebookSocket conn, Notebook notebook, Message fromMessa throws IOException, CloneNotSupportedException { String noteId = getOpenNoteId(conn); String name = (String) fromMessage.get("name"); - Note newNote = notebook.cloneNote(noteId, name); - addConnectionToNote(newNote.id(), (NotebookSocket) conn); + Note newNote = notebook.cloneNote(noteId, name, fromMessage.principal); + addConnectionToNote(newNote.id(), conn); broadcastNote(newNote); - broadcastNoteList(); + broadcastNoteList(fromMessage.principal); } protected Note importNote(NotebookSocket conn, Notebook notebook, Message fromMessage) throws IOException { - Note note = notebook.createNote(); + Note note = notebook.createNote(fromMessage.principal); if (fromMessage != null) { String noteName = (String) ((Map) fromMessage.get("notebook")).get("name"); if (noteName == null || noteName.isEmpty()) { @@ -513,19 +575,18 @@ protected Note importNote(NotebookSocket conn, Notebook notebook, Message fromMe note.persist(); broadcastNote(note); - broadcastNoteList(); + broadcastNoteList(fromMessage.principal); return note; } private void removeParagraph(NotebookSocket conn, Notebook notebook, - Message fromMessage) throws IOException { + Message fromMessage) throws IOException { final String paragraphId = (String) fromMessage.get("id"); if (paragraphId == null) { return; } - - final Note note = notebook.getNote(getOpenNoteId(conn)); - /** We dont want to remove the last paragraph */ + final Note note = notebook.getNote(getOpenNoteId(conn), fromMessage.principal); + /** We don't want to remove the last paragraph */ if (!note.isLastParagraph(paragraphId)) { note.removeParagraph(paragraphId); note.persist(); @@ -540,13 +601,13 @@ private void clearParagraphOutput(NotebookSocket conn, Notebook notebook, return; } - final Note note = notebook.getNote(getOpenNoteId(conn)); + final Note note = notebook.getNote(getOpenNoteId(conn), fromMessage.principal); note.clearParagraphOutput(paragraphId); broadcastNote(note); } private void completion(NotebookSocket conn, Notebook notebook, - Message fromMessage) throws IOException { + Message fromMessage) throws IOException { String paragraphId = (String) fromMessage.get("id"); String buffer = (String) fromMessage.get("buf"); int cursor = (int) Double.parseDouble(fromMessage.get("cursor").toString()); @@ -556,7 +617,7 @@ private void completion(NotebookSocket conn, Notebook notebook, return; } - final Note note = notebook.getNote(getOpenNoteId(conn)); + final Note note = notebook.getNote(getOpenNoteId(conn), fromMessage.principal); List candidates = note.completion(paragraphId, buffer, cursor); resp.put("completions", candidates); conn.send(serializeMessage(resp)); @@ -565,8 +626,8 @@ private void completion(NotebookSocket conn, Notebook notebook, /** * When angular object updated from client * - * @param conn the web socket. - * @param notebook the notebook. + * @param conn the web socket. + * @param notebook the notebook. * @param fromMessage the message. */ private void angularObjectUpdated(NotebookSocket conn, Notebook notebook, @@ -578,7 +639,7 @@ private void angularObjectUpdated(NotebookSocket conn, Notebook notebook, AngularObject ao = null; boolean global = false; // propagate change to (Remote) AngularObjectRegistry - Note note = notebook.getNote(noteId); + Note note = notebook.getNote(noteId, fromMessage.principal); if (note != null) { List settings = note.getNoteReplLoader() .getInterpreterSettings(); @@ -643,59 +704,56 @@ private void angularObjectUpdated(NotebookSocket conn, Notebook notebook, } private void moveParagraph(NotebookSocket conn, Notebook notebook, - Message fromMessage) throws IOException { + Message fromMessage) throws IOException { final String paragraphId = (String) fromMessage.get("id"); if (paragraphId == null) { return; } - final int newIndex = (int) Double.parseDouble(fromMessage.get("index") - .toString()); - final Note note = notebook.getNote(getOpenNoteId(conn)); + final int newIndex = (int) Double.parseDouble(fromMessage.get("index").toString()); + final Note note = notebook.getNote(getOpenNoteId(conn), fromMessage.principal); note.moveParagraph(paragraphId, newIndex); note.persist(); broadcastNote(note); } - private void insertParagraph(NotebookSocket conn, Notebook notebook, - Message fromMessage) throws IOException { - final int index = (int) Double.parseDouble(fromMessage.get("index") - .toString()); - final Note note = notebook.getNote(getOpenNoteId(conn)); + private void insertParagraph(NotebookSocket conn, Notebook notebook, Message fromMessage) + throws IOException { + final int index = (int) Double.parseDouble(fromMessage.get("index").toString()); + final Note note = notebook.getNote(getOpenNoteId(conn), fromMessage.principal); note.insertParagraph(index); note.persist(); broadcastNote(note); } private void cancelParagraph(NotebookSocket conn, Notebook notebook, - Message fromMessage) throws IOException { + Message fromMessage) throws IOException { final String paragraphId = (String) fromMessage.get("id"); if (paragraphId == null) { return; } - final Note note = notebook.getNote(getOpenNoteId(conn)); + final Note note = notebook.getNote(getOpenNoteId(conn), fromMessage.principal); Paragraph p = note.getParagraph(paragraphId); p.abort(); } private void runParagraph(NotebookSocket conn, Notebook notebook, - Message fromMessage) throws IOException { + Message fromMessage) throws IOException { final String paragraphId = (String) fromMessage.get("id"); if (paragraphId == null) { return; } - - final Note note = notebook.getNote(getOpenNoteId(conn)); + final Note note = notebook.getNote(getOpenNoteId(conn), fromMessage.principal); Paragraph p = note.getParagraph(paragraphId); String text = (String) fromMessage.get("paragraph"); p.setText(text); p.setTitle((String) fromMessage.get("title")); Map params = (Map) fromMessage - .get("params"); + .get("params"); p.settings.setParams(params); Map config = (Map) fromMessage - .get("config"); + .get("config"); p.setConfig(config); // if it's the last paragraph, let's add a new one boolean isTheLastParagraph = note.getLastParagraph().getId() @@ -720,7 +778,6 @@ private void runParagraph(NotebookSocket conn, Notebook notebook, /** * Need description here. - * */ public static class ParagraphJobListener implements JobListener { private NotebookServer notebookServer; @@ -801,7 +858,7 @@ public void onUpdate(String interpreterGroupId, AngularObject object) { return; } - List notes = notebook.getAllNotes(); + List notes = notebook.getAllNotes(object.getPrincipal()); for (Note note : notes) { if (object.getNoteId() != null && !note.id().equals(object.getNoteId())) { continue; @@ -825,11 +882,11 @@ public void onUpdate(String interpreterGroupId, AngularObject object) { } @Override - public void onRemove(String interpreterGroupId, String name, String noteId) { + public void onRemove(String interpreterGroupId, AngularObject object) { Notebook notebook = notebook(); - List notes = notebook.getAllNotes(); + List notes = notebook.getAllNotes(object.getPrincipal()); for (Note note : notes) { - if (noteId != null && !note.id().equals(noteId)) { + if (object.getNoteId() != null && !note.id().equals(object.getNoteId())) { continue; } @@ -838,8 +895,8 @@ public void onRemove(String interpreterGroupId, String name, String noteId) { if (id.equals(interpreterGroupId)) { broadcast( note.id(), - new Message(OP.ANGULAR_OBJECT_REMOVE).put("name", name).put( - "noteId", noteId)); + new Message(OP.ANGULAR_OBJECT_REMOVE).put("name", object.getName()).put( + "noteId", object.getNoteId())); } } } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookSocketListener.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookSocketListener.java index 77fed6ed7b1..475a21e4205 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookSocketListener.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookSocketListener.java @@ -22,5 +22,6 @@ public interface NotebookSocketListener { public void onClose(NotebookSocket socket, int code, String message); public void onOpen(NotebookSocket socket); + public void onError(NotebookSocket conn, Exception message); public void onMessage(NotebookSocket socket, String message); } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java b/zeppelin-server/src/main/java/org/apache/zeppelin/ticket/SecurityUtils.java similarity index 82% rename from zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java rename to zeppelin-server/src/main/java/org/apache/zeppelin/ticket/SecurityUtils.java index 732c7c8b4e6..dbce112b7f3 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/ticket/SecurityUtils.java @@ -14,8 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.zeppelin.utils; +package org.apache.zeppelin.ticket; +import org.apache.shiro.subject.Subject; import org.apache.zeppelin.conf.ZeppelinConfiguration; import java.net.InetAddress; @@ -44,4 +45,16 @@ public static Boolean isValidOrigin(String sourceHost, ZeppelinConfiguration con "localhost".equals(sourceUriHost) || conf.getAllowedOrigins().contains(sourceHost); } + + public static String getPrincipal() { + Subject subject = org.apache.shiro.SecurityUtils.getSubject(); + String principal; + if (subject.isAuthenticated()) { + principal = subject.getPrincipal().toString(); + } + else { + principal = "anonymous"; + } + return principal; + } } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/ticket/TicketContainer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/ticket/TicketContainer.java new file mode 100644 index 00000000000..7a267f5f241 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/ticket/TicketContainer.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zeppelin.ticket; + +import java.util.Calendar; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by hayssams on 24/04/15. + * Very simple ticket container + * No cleanup is done, since the same user accross different devices share the same ticket + * The Map size is at most the number of different user names having access to a Zeppelin instance + */ + + +public class TicketContainer { + private static class Entry { + public final String ticket; + // lastAccessTime still unused + public final long lastAccessTime; + + Entry(String ticket) { + this.ticket = ticket; + this.lastAccessTime = Calendar.getInstance().getTimeInMillis(); + } + } + + private Map sessions = new ConcurrentHashMap<>(); + + public static final TicketContainer instance = new TicketContainer(); + + public boolean isValid(String principal, String ticket) { + if ("anonymous".equals(principal) && "anonymous".equals(ticket)) + return true; + Entry entry = sessions.get(principal); + return entry != null && entry.ticket.equals(ticket); + } + + public synchronized String getTicket(String principal) { + Entry entry = sessions.get(principal); + String ticket; + if (entry == null) { + if (principal.equals("anonymous")) + ticket = "anonymous"; // enable testing on anonymous when ticket is required in the url + else + ticket = UUID.randomUUID().toString(); + } else { + ticket = entry.ticket; + } + entry = new Entry(ticket); + sessions.put(principal, entry); + return ticket; + } +} diff --git a/zeppelin-server/src/main/resources/shiro.ini b/zeppelin-server/src/main/resources/shiro.ini new file mode 100644 index 00000000000..30137253ce6 --- /dev/null +++ b/zeppelin-server/src/main/resources/shiro.ini @@ -0,0 +1,36 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +[users] +admin = password +user1 = password +user2 = password + +[main] + +# Let's use some in-memory caching to reduce the number of runtime lookups against Stormpath. +# A real application might want to use a more robust caching solution (e.g. ehcache or a +# distributed cache). When using such caches, be aware of your cache TTL settings: too high +# a TTL and the cache won't reflect any potential changes in Stormpath fast enough. Too low +# and the cache could evict too often, reducing performance. +cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager +securityManager.cacheManager = $cacheManager + + +[urls] +#/** = anon +/** = authcBasic diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java new file mode 100644 index 00000000000..b496f99a117 --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zeppelin.rest; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.apache.commons.httpclient.methods.GetMethod; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.*; + +public class SecurityRestApiTest extends AbstractTestRestApi { + Gson gson = new Gson(); + + @BeforeClass + public static void init() throws Exception { + AbstractTestRestApi.startUp(); + } + + @AfterClass + public static void destroy() throws Exception { + AbstractTestRestApi.shutDown(); + } + + @Test + public void testTicket() throws IOException { + GetMethod get = httpGet("/security/ticket"); + get.addRequestHeader("Origin", "http://localhost"); + Map resp = gson.fromJson(get.getResponseBodyAsString(), + new TypeToken>(){}.getType()); + Map body = (Map) resp.get("body"); + assertEquals("anonymous", body.get("principal")); + assertEquals("anonymous", body.get("ticket")); + get.releaseConnection(); + } + +} + diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java index 0fde5bf586f..e447af50459 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java @@ -132,7 +132,7 @@ public void testSettingsCRUD() throws IOException { @Test public void testInterpreterAutoBinding() throws IOException { // create note - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); // check interpreter is binded GetMethod get = httpGet("/notebook/interpreter/bind/"+note.id()); @@ -144,13 +144,13 @@ public void testInterpreterAutoBinding() throws IOException { get.releaseConnection(); //cleanup - ZeppelinServer.notebook.removeNote(note.getId()); + ZeppelinServer.notebook.removeNote(note.getId(), "anonymous"); } @Test public void testInterpreterRestart() throws IOException, InterruptedException { // create new note - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); note.addParagraph(); Paragraph p = note.getLastParagraph(); Map config = p.getConfig(); @@ -184,14 +184,14 @@ public void testInterpreterRestart() throws IOException, InterruptedException { } assertEquals("

markdown restarted

\n", p.getResult().message()); //cleanup - ZeppelinServer.notebook.removeNote(note.getId()); + ZeppelinServer.notebook.removeNote(note.getId(), "anonymous"); } @Test public void testGetNotebookInfo() throws IOException { LOG.info("testGetNotebookInfo"); // Create note to get info - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); assertNotNull("can't create new note", note); note.setName("note"); Paragraph paragraph = note.addParagraph(); @@ -248,7 +248,7 @@ public void testNotebookCreateWithParagraphs() throws IOException { String newNotebookId = (String) resp.get("body"); LOG.info("newNotebookId:=" + newNotebookId); - Note newNote = ZeppelinServer.notebook.getNote(newNotebookId); + Note newNote = ZeppelinServer.notebook.getNote(newNotebookId, "anonymous"); assertNotNull("Can not find new note by id", newNote); // This is partial test as newNote is in memory but is not persistent String newNoteName = newNote.getName(); @@ -267,7 +267,7 @@ public void testNotebookCreateWithParagraphs() throws IOException { assertTrue("paragraph text check failed", p.getText().startsWith("text")); } // cleanup - ZeppelinServer.notebook.removeNote(newNotebookId); + ZeppelinServer.notebook.removeNote(newNotebookId, "anonymous"); post.releaseConnection(); } @@ -283,7 +283,7 @@ private void testNotebookCreate(String noteName) throws IOException { String newNotebookId = (String) resp.get("body"); LOG.info("newNotebookId:=" + newNotebookId); - Note newNote = ZeppelinServer.notebook.getNote(newNotebookId); + Note newNote = ZeppelinServer.notebook.getNote(newNotebookId, "anonymous"); assertNotNull("Can not find new note by id", newNote); // This is partial test as newNote is in memory but is not persistent String newNoteName = newNote.getName(); @@ -294,7 +294,7 @@ private void testNotebookCreate(String noteName) throws IOException { } assertEquals("compare note name", expectedNoteName, newNoteName); // cleanup - ZeppelinServer.notebook.removeNote(newNotebookId); + ZeppelinServer.notebook.removeNote(newNotebookId, "anonymous"); post.releaseConnection(); } @@ -303,7 +303,7 @@ private void testNotebookCreate(String noteName) throws IOException { public void testDeleteNote() throws IOException { LOG.info("testDeleteNote"); //Create note and get ID - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); String noteId = note.getId(); testDeleteNotebook(noteId); } @@ -324,7 +324,7 @@ private void testDeleteNotebook(String notebookId) throws IOException { delete.releaseConnection(); // make sure note is deleted if (!notebookId.isEmpty()) { - Note deletedNote = ZeppelinServer.notebook.getNote(notebookId); + Note deletedNote = ZeppelinServer.notebook.getNote(notebookId, "anonymous"); assertNull("Deleted note should be null", deletedNote); } } @@ -333,7 +333,7 @@ private void testDeleteNotebook(String notebookId) throws IOException { public void testCloneNotebook() throws IOException, CloneNotSupportedException, IllegalArgumentException { LOG.info("testCloneNotebook"); // Create note to clone - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); assertNotNull("can't create new note", note); note.setName("source note for clone"); Paragraph paragraph = note.addParagraph(); @@ -356,13 +356,13 @@ public void testCloneNotebook() throws IOException, CloneNotSupportedException, String newNotebookId = (String) resp.get("body"); LOG.info("newNotebookId:=" + newNotebookId); - Note newNote = ZeppelinServer.notebook.getNote(newNotebookId); + Note newNote = ZeppelinServer.notebook.getNote(newNotebookId, "anonymous"); assertNotNull("Can not find new note by id", newNote); assertEquals("Compare note names", noteName, newNote.getName()); assertEquals("Compare paragraphs count", note.getParagraphs().size(), newNote.getParagraphs().size()); //cleanup - ZeppelinServer.notebook.removeNote(note.getId()); - ZeppelinServer.notebook.removeNote(newNote.getId()); + ZeppelinServer.notebook.removeNote(note.getId(), "anonymous"); + ZeppelinServer.notebook.removeNote(newNote.getId(), "anonymous"); post.releaseConnection(); } @@ -382,7 +382,7 @@ public void testListNotebooks() throws IOException { public void testNoteJobs() throws IOException, InterruptedException { LOG.info("testNoteJobs"); // Create note to run test. - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); assertNotNull("can't create new note", note); note.setName("note for run test"); Paragraph paragraph = note.addParagraph(); @@ -430,14 +430,14 @@ public void testNoteJobs() throws IOException, InterruptedException { Thread.sleep(1000); //cleanup - ZeppelinServer.notebook.removeNote(note.getId()); + ZeppelinServer.notebook.removeNote(note.getId(), "anonymous"); } @Test public void testGetNotebookJob() throws IOException, InterruptedException { LOG.info("testGetNotebookJob"); // Create note to run test. - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); assertNotNull("can't create new note", note); note.setName("note for run test"); Paragraph paragraph = note.addParagraph(); @@ -483,14 +483,14 @@ public void testGetNotebookJob() throws IOException, InterruptedException { } } - ZeppelinServer.notebook.removeNote(note.getId()); + ZeppelinServer.notebook.removeNote(note.getId(), "anonymous"); } @Test public void testRunParagraphWithParams() throws IOException, InterruptedException { LOG.info("testRunParagraphWithParams"); // Create note to run test. - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); assertNotNull("can't create new note", note); note.setName("note for run test"); Paragraph paragraph = note.addParagraph(); @@ -521,20 +521,20 @@ public void testRunParagraphWithParams() throws IOException, InterruptedExceptio postParagraph.releaseConnection(); Thread.sleep(1000); - Note retrNote = ZeppelinServer.notebook.getNote(noteID); + Note retrNote = ZeppelinServer.notebook.getNote(noteID, "anonymous"); Paragraph retrParagraph = retrNote.getParagraph(paragraph.getId()); Map params = retrParagraph.settings.getParams(); assertEquals("hello", params.get("param")); assertEquals("world", params.get("param2")); //cleanup - ZeppelinServer.notebook.removeNote(note.getId()); + ZeppelinServer.notebook.removeNote(note.getId(), "anonymous"); } @Test public void testCronJobs() throws InterruptedException, IOException{ // create a note and a paragraph - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); note.setName("note for run test"); Paragraph paragraph = note.addParagraph(); @@ -578,12 +578,12 @@ public void testCronJobs() throws InterruptedException, IOException{ DeleteMethod deleteCron = httpDelete("/notebook/cron/" + note.getId()); assertThat("", deleteCron, isAllowed()); deleteCron.releaseConnection(); - ZeppelinServer.notebook.removeNote(note.getId()); - } + ZeppelinServer.notebook.removeNote(note.getId(), "anonymous"); + } @Test public void testRegressionZEPPELIN_527() throws IOException { - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); note.setName("note for run test"); Paragraph paragraph = note.addParagraph(); @@ -600,12 +600,12 @@ public void testRegressionZEPPELIN_527() throws IOException { assertFalse(body.get(0).containsKey("finished")); getNoteJobs.releaseConnection(); - ZeppelinServer.notebook.removeNote(note.getId()); + ZeppelinServer.notebook.removeNote(note.getId(), "anonymous"); } @Test public void testInsertParagraph() throws IOException { - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); String jsonRequest = "{\"title\": \"title1\", \"text\": \"text1\"}"; PostMethod post = httpPost("/notebook/" + note.getId() + "/paragraph", jsonRequest); @@ -619,7 +619,7 @@ public void testInsertParagraph() throws IOException { String newParagraphId = (String) resp.get("body"); LOG.info("newParagraphId:=" + newParagraphId); - Note retrNote = ZeppelinServer.notebook.getNote(note.getId()); + Note retrNote = ZeppelinServer.notebook.getNote(note.getId(), "anonymous"); Paragraph newParagraph = retrNote.getParagraph(newParagraphId); assertNotNull("Can not find new paragraph by id", newParagraph); @@ -640,12 +640,12 @@ public void testInsertParagraph() throws IOException { assertEquals("title2", paragraphAtIdx0.getTitle()); assertEquals("text2", paragraphAtIdx0.getText()); - ZeppelinServer.notebook.removeNote(note.getId()); + ZeppelinServer.notebook.removeNote(note.getId(), "anonymous"); } @Test public void testGetParagraph() throws IOException { - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); Paragraph p = note.addParagraph(); p.setTitle("hello"); @@ -669,12 +669,12 @@ public void testGetParagraph() throws IOException { assertEquals("hello", body.get("title")); assertEquals("world", body.get("text")); - ZeppelinServer.notebook.removeNote(note.getId()); + ZeppelinServer.notebook.removeNote(note.getId(), "anonymous"); } @Test public void testMoveParagraph() throws IOException { - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); Paragraph p = note.addParagraph(); p.setTitle("title1"); @@ -690,7 +690,7 @@ public void testMoveParagraph() throws IOException { assertThat("Test post method: ", post, isAllowed()); post.releaseConnection(); - Note retrNote = ZeppelinServer.notebook.getNote(note.getId()); + Note retrNote = ZeppelinServer.notebook.getNote(note.getId(), "anonymous"); Paragraph paragraphAtIdx0 = retrNote.getParagraphs().get(0); assertEquals(p2.getId(), paragraphAtIdx0.getId()); @@ -701,12 +701,12 @@ public void testMoveParagraph() throws IOException { assertThat("Test post method: ", post2, isBadRequest()); post.releaseConnection(); - ZeppelinServer.notebook.removeNote(note.getId()); + ZeppelinServer.notebook.removeNote(note.getId(), "anonymous"); } @Test public void testDeleteParagraph() throws IOException { - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); Paragraph p = note.addParagraph(); p.setTitle("title1"); @@ -718,11 +718,11 @@ public void testDeleteParagraph() throws IOException { assertThat("Test delete method: ", delete, isAllowed()); delete.releaseConnection(); - Note retrNote = ZeppelinServer.notebook.getNote(note.getId()); + Note retrNote = ZeppelinServer.notebook.getNote(note.getId(), "anonymous"); Paragraph retrParagrah = retrNote.getParagraph(p.getId()); assertNull("paragraph should be deleted", retrParagrah); - ZeppelinServer.notebook.removeNote(note.getId()); + ZeppelinServer.notebook.removeNote(note.getId(), "anonymous"); } } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java index ffe5d545b02..3cab4a574a9 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java @@ -68,7 +68,7 @@ private void waitForFinish(Paragraph p) { @Test public void basicRDDTransformationAndActionTest() throws IOException { // create new note - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); // run markdown paragraph, again Paragraph p = note.addParagraph(); @@ -80,13 +80,14 @@ public void basicRDDTransformationAndActionTest() throws IOException { waitForFinish(p); assertEquals(Status.FINISHED, p.getStatus()); assertEquals("55", p.getResult().message()); - ZeppelinServer.notebook.removeNote(note.id()); + ZeppelinServer.notebook.removeNote(note.id(), "anonymous"); } @Test public void pySparkTest() throws IOException { // create new note - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); + int sparkVersion = getSparkVersionNumber(note); if (isPyspark() && sparkVersion >= 12) { // pyspark supported from 1.2.1 @@ -101,13 +102,13 @@ public void pySparkTest() throws IOException { assertEquals(Status.FINISHED, p.getStatus()); assertEquals("55\n", p.getResult().message()); } - ZeppelinServer.notebook.removeNote(note.id()); + ZeppelinServer.notebook.removeNote(note.id(), "anonymous"); } @Test public void pySparkAutoConvertOptionTest() throws IOException { // create new note - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); int sparkVersion = getSparkVersionNumber(note); @@ -124,13 +125,13 @@ public void pySparkAutoConvertOptionTest() throws IOException { assertEquals(Status.FINISHED, p.getStatus()); assertEquals("10\n", p.getResult().message()); } - ZeppelinServer.notebook.removeNote(note.id()); + ZeppelinServer.notebook.removeNote(note.id(), "anonymous"); } @Test public void zRunTest() throws IOException { // create new note - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); Paragraph p0 = note.addParagraph(); Map config0 = p0.getConfig(); config0.put("enabled", true); @@ -156,18 +157,18 @@ public void zRunTest() throws IOException { assertEquals(Status.FINISHED, p2.getStatus()); assertEquals("10", p2.getResult().message()); - ZeppelinServer.notebook.removeNote(note.id()); + ZeppelinServer.notebook.removeNote(note.id(), "anonymous"); } @Test public void pySparkDepLoaderTest() throws IOException { // create new note - Note note = ZeppelinServer.notebook.createNote(); + Note note = ZeppelinServer.notebook.createNote("anonymous"); if (isPyspark() && getSparkVersionNumber(note) >= 14) { // restart spark interpreter List settings = - ZeppelinServer.notebook.getBindedInterpreterSettings(note.id()); + ZeppelinServer.notebook.getBindedInterpreterSettings(note.id(), "anonymous"); for (InterpreterSetting setting : settings) { if (setting.getGroup().equals("spark")) { diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/security/SecurityUtilsTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/security/SecurityUtilsTest.java index 0100bb7b08e..c45156e4e74 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/security/SecurityUtilsTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/security/SecurityUtilsTest.java @@ -19,7 +19,7 @@ import static org.junit.Assert.*; import org.apache.commons.configuration.ConfigurationException; import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.utils.SecurityUtils; +import org.apache.zeppelin.ticket.SecurityUtils; import org.junit.Test; import java.net.URISyntaxException; 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 67d12b7edf2..1a4b029818a 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 @@ -86,7 +86,7 @@ public void checkInvalidOrigin(){ @Test public void testMakeSureNoAngularObjectBroadcastToWebsocketWhoFireTheEvent() throws IOException { // create a notebook - Note note1 = notebook.createNote(); + Note note1 = notebook.createNote("anonymous"); // get reference to interpreterGroup InterpreterGroup interpreterGroup = null; @@ -133,7 +133,7 @@ public void testMakeSureNoAngularObjectBroadcastToWebsocketWhoFireTheEvent() thr verify(sock1, times(0)).send(anyString()); verify(sock2, times(1)).send(anyString()); - notebook.removeNote(note1.getId()); + notebook.removeNote(note1.getId(), "anonymous"); } @Test @@ -151,10 +151,10 @@ public void testImportNotebook() throws IOException { //broadcastNoteList(); failed nothing to worry. } - assertNotEquals(null, notebook.getNote(note.getId())); - assertEquals("Test Zeppelin notebook import", notebook.getNote(note.getId()).getName()); - assertEquals("Test paragraphs import", notebook.getNote(note.getId()).getParagraphs().get(0).getText()); - notebook.removeNote(note.getId()); + assertNotEquals(null, notebook.getNote(note.getId(), "anonymous")); + assertEquals("Test Zeppelin notebook import", notebook.getNote(note.getId(), "anonymous").getName()); + assertEquals("Test paragraphs import", notebook.getNote(note.getId(), "anonymous").getParagraphs().get(0).getText()); + notebook.removeNote(note.getId(), "anonymous"); } private NotebookSocket createWebSocket() { diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/TicketContainerTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/TicketContainerTest.java new file mode 100644 index 00000000000..b3958055d70 --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/TicketContainerTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zeppelin.socket; + +import org.apache.zeppelin.ticket.TicketContainer; +import org.junit.Before; +import org.junit.Test; + +import java.net.UnknownHostException; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TicketContainerTest { + private TicketContainer container; + + @Before + public void setUp() throws Exception { + container = TicketContainer.instance; + } + + @Test + public void isValidAnonymous() throws UnknownHostException { + boolean ok = container.isValid("anonymous", "anonymous"); + assertTrue(ok); + } + + @Test + public void isValidExistingPrincipal() throws UnknownHostException { + String ticket = container.getTicket("someuser1"); + boolean ok = container.isValid("someuser1", ticket); + assertTrue(ok); + } + + @Test + public void isValidNonExistingPrincipal() throws UnknownHostException { + boolean ok = container.isValid("unknownuser", "someticket"); + assertFalse(ok); + } + + @Test + public void isValidunkownTicket() throws UnknownHostException { + String ticket = container.getTicket("someuser2"); + boolean ok = container.isValid("someuser2", ticket+"makeitinvalid"); + assertFalse(ok); + } +} + diff --git a/zeppelin-web/src/app/home/home.controller.js b/zeppelin-web/src/app/home/home.controller.js index 64ff8801557..691ee57d0a9 100644 --- a/zeppelin-web/src/app/home/home.controller.js +++ b/zeppelin-web/src/app/home/home.controller.js @@ -14,7 +14,13 @@ 'use strict'; angular.module('zeppelinWebApp').controller('HomeCtrl', function($scope, notebookListDataFactory, websocketMsgSrv, $rootScope, arrayOrderingSrv) { - + if (!$rootScope.ticket) { + $rootScope.ticket = { + 'principal':'anonymous', + 'ticket':'anonymous' + }; + } + var vm = this; vm.notes = notebookListDataFactory; vm.websocketMsgSrv = websocketMsgSrv; diff --git a/zeppelin-web/src/components/navbar/navbar.controller.js b/zeppelin-web/src/components/navbar/navbar.controller.js index 30e6ac27892..75cc4e658d4 100644 --- a/zeppelin-web/src/components/navbar/navbar.controller.js +++ b/zeppelin-web/src/components/navbar/navbar.controller.js @@ -15,7 +15,13 @@ 'use strict'; angular.module('zeppelinWebApp').controller('NavCtrl', function($scope, $rootScope, $routeParams, - $location, notebookListDataFactory, websocketMsgSrv, arrayOrderingSrv) { + $location, notebookListDataFactory, websocketMsgSrv, arrayOrderingSrv, $http) { + if (!$rootScope.ticket) { + $rootScope.ticket = { + 'principal':'anonymous', + 'ticket':'anonymous' + }; + } /** Current list of notes (ids) */ var vm = this; @@ -23,6 +29,7 @@ angular.module('zeppelinWebApp').controller('NavCtrl', function($scope, $rootSco vm.connected = websocketMsgSrv.isConnected(); vm.websocketMsgSrv = websocketMsgSrv; vm.arrayOrderingSrv = arrayOrderingSrv; + vm.authenticated = $rootScope.ticket.principal !== 'anonymous'; angular.element('#notebook-list').perfectScrollbar({suppressScrollX: true}); @@ -51,13 +58,28 @@ angular.module('zeppelinWebApp').controller('NavCtrl', function($scope, $rootSco websocketMsgSrv.getNotebookList(); } + /** ask for a ticket for websocket access + * Shiro will require credentials here + * */ + $http.get('/api/security/ticket'). + success(function(ticket, status, headers, config) { + if (status === 401 || status === 403) { + // Dislay error message here + } + else { + $rootScope.ticket = angular.fromJson(ticket).body; + vm.loadNotes = loadNotes; + vm.isActive = isActive; + vm.loadNotes(); + vm.authenticated = $rootScope.ticket.principal !== 'anonymous'; + } + }). + error(function(data, status, headers, config) { + console.log('Could not get ticket'); + }); + function isActive(noteId) { return ($routeParams.noteId === noteId); } - vm.loadNotes = loadNotes; - vm.isActive = isActive; - - vm.loadNotes(); - }); diff --git a/zeppelin-web/src/components/navbar/navbar.html b/zeppelin-web/src/components/navbar/navbar.html index 86a85122add..20ee0241c90 100644 --- a/zeppelin-web/src/components/navbar/navbar.html +++ b/zeppelin-web/src/components/navbar/navbar.html @@ -73,8 +73,8 @@
  • - Connected - Disconnected + {{ticket.principal}} connected + Disconnected
  • diff --git a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js index dad2cb5545f..68b1ed40496 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js +++ b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js @@ -28,7 +28,9 @@ angular.module('zeppelinWebApp').factory('websocketEvents', function($rootScope, }); websocketCalls.sendNewEvent = function(data) { - console.log('Send >> %o, %o', data.op, data); + data.principal = $rootScope.ticket.principal; + data.ticket = $rootScope.ticket.ticket; + console.log('Send >> %o, %o, %o, %o', data.op, data.principal, data.ticket, data); websocketCalls.ws.send(JSON.stringify(data)); }; diff --git a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js index a10bc875242..8762cdfd6d6 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js +++ b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js @@ -122,7 +122,7 @@ angular.module('zeppelinWebApp').service('websocketMsgSrv', function($rootScope, }); }, - isConnected: function(){ + isConnected: function() { return websocketEvents.isConnected(); } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java index 87d1c20aa00..02b1a900c75 100755 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java @@ -433,7 +433,8 @@ public static enum ConfVars { ZEPPELIN_CONF_DIR("zeppelin.conf.dir", "conf"), // Allows a way to specify a ',' separated list of allowed origins for rest and websockets // i.e. http://localhost:8080 - ZEPPELIN_ALLOWED_ORIGINS("zeppelin.server.allowed.origins", "*"); + ZEPPELIN_ALLOWED_ORIGINS("zeppelin.server.allowed.origins", "*"), + ZEPPELIN_ANONYMOUS_ALLOWED("zeppelin.anonymous.allowed", true); private String varName; @SuppressWarnings("rawtypes") 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 392b9682ab2..43559eb26c8 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 @@ -54,6 +54,7 @@ public class Note implements Serializable, JobListener { final List paragraphs = new LinkedList<>(); private String name = ""; private String id; + private String owner = "anonymous"; @SuppressWarnings("rawtypes") Map> angularObjects = new HashMap<>(); @@ -80,12 +81,14 @@ public class Note implements Serializable, JobListener { public Note() {} - public Note(NotebookRepo repo, NoteInterpreterLoader replLoader, - JobListenerFactory jlFactory, SearchService noteIndex) { + public Note(NotebookRepo repo, + NoteInterpreterLoader replLoader, + JobListenerFactory jobListenerFactory, SearchService noteIndex, String owner) { this.repo = repo; this.replLoader = replLoader; - this.jobListenerFactory = jlFactory; + this.jobListenerFactory = jobListenerFactory; this.index = noteIndex; + this.owner = owner; generateId(); } @@ -101,6 +104,10 @@ public String getId() { return id; } + public String getOwner() { return this.owner; } + + public String setOwner() { return this.owner; } + public String getName() { return name; } @@ -145,7 +152,6 @@ public Map> getAngularObjects() { /** * Add paragraph last. * - * @param p */ public Paragraph addParagraph() { Paragraph p = new Paragraph(this, this, replLoader); @@ -187,7 +193,6 @@ public void addCloneParagraph(Paragraph srcParagraph) { * Insert paragraph in given index. * * @param index - * @param p */ public Paragraph insertParagraph(int index) { Paragraph p = new Paragraph(this, this, replLoader); @@ -340,7 +345,6 @@ public List> generateParagraphsInfo (){ /** * Run all paragraphs sequentially. * - * @param jobListener */ public void runAll() { synchronized (paragraphs) { @@ -406,7 +410,7 @@ public void persist() throws IOException { } public void unpersist() throws IOException { - repo.remove(id()); + repo.remove(id(), this.owner); } public Map getConfig() { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteInfo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteInfo.java index f75a107e36d..94438caf7fa 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteInfo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteInfo.java @@ -26,18 +26,21 @@ public class NoteInfo { String id; String name; + String owner; private Map config = new HashMap(); - public NoteInfo(String id, String name, Map config) { + public NoteInfo(String id, String name, String owner, Map config) { super(); this.id = id; this.name = name; + this.owner = owner; this.config = config; } public NoteInfo(Note note) { id = note.id(); name = note.getName(); + owner = note.getOwner(); config = note.getConfig(); } @@ -57,6 +60,10 @@ public void setName(String name) { this.name = name; } + public String getOwner() { return owner; } + + public void setOwner(String owner) { this.owner = owner; } + public Map getConfig() { return config; } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java index 79d0a0d980d..dde90321143 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java @@ -18,15 +18,7 @@ package org.apache.zeppelin.notebook; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.TimeUnit; import org.apache.zeppelin.conf.ZeppelinConfiguration; @@ -64,7 +56,7 @@ public class Notebook { private InterpreterFactory replFactory; /** Keep the order. */ - Map notes = new LinkedHashMap(); + Map> notes = new LinkedHashMap<>(); private ZeppelinConfiguration conf; private StdSchedulerFactory quertzSchedFact; private org.quartz.Scheduler quartzSched; @@ -104,7 +96,11 @@ public Notebook(ZeppelinConfiguration conf, NotebookRepo notebookRepo, if (this.notebookIndex != null) { long start = System.nanoTime(); logger.info("Notebook indexing started..."); - notebookIndex.addIndexDocs(notes.values()); + Collection notesToIndex = new ArrayList<>(); + for (Map userNotes : notes.values()) { + notesToIndex.addAll(userNotes.values()); + } + notebookIndex.addIndexDocs(notesToIndex); logger.info("Notebook indexing finished: {} indexed in {}s", notes.size(), TimeUnit.NANOSECONDS.toSeconds(start - System.nanoTime())); } @@ -117,32 +113,41 @@ public Notebook(ZeppelinConfiguration conf, NotebookRepo notebookRepo, * @return * @throws IOException */ - public Note createNote() throws IOException { + public Note createNote(String principal) throws IOException { Note note; if (conf.getBoolean(ConfVars.ZEPPELIN_NOTEBOOK_AUTO_INTERPRETER_BINDING)) { - note = createNote(replFactory.getDefaultInterpreterSettingList()); + note = createNote(replFactory.getDefaultInterpreterSettingList(), principal); } else { - note = createNote(null); + note = createNote(null, principal); } notebookIndex.addIndexDoc(note); return note; } + private Map getUserNotes(String principal) { + synchronized (notes) { + Map userNotes = notes.get(principal); + if (userNotes == null) + userNotes = new HashMap<>(); + notes.put(principal, userNotes); + return userNotes; + } + } /** * Create new note. * * @return * @throws IOException */ - public Note createNote(List interpreterIds) throws IOException { + public Note createNote(List interpreterIds, String principal) throws IOException { NoteInterpreterLoader intpLoader = new NoteInterpreterLoader(replFactory); - Note note = new Note(notebookRepo, intpLoader, jobListenerFactory, notebookIndex); + Note note = new Note(notebookRepo, intpLoader, jobListenerFactory, notebookIndex, principal); intpLoader.setNoteId(note.id()); synchronized (notes) { - notes.put(note.id(), note); + getUserNotes(principal).put(note.id(), note); } if (interpreterIds != null) { - bindInterpretersToNote(note.id(), interpreterIds); + bindInterpretersToNote(note.id(), interpreterIds, principal); } notebookIndex.addIndexDoc(note); @@ -157,20 +162,21 @@ public Note createNote(List interpreterIds) throws IOException { * @return noteId * @throws IOException, CloneNotSupportedException, IllegalArgumentException */ - public Note cloneNote(String sourceNoteID, String newNoteName) throws + public Note cloneNote(String sourceNoteID, String newNoteName, String principal) throws IOException, CloneNotSupportedException, IllegalArgumentException { - Note sourceNote = getNote(sourceNoteID); + Note sourceNote = getNote(sourceNoteID, principal); if (sourceNote == null) { throw new IllegalArgumentException(sourceNoteID + "not found"); } - Note newNote = createNote(); + Note newNote = createNote(principal); if (newNoteName != null) { newNote.setName(newNoteName); } // Copy the interpreter bindings - List boundInterpreterSettingsIds = getBindedInterpreterSettingsIds(sourceNote.id()); - bindInterpretersToNote(newNote.id(), boundInterpreterSettingsIds); + List boundInterpreterSettingsIds = + getBindedInterpreterSettingsIds(sourceNote.id(), principal); + bindInterpretersToNote(newNote.id(), boundInterpreterSettingsIds, principal); List paragraphs = sourceNote.getParagraphs(); for (Paragraph p : paragraphs) { @@ -183,16 +189,16 @@ public Note cloneNote(String sourceNoteID, String newNoteName) throws } public void bindInterpretersToNote(String id, - List interpreterSettingIds) throws IOException { - Note note = getNote(id); + List interpreterSettingIds, String principal) throws IOException { + Note note = getNote(id, principal); if (note != null) { note.getNoteReplLoader().setInterpreters(interpreterSettingIds); replFactory.putNoteInterpreterSettingBinding(id, interpreterSettingIds); } } - public List getBindedInterpreterSettingsIds(String id) { - Note note = getNote(id); + public List getBindedInterpreterSettingsIds(String id, String principal) { + Note note = getNote(id, principal); if (note != null) { return note.getNoteReplLoader().getInterpreters(); } else { @@ -200,8 +206,8 @@ public List getBindedInterpreterSettingsIds(String id) { } } - public List getBindedInterpreterSettings(String id) { - Note note = getNote(id); + public List getBindedInterpreterSettings(String id, String principal) { + Note note = getNote(id, principal); if (note != null) { return note.getNoteReplLoader().getInterpreterSettings(); } else { @@ -209,17 +215,17 @@ public List getBindedInterpreterSettings(String id) { } } - public Note getNote(String id) { + public Note getNote(String id, String principal) { synchronized (notes) { - return notes.get(id); + return getUserNotes(principal).get(id); } } - public void removeNote(String id) { + public void removeNote(String id, String principal) { Note note; synchronized (notes) { - note = notes.remove(id); + note = getUserNotes(principal).remove(id); } notebookIndex.deleteIndexDocs(note); @@ -241,10 +247,10 @@ public void removeNote(String id) { } @SuppressWarnings("rawtypes") - private Note loadNoteFromRepo(String id) { + private Note loadNoteFromRepo(String id, String owner) { Note note = null; try { - note = notebookRepo.get(id); + note = notebookRepo.get(id, owner); } catch (IOException e) { logger.error("Failed to load " + id, e); } @@ -291,8 +297,8 @@ private Note loadNoteFromRepo(String id) { } synchronized (notes) { - notes.put(note.id(), note); - refreshCron(note.id()); + getUserNotes(owner).put(note.id(), note); + refreshCron(note.id(), owner); } for (String name : angularObjectSnapshot.keySet()) { @@ -317,9 +323,8 @@ private Note loadNoteFromRepo(String id) { private void loadAllNotes() throws IOException { List noteInfos = notebookRepo.list(); - for (NoteInfo info : noteInfos) { - loadNoteFromRepo(info.getId()); + loadNoteFromRepo(info.getId(), info.getOwner()); } } @@ -336,7 +341,7 @@ private void reloadAllNotes() throws IOException { } List noteInfos = notebookRepo.list(); for (NoteInfo info : noteInfos) { - loadNoteFromRepo(info.getId()); + loadNoteFromRepo(info.getId(), info.getOwner()); } } @@ -365,6 +370,31 @@ public Date getLastUpdate() { } } + public List getAllNotes(String principal) { + synchronized (notes) { + List noteList = new ArrayList<>(getUserNotes(principal).values()); + Collections.sort(noteList, new Comparator() { + @Override + public int compare(Object one, Object two) { + Note note1 = (Note) one; + Note note2 = (Note) two; + + String name1 = note1.id(); + if (note1.getName() != null) { + name1 = note1.getName(); + } + String name2 = note2.id(); + if (note2.getName() != null) { + name2 = note2.getName(); + } + ((Note) one).getName(); + return name1.compareTo(name2); + } + }); + return noteList; + } + } + public List getAllNotes() { if (conf.getBoolean(ConfVars.ZEPPELIN_NOTEBOOK_RELOAD_FROM_STORAGE)) { try { @@ -374,7 +404,11 @@ public List getAllNotes() { } } synchronized (notes) { - List noteList = new ArrayList(notes.values()); + Collection> usersNotes = notes.values(); + List noteList = new ArrayList<>(); + for (Map userNotes : usersNotes) { + noteList.addAll(userNotes.values()); + } Collections.sort(noteList, new Comparator() { @Override public int compare(Note note1, Note note2) { @@ -409,9 +443,9 @@ public static class CronJob implements org.quartz.Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { - + String principal = context.getJobDetail().getJobDataMap().getString("principal"); String noteId = context.getJobDetail().getJobDataMap().getString("noteId"); - Note note = notebook.getNote(noteId); + Note note = notebook.getNote(noteId, principal); note.runAll(); while (!note.getLastParagraph().isTerminated()) { @@ -436,11 +470,11 @@ public void execute(JobExecutionContext context) throws JobExecutionException { } } - public void refreshCron(String id) { + public void refreshCron(String id, String principal) { removeCron(id); synchronized (notes) { - Note note = notes.get(id); + Note note = getUserNotes(principal).get(id); if (note == null) { return; } @@ -456,8 +490,11 @@ public void refreshCron(String id) { JobDetail newJob = - JobBuilder.newJob(CronJob.class).withIdentity(id, "note").usingJobData("noteId", id) - .build(); + JobBuilder.newJob(CronJob.class) + .withIdentity(id, "note") + .usingJobData("noteId", id) + .usingJobData("principal", principal) + .build(); Map info = note.getInfo(); info.put("cron", null); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GitNotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GitNotebookRepo.java index fe4975353eb..ab7f31e21a1 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GitNotebookRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/GitNotebookRepo.java @@ -87,9 +87,9 @@ private void maybeAddAndCommit(String pattern) { } @Override - public Note get(String noteId, String rev) throws IOException { + public Note get(String noteId, String rev, String owner) throws IOException { //TODO(bzz): something like 'git checkout rev', that will not change-the-world though - return super.get(noteId); + return super.get(noteId, owner); } @Override diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepo.java index f8e0b57fa4b..604ddcd07ca 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepo.java @@ -28,10 +28,10 @@ */ public interface NotebookRepo { public List list() throws IOException; - public Note get(String noteId) throws IOException; + public List list(String owner) throws IOException; + public Note get(String noteId, String owner) throws IOException; public void save(Note note) throws IOException; - public void remove(String noteId) throws IOException; - + public void remove(String noteId, String owner) throws IOException; /** * Release any underlying resources */ 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 a5bf6b3dfdd..fb0d39e42ed 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 @@ -46,11 +46,11 @@ public class NotebookRepoSync implements NotebookRepo { private List repos = new ArrayList(); /** - * @param noteIndex * @param (conf) * @throws - Exception */ public NotebookRepoSync(ZeppelinConfiguration conf) throws Exception { + config = conf; String allStorageClassNames = conf.getString(ConfVars.ZEPPELIN_NOTEBOOK_STORAGE).trim(); @@ -86,22 +86,30 @@ public List list() throws IOException { return getRepo(0).list(); } + public List list(String owner) throws IOException { + return getRepo(0).list(owner); + } + /* list from specific repo (for tests) */ List list(int repoIndex) throws IOException { return getRepo(repoIndex).list(); } + List list(int repoIndex, String owner) throws IOException { + return getRepo(repoIndex).list(owner); + } + /** * Returns from Notebook from the first repository */ @Override - public Note get(String noteId) throws IOException { - return getRepo(0).get(noteId); + public Note get(String noteId, String owner) throws IOException { + return getRepo(0).get(noteId, owner); } /* get note from specific repo (for tests) */ - Note get(int repoIndex, String noteId) throws IOException { - return getRepo(repoIndex).get(noteId); + Note get(int repoIndex, String noteId, String owner) throws IOException { + return getRepo(repoIndex).get(noteId, owner); } /** @@ -126,9 +134,9 @@ void save(int repoIndex, Note note) throws IOException { } @Override - public void remove(String noteId) throws IOException { + public void remove(String noteId, String owner) throws IOException { for (NotebookRepo repo : repos) { - repo.remove(noteId); + repo.remove(noteId, owner); } /* TODO(khalid): handle case when removing from secondary storage fails */ } @@ -144,14 +152,16 @@ void sync(int sourceRepoIndex, int destRepoIndex) throws IOException { NotebookRepo dstRepo = getRepo(destRepoIndex); List srcNotes = srcRepo.list(); List dstNotes = dstRepo.list(); - - Map> noteIDs = notesCheckDiff(srcNotes, srcRepo, dstNotes, dstRepo); - List pushNoteIDs = noteIDs.get(pushKey); - List pullNoteIDs = noteIDs.get(pullKey); + Map> noteIDs = notesCheckDiff(srcNotes, + srcRepo, + dstNotes, + dstRepo); + List pushNoteIDs = noteIDs.get(pushKey); + List pullNoteIDs = noteIDs.get(pullKey); if (!pushNoteIDs.isEmpty()) { LOG.info("Notes with the following IDs will be pushed"); - for (String id : pushNoteIDs) { - LOG.info("ID : " + id); + for (NoteInfo noteInfo : pushNoteIDs) { + LOG.info("ID : " + noteInfo.getId()); } pushNotes(pushNoteIDs, srcRepo, dstRepo); } else { @@ -160,8 +170,8 @@ void sync(int sourceRepoIndex, int destRepoIndex) throws IOException { if (!pullNoteIDs.isEmpty()) { LOG.info("Notes with the following IDs will be pulled"); - for (String id : pullNoteIDs) { - LOG.info("ID : " + id); + for (NoteInfo noteInfo : pullNoteIDs) { + LOG.info("ID : " + noteInfo.getId()); } pushNotes(pullNoteIDs, dstRepo, srcRepo); } else { @@ -175,10 +185,10 @@ public void sync() throws IOException { sync(0, 1); } - private void pushNotes(List ids, NotebookRepo localRepo, - NotebookRepo remoteRepo) throws IOException { - for (String id : ids) { - remoteRepo.save(localRepo.get(id)); + private void pushNotes(List noteInfos, NotebookRepo localRepo, + NotebookRepo remoteRepo) throws IOException { + for (NoteInfo noteInfo: noteInfos) { + remoteRepo.save(localRepo.get(noteInfo.getId(), noteInfo.getOwner())); } } @@ -197,34 +207,35 @@ private NotebookRepo getRepo(int repoIndex) throws IOException { return repos.get(repoIndex); } - private Map> notesCheckDiff(List sourceNotes, - NotebookRepo sourceRepo, List destNotes, NotebookRepo destRepo) - throws IOException { - List pushIDs = new ArrayList(); - List pullIDs = new ArrayList(); - + private Map> notesCheckDiff(List sourceNotes, + NotebookRepo sourceRepo, + List destNotes, + NotebookRepo destRepo) throws IOException { + List pushIDs = new ArrayList(); + List pullIDs = new ArrayList(); + NoteInfo dnote; Date sdate, ddate; for (NoteInfo snote : sourceNotes) { dnote = containsID(destNotes, snote.getId()); if (dnote != null) { /* note exists in source and destination storage systems */ - sdate = lastModificationDate(sourceRepo.get(snote.getId())); - ddate = lastModificationDate(destRepo.get(dnote.getId())); + sdate = lastModificationDate(sourceRepo.get(snote.getId(), snote.getOwner())); + ddate = lastModificationDate(destRepo.get(dnote.getId(), snote.getOwner())); if (sdate.after(ddate)) { /* source contains more up to date note - push */ - pushIDs.add(snote.getId()); + pushIDs.add(snote); LOG.info("Modified note is added to push list : " + sdate); } else if (sdate.compareTo(ddate) != 0) { /* destination contains more up to date note - pull */ LOG.info("Modified note is added to pull list : " + ddate); - pullIDs.add(snote.getId()); + pullIDs.add(snote); } } else { /* note exists in source storage, and absent in destination * view source as up to date - push * (another scenario : note was deleted from destination - not considered)*/ - pushIDs.add(snote.getId()); + pushIDs.add(snote); } } @@ -232,11 +243,10 @@ private Map> notesCheckDiff(List sourceNotes, dnote = containsID(sourceNotes, note.getId()); if (dnote == null) { /* note exists in destination storage, and absent in source - pull*/ - pullIDs.add(note.getId()); + pullIDs.add(note); } } - - Map> map = new HashMap>(); + Map> map = new HashMap>(); map.put(pushKey, pushIDs); map.put(pullKey, pullIDs); return map; @@ -318,5 +328,4 @@ public void close() { repo.close(); } } - } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoVersioned.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoVersioned.java index 4615afd900d..f75047c0ecf 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoVersioned.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoVersioned.java @@ -32,10 +32,11 @@ public interface NotebookRepoVersioned extends NotebookRepo { * * @param noteId Id of the Notebook * @param rev revision of the Notebook + * @param owner revision of the Notebook * @return a Notebook * @throws IOException */ - public Note get(String noteId, String rev) throws IOException; + public Note get(String noteId, String rev, String owner) throws IOException; /** * List of revisions of the given Notebook diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java index 870aa8635d7..49a75cdca55 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/S3NotebookRepo.java @@ -84,12 +84,18 @@ public S3NotebookRepo(ZeppelinConfiguration conf) throws IOException { @Override public List list() throws IOException { + return list(null); + } + + @Override + public List list(String owner) throws IOException { List infos = new LinkedList(); NoteInfo info = null; + String ownerPath = owner == null ? "" : "/users/" + owner; try { ListObjectsRequest listObjectsRequest = new ListObjectsRequest() .withBucketName(bucketName) - .withPrefix(user + "/" + "notebook"); + .withPrefix(user + "/" + "notebook" + ownerPath); ObjectListing objectListing; do { objectListing = s3client.listObjects(listObjectsRequest); @@ -149,8 +155,8 @@ private NoteInfo getNoteInfo(String key) throws IOException { } @Override - public Note get(String noteId) throws IOException { - return getNote(user + "/" + "notebook" + "/" + noteId + "/" + "note.json"); + public Note get(String noteId, String owner) throws IOException { + return getNote(user + "/notebook/users/" + owner + "/" + noteId + "/note.json"); } @Override @@ -159,8 +165,9 @@ public void save(Note note) throws IOException { gsonBuilder.setPrettyPrinting(); Gson gson = gsonBuilder.create(); String json = gson.toJson(note); - String key = user + "/" + "notebook" + "/" + note.id() + "/" + "note.json"; - + String key = user + "/notebook/users/" + + note.getOwner() + "/" + note.id() + "/note.json"; + File file = File.createTempFile("note", "json"); file.deleteOnExit(); Writer writer = new OutputStreamWriter(new FileOutputStream(file)); @@ -171,8 +178,9 @@ public void save(Note note) throws IOException { } @Override - public void remove(String noteId) throws IOException { - String key = user + "/" + "notebook" + "/" + noteId; + public void remove(String noteId, String owner) throws IOException { + + String key = user + "/notebook/users/" + owner + "/" + noteId; final ListObjectsRequest listObjectsRequest = new ListObjectsRequest() .withBucketName(bucketName).withPrefix(key); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java index 67f9b643888..368f97f1b94 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepo.java @@ -99,8 +99,8 @@ private boolean isDirectory(FileObject fo) throws IOException { } @Override - public List list() throws IOException { - FileObject rootDir = getRootDir(); + public List list(String owner) throws IOException { + FileObject rootDir = getRootDir(owner); FileObject[] children = rootDir.getChildren(); @@ -108,9 +108,9 @@ public List list() throws IOException { for (FileObject f : children) { String fileName = f.getName().getBaseName(); if (f.isHidden() - || fileName.startsWith(".") - || fileName.startsWith("#") - || fileName.startsWith("~")) { + || fileName.startsWith(".") + || fileName.startsWith("#") + || fileName.startsWith("~")) { // skip hidden, temporary files continue; } @@ -132,7 +132,45 @@ public List list() throws IOException { logger.error("Can't read note " + f.getName().toString(), e); } } + return infos; + } + + @Override + public List list() throws IOException { + FileObject rootDir = fsManager.resolveFile(getRootDir(), "users"); + if (!rootDir.exists()) + rootDir.createFolder(); + + logger.info(rootDir.getName().getPath()); + FileObject[] children = rootDir.getChildren(); + + List infos = new LinkedList<>(); + for (FileObject f : children) { + String owner = f.getName().getBaseName(); + if (f.isHidden() + || owner.startsWith(".") + || owner.startsWith("#") + || owner.startsWith("~")) { + // skip hidden, temporary files + continue; + } + logger.info("OWNER=" + owner); + + if (!isDirectory(f)) { + // currently one directory per user saved like, users/[OWNER]/[NOTE_ID]/note.json. + // so it must be a directory + continue; + } + try { + List ownerInfos = list(owner); + if (ownerInfos != null) { + infos.addAll(ownerInfos); + } + } catch (IOException e) { + logger.error("Can't read note " + f.getName().toString(), e); + } + } return infos; } @@ -174,15 +212,15 @@ private NoteInfo getNoteInfo(FileObject noteDir) throws IOException { } @Override - public Note get(String noteId) throws IOException { - FileObject rootDir = fsManager.resolveFile(getPath("/")); + public Note get(String noteId, String owner) throws IOException { + FileObject rootDir = fsManager.resolveFile(getPath("/users/" + owner)); FileObject noteDir = rootDir.resolveFile(noteId, NameScope.CHILD); return getNote(noteDir); } - protected FileObject getRootDir() throws IOException { - FileObject rootDir = fsManager.resolveFile(getPath("/")); + protected FileObject getRootDir(String owner) throws IOException { + FileObject rootDir = fsManager.resolveFile(getPath(owner != null ? "/users/" + owner : "/")); if (!rootDir.exists()) { throw new IOException("Root path does not exists"); @@ -195,16 +233,29 @@ protected FileObject getRootDir() throws IOException { return rootDir; } + protected FileObject getRootDir() throws IOException { + return getRootDir(null); + } + @Override public synchronized void save(Note note) throws IOException { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.setPrettyPrinting(); Gson gson = gsonBuilder.create(); String json = gson.toJson(note); - FileObject rootDir = getRootDir(); - FileObject noteDir = rootDir.resolveFile(note.id(), NameScope.CHILD); + rootDir.createFolder(); + + FileObject usersDir = fsManager.resolveFile(rootDir, "users"); + if (!usersDir.exists()) + usersDir.createFolder(); + + FileObject ownerDir = fsManager.resolveFile(usersDir, note.getOwner()); + if (!ownerDir.exists()) + ownerDir.createFolder(); + + FileObject noteDir = ownerDir.resolveFile(note.id(), NameScope.CHILD); if (!noteDir.exists()) { noteDir.createFolder(); @@ -222,8 +273,8 @@ public synchronized void save(Note note) throws IOException { } @Override - public void remove(String noteId) throws IOException { - FileObject rootDir = fsManager.resolveFile(getPath("/")); + public void remove(String noteId, String owner) throws IOException { + FileObject rootDir = fsManager.resolveFile(getPath("/users/" + owner)); FileObject noteDir = rootDir.resolveFile(noteId, NameScope.CHILD); if (!noteDir.exists()) { diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/search/LuceneSearch.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/search/LuceneSearch.java index 7f9cbbdd9db..d1963b65b39 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/search/LuceneSearch.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/search/LuceneSearch.java @@ -72,6 +72,7 @@ public class LuceneSearch implements SearchService { private static final String SEARCH_FIELD = "contents"; static final String PARAGRAPH = "paragraph"; static final String ID_FIELD = "id"; + static final String OWNER_FIELD = "owner"; Directory ramDirectory; Analyzer analyzer; @@ -93,7 +94,7 @@ public LuceneSearch() { * @see org.apache.zeppelin.search.Search#query(java.lang.String) */ @Override - public List> query(String queryStr) { + public List> query(String queryStr, String owner) { if (null == ramDirectory) { throw new IllegalStateException( "Something went wrong on instance creation time, index dir is null"); @@ -110,7 +111,7 @@ public List> query(String queryStr) { SimpleHTMLFormatter htmlFormatter = new SimpleHTMLFormatter(); Highlighter highlighter = new Highlighter(htmlFormatter, new QueryScorer(query)); - result = doSearch(indexSearcher, query, analyzer, highlighter); + result = doSearch(indexSearcher, query, analyzer, highlighter, owner); indexReader.close(); } catch (IOException e) { LOG.error("Failed to open index dir {}, make sure indexing finished OK", ramDirectory, e); @@ -121,7 +122,7 @@ public List> query(String queryStr) { } private List> doSearch(IndexSearcher searcher, Query query, - Analyzer analyzer, Highlighter highlighter) { + Analyzer analyzer, Highlighter highlighter, String owner) { List> matchingParagraphs = Lists.newArrayList(); ScoreDoc[] hits; try { @@ -132,29 +133,33 @@ private List> doSearch(IndexSearcher searcher, Query query, int id = hits[i].doc; Document doc = searcher.doc(id); String path = doc.get(ID_FIELD); - if (path != null) { - LOG.debug((i + 1) + ". " + path); - String title = doc.get("title"); - if (title != null) { - LOG.debug(" Title: {}", doc.get("title")); - } + String docOwner = doc.get(OWNER_FIELD); + + if (docOwner != null && docOwner.equalsIgnoreCase(owner)) { + if (path != null) { + LOG.debug((i + 1) + ". " + path); + String title = doc.get("title"); + if (title != null) { + LOG.debug(" Title: {}", doc.get("title")); + } - String text = doc.get(SEARCH_FIELD); - TokenStream tokenStream = TokenSources.getTokenStream(searcher.getIndexReader(), id, - SEARCH_FIELD, analyzer); - TextFragment[] frag = highlighter.getBestTextFragments(tokenStream, text, true, 3); - LOG.debug(" {} fragments found for query '{}'", frag.length, query); - for (int j = 0; j < frag.length; j++) { - if ((frag[j] != null) && (frag[j].getScore() > 0)) { - LOG.debug(" Fragment: {}", frag[j].toString()); + String text = doc.get(SEARCH_FIELD); + TokenStream tokenStream = TokenSources.getTokenStream(searcher.getIndexReader(), id, + SEARCH_FIELD, analyzer); + TextFragment[] frag = highlighter.getBestTextFragments(tokenStream, text, true, 3); + LOG.debug(" {} fragments found for query '{}'", frag.length, query); + for (int j = 0; j < frag.length; j++) { + if ((frag[j] != null) && (frag[j].getScore() > 0)) { + LOG.debug(" Fragment: {}", frag[j].toString()); + } } - } - String fragment = (frag != null && frag.length > 0) ? frag[0].toString() : ""; + String fragment = (frag != null && frag.length > 0) ? frag[0].toString() : ""; - matchingParagraphs.add(ImmutableMap.of("id", path, // /paragraph/ - "name", title, "snippet", fragment, "text", text)); - } else { - LOG.info("{}. No {} for this document", i + 1, ID_FIELD); + matchingParagraphs.add(ImmutableMap.of("id", path, // /paragraph/ + "name", title, "snippet", fragment, "text", text)); + } else { + LOG.info("{}. No {} for this document", i + 1, ID_FIELD); + } } } } catch (IOException | InvalidTokenOffsetsException e) { @@ -182,7 +187,7 @@ private void updateIndexNoteName(Note note) throws IOException { LOG.debug("Skipping empty notebook name"); return; } - updateDoc(noteId, noteName, null); + updateDoc(noteId, noteName, null, note.getOwner()); } private void updateIndexParagraph(Note note, Paragraph p) throws IOException { @@ -190,7 +195,7 @@ private void updateIndexParagraph(Note note, Paragraph p) throws IOException { LOG.debug("Skipping empty paragraph"); return; } - updateDoc(note.getId(), note.getName(), p); + updateDoc(note.getId(), note.getName(), p, note.getOwner()); } /** @@ -202,9 +207,10 @@ private void updateIndexParagraph(Note note, Paragraph p) throws IOException { * @param p * @throws IOException */ - private void updateDoc(String noteId, String noteName, Paragraph p) throws IOException { + private void updateDoc(String noteId, String noteName, Paragraph p, String owner) + throws IOException { String id = formatId(noteId, p); - Document doc = newDocument(id, noteName, p); + Document doc = newDocument(id, noteName, p, owner); try { writer.updateDocument(new Term(ID_FIELD, id), doc); writer.commit(); @@ -244,12 +250,13 @@ static String formatDeleteId(String noteId, Paragraph p) { * @param p paragraph * @return */ - private Document newDocument(String id, String noteName, Paragraph p) { + private Document newDocument(String id, String noteName, Paragraph p, String owner) { Document doc = new Document(); Field pathField = new StringField(ID_FIELD, id, Field.Store.YES); doc.add(pathField); doc.add(new StringField("title", noteName, Field.Store.YES)); + doc.add(new StringField(OWNER_FIELD, owner, Field.Store.YES)); if (null != p) { doc.add(new TextField(SEARCH_FIELD, p.getText(), Field.Store.YES)); @@ -307,13 +314,13 @@ public void addIndexDoc(Note note) { * @throws IOException */ private void addIndexDocAsync(Note note) throws IOException { - indexNoteName(writer, note.getId(), note.getName()); + indexNoteName(writer, note.getId(), note.getName(), note.getOwner()); for (Paragraph doc : note.getParagraphs()) { if (doc.getText() == null) { LOG.debug("Skipping empty paragraph"); continue; } - indexDoc(writer, note.getId(), note.getName(), doc); + indexDoc(writer, note.getId(), note.getName(), doc, note.getOwner()); } } @@ -367,13 +374,14 @@ public void close() { * * @throws IOException */ - private void indexNoteName(IndexWriter w, String noteId, String noteName) throws IOException { + private void indexNoteName(IndexWriter w, String noteId, String noteName, String owner) + throws IOException { LOG.debug("Indexing Notebook {}, '{}'", noteId, noteName); if (null == noteName || noteName.isEmpty()) { LOG.debug("Skipping empty notebook name"); return; } - indexDoc(w, noteId, noteName, null); + indexDoc(w, noteId, noteName, null, owner); } /** @@ -381,10 +389,10 @@ private void indexNoteName(IndexWriter w, String noteId, String noteName) throws * - code of the paragraph (if non-null) * - or just a note name */ - private void indexDoc(IndexWriter w, String noteId, String noteName, Paragraph p) + private void indexDoc(IndexWriter w, String noteId, String noteName, Paragraph p, String owner) throws IOException { String id = formatId(noteId, p); - Document doc = newDocument(id, noteName, p); + Document doc = newDocument(id, noteName, p, owner); w.addDocument(doc); } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/search/SearchService.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/search/SearchService.java index 64f2b758be2..ff357f5fb85 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/search/SearchService.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/search/SearchService.java @@ -39,7 +39,7 @@ public interface SearchService { * @param queryStr a query * @return A list of matching paragraphs (id, text, snippet w/ highlight) */ - public List> query(String queryStr); + public List> query(String queryStr, String owner); /** * Updates all documents in index for the given note: 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 a9cd30fff08..0020fa5bcda 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 @@ -100,7 +100,7 @@ public void tearDown() throws Exception { @Test public void testSelectingReplImplementation() throws IOException { - Note note = notebook.createNote(); + Note note = notebook.createNote("anonymous"); note.getNoteReplLoader().setInterpreters(factory.getDefaultInterpreterSettingList()); // run with defatul repl @@ -126,7 +126,8 @@ public void testSelectingReplImplementation() throws IOException { public void testGetAllNotes() throws IOException { // get all notes after copy the {notebookId}/note.json into notebookDir File srcDir = new File("src/test/resources/2A94M5J1Z"); - File destDir = new File(notebookDir.getAbsolutePath() + "/2A94M5J1Z"); + File destDir = new File(notebookDir.getAbsolutePath() + "/users/anonymous/2A94M5J1Z"); + destDir.getParentFile().mkdirs(); try { FileUtils.copyDirectory(srcDir, destDir); @@ -134,7 +135,7 @@ public void testGetAllNotes() throws IOException { e.printStackTrace(); } - Note copiedNote = notebookRepo.get("2A94M5J1Z"); + Note copiedNote = notebookRepo.get("2A94M5J1Z", "anonymous"); // when ZEPPELIN_NOTEBOOK_GET_FROM_REPO set to be false System.setProperty(ConfVars.ZEPPELIN_NOTEBOOK_RELOAD_FROM_STORAGE.getVarName(), "false"); @@ -165,7 +166,7 @@ public void testGetAllNotes() throws IOException { @Test public void testPersist() throws IOException, SchedulerException{ - Note note = notebook.createNote(); + Note note = notebook.createNote("anonymous"); // run with default repl Paragraph p1 = note.addParagraph(); @@ -182,7 +183,7 @@ public void testPersist() throws IOException, SchedulerException{ @Test public void testClearParagraphOutput() throws IOException, SchedulerException{ - Note note = notebook.createNote(); + Note note = notebook.createNote("anonymous"); Paragraph p1 = note.addParagraph(); Map config = p1.getConfig(); config.put("enabled", true); @@ -200,7 +201,7 @@ public void testClearParagraphOutput() throws IOException, SchedulerException{ @Test public void testRunAll() throws IOException { - Note note = notebook.createNote(); + Note note = notebook.createNote("anonymous"); note.getNoteReplLoader().setInterpreters(factory.getDefaultInterpreterSettingList()); Paragraph p1 = note.addParagraph(); Map config = p1.getConfig(); @@ -221,7 +222,7 @@ public void testRunAll() throws IOException { @Test public void testSchedule() throws InterruptedException, IOException{ // create a note and a paragraph - Note note = notebook.createNote(); + Note note = notebook.createNote("anonymous"); note.getNoteReplLoader().setInterpreters(factory.getDefaultInterpreterSettingList()); Paragraph p = note.addParagraph(); @@ -236,13 +237,13 @@ public void testSchedule() throws InterruptedException, IOException{ config.put("enabled", true); config.put("cron", "* * * * * ?"); note.setConfig(config); - notebook.refreshCron(note.id()); + notebook.refreshCron(note.id(), "anonymous"); Thread.sleep(1*1000); // remove cron scheduler. config.put("cron", null); note.setConfig(config); - notebook.refreshCron(note.id()); + notebook.refreshCron(note.id(), "anonymous"); Thread.sleep(1000); dateFinished = p.getDateFinished(); assertNotNull(dateFinished); @@ -294,7 +295,7 @@ public void testAutoRestartInterpreterAfterSchedule() throws InterruptedExceptio @Test public void testCloneNote() throws IOException, CloneNotSupportedException, InterruptedException { - Note note = notebook.createNote(); + Note note = notebook.createNote("anonymous"); note.getNoteReplLoader().setInterpreters(factory.getDefaultInterpreterSettingList()); final Paragraph p = note.addParagraph(); @@ -303,7 +304,7 @@ public void testCloneNote() throws IOException, CloneNotSupportedException, while(p.isTerminated()==false || p.getResult()==null) Thread.yield(); p.setStatus(Status.RUNNING); - Note cloneNote = notebook.cloneNote(note.getId(), "clone note"); + Note cloneNote = notebook.cloneNote(note.getId(), "clone note", "anonymous"); Paragraph cp = cloneNote.paragraphs.get(0); assertEquals(cp.getStatus(), Status.READY); assertNotEquals(cp.getId(), p.getId()); @@ -315,7 +316,7 @@ public void testCloneNote() throws IOException, CloneNotSupportedException, public void testAngularObjectRemovalOnNotebookRemove() throws InterruptedException, IOException { // create a note and a paragraph - Note note = notebook.createNote(); + Note note = notebook.createNote("anonymous"); note.getNoteReplLoader().setInterpreters(factory.getDefaultInterpreterSettingList()); AngularObjectRegistry registry = note.getNoteReplLoader() @@ -328,7 +329,7 @@ public void testAngularObjectRemovalOnNotebookRemove() throws InterruptedExcepti registry.add("o2", "object2", null); // remove notebook - notebook.removeNote(note.id()); + notebook.removeNote(note.id(), "anonymous"); // local object should be removed assertNull(registry.get("o1", note.id())); @@ -340,7 +341,7 @@ public void testAngularObjectRemovalOnNotebookRemove() throws InterruptedExcepti public void testAngularObjectRemovalOnInterpreterRestart() throws InterruptedException, IOException { // create a note and a paragraph - Note note = notebook.createNote(); + Note note = notebook.createNote("anonymous"); note.getNoteReplLoader().setInterpreters(factory.getDefaultInterpreterSettingList()); AngularObjectRegistry registry = note.getNoteReplLoader() @@ -355,19 +356,19 @@ public void testAngularObjectRemovalOnInterpreterRestart() throws InterruptedExc // restart interpreter factory.restart(note.getNoteReplLoader().getInterpreterSettings().get(0).id()); registry = note.getNoteReplLoader() - .getInterpreterSettings().get(0).getInterpreterGroup() - .getAngularObjectRegistry(); + .getInterpreterSettings().get(0).getInterpreterGroup() + .getAngularObjectRegistry(); // local and global scope object should be removed assertNull(registry.get("o1", note.id())); assertNull(registry.get("o2", null)); - notebook.removeNote(note.id()); + notebook.removeNote(note.id(), "anonymous"); } @Test public void testAbortParagraphStatusOnInterpreterRestart() throws InterruptedException, IOException { - Note note = notebook.createNote(); + Note note = notebook.createNote("anonymous"); note.getNoteReplLoader().setInterpreters(factory.getDefaultInterpreterSettingList()); ArrayList paragraphs = new ArrayList<>(); 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 6d8c50dd1aa..9238d68860a 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 @@ -112,7 +112,7 @@ public void testSyncOnCreate() throws IOException { assertEquals(0, notebookRepoSync.list(1).size()); /* create note */ - Note note = notebookSync.createNote(); + Note note = notebookSync.createNote("anonymous"); // check that automatically saved on both storages assertEquals(1, notebookRepoSync.list(0).size()); @@ -128,7 +128,7 @@ public void testSyncOnDelete() throws IOException { assertEquals(0, notebookRepoSync.list(0).size()); assertEquals(0, notebookRepoSync.list(1).size()); - Note note = notebookSync.createNote(); + Note note = notebookSync.createNote("anonymous"); /* check that created in both storage systems */ assertEquals(1, notebookRepoSync.list(0).size()); @@ -136,7 +136,7 @@ public void testSyncOnDelete() throws IOException { assertEquals(notebookRepoSync.list(0).get(0).getId(),notebookRepoSync.list(1).get(0).getId()); /* remove Note */ - notebookSync.removeNote(notebookRepoSync.list(0).get(0).getId()); + notebookSync.removeNote(notebookRepoSync.list(0).get(0).getId(), "anonymous"); /* check that deleted in both storages */ assertEquals(0, notebookRepoSync.list(0).size()); @@ -148,7 +148,7 @@ public void testSyncOnDelete() throws IOException { public void testSyncUpdateMain() throws IOException { /* create note */ - Note note = notebookSync.createNote(); + Note note = notebookSync.createNote("anonymous"); Paragraph p1 = note.addParagraph(); Map config = p1.getConfig(); config.put("enabled", true); @@ -160,29 +160,29 @@ public void testSyncUpdateMain() throws IOException { /* new paragraph not yet saved into storages */ assertEquals(0, notebookRepoSync.get(0, - notebookRepoSync.list(0).get(0).getId()).getParagraphs().size()); + notebookRepoSync.list(0).get(0).getId(), "anonymous").getParagraphs().size()); assertEquals(0, notebookRepoSync.get(1, - notebookRepoSync.list(1).get(0).getId()).getParagraphs().size()); + notebookRepoSync.list(1).get(0).getId(), "anonymous").getParagraphs().size()); /* save to storage under index 0 (first storage) */ notebookRepoSync.save(0, note); /* check paragraph saved to first storage */ assertEquals(1, notebookRepoSync.get(0, - notebookRepoSync.list(0).get(0).getId()).getParagraphs().size()); + notebookRepoSync.list(0).get(0).getId(), "anonymous").getParagraphs().size()); /* check paragraph isn't saved to second storage */ assertEquals(0, notebookRepoSync.get(1, - notebookRepoSync.list(1).get(0).getId()).getParagraphs().size()); + notebookRepoSync.list(1).get(0).getId(), "anonymous").getParagraphs().size()); /* apply sync */ notebookRepoSync.sync(); /* check whether added to second storage */ assertEquals(1, notebookRepoSync.get(1, - notebookRepoSync.list(1).get(0).getId()).getParagraphs().size()); + notebookRepoSync.list(1).get(0).getId(), "anonymous").getParagraphs().size()); /* check whether same paragraph id */ assertEquals(p1.getId(), notebookRepoSync.get(0, - notebookRepoSync.list(0).get(0).getId()).getLastParagraph().getId()); + notebookRepoSync.list(0).get(0).getId(), "anonymous").getLastParagraph().getId()); assertEquals(p1.getId(), notebookRepoSync.get(1, - notebookRepoSync.list(1).get(0).getId()).getLastParagraph().getId()); + notebookRepoSync.list(1).get(0).getId(), "anonymous").getLastParagraph().getId()); } @Test @@ -194,7 +194,7 @@ public void testSyncOnList() throws IOException { assertEquals(0, notebookRepoSync.list(1).size()); File srcDir = new File("src/test/resources/2A94M5J1Z"); - File destDir = new File(secNotebookDir + "/2A94M5J1Z"); + File destDir = new File(secNotebookDir + "/users/anonymous/2A94M5J1Z"); /* copy manually new notebook into secondary storage repo and check repos */ try { diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java index 80d1174e2f9..fba5d47e5c0 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/VFSNotebookRepoTest.java @@ -92,7 +92,7 @@ public void tearDown() throws Exception { @Test public void testSaveNotebook() throws IOException, InterruptedException { - Note note = notebook.createNote(); + Note note = notebook.createNote("anonymous"); note.getNoteReplLoader().setInterpreters(factory.getDefaultInterpreterSettingList()); Paragraph p1 = note.addParagraph(); diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/search/LuceneSearchTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/search/LuceneSearchTest.java index f74d95eb375..2dd6ff0d46d 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/search/LuceneSearchTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/search/LuceneSearchTest.java @@ -80,7 +80,7 @@ public void shutDown() { notebookIndex.addIndexDocs(Arrays.asList(note1, note2)); //when - List> results = notebookIndex.query("all"); + List> results = notebookIndex.query("all", "anonymous"); //then assertThat(results).isNotEmpty(); @@ -96,7 +96,7 @@ public void shutDown() { notebookIndex.addIndexDocs(Arrays.asList(note1, note2)); //when - List> results = notebookIndex.query("Notebook1"); + List> results = notebookIndex.query("Notebook1", "anonymous"); //then assertThat(results).isNotEmpty(); @@ -120,7 +120,7 @@ public void shutDown() { public void canNotSearchBeforeIndexing() { //given NO notebookIndex.index() was called //when - List> result = notebookIndex.query("anything"); + List> result = notebookIndex.query("anything", "anonymous"); //then assertThat(result).isEmpty(); //assert logs were printed @@ -139,10 +139,10 @@ public void canNotSearchBeforeIndexing() { notebookIndex.updateIndexDoc(note2); //then - List> results = notebookIndex.query("all"); + List> results = notebookIndex.query("all", "anonymous"); assertThat(results).isEmpty(); - results = notebookIndex.query("indeed"); + results = notebookIndex.query("indeed", "anonymous"); assertThat(results).isNotEmpty(); } @@ -164,7 +164,7 @@ public void canNotSearchBeforeIndexing() { notebookIndex.deleteIndexDocs(note2); //then - assertThat(notebookIndex.query("all")).isEmpty(); + assertThat(notebookIndex.query("all", "anonymous")).isEmpty(); assertThat(resultForQuery("Notebook2")).isEmpty(); List> results = resultForQuery("test"); @@ -215,7 +215,7 @@ public void canNotSearchBeforeIndexing() { } private List> resultForQuery(String q) { - return notebookIndex.query(q); + return notebookIndex.query(q, "anonymous"); } /** @@ -251,7 +251,7 @@ private Paragraph addParagraphWithText(Note note, String text) { } private Note newNote(String name) { - Note note = new Note(notebookRepoMock, replLoaderMock, null, notebookIndex); + Note note = new Note(notebookRepoMock, replLoaderMock, null, notebookIndex, "anonymous"); note.setName(name); return note; }