-
Notifications
You must be signed in to change notification settings - Fork 354
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
8224 report required permissions when authorization fails #8314
8224 report required permissions when authorization fails #8314
Conversation
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.
Nice! Can you please show what outputs we can expect? Or is that in the next commit with tests etc.?
pkg/auth/service.go
Outdated
deniedStr := "denied from:\n" | ||
for _, statement := range n.Denied { | ||
deniedStr += strings.Join(statement.Action, "\n") | ||
} | ||
return deniedStr |
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.
- Not sure how this code can successfully format deniedStr if
len(n.Denied) > 0
. There will be no newline after the last row of the Action list on each statement. - It will probably be easier to use strings.Builder. This is actually an io.Writer, so you can just write to it using the normal operations. Here's a playground.
pkg/auth/service.go
Outdated
allowed := CheckNeutral | ||
hasPermission := false |
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.
This var is used only in case permissions.NoteTypeNode
. So please keep it in scope there: you could just put that entire case inside curly brackets, and that var in there. Or extract it to another func, or even go full OO and make each of these a method. Right now the code confuses me about what might happen with this variable in other cases.
pkg/auth/service.go
Outdated
@@ -1165,23 +1189,28 @@ func CheckPermissions(ctx context.Context, node permissions.Node, username strin | |||
|
|||
if stmt.Effect == model.StatementEffectDeny { | |||
// this is a "Deny" and it takes precedence | |||
return CheckDeny | |||
perm.Denied = append(perm.Denied, stmt) |
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.
This is confusing: I take perm
as an input, now I modify it and return it. So I've changed my input, but also returned it as an output.
I think either make perm into an object, pass it by pointer, and keep state on it, or explicitly handle the lists.
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.
Was thinking the same, agree with @arielshaqed 100%
The current code returns:
|
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.
Thanks!
Final stretch: the actual message. The current message is too detailed and as a result needs too many lines. Let's start by playing safely and never report anything other than action names. Reporting an entire statement gives a wealth of information that might not visible to the user - such as permissions of other users.
@@ -234,7 +235,7 @@ func TestCreateRepo_Unauthorized(t *testing.T) { | |||
}) | |||
require.NoError(t, err) | |||
require.NotNil(t, resp.JSON401) | |||
if resp.JSON401.Message != auth.ErrInsufficientPermissions.Error() { | |||
if !strings.Contains(resp.JSON401.Message, auth.ErrInsufficientPermissions.Error()) { |
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.
Can we test here (and below) that we got an expected value? Obviously doing so makes sense only after we finalize the message contents. So it is fine to open an issue to improve these checks in this test file.
pkg/api/controller_test.go
Outdated
}, | ||
}, | ||
username: "user1", | ||
expected: "lacking permissions for:\nfs:CreateRepository", |
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.
I would expect it to report both missing permissions. Reporting only one is fine, but then the message is surprising - it says "permissions" when it only ever means a single permissions.
pkg/auth/service.go
Outdated
|
||
if allowed != CheckAllow { | ||
return &AuthorizationResponse{ | ||
Allowed: false, | ||
Error: ErrInsufficientPermissions, | ||
Error: fmt.Errorf("%w\n%s", ErrInsufficientPermissions, perm.String()), |
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.
Looking at the results in the tests, I prefer without the newline, error messages are rarely >1 line.
Error: fmt.Errorf("%w\n%s", ErrInsufficientPermissions, perm.String()), | |
Error: fmt.Errorf("%w: %s", ErrInsufficientPermissions, perm.String()), |
pkg/auth/service.go
Outdated
fmt.Fprintln(w, action) | ||
} | ||
} | ||
return strings.TrimSpace(w.String()) |
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 is quite rare to need to TrimSpace on a string you created.
pkg/auth/service.go
Outdated
fmt.Fprintln(w, "denied from:") | ||
for _, statement := range n.Denied { | ||
for _, action := range statement.Action { | ||
fmt.Fprintln(w, action) | ||
} | ||
} | ||
return strings.TrimSpace(w.String()) |
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.
I think you don't even need a strings.Builder here:
fmt.Fprintln(w, "denied from:") | |
for _, statement := range n.Denied { | |
for _, action := range statement.Action { | |
fmt.Fprintln(w, action) | |
} | |
} | |
return strings.TrimSpace(w.String()) | |
return fmt.Sprintf("denied permission to %s", strings.Join(n.Denied, ", ")) |
The same of course in the next section.
pkg/auth/service.go
Outdated
} | ||
return strings.TrimSpace(w.String()) | ||
} | ||
return strings.TrimSpace(w.String()) |
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.
This returns ""
. If that is not helpful, please return some constant string like "not allowed".
pkg/auth/service.go
Outdated
Denied []model.Statement | ||
Unauthorized []permissions.Node |
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.
These are a bit too informative IMO. Let's just keep the action name here? Otherwise I learn details of the policy including denied paths that I might not have known existed!
Denied []model.Statement | |
Unauthorized []permissions.Node | |
// Denied is a list of actions the user was denied for the attempt. | |
Denied []string | |
// Unauthorized is a list of actions the user did not have for the attempt. | |
Unauthorized []string |
pkg/auth/service.go
Outdated
return strings.TrimSpace(w.String()) | ||
} | ||
|
||
func CheckPermissions(ctx context.Context, node permissions.Node, username string, policies []*model.Policy, perm *NeededPermissions) CheckResult { |
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.
nit: variable name perm
is too generic.
Can you use something more explicit?
for example: audit
, denyAudit
, permAudit
, history
etc...
So that in the future when extending, debugging or just reading the code we can immediately understand what it is used for.
pkg/auth/service.go
Outdated
@@ -1165,23 +1189,28 @@ func CheckPermissions(ctx context.Context, node permissions.Node, username strin | |||
|
|||
if stmt.Effect == model.StatementEffectDeny { | |||
// this is a "Deny" and it takes precedence | |||
return CheckDeny | |||
perm.Denied = append(perm.Denied, stmt) |
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.
Was thinking the same, agree with @arielshaqed 100%
pkg/auth/service.go
Outdated
@@ -44,6 +44,11 @@ type AuthorizationResponse struct { | |||
Error error | |||
} | |||
|
|||
type NeededPermissions struct { |
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.
rename, MissingPermissions
or PermissionsAudit
or DenyHistory
w/e would be more appropriate?
contrib/auth/acl/service.go
Outdated
@@ -895,8 +895,8 @@ func (s *AuthService) Authorize(ctx context.Context, req *auth.AuthorizationRequ | |||
if err != nil { | |||
return nil, err | |||
} | |||
|
|||
allowed := auth.CheckPermissions(ctx, req.RequiredPermissions, req.Username, policies) | |||
perm := &auth.NeededPermissions{} |
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.
sorry, Im confused what are you doing with this variable?
539918d
to
704af7d
Compare
Thank you for your reviews! |
972dc58
to
758d910
Compare
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.
Very elegant, thanks! I think this will be a great improvement in quality of life for lakeFS admins who need to configure permissions.
Just a note about phrasing one of the error messages; please decide if it will improve things and pull either way.
pkg/api/controller_test.go
Outdated
}, ///////////////////////////////////////////////////////////////// | ||
{ |
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.
Please avoid adding filler comments. They are pleasant for different people at different widths, they are never consistent, and they end up not appearing everywhere.
}, ///////////////////////////////////////////////////////////////// | |
{ | |
}, { |
pkg/auth/service.go
Outdated
return fmt.Sprintf("denied permission to %s", strings.Join(n.Denied, ",")) | ||
} | ||
if len(n.Unauthorized) != 0 { | ||
return fmt.Sprintf("missing permission to %s", strings.Join(n.Unauthorized, ",")) |
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.
l.935 will cause a message that says "insufficient permissions: missing permission to ..."
. That's a bit wordy, so perhaps
return fmt.Sprintf("missing permission to %s", strings.Join(n.Unauthorized, ",")) | |
return fmt.Sprintf("not allowed to %s", strings.Join(n.Unauthorized, ",")) |
An added benefit is that it uses the same word "allow" from the policy language.
pkg/auth/service.go
Outdated
} else { | ||
hasPermission = true | ||
allowed = CheckAllow |
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.
} else { | |
hasPermission = true | |
allowed = CheckAllow | |
hasPermission = true | |
allowed = CheckAllow |
is closer to the previous version.
We prefer to use an "early return" style: errors return immediately - like l. 1188 does; successes keep going with no "else" and no need to indent.
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.
single comment regarding ACL authorize check. its not used there.
Don't want to block PTAL
contrib/auth/acl/service.go
Outdated
@@ -895,8 +895,8 @@ func (s *AuthService) Authorize(ctx context.Context, req *auth.AuthorizationRequ | |||
if err != nil { | |||
return nil, err | |||
} | |||
|
|||
allowed := auth.CheckPermissions(ctx, req.RequiredPermissions, req.Username, policies) | |||
perm := &auth.MissingPermissions{} |
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.
Going back to prev question - what are you doing with perm
after auth.CheckPermissions
? I don't see it being used.
Closes #(8224)
Change Description
The added code helps the user understand why they are not authorized to perform a certain action.
Background
Up until now in case a user had no permissions or was denied of some action, they would only get a
401 unauthorized
message, making it hard to understand what is missing in order to perform that action.The enhancement will report all denied actions in case there are any, or if there aren't any - report missing permissions.
Testing Details
Changes were not tested yet, no existing test got broken
Additional info