Skip to content
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

Merged

Conversation

ItamarYuran
Copy link
Contributor

@ItamarYuran ItamarYuran commented Oct 28, 2024

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

Screenshot 2024-10-28 at 13 59 02 Screenshot 2024-10-28 at 14 02 29

@ItamarYuran ItamarYuran linked an issue Oct 28, 2024 that may be closed by this pull request
@ItamarYuran ItamarYuran added the exclude-changelog PR description should not be included in next release changelog label Oct 28, 2024
Copy link

github-actions bot commented Oct 28, 2024

E2E Test Results - Quickstart

11 passed

Copy link

E2E Test Results - DynamoDB Local - Local Block Adapter

13 passed

@ItamarYuran ItamarYuran marked this pull request as draft October 28, 2024 12:53
@ItamarYuran ItamarYuran marked this pull request as ready for review October 29, 2024 12:11
Copy link
Contributor

@arielshaqed arielshaqed left a 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.?

Comment on lines 1157 to 1161
deniedStr := "denied from:\n"
for _, statement := range n.Denied {
deniedStr += strings.Join(statement.Action, "\n")
}
return deniedStr
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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.
  2. 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.

allowed := CheckNeutral
hasPermission := false
Copy link
Contributor

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.

@@ -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)
Copy link
Contributor

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.

Copy link
Contributor

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%

@ItamarYuran
Copy link
Contributor Author

The current code returns:

  • All denied actions in a policy in case there are any
  • First missing permission in case there is no relevant denied action

Copy link
Contributor

@arielshaqed arielshaqed left a 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()) {
Copy link
Contributor

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.

},
},
username: "user1",
expected: "lacking permissions for:\nfs:CreateRepository",
Copy link
Contributor

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.


if allowed != CheckAllow {
return &AuthorizationResponse{
Allowed: false,
Error: ErrInsufficientPermissions,
Error: fmt.Errorf("%w\n%s", ErrInsufficientPermissions, perm.String()),
Copy link
Contributor

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.

Suggested change
Error: fmt.Errorf("%w\n%s", ErrInsufficientPermissions, perm.String()),
Error: fmt.Errorf("%w: %s", ErrInsufficientPermissions, perm.String()),

fmt.Fprintln(w, action)
}
}
return strings.TrimSpace(w.String())
Copy link
Contributor

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.

Comment on lines 1158 to 1164
fmt.Fprintln(w, "denied from:")
for _, statement := range n.Denied {
for _, action := range statement.Action {
fmt.Fprintln(w, action)
}
}
return strings.TrimSpace(w.String())
Copy link
Contributor

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:

Suggested change
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.

}
return strings.TrimSpace(w.String())
}
return strings.TrimSpace(w.String())
Copy link
Contributor

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".

Comment on lines 48 to 49
Denied []model.Statement
Unauthorized []permissions.Node
Copy link
Contributor

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!

Suggested change
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

return strings.TrimSpace(w.String())
}

func CheckPermissions(ctx context.Context, node permissions.Node, username string, policies []*model.Policy, perm *NeededPermissions) CheckResult {
Copy link
Contributor

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.

@@ -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)
Copy link
Contributor

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%

@@ -44,6 +44,11 @@ type AuthorizationResponse struct {
Error error
}

type NeededPermissions struct {
Copy link
Contributor

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?

@@ -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{}
Copy link
Contributor

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?

@ItamarYuran ItamarYuran force-pushed the 8224-report-required-permissions-when-authorization-fails branch from 539918d to 704af7d Compare November 4, 2024 17:08
@ItamarYuran
Copy link
Contributor Author

Thank you for your reviews!
These changes will return all denied permissions, only the ones that regard the request (not all permission in the policy)
Same goes for missing permissions, all missing one will return. In case of denied + missing permissions only denied ones return.

@ItamarYuran ItamarYuran force-pushed the 8224-report-required-permissions-when-authorization-fails branch from 972dc58 to 758d910 Compare November 5, 2024 14:02
Copy link
Contributor

@arielshaqed arielshaqed left a 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.

Comment on lines 5391 to 5392
}, /////////////////////////////////////////////////////////////////
{
Copy link
Contributor

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.

Suggested change
}, /////////////////////////////////////////////////////////////////
{
}, {

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, ","))
Copy link
Contributor

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

Suggested change
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.

Comment on lines 1189 to 1191
} else {
hasPermission = true
allowed = CheckAllow
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} 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.

Copy link
Contributor

@Isan-Rivkin Isan-Rivkin left a 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

@@ -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{}
Copy link
Contributor

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.

@ItamarYuran ItamarYuran merged commit 3d0db30 into master Nov 12, 2024
38 checks passed
@ItamarYuran ItamarYuran deleted the 8224-report-required-permissions-when-authorization-fails branch November 12, 2024 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
exclude-changelog PR description should not be included in next release changelog
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Report required permission(s) when authorization fails
3 participants