-
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: ActionsRequringPermission #5097
Conversation
@@ -414,8 +413,7 @@ public virtual Task<ActionResult> CreatePackagePost() | |||
} | |||
|
|||
// For a new package id verify that the user is allowed to push to the matching namespaces, if any. | |||
var isPushAllowed = ReservedNamespaceService.IsPushAllowed(id, user, out userOwnedNamespaces); | |||
if (!isPushAllowed) | |||
if (!ActionsRequiringPermissions.UploadNewPackageId.TryGetAccountsIsAllowedOnBehalfOf(user, new ActionOnNewPackageContext(id, ReservedNamespaceService), out var 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.
TryGetAccountsIsAllowedOnBehalfOf
sounds awkward:
TryGetAccountIsAllowedOnBehalfOf
TryGetAccountsAreAllowedOnBehalfOf
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 is this out
parameter used?
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 used in this PR--it's used in the upload flow to determine the list of accounts that the current user can upload a package on behalf of
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 use the Try pattern here? There should always be 1 account allowed (self). What is the scenario where you expect this API to return false?
Maybe just GetOnBehalfOfAccounts
?
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 returns accounts the action is allowed ON BEHALF OF.
For example, if you were a member of two organizations that owned a package and you called this method on them, the out
parameter would return both, because you can do the action on behalf of both of them.
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.
Does the return value of TryGetAccountsIsAllowedOnBehalfOf have any correlation with the collection count of the out parameter?
If not, would it be better to have two methods instead of one?
// For a new package id verify if the user is allowed to use it. | ||
if (packageRegistration == null) | ||
if (packageRegistration == null && !ActionsRequiringPermissions.UploadNewPackageId.TryGetAccountsIsAllowedOnBehalfOf(currentUser, new ActionOnNewPackageContext(id, _reservedNamespaceService), out 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.
Wait, why are we passing a dependency as a parameter? I see that this is a static... could it become an injected thing?
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.
(That would also facilitate unit testing)
{ | ||
if (currentUser == null) | ||
{ | ||
return PermissionLevelsIntersect(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.
The semantics of intersect suggest a constant output, i.e. A ∩ ∅ = ∅
} | ||
|
||
var matchingMembers = entityOwners | ||
.Where(o => o is 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 equivalent to OfType<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.
TIL, nice catch
I am still reviewing this. Very large PR 🤓. Could we have built the framework as one PR and integrated in another? |
@@ -414,8 +413,7 @@ public virtual Task<ActionResult> CreatePackagePost() | |||
} | |||
|
|||
// For a new package id verify that the user is allowed to push to the matching namespaces, if any. | |||
var isPushAllowed = ReservedNamespaceService.IsPushAllowed(id, user, out userOwnedNamespaces); | |||
if (!isPushAllowed) | |||
if (!ActionsRequiringPermissions.UploadNewPackageId.TryGetAccountsIsAllowedOnBehalfOf(user, new ActionOnNewPackageContext(id, ReservedNamespaceService), out var 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.
Why use the Try pattern here? There should always be 1 account allowed (self). What is the scenario where you expect this API to return false?
Maybe just GetOnBehalfOfAccounts
?
|
||
namespace NuGetGallery | ||
{ | ||
public class PermissionsHelpers |
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.
static class
public class PermissionsHelpers | ||
{ | ||
/// <summary> | ||
/// Is <paramref name="currentPrincipal"/> allowed to perform an action with a requirement of <paramref name="permissionsRequirement"/> on <paramref name="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.
Lines are long in this file, making it hard to review - can you wrap?
Do we have guideline as team for max length -- maybe 120 or 130?
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.
IMO line length should be something handled by the tooling.
Not sure the best way to fix the line length, but I was under the impression the only long lines are the comments.
Also this file is 90% the same from before besides renaming.
private bool ShouldForceSharedNamespace(string value) | ||
{ | ||
var liberalMatchingNamespaces = GetReservedNamespacesForId(value); | ||
return liberalMatchingNamespaces.Any(rn => rn.IsSharedNamespace); | ||
} | ||
|
||
public bool ShouldMarkNewPackageIdVerified(User account, string id, out IReadOnlyCollection<ReservedNamespace> ownedMatchingReservedNamespaces) |
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.
ReservedNamespaceService refactoring doesn't look related to the rest of this PR?
What's the need for the refactoring? Should it be in a separate PR?
<Compile Include="Services\ActionRequiringEntityPermissions.cs" /> | ||
<Compile Include="Services\ActionRequiringPackagePermissions.cs" /> | ||
<Compile Include="Services\ActionRequiringReservedNamespacePermissions.cs" /> | ||
<Compile Include="Services\ActionsRequiringPermissions.cs" /> |
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.
We no longer have a permissions service. Should these permissions classes still be in the services namespace?
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.
Two notes:
1 - We don't actually have a NuGetGallery.Services
namespace, everything is in NuGetGallery
.
2 - I think they're ok in services, but TBH I'd prefer to split everything up more granularly and add a Permissions
folder
/// An action requiring permissions on an entity that can be done on behalf of another <see cref="User"/>. | ||
/// </summary> | ||
public abstract class ActionRequiringEntityPermissions<TEntity> | ||
: IActionRequiringEntityPermissions<TEntity> |
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 there be where TEntity : IEntity
?
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--we also allow ListPackageItemViewModel
to be here.
I was considering making a superclass (IOwnedEntity
) and making PackageRegistration
, ReservedNamespace
, User
, and ListPackageItemViewModel
extend it, but I decided it would be better to leave it alone.
/// </summary> | ||
/// <param name="accountsAllowedOnBehalfOf">A <see cref="IEnumerable{User}"/> containing all accounts that <paramref name="currentUser"/> can perform this action on <paramref name="entity"/> on behalf of.</param> | ||
/// <returns>True if and only if <paramref name="currentPrincipal"/> can perform this action on <paramref name="entity"/> on behalf of any <see cref="User"/>.</returns> | ||
public 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.
See other comment... why use the Try pattern here?
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 returns accounts the action is allowed ON BEHALF OF.
In other words, the other method (IsAllowed
) requires you to specify an account you want to perform the action on behalf of. This method figures out if what accounts you can do the action on behalf of.
For example, if you were a member of two organizations that owned a package and you called this method on them, the out
parameter would return both, because you can do the action on behalf of both of them. If you were also an owner of the package, it would return you as well.
The method as a whole returns whether or not you can do it on behalf of anyone.
As I mentioned above, the primary use case is determined who you can push a package on behalf of.
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 not remove the the out and return the IEnum and the caller of this method can do the .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.
I prefer the TryGet
model rather than a boolean because
1 - This method is frequently used simply as a boolean, without concern for the result of the out
parameter and removing it would add a lot of excess .Any()
calls around the code
2 - The caller would now need to understand that if an action does not have any accounts it can be performed on behalf of, it is not allowed.
return packageRegistration != null ? packageRegistration.Owners : Enumerable.Empty<User>(); | ||
} | ||
|
||
private PackageRegistration ConvertPackageToRegistration(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.
Get
is more appropriate than Convert
. Method doesn't look necessary, would just inline.
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 the naming change.
I added this method because I wanted to guarantee that the method for converting between a Package
and a PackageRegistration
is consistent.
/// <summary> | ||
/// A collection of all <see cref="ActionRequiringAccountPermissions"/> and <see cref="IActionRequiringEntityPermissions{TEntity}"/>s. | ||
/// </summary> | ||
public static class ActionsRequiringPermissions |
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.
Maybe members should be sorted by entity type, possibly using regions?
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 wasn't sure how to sort the file, but entity type sounds better.
|
||
namespace NuGetGallery | ||
{ | ||
public class PermissionsHelpers |
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.
APIs look a lot cleaner!
As mentioned, only concern with static class is that it could be more difficult to integrate auditing/telemetry into the permissions helpers. We can figure it out if that becomes necessary.
if (package == null | ||
|| ((package.PackageStatusKey == PackageStatus.Validating | ||
|| package.PackageStatusKey == PackageStatus.FailedValidation) | ||
&& !PermissionsService.IsActionAllowed(package, User, PackageActions.DisplayPrivatePackage))) | ||
&& !ActionsRequiringPermissions.DisplayPrivatePackageMetadata.TryGetAccountsIsAllowedOnBehalfOf(currentUser, package, out 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.
nit: shorter lines?
// Validating packages should be hidden to everyone but the owners and admins. | ||
var currentUser = GetCurrentUser(); | ||
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.
Is this used anywhere?
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.
just to contain the unused out
parameter
the out
parameter will be used in future upload flows
private static bool IsRequirementSatisfied(PermissionsRequirement permissionsRequirement, bool isUserAdmin, Func<User, bool> isUserMatch, IEnumerable<User> entityOwners) | ||
{ | ||
if ((entityOwners == null || !entityOwners.Any()) && | ||
PermissionLevelsIntersect(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.
Should not always return true on PermissionRequirement.None ?
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, I can restructure this code to make that check first.
} | ||
|
||
if (entityOwners.Any(isUserMatch) && | ||
PermissionLevelsIntersect(PermissionsRequirement.Owner, 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.
Maybe change the order of the operands to not execute Any(isUserMarch) if not the Owner permissionRequirement
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.
applying that logic to all of these checks would probably make it substantially faster...nice catch!
/// <summary> | ||
/// Determines whether <paramref name="currentUser"/> can perform this action on <paramref name="account"/>. | ||
/// </summary> | ||
public PermissionsFailure IsAllowed(User currentUser, User account) |
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.
PermissionFailure suggests a failure. Will this call be always a failure? Why not returning bool?
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.
PermissionsCheckResult
instead? Almost all of the enum
results are failures which is why I called it PermissionsFailure
, but it probably could be clearer if it was not named that.
protected abstract PermissionsFailure IsAllowedOnEntity(User account, TEntity entity); | ||
|
||
/// <summary> | ||
/// Determines whether <paramref name="currentPrincipal"/> can perform this action on <paramref name="entity"/> on behalf of any <see cref="User"/>. |
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.
currentPrincipal -> currentUser?
Closing this PR because I'm planning on splitting it up. |
I'm merging this into a feature branch because I want to add some more unit tests with future PRs, but would like to merge this in now and then start working on unit tests of controller methods.
PermissionsService
intoPermissionsHelpers
PermissionLevel
toPermissionsRequirement
ActionRequiringAccountPermissions
,IActionRequiringEntityPermissions
,ActionRequiringEntityPermissions
,ActionRequiringPackagePermissions
andActionRequiringReservedNamespacePermissions
for consolidating permissions logicActionsRequiringPermissions
to store statically declared instances of the classes described above-Replace old
PermissionsService
calls with newActionsRequiringPermissions
calls.Old API:
New API: