Notebook Authorization

This commit is contained in:
Prasad Wagle 2016-01-31 22:30:51 -08:00
parent e6a845fc28
commit 6e85730343
8 changed files with 291 additions and 1 deletions

View file

@ -18,6 +18,8 @@
package org.apache.zeppelin.rest;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -75,6 +77,48 @@ public class NotebookRestApi {
this.notebookIndex = search;
}
/**
* list note owners
*/
@GET
@Path("{noteId}/permissions")
public Response getNotePermissions(@PathParam("noteId") String noteId) {
Note note = notebook.getNote(noteId);
HashMap<String, HashSet> permissionsMap = new HashMap<String, HashSet>();
permissionsMap.put("owners", note.getOwners());
permissionsMap.put("readers", note.getReaders());
permissionsMap.put("writers", note.getWriters());
return new JsonResponse<>(Status.OK, "", permissionsMap).build();
}
/**
* Set note owners
*/
@PUT
@Path("{noteId}/permissions")
public Response putNotePermissions(@PathParam("noteId") String noteId, String req)
throws IOException {
HashMap<String, HashSet> permMap = gson.fromJson(req,
new TypeToken<HashMap<String, HashSet>>(){}.getType());
Note note = notebook.getNote(noteId);
LOG.debug("Set permissions {} {} {}", permMap.get("owners"),
permMap.get("readers"),
permMap.get("writers"));
// TODO(prasadwagle) Authenticate and check authorization
// if (!note.isOwner(userAndGroups())) {
// return new JsonResponse<>(Status.FORBIDDEN, ownerPermissionError(userAndGroups,
// note)).build();
// }
note.setOwners(permMap.get("owners"));
note.setReaders(permMap.get("readers"));
note.setWriters(permMap.get("writers"));
LOG.debug("After set permissions {} {} {}", note.getOwners(), note.getReaders(),
note.getWriters());
note.persist();
notebookServer.broadcastNote(note);
return new JsonResponse<>(Status.OK).build();
}
/**
* bind a setting to note
* @throws IOException

View file

@ -96,6 +96,7 @@ public class Message {
PARAGRAPH_APPEND_OUTPUT, // [s-c] append output
PARAGRAPH_UPDATE_OUTPUT, // [s-c] update (replace) output
PING,
AUTH_INFO,
ANGULAR_OBJECT_UPDATE, // [s-c] add/update angular object
ANGULAR_OBJECT_REMOVE, // [s-c] add angular object del

View file

@ -358,8 +358,32 @@ public class NotebookServer extends WebSocketServlet implements
broadcastAll(new Message(OP.NOTES_INFO).put("notes", notesInfo));
}
void readPermissionEror(NotebookSocket conn, String principal, Note note) throws IOException {
LOG.info("Cannot read. Connection readers {}. Allowed readers {}",
principal, note.getReaders());
conn.send(serializeMessage(new Message(OP.AUTH_INFO).put("info",
"Insufficient privileges to read note.\n" +
"Allowed readers: " + note.getReaders().toString() + "\n" +
"User belongs to: " + principal)));
}
void writePermissionError(NotebookSocket conn, String principal, Note note) throws IOException {
LOG.info("Cannot write. Connection writers {}. Allowed writers {}",
principal, note.getWriters());
conn.send(serializeMessage(new Message(OP.AUTH_INFO).put("info",
"Insufficient privileges to write note.\n" +
"Allowed writers: " + note.getWriters().toString() + "\n" +
"User belongs to: " + principal)));
}
private void sendNote(NotebookSocket conn, Notebook notebook,
Message fromMessage) throws IOException {
LOG.info("New operation from {} : {} : {} : {} : {}", conn.getRequest().getRemoteAddr(),
conn.getRequest().getRemotePort(),
fromMessage.principal, fromMessage.op, fromMessage.get("id")
);
String noteId = (String) fromMessage.get("id");
if (noteId == null) {
return;
@ -367,6 +391,14 @@ public class NotebookServer extends WebSocketServlet implements
Note note = notebook.getNote(noteId);
if (note != null) {
HashSet<String> usersAndGroups = new HashSet<String>();
usersAndGroups.add(fromMessage.principal);
// TODO(prasadwagle) add groups to usersAndGroups
if (!note.isReader(usersAndGroups)) {
readPermissionEror(conn, fromMessage.principal, note);
broadcastNoteList();
return;
}
addConnectionToNote(note.id(), conn);
conn.send(serializeMessage(new Message(OP.NOTE).put("note", note)));
sendAllAngularObjects(note, conn);
@ -476,6 +508,15 @@ public class NotebookServer extends WebSocketServlet implements
Map<String, Object> config = (Map<String, Object>) fromMessage
.get("config");
final Note note = notebook.getNote(getOpenNoteId(conn));
HashSet<String> usersAndGroups = new HashSet<String>();
usersAndGroups.add(fromMessage.principal);
// TODO(prasadwagle) add groups to usersAndGroups
if (!note.isWriter(usersAndGroups)) {
writePermissionError(conn, fromMessage.principal, note);
return;
}
Paragraph p = note.getParagraph(paragraphId);
p.settings.setParams(params);
p.setConfig(config);
@ -517,6 +558,15 @@ public class NotebookServer extends WebSocketServlet implements
}
final Note note = notebook.getNote(getOpenNoteId(conn));
HashSet<String> usersAndGroups = new HashSet<String>();
usersAndGroups.add(fromMessage.principal);
// TODO(prasadwagle) add groups to usersAndGroups
if (!note.isWriter(usersAndGroups)) {
writePermissionError(conn, fromMessage.principal, note);
return;
}
/** We dont want to remove the last paragraph */
if (!note.isLastParagraph(paragraphId)) {
note.removeParagraph(paragraphId);
@ -533,6 +583,15 @@ public class NotebookServer extends WebSocketServlet implements
}
final Note note = notebook.getNote(getOpenNoteId(conn));
HashSet<String> usersAndGroups = new HashSet<String>();
usersAndGroups.add(fromMessage.principal);
// TODO(prasadwagle) add groups to usersAndGroups
if (!note.isWriter(usersAndGroups)) {
writePermissionError(conn, fromMessage.principal, note);
return;
}
note.clearParagraphOutput(paragraphId);
broadcastNote(note);
}
@ -654,6 +713,15 @@ public class NotebookServer extends WebSocketServlet implements
final int newIndex = (int) Double.parseDouble(fromMessage.get("index")
.toString());
final Note note = notebook.getNote(getOpenNoteId(conn));
HashSet<String> usersAndGroups = new HashSet<String>();
usersAndGroups.add(fromMessage.principal);
// TODO(prasadwagle) add groups to usersAndGroups
if (!note.isWriter(usersAndGroups)) {
writePermissionError(conn, fromMessage.principal, note);
return;
}
note.moveParagraph(paragraphId, newIndex);
note.persist();
broadcastNote(note);
@ -664,6 +732,15 @@ public class NotebookServer extends WebSocketServlet implements
final int index = (int) Double.parseDouble(fromMessage.get("index")
.toString());
final Note note = notebook.getNote(getOpenNoteId(conn));
HashSet<String> usersAndGroups = new HashSet<String>();
usersAndGroups.add(fromMessage.principal);
// TODO(prasadwagle) add groups to usersAndGroups
if (!note.isWriter(usersAndGroups)) {
writePermissionError(conn, fromMessage.principal, note);
return;
}
note.insertParagraph(index);
note.persist();
broadcastNote(note);
@ -677,6 +754,15 @@ public class NotebookServer extends WebSocketServlet implements
}
final Note note = notebook.getNote(getOpenNoteId(conn));
HashSet<String> usersAndGroups = new HashSet<String>();
usersAndGroups.add(fromMessage.principal);
// TODO(prasadwagle) add groups to usersAndGroups
if (!note.isWriter(usersAndGroups)) {
writePermissionError(conn, fromMessage.principal, note);
return;
}
Paragraph p = note.getParagraph(paragraphId);
p.abort();
}
@ -689,6 +775,15 @@ public class NotebookServer extends WebSocketServlet implements
}
final Note note = notebook.getNote(getOpenNoteId(conn));
HashSet<String> usersAndGroups = new HashSet<String>();
usersAndGroups.add(fromMessage.principal);
// TODO(prasadwagle) add groups to usersAndGroups
if (!note.isWriter(usersAndGroups)) {
writePermissionError(conn, fromMessage.principal, note);
return;
}
Paragraph p = note.getParagraph(paragraphId);
String text = (String) fromMessage.get("paragraph");
p.setText(text);

View file

@ -129,6 +129,11 @@ limitations under the License.
tooltip-placement="bottom" tooltip="Interpreter binding">
<i class="fa fa-cog" ng-style="{color: showSetting ? '#3071A9' : 'black' }"></i>
</span>
<span style="position:relative; top:2px; margin-right:4px; cursor:pointer;"
ng-click="togglePermissions()"
tooltip-placement="bottom" tooltip="Note permissions">
<i class="fa fa-lock" ng-style="{color: showSetting ? '#3071A9' : 'black' }"></i>
</span>
<span class="btn-group">
<button type="button" class="btn btn-default btn-xs dropdown-toggle"

View file

@ -617,6 +617,69 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
}
};
var getPermissions = function(callback) {
$http.get(baseUrlSrv.getRestApiBase()+ '/notebook/' +$scope.note.id + '/permissions').
success(function(data, status, headers, config) {
$scope.permissions = data.body;
$scope.permissionsOrig = angular.copy($scope.permissions); // to check dirty
if (callback) {
callback();
}
}).
error(function(data, status, headers, config) {
if (status !== 0) {
console.log('Error %o %o', status, data.message);
}
});
};
$scope.openPermissions = function() {
$scope.showPermissions = true;
getPermissions();
};
$scope.closePermissions = function() {
if (isPermissionsDirty()) {
BootstrapDialog.confirm({
closable: true,
title: '',
message: 'Changes will be discarded.',
callback: function(result) {
if (result) {
$scope.$apply(function() {
$scope.showPermissions = false;
});
}
}
});
} else {
$scope.showPermissions = false;
}
};
$scope.savePermissions = function() {
$http.put(baseUrlSrv.getRestApiBase() + '/notebook/' +$scope.note.id + '/permissions',
$scope.permissions, {withCredentials: true}).
success(function(data, status, headers, config) {
console.log('Note permissions %o saved', $scope.permissions);
$scope.showPermissions = false;
}).
error(function(data, status, headers, config) {
console.log('Error %o %o', status, data.message);
alert(data.message);
});
};
$scope.togglePermissions = function() {
if ($scope.showPermissions) {
$scope.closePermissions();
} else {
$scope.openPermissions();
}
};
var isSettingDirty = function() {
if (angular.equals($scope.interpreterBindings, $scope.interpreterBindingsOrig)) {
return false;
@ -624,4 +687,13 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
return true;
}
};
var isPermissionsDirty = function() {
if (angular.equals($scope.permissions, $scope.permissionsOrig)) {
return false;
} else {
return true;
}
};
});

