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

PermissionsService 2.0: ActionsRequringPermission #5097

Closed
wants to merge 8 commits into from

Conversation

scottbommarito
Copy link
Contributor

@scottbommarito scottbommarito commented Nov 30, 2017

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.

  • Rename old PermissionsService into PermissionsHelpers
  • Rename PermissionLevel to PermissionsRequirement
  • Add ActionRequiringAccountPermissions, IActionRequiringEntityPermissions, ActionRequiringEntityPermissions, ActionRequiringPackagePermissions and ActionRequiringReservedNamespacePermissions for consolidating permissions logic
  • Add static ActionsRequiringPermissions to store statically declared instances of the classes described above
    -Replace old PermissionsService calls with new ActionsRequiringPermissions calls.

Old API:

if (!PermissionsService.IsActionAllowed(accountOnBehalfOf, HttpContext.User, AccountActions.UploadNewVersionOnBehalfOf))
{
    ...
}

if (!PermissionsService.IsActionAllowed(package, accountOnBehalfOf, PackageActions.UploadNewVersion))
{
    ...
}

New API:

if (ActionsRequiringPermissions.UploadNewPackageVersion.IsAllowed(currentUser, accountOnBehalfOf, package) != PermissionsFailure.None)
{
    ...
}

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

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

Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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))
Copy link
Member

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?

Copy link
Member

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);
Copy link
Member

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)
Copy link
Member

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>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TIL, nice catch

@joelverhagen
Copy link
Member

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))
Copy link
Member

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
Copy link
Member

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"/>?
Copy link
Member

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?

Copy link
Contributor Author

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)
Copy link
Member

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" />
Copy link
Member

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?

Copy link
Contributor Author

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>
Copy link
Member

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?

Copy link
Contributor Author

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)
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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() ?

Copy link
Contributor Author

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)
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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

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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this used anywhere?

Copy link
Contributor Author

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

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 ?

Copy link
Contributor Author

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

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

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

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"/>.
Copy link
Contributor

Choose a reason for hiding this comment

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

currentPrincipal -> currentUser?

@scottbommarito
Copy link
Contributor Author

Closing this PR because I'm planning on splitting it up.

@scottbommarito scottbommarito deleted the sb-actionrequiring branch May 1, 2018 19:50
@scottbommarito scottbommarito restored the sb-actionrequiring branch July 22, 2019 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants