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

Integrate PermissionsService 2.0 with Gallery #5180

Merged
merged 5 commits into from
Jan 3, 2018

Conversation

scottbommarito
Copy link
Contributor

This PR removes the old PermissionsService from the gallery and integrates the new permissions service.

@@ -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 TryGetAccountsIsAllowedOnBehalfOf when out parameter accountsAllowedOnBehalfOf isn't used? I'd expect this API to be used wherever we need to populate a drop-down of package owners for an account.

Why not ActionsRequiringPermissions.UploadNewPackageId.CheckPermissions(user, scopeOwner, packageContext)?

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 - I call TryGetAccountsIsAllowedOnBehalfOf whenever I want to know if an action is allowed on behalf of any account, regardless of whether or not I care about the list of users. CheckPermissions is for when the caller knows the exact account the action is attempting to be done on behalf of.
2 - The upcoming UI upload PR will fix this flow to actually work as intended with organizations--this is merely a quick syntax replacement of the old broken logic.

Copy link
Member

Choose a reason for hiding this comment

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

The TryParse pattern in the .NET framework is intended for improving performance in what would normally be an exception case. see: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/exceptions-and-performance

Looking at the following API, I assume I'm trying to get a list of accounts but that the get operation might throw for some reason.

bool TryGetAccountsIsAllowedOnBehalfOf(user, packageContext, out accountsAllowed);

Whereas the following API looks like it checks if the user has permissions:

bool CheckPermissions(currentUser, owner, package);

You should reconsider the Try API, as it doesn't come across doing what it's intended for. Another sign that the pattern is misused is that the out parameter is often ignored by the callers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you suggesting I rename it to

bool IsAllowedOnBehalfOfAnAccount(user, package, out accountsAllowed)

?

Copy link
Member

Choose a reason for hiding this comment

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

Ideally APIs have a strong contract for callers. If comments/docs are necessary to use the API, then it could be a bad name or poor design (i.e., doing too much).

In this case, it looks like the API was created with two intents:

  • Can User X do Action A on behalf of User Y?
    AND
  • Get Users that User X can do Action A on behalf of

If that's the case, then I suggest both rename and splitting this into multiple APIs with a well defined purpose. For instance:

interface IActionRequiringEntityPermissions<TEntity> {
    bool IsAllowedOnBehalfOfAnyOwner(User currentUser, TEntity);
    bool IsAllowedOnBehalfOfOwner(User currentUser, User entityOwner, TEntity);
    List<TEntity> GetAccountsAllowedOnBehalfOf(User currentUser, TEntity);
}

Copy link
Member

Choose a reason for hiding this comment

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

What may also be confusing me is the following public API on IActionRequiringEntityPermission:

PermissionsCheckResult CheckPermissions(User currentUser, User account, TEntity entity);

I had the impression that this API would be used by callers checking permissions, and the other was for callers that needed a list of accounts (i.e, for populating a drop-down). Is CheckPermissions intended to be used by callers?

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 don't want to split IsAllowedOnBehalfOfAnAccount into two parts because
1 - the list of accounts is meaningless if the action is not allowed on behalf of any account
2 - when you want both whether or not the action is allowed on behalf of any accounts and the list of the accounts it is allowed on behalf of, splitting the method into two would require the same complex logic to be run twice, which is an unnecessary performance hit
3 - checking if an account is allowed on behalf of any account and the list of those accounts are very closely related
4 - although I agree with you that the earlier name (TryGet) was confusing, I believe the newer proposition IsAllowedOnBehalfOfAnyAccount is very clear
5 - ultimately I don't think ignoring an out parameter is too concerning, although I'd be ok creating a version of the method without the out parameter while keeping the version with it

So, what I'm thinking of

PermissionsCheckResult CheckPermissions(currentUser, accountOnBehalfOf, entity)
bool IsAllowedOnBehalfOfAnyAccount(currentUser, entity)
bool IsAllowedOnBehalfOfAnyAccount(currentUser, entity, out accountsAllowed)

