-
Notifications
You must be signed in to change notification settings - Fork 644
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
PermissionsService 2.0 (just implementation) #5158
Conversation
/// <summary> | ||
/// Is <paramref name="currentPrincipal"/> allowed to perform an action with a requirement of <paramref name="permissionsRequirement"/> on <paramref name="packageRegistration"/>? | ||
/// </summary> | ||
public static bool IsRequirementSatisfied(PermissionsRequirement permissionsRequirement, IPrincipal currentPrincipal, PackageRegistration packageRegistration) |
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 file is mostly identical to the existent PermissionsService
except:
1 - IsActionAllowed
is now IsRequirementSatisfied
2 - Added a method for ReservedNamespace
s
3 - Made some performance checks as suggested by @cristinamanum (specifically, we now check if having a specific kind of ownership would satisfy the requirement before checking if we have that ownership)
public static ActionRequiringPackagePermissions UploadNewPackageVersion = | ||
new ActionRequiringPackagePermissions( | ||
PermissionsRequirement.Owner | PermissionsRequirement.OrganizationAdmin | PermissionsRequirement.OrganizationCollaborator, | ||
PermissionsRequirement.Owner | PermissionsRequirement.SiteAdmin); |
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.
PermissionsRequirement.SiteAdmin [](start = 47, length = 32)
This one is wrong. Admins shouldn't be able to upload new versions.
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.
fixed
/// An action requiring permissions on a <see cref="ReservedNamespace"/> that can be done on behalf of another <see cref="User"/>. | ||
/// </summary> | ||
public class ActionRequiringReservedNamespacePermissions | ||
: ActionRequiringEntityPermissions<IEnumerable<ReservedNamespace>>, IActionRequiringEntityPermissions<ActionOnNewPackageContext> |
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.
IEnumerable [](start = 43, length = 30)
Might be worth leaving a comment about why this is multiple namespaces, i.e. multiple namespaces could apply to a single ID.
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.
done
|
||
var possibleAccountsOnBehalfOf = | ||
new[] { currentUser } | ||
.Union(GetOwners(entity)); |
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.
Union [](start = 21, length = 5)
Are set semantics necessary here? Are we sure comparison of user instances is working? Concat may be simpler.
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 wanted to get rid of duplicates. I will replace this with Concat
and Distinct
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.
Do you really care about duplicates here? Like most of the time it will be unique right? Distinct uses default comparer which is likely reference equals (or did we implement GetHashCode
and Equals
?)
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, the values calculated here will be piped directly to the UI for upload and API key scenarios.
var failure = IsAllowed(currentUser, accountOnBehalfOf, entity); | ||
if (failure == PermissionsCheckResult.Allowed) | ||
{ | ||
accountsAllowedOnBehalfOf = accountsAllowedOnBehalfOf.Concat(new[] { accountOnBehalfOf }); |
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.
Concat [](start = 74, length = 6)
Use List
, don't need LINQ for everything
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.
makes sense
/// </summary> | ||
private static bool WouldSatisfy(PermissionsRequirement permissionsRequirementToCheck, PermissionsRequirement permissionsRequirementToSatisfy) | ||
{ | ||
return (permissionsRequirementToCheck & permissionsRequirementToSatisfy) > 0; |
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.
[](start = 85, length = 1)
!=
unless this is an unsigned flags or if you have tests asserting never negative flag values
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.
==
and !=
work with Flags
?
I'm using the int logic here because the second parameter is a combination of multiple enum values, so I want to compare that they just have at least one value in common.
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.
[Flags]
is just an attribute. The important details is that the enum
is an int
(unless you changed that...)
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.
No what I mean is we'll be comparing 0b0010 and 0b1010 and expect it to return true. I didn't think enum
or Flags
made this comparison return true.
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.
Oh I see now
if (entityOwners == null || !entityOwners.Any()) | ||
{ | ||
return 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 breaks admin permissions (on things they would otherwise do)
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.
fixed--it was not only a logic issue but a tests issues
.OfType<Organization>() | ||
.SelectMany(o => o.Members) | ||
.Where(m => isUserMatch(m.Member)) | ||
.ToArray(); |
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.
ToArray [](start = 17, length = 7)
ToList requires fewer allocations and is a better default for these one-off 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.
done
return true; | ||
} | ||
|
||
return 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.
false [](start = 19, length = 5)
This is such a generic code path, I have a hard time thinking of cases that would fall through here. Might be worth commenting about that.
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 did some restructuring that should make this clearer to understand
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 nice PR. Thanks for the break-down.
} | ||
|
||
if (WouldSatisfy(PermissionsRequirement.SiteAdmin, permissionsRequirement) && | ||
isUserAdmin) |
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.
perf nit: always put the cheaper condition first so the 'and' operation will fail if it's 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.
nice catch
accountsAllowedOnBehalfOf = Enumerable.Empty<User>(); | ||
|
||
var possibleAccountsOnBehalfOf = | ||
new[] { currentUser } |
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.
Is it ok to add the currentUser if the currentUser == null ?
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.
yup, it's necessary--if the current user is logged out but still able to perform the action, we still want to return true
} | ||
|
||
return reservedNamespaces.Any(rn => PermissionsHelpers.IsRequirementSatisfied(ReservedNamespacePermissionsRequirement, account, rn)) ? | ||
PermissionsCheckResult.Allowed : PermissionsCheckResult.ReservedNamespaceFailure; |
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.
Why is it fine to have Any? Should not be permissions allowed for All the namespaces?
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.
return true; | ||
} | ||
|
||
if (entityOwners == null || !entityOwners.Any()) |
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.
There is a multiple enumeration of an IEnumerable
(between here and lines 124 and 129). Since it is unlikely could be fixed, please use something else (ICollection
at least).
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.
switched all methods in this class to use ICollection
/// <summary> | ||
/// Determines whether <paramref name="currentUser"/> can perform this action on <paramref name="entity"/> on behalf of <paramref name="account"/>. | ||
/// </summary> | ||
public PermissionsCheckResult IsAllowed(User currentUser, User account, TEntity entity) |
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.
If it's not returning bool
it shouldn't be called IsAllowed
, same for overload below
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.
renamed to CheckPermissions
PackageRegistrationPermissionsRequirement = packageRegistrationPermissionsRequirement; | ||
} | ||
|
||
public PermissionsCheckResult IsAllowed(User currentUser, User account, Package package) |
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.
Same comment as above: method name should be different.
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.
renamed to CheckPermissions
|
||
protected override PermissionsCheckResult IsAllowedOnEntity(User account, IEnumerable<ReservedNamespace> reservedNamespaces) | ||
{ | ||
if (!reservedNamespaces.Any()) |
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.
Multiple enumeration of IEnumerable
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.
changed to IReadOnlyCollection
/// <summary> | ||
/// No requirement--any user can satisfy the requirement. | ||
/// </summary> | ||
None = 1, |
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.
Replace values with 1 << 0
, 1 << 1
, 1 << 2
, etc. maybe to emphasize bit masking?
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.
done
|
||
private static bool Includes(int i, ReturnsSatisfiedRequirementWhenExpected_State state) | ||
{ | ||
return (i & (int)state) > 0; |
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.
Should be !=
instead of >
, strictly speaking, but wouldn't be an issue unless we have more stuff in the ReturnsSatisfiedRequirementWhenExpected_State
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.
got it
/// </summary> | ||
public static bool IsRequirementSatisfied(PermissionsRequirement permissionsRequirement, IPrincipal currentPrincipal, IEnumerable<User> entityOwners) | ||
{ | ||
if (WouldSatisfy(PermissionsRequirement.None, permissionsRequirement)) |
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.
There are 2 WouldSatisfy checks when prinipal is non-null. Recommend changing to:
if (currentPrincipal == null) {
return WouldSatisfy(PermissionsRequirement.None, permissionsRequirement);
}
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.
done
if (currentUser == null) | ||
{ | ||
return 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.
see comment above about the WouldSatisfy check...
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.
done
|
||
foreach (var accountOnBehalfOf in possibleAccountsOnBehalfOf) | ||
{ | ||
var failure = IsAllowed(currentUser, accountOnBehalfOf, entity); |
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.
Shouldn't need to check IsAllowed on currentUser, right?
Also, can you just do the IsAllowed check on org owners when populating the original list? i.e., GetOwners(entity).Where(o => IsAllowed(currentUser, o, entity))
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.
currentUser
is required in all scenarios now--"can currentUser
perform this action on entity
on behalf of accountOnBehalfOf
" is how this function call should be read
I tried to make it all LINQ, but since I have to split the initial population of the list into two pieces (see the currentUser != null
check) I think this is cleaner.
/// An action requiring permissions on a <see cref="PackageRegistration"/> or <see cref="Package"/> that can be done on behalf of another <see cref="User"/>. | ||
/// </summary> | ||
public class ActionRequiringPackagePermissions | ||
: ActionRequiringEntityPermissions<PackageRegistration>, IActionRequiringEntityPermissions<Package> |
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.
Seems unusual that the base class generic parameter is the package registration, but the interface parameter is the package version. Should/could both use package as the generic parameter?
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.
base class extends the interface for PackageRegistration
and provides the actual implementation
by extending the interface for Package
here we allow users to pass in Package
instead of PackageRegistration
for convenience
public static ActionRequiringPackagePermissions DisplayPrivatePackageMetadata = | ||
new ActionRequiringPackagePermissions( | ||
PermissionsRequirement.Owner | PermissionsRequirement.OrganizationAdmin | PermissionsRequirement.OrganizationCollaborator, | ||
PermissionsRequirement.Owner | PermissionsRequirement.SiteAdmin); |
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: might be nice if these used named args, assuming shorter arg names:
new ActionRequiringPackagePermissions(
onBehalfOfRequirement: PermissionsRequirement.Owner
| PermissionsRequirement.OrganizationAdmin
| PermissionsRequirement.OrganizationCollaborator,
actionRequirement: PermissionsRequirement.Owner
| PermissionsRequirement.SiteAdmin);
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.
Or, maybe improve readability by introducing private fields for OnBehalfOf combinations:
static PermissionsRequirement OnBehalfOfOwnerOrOrganizationMember;
static PermissionsRequirement OnBehalfOfOwnerOrOrganizationAdmin;
new ActionRequiringPackagePermissions(
OnBehalfOfOwnerOrOrganizationMember,
PermissionsRequirement.Owner | PermissionsRequirement.SiteAdmin);
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 like both of these--will do both
/// <summary> | ||
/// The reason an action is not allowed is not known. | ||
/// </summary> | ||
UnknownFailure, |
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.
Is this used outside tests?
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 initially was, but is no longer. will remove
/// Describes the result of checking if an <see cref="ActionsRequiringPermissions"/> is allowed. | ||
/// </summary> | ||
public enum PermissionsCheckResult | ||
{ |
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.
Do we need this class, or is a bool (success/failure) sufficient?
Seems intended to provide 4 possible results:
- allowed
- exception
- on-behalf-of operation not allowed
- entity operation not allowed
I'm not sure we need to distinguish 3 & 4, and 2 seems like it should be normal exception handling.
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 this enum
is necessary to provide the necessary messaging to the user.
"You don't have permission to upload new versions on behalf of 'y'." or "User 'y' does not have permissions to upload new versions of 'x'." are much more descriptive than "You don't have permissions to do something but we don't know what."
/// <summary> | ||
/// No user can satisfy the requirement. | ||
/// </summary> | ||
Unsatisfiable = 0, |
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.
How will this be used? If a requirement can't be satisfied, then we don't support it?
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.
exists for syntactical cleanliness
public ActionOnNewPackageContext(string packageId, IReservedNamespaceService reservedNamespaceService) | ||
{ | ||
PackageId = packageId; | ||
ReservedNamespaceService = reservedNamespaceService; |
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.
null check
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.
good catch
59e9650
to
ea9b257
Compare
@joelverhagen @chenriksson @skofman1 @agr All feedback was addressed. |
/// <summary> | ||
/// Context object for checking permissions of an action involving a new package ID. | ||
/// </summary> | ||
public class ActionOnNewPackageContext |
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.
one class per file. makes github search by file much easier
.SelectMany(o => o.Members) | ||
.Where(m => isUserMatch(m.Member)) | ||
.ToList() | ||
.AsReadOnly(); |
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.
Why do you need AsReadOnly? Seems like an unnecessary allocation for a collection only used internally.
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 was thinking it might be cleaner if we guaranteed this was read-only but I suppose it doesn't really matter.
{ | ||
private const PermissionsRequirement RequireOwnerOrSiteAdmin = PermissionsRequirement.Owner | PermissionsRequirement.SiteAdmin; | ||
private const PermissionsRequirement RequireOwnerOrOrganizationAdmin = PermissionsRequirement.Owner | PermissionsRequirement.OrganizationAdmin; | ||
private const PermissionsRequirement RequireOwnerOrOrganizationMember = PermissionsRequirement.Owner | PermissionsRequirement.OrganizationAdmin | PermissionsRequirement.OrganizationCollaborator; |
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: wrap lines?
public bool TryGetAccountsIsAllowedOnBehalfOf(User currentUser, TEntity entity, out IEnumerable<User> accountsAllowedOnBehalfOf) | ||
{ | ||
var accountsAllowedOnBehalfOfList = new List<User>(); | ||
accountsAllowedOnBehalfOf = accountsAllowedOnBehalfOfList; |
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.
why do you need the local variable?
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.
the out
parameter is an IEnumerable<User>
and not a List<User>
, so I need to either store this local variable or cast the out parameter.
I'll switch to casting the out
parameter instead of having another local variable, it looks cleaner.
foreach (var accountOnBehalfOf in possibleAccountsOnBehalfOf) | ||
{ | ||
var failure = CheckPermissions(currentUser, accountOnBehalfOf, entity); | ||
if (failure == PermissionsCheckResult.Allowed) |
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: failure sounds odd; maybe permissionsResult?
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.
the enum used to be named PermissionsFailure
and was renamed PermissionsCheckResult
but this variable was never updated. It is now updated to result
.
.Concat(currentUser.Organizations.Select(o => o.Organization)); | ||
} | ||
|
||
possibleAccountsOnBehalfOf = possibleAccountsOnBehalfOf.Distinct(new UserEqualityComparer()); |
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.
Why do we need Distinct here?
The membership table has PK of (OrganizationKey, MemberKey), so I don't think a User can have multiple memberships in the same Organization.
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 a distinct on not only the user's organizations, but also the owners of the entity we are checking permissions of (e.g. the owners of the package). Therefore, there can be duplicates.
{ | ||
PermissionsCheckResult CheckPermissions(User currentUser, User account, TEntity entity); | ||
PermissionsCheckResult CheckPermissions(IPrincipal currentPrincipal, User account, TEntity entity); | ||
bool TryGetAccountsIsAllowedOnBehalfOf(User currentUser, TEntity entity, out IEnumerable<User> accountsAllowedOnBehalfOf); |
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.
Should this be TryGetAccountsAllowedOnBehalfOf
?
@scottbommarito My only remaining feedback is with the distinct call... please look at that, then I'm good. |
This is the implementation of the new version of the Permissions Service, without changes to the existing code.
You may have seen this code before in #5097