View file

@ -14,6 +14,29 @@ limitations under the License.
<!-- Here the controller <NotebookCtrl> is not needed because explicitly set in the app.js (route) -->
<div ng-include src="'app/notebook/notebook-actionBar.html'"></div>
<div style="padding-top: 36px;">
<!-- permissions -->
<div ng-if="showPermissions" class="permissions">
<div>
<h4>Note Permissions (Only note owners can change)</h4>
</div>
<hr />
<div>
<p>
Enter comma separated users and groups in the fields. Empty set means anyone can do the operation.
</p>
<div class="notePermissions"
data-ng-model="permissions">
<p>Owners : <input ng-list ng-model="permissions.owners"> Owners can change permissions, read and write the note. </p>
<p>Readers : <input ng-list ng-model="permissions.readers"> Readers can only read the note.</p>
<p>Writers : <input ng-list ng-model="permissions.writers"> Writers can read and write the note.</p>
</div>
</div>
<br />
<div>
<button class="btn btn-primary" ng-click="savePermissions()">Save</button>
<button class="btn btn-default" ng-click="closePermissions()">Cancel</button>
</div>
</div>
<!-- settings -->
<div ng-if="showSetting" class="setting">
<div>

View file

@ -52,12 +52,14 @@ angular.module('zeppelinWebApp').factory('websocketEvents', function($rootScope,
$location.path('notebook/' + data.note.id);
} else if (op === 'NOTES_INFO') {
$rootScope.$broadcast('setNoteMenu', data.notes);
} else if (op === 'AUTH_INFO') {
alert(data.info.toString());
} else if (op === 'PARAGRAPH') {
$rootScope.$broadcast('updateParagraph', data);
} else if (op === 'PARAGRAPH_APPEND_OUTPUT') {
$rootScope.$broadcast('appendParagraphOutput', data);
} else if (op === 'PARAGRAPH_UPDATE_OUTPUT') {
$rootScope.$broadcast('updateParagraphOutput', data);
$rootScope.$broadcast('updateParagraphOutput', data);
} else if (op === 'PROGRESS') {
$rootScope.$broadcast('updateProgress', data);
} else if (op === 'COMPLETION_LIST') {

View file

@ -58,6 +58,9 @@ public class Note implements Serializable, JobListener {
private String name = "";
private String id;
private HashSet<String> owners = new HashSet<String>();
private HashSet<String> readers = new HashSet<String>();
private HashSet<String> writers = new HashSet<String>();
@SuppressWarnings("rawtypes")
Map<String, List<AngularObject>> angularObjects = new HashMap<>();
@ -114,6 +117,51 @@ public class Note implements Serializable, JobListener {
this.name = name;
}
public HashSet<String> getOwners() {
return (new HashSet<String>(owners));
}
public void setOwners(HashSet<String> owners) {
this.owners = new HashSet<String>(owners);
}
public HashSet<String> getReaders() {
return (new HashSet<String>(readers));
}
public void setReaders(HashSet<String> readers) {
this.readers = new HashSet<String>(readers);
}
public HashSet<String> getWriters() {
return (new HashSet<String>(writers));
}
public void setWriters(HashSet<String> writers) {
this.writers = new HashSet<String>(writers);
}
public boolean isOwner(HashSet<String> entities) {
return isMember(entities, this.owners);
}
public boolean isWriter(HashSet<String> entities) {
return isMember(entities, this.writers) || isMember(entities, this.owners);
}
public boolean isReader(HashSet<String> entities) {
return isMember(entities, this.readers) ||
isMember(entities, this.owners) ||
isMember(entities, this.writers);
}
// return true if b is empty or if (a intersection b) is non-empty
private boolean isMember(HashSet<String> a, HashSet<String> b) {
Set<String> intersection = new HashSet<String>(b);
intersection.retainAll(a);
return (b.isEmpty() || (intersection.size() > 0));
}
public NoteInterpreterLoader getNoteReplLoader() {
return replLoader;
}