mirror of
https://github.com/apache/zeppelin
synced 2026-05-24 09:38:26 +00:00
Notebook Authorization
This commit is contained in:
parent
e6a845fc28
commit
6e85730343
8 changed files with 291 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue