[ZEPPELIN-5934] Check folder permissions before rename, trash and remove - #5383
Open
sylee6529 wants to merge 1 commit into
Open
[ZEPPELIN-5934] Check folder permissions before rename, trash and remove#5383sylee6529 wants to merge 1 commit into
sylee6529 wants to merge 1 commit into
Conversation
NotebookService applied folder level operations without looking at permissions at all: renameFolder and moveFolderToTrash carried a "TODO(zjffdu) folder permission check", and removeFolder and restoreFolder had no check to begin with. A user who could only read a note was able to rename, trash or permanently delete the whole folder holding it, even though that same user could not touch the note itself. Zeppelin has no folder level ACL. A folder is only the in-memory tree NoteManager builds out of note paths, so checkFolderPermission derives the permission of a folder from the notes under it, recursively, and allows the operation only when the caller holds the required permission on every one of them. The levels follow the note level policy they mirror: OWNER for rename, trash and remove, matching NOTE_RENAME, MOVE_NOTE_TO_TRASH and DEL_NOTE, and WRITER for restore, matching RESTORE_NOTE. The check is all-or-nothing and runs before any repository call, so a refused operation leaves the folder exactly as it was rather than half applied. The error message names the folder and how many notes blocked the call, but not which ones, because the caller may not be allowed to read them; those paths go to the server log instead, the same split NotebookRestApi.ownerPermissionError already uses. Scope is the four operations that act on one named folder. EMPTY_TRASH and RESTORE_ALL act on the shared trash as a whole and stay untouched: emptyTrash already bypasses the OWNER check that removeNote performs on a single note, so that gap belongs to note level enforcement and needs its own decision about who may empty a trash that every user shares.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this PR for?
Folder level operations in
NotebookServiceran without any permission check.renameFolderandmoveFolderToTrashcarried a//TODO(zjffdu) folder permission check, andremoveFolderandrestoreFolderhad no check at all. A user who could only read one note in a folder was able to rename, trash or permanently delete that whole folder, even though the same user could not touch the note itself.Deriving a folder permission. Zeppelin has no folder level ACL.
AuthorizationServiceis keyed by note id only and the word "folder" does not appear in it; a folder is just the in-memory treeNoteManagerbuilds out of note paths. SocheckFolderPermissionderives the permission of a folder from the notes it holds, walked recursively, and allows the operation only when the caller holds the required permission on every one of them. No new ACL concept, no storage or migration change.Permission levels follow the note level policy they mirror, so this PR extends an existing rule rather than inventing one:
FOLDER_RENAMENOTE_RENAME= OWNERMOVE_FOLDER_TO_TRASHMOVE_NOTE_TO_TRASH= OWNERREMOVE_FOLDERDEL_NOTE= OWNERRESTORE_FOLDERRESTORE_NOTE= WRITERRestore being lower than trash looks asymmetric, but it is exactly the asymmetry the note level policy already has.
All-or-nothing. The check completes before any repository call, so a refused operation leaves the folder exactly as it was instead of half applied. Applying a move or a remove only to the subset the caller owns would leave a partially emptied folder that the caller can neither see nor undo.
A folder holding no note is allowed. Removing the last note leaves the folder node in the tree, and there is nothing left to protect. Refusing instead would leave empty folders nobody can ever clean up.
What the error message reveals. It names the folder and how many notes blocked the call, but not which ones, since the caller may not be allowed to read them. Those paths go to the server log instead. This is the same split
NotebookRestApi.ownerPermissionErrorandNotebookServer.permissionErroralready use, and it matches the existing note level message, which also does not name the note. It is the one place the folder message deliberately differs from the note one: it omitsAllowed users or roles, because a folder has many notes and the union of their owners would leak exactly what the message is trying to withhold.checkPermissionwas split intohasPermissionandgetAllowedEntitiesso the folder check can reuse the level dispatch as a plain predicate. The user facing message and behaviour of the note level check are unchanged.Relation to earlier work. PR #4624 (ZEPPELIN-5934, closed by the stale bot after inactivity, never merged) took the same "derive from the notes" approach, and this PR keeps that idea. It differs in four ways:
restoreFolderis WRITER rather than OWNER so it lines up withRESTORE_NOTE;NoteManager.getFolderstays private, since the already publicgetNoteInfoRecursivelyis enough andNoteManager.Folderneed not reach the service layer; the unrelatedFolderPathAlreadyExistsExceptioncommit is left out; and the tests the reviewer asked for are included.Scope
Limited to the four operations that act on one named folder.
EMPTY_TRASHandRESTORE_ALLact on the shared trash as a whole and are untouched, on purpose.emptyTrashdoes destroy trashed notes without a check, so it can still remove a folder thatremoveFoldernow refuses. That gap is not introduced here:removeNotealready requires OWNER for a single note today, andemptyTrashalready bypasses it. It belongs to note level enforcement, and deciding who may empty a trash that every user shares is a product question worth its own issue rather than a rider on this fix. Happy to file it as a follow-up.Two adjacent issues are also left alone:
renameFolderchecks the source folder but not the destination, so renaming onto an existing folder path is still unguarded (ZEPPELIN-5333), andmoveFolderToTrashdetects trash name conflicts withcontainsNoteon a folder path, which never matches.Known limitation
The check and the operation are not atomic. If another user adds a note to the folder between them, that note is included in the operation without having been checked. Every existing note level
checkPermissioncall has the same shape, so closing it would mean introducing locking across the whole permission layer rather than in this path alone.What type of PR is it?
Bug Fix
What is the Jira issue?
How should this be tested?
Automated.
mvn test -pl zeppelin-server -Dtest='NotebookServiceTest,NotebookServerTest,NotebookSecurityRestApiTest'NotebookServiceTesttestFolderOperationsRequirePermissionOnEveryNote— a folder holds two notes, the caller owns one of them, and the blocking note sits in a sub folder so the check is only reached by walking recursively.renameFolder,moveFolderToTrashandremoveFolderare all refused with aForbiddenException, both notes are still at their original paths afterwards, and the message names the folder but not the blocking note. The owner of both notes then performs the same operations successfully.testRestoreFolderRequiresWriterPermission— a reader is refused, a writer succeeds and the note is back at its original path.testRemoveFolderHoldingNoNoteIsAllowed— pins the empty folder decision.NotebookServerTest#testRemoveFolderRequiresOwnerOnEveryNotecovers the real entry point. There is no REST endpoint for folder operations; every caller of these service methods is inNotebookServer, so this drives a realMiniZeppelinServerwith an actualREMOVE_FOLDERWebSocket message carrying aTicketContainerprincipal. It asserts the notes survive and the client receivesAUTH_INFO, then that the same message succeeds once the caller owns everything.Without the production change, two of the
NotebookServiceTestcases and theNotebookServerTestcase fail; the WebSocket one fails on the folder actually having been deleted by a non-owner.NotebookSecurityRestApiTest(a real server with Shiro over HTTP) passes unchanged, covering thecheckPermissionsplit.Manual: with two users, give user A owner of a note inside a folder and leave user B as reader only, then have B rename, trash and delete the folder from the notebook list. Each is refused and the folder stays.
Questions