[PM-43035] - Set ActingUser Type for providers - #8313
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8313 +/- ##
==========================================
- Coverage 64.03% 63.97% -0.06%
==========================================
Files 2473 2472 -1
Lines 106017 105812 -205
Branches 9613 9598 -15
==========================================
- Hits 67886 67693 -193
+ Misses 35771 35769 -2
+ Partials 2360 2350 -10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE This PR introduces Code Review Details
|
| var membership = currentContext.GetOrganization(organizationId); | ||
| if (membership is not null) | ||
| { | ||
| return new StandardUser(userId, membership.Type == OrganizationUserType.Owner, membership.Type, membership.Permissions); | ||
| } |
There was a problem hiding this comment.
❓ QUESTION: A caller who is both an org member and a confirmed provider user for that org now resolves as StandardUser and loses provider authority — is that intended?
Details
Membership wins here, and CurrentContext.ProviderIdForOrg also returns null when the caller is a member of the org, so such a caller can never resolve to ProviderUser.
Trace for a User-role member who is also a confirmed ProviderAdmin over the org:
ManageUsersRequirementstill authorizes the request —BasePermissionRequirementfalls through to the provider check.ValidateRoleChangethen runs with the member role only and returns a cannot-manage error, so the update fails.
Before this change, both the v2 service (IsProviderAsync) and the legacy path (CurrentContext.OrganizationOwner, which returns true for any provider user of the org) granted provider authority regardless of membership. If a provider user is never expected to also hold a membership in the managed organization, this is a non-issue.
There was a problem hiding this comment.
This Claude feedback is correct. I suggest:
var isProvider = await currentContext.ProviderUserForOrgAsync(organziationId);
var membership = currentContext.GetOrganization(organizationId);
return new StandardUser(userId, isProvider, membership?.Type, membership?.Permissions);We are losing the lazy evaluation of provider status, that might be something we discuss as a follow-up.
There was a problem hiding this comment.
I had to back up and look at the problem being solved here. As I understand it, the problem is in UpdateOrganizationUserValidator:
private async Task<Error?> ValidateRoleChangeAsync(UpdateOrganizationUserRequest request)
{
if (request.PerformedBy is not StandardUser standardUser)
{
return null;
}
var actingUser = new OrganizationUserRole(
standardUser.OrganizationUserType!.Value, // null reference exception
request.OrganizationUserToUpdate.OrganizationId,
standardUser.Permissions);
// snip
return await organizationUserValidationService.CanManageRoleChangeAsync(
standardUser.UserId!.Value,
actingUser,
request.OrganizationUserToUpdate,
newTargetUser);
}So the interesting thing here is that the type system already wants you to consider providers - OrganizationUserType is nullable because not everyone is a member. But, this is a lot of semantic information to represent just by the property being nullable. It's easy to assume the type system is wrong and override it (incorrectly) - so you are representing it with a separate type entirely.
I am on the fence about this:
- now the validator is skipping the role escalation check if the acting user is a provider. That is a leaky abstraction; the caller knows about these cases in which role escalation doesn't apply and then skips calling the method that is meant to encapsulate it.
- it muddies
IActingUser, which admittedly is a bit unclear at the moment, but it was originally meant to distinguish between private and public API actors. I also think it's more straightforward to keep all private-api-authenticated-users as a single type so that understanding the type hierarchy is not required to consume it properly. - a user can be a member OR a provider OR both.
StandardUservsProviderUsercan only represent either/or.
On the other hand, the choices for making this property non-nullable are either:
- separate types like you've done, so it can be omitted entirely
- representing all providers as Owners, which is what we've done to date, but I think it's misleading. There are some actions that an Owner can take that a provider can't, unless they are also an owner.
- have an enum similar to
OrganizationUserStatusType, but with aNot a membervalue - representing what null does today. That is probably my favourite option, but it's boilerplate.
For now, the best I can think of is...
- drop
ProviderUser : IActingUserand keep usingStandardUserfor this case - pass in a
StandardUseras the first argument toCanManageRoleChangeAsync, so that method has the full context for the acting user. That solves the root cause of the bug where you have to map a nullable value to a non-nullable value. - deprecate
IsOrganizationOwnerOrProvideronIActingUserand addStandardUser.IsProviderinstead, to remove the overlap withOrganizationUserStatusType, which is confusing imo. Now the type clearly represents the either/or/both possibilities. - ... and
OrganizationUserStatusTypeis still nullable. That is the weakness here. But we truly do need to represent a lack of role.
The query is good and useful given that you need to pull multiple data sources together to construct the object 👍
None of this is perfect so happy to discuss further.
…t orguservalidation.
…ion service since provider users shouldn't get there.
c1f77cf to
669856e
Compare
I made most of those changes with a few caveats that I think clean this up.
|
…property for provider or owner and added explicit is provider bool.
r-tome
left a comment
There was a problem hiding this comment.
Looks great to me! Fixed the bug and tidied up the code, nice work.
There was a problem hiding this comment.
💡 We could add a test to assert what should happen when a user is both a member and a provider
| var membership = currentContext.GetOrganization(organizationId); | ||
| if (membership is not null) | ||
| { | ||
| return new StandardUser(userId, membership.Type == OrganizationUserType.Owner, membership.Type, membership.Permissions); | ||
| } |
There was a problem hiding this comment.
This Claude feedback is correct. I suggest:
var isProvider = await currentContext.ProviderUserForOrgAsync(organziationId);
var membership = currentContext.GetOrganization(organizationId);
return new StandardUser(userId, isProvider, membership?.Type, membership?.Permissions);We are losing the lazy evaluation of provider status, that might be something we discuss as a follow-up.
🎟️ Tracking
PM-43035
📔 Objective
An error is being thrown when a provider attempts to update an organization member in an organization they manage. This will add a
StandardUserrepresentation for them and clean up the abstraction of validating user actions and updating. Callers will no longer have to construct theIOrganizationUserRoleif they have anIActingUserobject.