If having the bool is too confusing with the `PermissionsCheckResult, I'm also ok with

PermissionsCheckResult CheckPermissions(currentUser, accountOnBehalfOf, entity)
PermissionsCheckResult CheckPermissionsOnBehalfOfAnyAccount(currentUser, entity)
PermissionsCheckResult CheckPermissionsOnBehalfOfAnyAccount(currentUser, entity, out accountsAllowed)

but that would require adding some logic to determine which PermissionsCheckResult to return when the case is not PermissionsCheckResult.Allowed (probably max int value of any enum state returned).

Copy link
Member

Choose a reason for hiding this comment

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

It sounds like CheckPermissions and IsAllowedOnBehalfOfAnyAccount are the same except:

  • different return value (bool vs. more granular enum)
  • check permissions for specific owner, vs any owner

If the purpose is so similar, why are the APIs so different? Could it just be (don't care which retval you use, just be consistent):

PermissionsCheckResult IsAllowedOnBehalfOf(currentUser, accountOnBehalfOf, entity);
PermissionsCheckResult IsAllowedOnBehalfOfAny(currentUser, entity);

The API that gets the list of accounts has a different purpose - getting accounts which pass the permissions check. I expect this to always return at least 1 account (currentUser), and should only be used by callers that need the list of accounts:

List<User> GetAccountsAllowedOnBehalfOf(currentUser, entity);

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 agree with your point on consistency in retval, but as mentioned above, I don't want to make a separate method for getting the list of accounts on behalf of.

I'm planning on the following API:

PermissionsCheckResult CheckPermissions(currentUser, accountOnBehalfOf, entity)
PermissionsCheckResult CheckPermissionsOnBehalfOfAnyAccount(currentUser, entity)
PermissionsCheckResult CheckPermissionsOnBehalfOfAnyAccount(currentUser, entity, out accountsAllowed)

I will open up a PR separate with this one with those changes and then make the changes to this branch retroactively.

@@ -71,6 +72,7 @@ public virtual ActionResult GetPackageOwners(string id, string version)
.Select(u => new PackageOwnersResultViewModel(
u,
currentUser,
packageRegistration,
Copy link
Member

Choose a reason for hiding this comment

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

Why not do the work (permissions check) in the controller and pass the result to the view model?

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 constructor of PackageOwnersResultViewModel does the checks and saves the results for the view to use. I didn't want to have to copy all of the permissions checks to every place this view model is used.

return false;
}

if (!ActionsRequiringPermissions.ManagePackageOwnership.TryGetAccountsIsAllowedOnBehalfOf(currentUser, package, 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.

Same comment as above... wondering why TryGetAccountsIsAllowedOnBehalfOf is used when the out parameter accountsAllowedOnBehalfOf is not used. Looks like there's more callers with this pattern below too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

see response to "Why TryGetAccountsIsAllowedOnBehalfOf when out parameter accountsAllowedOnBehalfOf isn't used?" above

@scottbommarito scottbommarito changed the base branch from sb-permserv2 to sb-feature-orgpermissions December 14, 2017 22:51
@chenriksson
Copy link
Member

chenriksson commented Dec 15, 2017

@scottbommarito Please see my comment about the TryParse pattern, which is relevant for both this PR and #5192 -- #5180 (comment)

@scottbommarito
Copy link
Contributor Author

I'm fixing this PR right now, sorry for how broken it became!

@scottbommarito
Copy link
Contributor Author

should be fixed now!

@chenriksson
Copy link
Member

@scottbommarito Looks like a rebase is needed? I'm seeing auditing changes related to the PII work.

@scottbommarito
Copy link
Contributor Author

@chenriksson woops...git is AWFUL at feature branches, will fix

@scottbommarito
Copy link
Contributor Author

@chenriksson ok it's actually fixed now!

@@ -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.CheckPermissionsOnBehalfOfAnyAccount(user, new ActionOnNewPackageContext(id, ReservedNamespaceService)) != PermissionsCheckResult.Allowed)
Copy link
Member

Choose a reason for hiding this comment

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

nit: wrap long lines

{
return self == null && user == null;
}

return self.Key == user.Key;
Copy link
Member

Choose a reason for hiding this comment

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

nit: could simplify to self?.Key == user?.Key

@@ -91,7 +91,7 @@ public bool Equals(User x, User y)

public int GetHashCode(User obj)
{
return obj.Key.GetHashCode();
return obj == null ? -1 : obj.Key;
Copy link
Member

Choose a reason for hiding this comment

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

nit: could simplify to obj?.Key ?? -1

.Setup(r => r.IsPushAllowed(It.IsAny<string>(), It.IsAny<User>(), out matchingNamespaces))
.Returns(true);
.Setup(s => s.GetReservedNamespacesForId(It.IsAny<string>()))
.Returns(new ReservedNamespace[0]);
Copy link
Member

Choose a reason for hiding this comment

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

nit: spacing?

@@ -69,7 +69,7 @@ public void ResolvePathIsCorrect()

// Act
var result = urlHelper.PackageRegistrationTemplate()
.Resolve(new ListPackageItemViewModel(package));
.Resolve(new ListPackageItemViewModel(package, null));
Copy link
Member

Choose a reason for hiding this comment

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

nit: use named parameter for nulls, if possible.

@scottbommarito scottbommarito merged this pull request into sb-feature-orgpermissions Jan 3, 2018
@scottbommarito scottbommarito deleted the sb-permservint2 branch May 1, 2018 19:50
@scottbommarito scottbommarito restored the sb-permservint2 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.

2 participants