-
Notifications
You must be signed in to change notification settings - Fork 199
Consolidate workspace folder ACL logic and add a writability check #5948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+428
−39
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| package permissions | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "path" | ||
| "strconv" | ||
|
|
||
| "github.com/databricks/cli/bundle/config/resources" | ||
| "github.com/databricks/databricks-sdk-go/apierr" | ||
| "github.com/databricks/databricks-sdk-go/service/iam" | ||
| "github.com/databricks/databricks-sdk-go/service/workspace" | ||
| ) | ||
|
|
||
| // adminsGroup is the workspace admins group. Its members can write to any | ||
| // workspace folder regardless of the folder ACL, so writability checks must not | ||
| // conclude "cannot write" for an admin. | ||
| const adminsGroup = "admins" | ||
|
|
||
| // FolderACL is the resolved access-control state of a workspace folder: the | ||
| // permissions that actually apply to it, resolved by walking up to the closest | ||
| // existing ancestor when the folder itself does not exist yet. | ||
| type FolderACL struct { | ||
| // RequestedPath is the folder the caller asked about. | ||
| RequestedPath string | ||
| // ResolvedPath is the existing object whose ACL was read. It equals | ||
| // RequestedPath when the folder exists, or the closest existing ancestor | ||
| // otherwise (a not-yet-created folder inherits its ancestor's ACL). | ||
| ResolvedPath string | ||
| // Permissions are the ACL entries that apply, keyed by principal. | ||
| Permissions *WorkspacePathPermissions | ||
| } | ||
|
|
||
| // ResolveFolderACL reads the ACL that applies to folderPath. When folderPath does | ||
| // not exist, it walks up to the closest existing ancestor, since a folder created | ||
| // under it will inherit that ancestor's permissions. | ||
| func ResolveFolderACL(ctx context.Context, w workspace.WorkspaceInterface, folderPath string) (*FolderACL, error) { | ||
| obj, err := closestExistingObject(ctx, w, folderPath) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| objPermissions, err := w.GetPermissions(ctx, workspace.GetWorkspaceObjectPermissionsRequest{ | ||
| WorkspaceObjectId: strconv.FormatInt(obj.ObjectId, 10), | ||
| WorkspaceObjectType: "directories", | ||
| }) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // Report against the requested path, not the resolved ancestor, so diagnostics | ||
| // name the folder the user configured. | ||
| return &FolderACL{ | ||
| RequestedPath: folderPath, | ||
| ResolvedPath: obj.Path, | ||
| Permissions: ObjectAclToResourcePermissions(folderPath, objPermissions.AccessControlList), | ||
| }, nil | ||
| } | ||
|
|
||
| // Writability is the outcome of a folder writability check. It is three-valued | ||
| // because the check cannot always reach a definite answer: reading a folder ACL | ||
| // itself requires manage access, so the common "cannot write" case surfaces as a | ||
| // permission error on the read rather than as a readable ACL that omits the user. | ||
| type Writability int | ||
|
|
||
| const ( | ||
| // WritabilityUnknown means writability could not be determined (e.g. the | ||
| // folder does not exist and has no existing ancestor, or the ACL read failed | ||
| // for a reason other than permission denied). Callers should not warn. | ||
| WritabilityUnknown Writability = iota | ||
| // WritabilityWritable means the user has write access (directly, via a group, | ||
| // or as an admin), or it cannot be ruled out. | ||
| WritabilityWritable | ||
| // WritabilityNotWritable means the user definitely lacks write access: the | ||
| // permissions API denied the ACL read (which requires manage access), or the | ||
| // ACL was readable but grants the user no write-capable level. | ||
| WritabilityNotWritable | ||
| ) | ||
|
|
||
| // CheckWritable reports whether user can write to folderPath, resolving the ACL | ||
| // (walking up to the closest existing ancestor for a not-yet-created folder) and | ||
| // interpreting a permission-denied error on the read as a definite "not writable". | ||
| // | ||
| // Reading a folder ACL requires CAN_MANAGE (managing permissions is exclusive to | ||
| // CAN_MANAGE per the workspace ACL model; the permissions GET returns 403 | ||
| // "does not have Manage permissions" otherwise), so a 403 means the user cannot | ||
| // manage, and therefore cannot write to, the folder. | ||
| // See https://docs.databricks.com/aws/en/security/auth/access-control/ | ||
| func CheckWritable(ctx context.Context, w workspace.WorkspaceInterface, folderPath string, user *iam.User) Writability { | ||
| acl, err := ResolveFolderACL(ctx, w, folderPath) | ||
| if err != nil { | ||
| if errors.Is(err, apierr.ErrPermissionDenied) { | ||
| return WritabilityNotWritable | ||
| } | ||
| return WritabilityUnknown | ||
| } | ||
|
|
||
| if acl.CanWrite(user) { | ||
| return WritabilityWritable | ||
| } | ||
| return WritabilityNotWritable | ||
| } | ||
|
|
||
| // CanWrite reports whether user has write access (CAN_EDIT or higher) to the | ||
| // folder, either directly or through one of their groups. | ||
| // | ||
| // It only inspects a readable ACL; prefer CheckWritable, which also interprets a | ||
| // permission-denied error on the ACL read. It is deliberately conservative: | ||
| // it assumes write access when the user is a workspace admin (admins bypass the | ||
| // folder ACL) or when their group membership is not known here, because | ||
| // concluding "cannot write" in those cases would be a false alarm. | ||
| func (f *FolderACL) CanWrite(user *iam.User) bool { | ||
| groups := userGroupNames(user) | ||
| if groups[adminsGroup] { | ||
| return true | ||
| } | ||
|
|
||
| for _, p := range f.Permissions.Permissions { | ||
| if !isWriteLevel(string(p.Level)) { | ||
| continue | ||
| } | ||
| switch { | ||
| case p.UserName != "" && p.UserName == user.UserName: | ||
| return true | ||
| case p.ServicePrincipalName != "" && p.ServicePrincipalName == user.UserName: | ||
| return true | ||
| case p.GroupName != "" && groups[p.GroupName]: | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| // isWriteLevel reports whether an ACL level grants write access to a folder, | ||
| // which is what deploying a bundle requires. CAN_EDIT is the lowest such level. | ||
| func isWriteLevel(level string) bool { | ||
| return resources.GetLevelScore(level) >= resources.GetLevelScore(resources.CAN_EDIT) | ||
| } | ||
|
|
||
| // userGroupNames returns the set of group display names the user belongs to. | ||
| func userGroupNames(user *iam.User) map[string]bool { | ||
| groups := make(map[string]bool, len(user.Groups)) | ||
| for _, g := range user.Groups { | ||
| if g.Display != "" { | ||
| groups[g.Display] = true | ||
| } | ||
| } | ||
| return groups | ||
| } | ||
|
|
||
| // closestExistingObject returns the object at folderPath, or the closest existing | ||
| // ancestor when folderPath does not exist. A not-yet-created folder inherits the | ||
| // permissions of its closest existing ancestor. | ||
| func closestExistingObject(ctx context.Context, w workspace.WorkspaceInterface, folderPath string) (*workspace.ObjectInfo, error) { | ||
| for { | ||
| obj, err := w.GetStatusByPath(ctx, folderPath) | ||
| if err == nil { | ||
| return obj, nil | ||
| } | ||
|
|
||
| if !apierr.IsMissing(err) { | ||
| return nil, err | ||
| } | ||
|
|
||
| parent := path.Dir(folderPath) | ||
| // If the parent is the same as the current folder, then we have reached the root | ||
| if folderPath == parent { | ||
| break | ||
| } | ||
|
|
||
| folderPath = parent | ||
| } | ||
|
|
||
| return nil, fmt.Errorf("folder %s and its parent folders do not exist", folderPath) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not called right now, is it expected for it to be called somewhere else later?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is prep for #5528 not performing the mkdir, but still having a writability check on validate.