-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[PM-26636] - Auto Confirm Org User Command #6488
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
base: main
Are you sure you want to change the base?
Conversation
|
Great job! No new security vulnerabilities introduced in this pull request |
src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
Outdated
Show resolved
Hide resolved
src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
Outdated
Show resolved
Hide resolved
src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
Outdated
Show resolved
Hide resolved
...izationFeatures/OrganizationUsers/Interfaces/IAutomaticallyConfirmOrganizationUserCommand.cs
Show resolved
Hide resolved
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #6488 +/- ##
==========================================
+ Coverage 53.08% 57.12% +4.04%
==========================================
Files 1912 1921 +9
Lines 85152 85490 +338
Branches 7622 7645 +23
==========================================
+ Hits 45203 48839 +3636
+ Misses 38190 34812 -3378
- Partials 1759 1839 +80 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
Outdated
Show resolved
Hide resolved
...ionFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs
Outdated
Show resolved
Hide resolved
...ionFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs
Outdated
Show resolved
Hide resolved
...ionFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserRequest.cs
Show resolved
Hide resolved
src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
Outdated
Show resolved
Hide resolved
...ionFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs
Show resolved
Hide resolved
eliykat
left a comment
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 work!
...izationFeatures/OrganizationUsers/Interfaces/IAutomaticallyConfirmOrganizationUserCommand.cs
Show resolved
Hide resolved
...ionFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs
Outdated
Show resolved
Hide resolved
...ionFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs
Show resolved
Hide resolved
...ionFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs
Outdated
Show resolved
Hide resolved
...ionFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs
Outdated
Show resolved
Hide resolved
test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs
Show resolved
Hide resolved
test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs
Outdated
Show resolved
Hide resolved
|
Also... enjoy your merge errors 😎 |
…m-user-command # Conflicts: # src/Api/AdminConsole/Controllers/OrganizationUsersController.cs # src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs # test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs
| /// <li>Creating the default collection if applicable.</li> | ||
| /// </ul> | ||
| /// | ||
| /// Each of these actions is performed independently of each other and not guaranteed to be performed in any order. |
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.
❌ Documentation inaccuracy: The xmldoc states "Each of these actions is performed independently of each other and not guaranteed to be performed in any order." However, this is incorrect based on the actual implementation.
In AutomaticallyConfirmOrganizationUserCommand.cs:54-60, the code explicitly enforces ordering:
CreateDefaultCollectionsAsyncexecutes before the parallel tasks- Then
Task.WhenAllruns the remaining operations in parallel
Additionally, within SyncOrganizationKeysAsync (lines 67-71), there's sequential ordering: DeleteDeviceRegistrationAsync must complete before PushSyncOrganizationKeysAsync.
The documentation should accurately reflect that:
- Default collection creation happens first (to ensure it exists before sync)
- Device registration deletion happens before push notification
- Other side effects (logging, email) execute in parallel without ordering guarantees
Suggested fix: Update xmldoc to describe the actual execution order and dependencies.
| OrganizationUserId = id, | ||
| Key = model.Key, | ||
| DefaultUserCollectionName = model.DefaultUserCollectionName, | ||
| PerformedBy = new StandardUser(userId.Value, await _currentContext.OrganizationOwner(orgId)), |
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.
❌ Authorization inconsistency: The StandardUser is created with await _currentContext.OrganizationOwner(orgId), which only returns true for Owners, not Admins.
However, the endpoint has [Authorize<ManageUsersRequirement>] which allows both Owners and Admins to manage users.
This creates an inconsistency where:
- Admins can pass the authorization check
- But
PerformedBywill haveIsOwner = falsefor Admins - If any downstream logic depends on
IsOwner, Admins won't be able to use this feature despite being authorized
Questions:
- Is this intentional? Should only Owners be able to auto-confirm users?
- If Admins should also be allowed, consider using
_currentContext.OrganizationAdmin(orgId)or a different property name that doesn't imply Owner-only access.
Note: This same pattern exists in other endpoints, but for auto-confirm specifically, the validator doesn't seem to check the PerformedBy.IsOwner property, so this may currently work but is semantically confusing.
| return new None(); | ||
| } | ||
|
|
||
| await CreateDefaultCollectionsAsync(validatedData.Request); |
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.
👍 Excellent work addressing the previous feedback! Creating the default collection before the parallel tasks ensures it exists before the sync notification is sent to clients. This prevents a race condition where clients might sync before the collection is created.
| new Permissions(), | ||
| OrganizationUserStatusType.Accepted); | ||
|
|
||
| var tenRequests = Enumerable.Range(0, 5) |
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.
📝 Test concurrency: Good addition of the concurrent requests test! This validates the idempotency check works correctly.
One minor observation: The test creates 5 concurrent requests and verifies only 1 default collection is created. Consider adding a comment explaining that the single collection creation validates the idempotency (helps future maintainers understand the assertion).
| return Invalid(request, new UserIsNotUserType()); | ||
| } | ||
|
|
||
| if (!await OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(request)) |
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.
👍 Great addition! Validating that the auto-confirm policy is enabled is critical security validation. This addresses @eliykat's feedback perfectly.
| return null; | ||
| } | ||
|
|
||
| var policyRequirement = await policyRequirementQuery |
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.
🎨 Consider caching optimization: This retrieves all policy requirements for the user, which could be expensive if called frequently. Since this validator already checks the auto-confirm policy via IPolicyRepository, you could potentially consolidate these checks.
However, this is likely fine for now given:
- Auto-confirm is a relatively infrequent operation
- The policy requirement abstraction provides good separation of concerns
- Performance optimization can be addressed later if needed
Non-blocking suggestion: Consider measuring if this becomes a bottleneck in production.
| ); | ||
| } | ||
|
|
||
| private async Task SyncOrganizationKeysAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) |
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.
💭 Sequential operation question: This method enforces sequential execution: delete device registrations first, then push sync keys.
Question: Is there a dependency between these operations? For example:
- Should the push notification only go to devices that haven't been unregistered?
- Or is this just a logical grouping?
If there's no dependency, these could be parallelized with the other operations. If there is a dependency, a comment explaining why would be helpful for future maintainers.
Given the method name SyncOrganizationKeysAsync implies they're related, the sequential execution seems intentional, but documentation would help clarify.
| { | ||
| try | ||
| { | ||
| var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value); |
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.
OrganizationUser.UserId is non-null, there's no guarantee the User entity exists in the database.
Suggested fix:
var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value);
if (user == null)
{
logger.LogError("User not found when sending confirmation email.");
return;
}
await mailService.SendOrganizationConfirmedEmailAsync(request.Organization!.Name,
user.Email,
request.OrganizationUser.AccessSecretsManager);This is caught by the try-catch, but explicit null checking provides clearer error logging.
|
|
||
| public async Task InitializeAsync() | ||
| { | ||
| _ownerEmail = $"org-user-integration-test-{Guid.NewGuid()}@example.com"; |
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.
@example.com domain (which is now correct here), but I notice you're using dynamic GUIDs in the email addresses.
While this works, consider if you need the GUID uniqueness for these integration tests. If you're creating fresh accounts each time and cleaning them up, simpler email addresses like [email protected] might be more readable in test output and logs.
Non-blocking: This is minor and current approach is acceptable.
| public record UserDoesNotHaveTwoFactorEnabled() : BadRequestError("User does not have two-step login enabled."); | ||
| public record OrganizationEnforcesSingleOrgPolicy() : BadRequestError("Cannot confirm this member to the organization until they leave or remove all other organizations"); | ||
| public record OtherOrganizationEnforcesSingleOrgPolicy() : BadRequestError("Cannot confirm this member to the organization because they are in another organization which forbids it."); | ||
| public record AutomaticallyConfirmUsersPolicyIsNotEnabled() : BadRequestError("Cannot confirm this member because the Automatically Confirm Users policy is not enabled."); |
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.
📝 Missing error definition: The validator checks that the auto-confirm policy is enabled (line 54-57 in the validator), and uses AutomaticallyConfirmUsersPolicyIsNotEnabled() error, which is defined here on line 13. Good!
However, @eliykat's feedback also mentioned checking if the organization can use auto-confirm via orgAbility cache. I don't see validation for the organization's ability/plan to use this feature.
Question: Should there be validation that the organization's plan supports auto-confirm? Or is this already handled elsewhere (e.g., by the feature flag or policy enablement check)?
| /// <returns>The result is a boolean value indicating whether a default collection should be created.</returns> | ||
| private async Task<bool> ShouldCreateDefaultCollectionAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) => | ||
| !string.IsNullOrWhiteSpace(request.DefaultUserCollectionName) | ||
| && (await policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(request.OrganizationUser!.UserId!.Value)) |
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-forgiving operator usage: The null-forgiving operators (!) here are safe because the validator guarantees these values are non-null. However, as mentioned in a previous review thread, this is actually less type-safe than using nullable reference types.
The compiler won't prevent you from accessing AsError or checking these properties without proper validation first, which could cause runtime errors. With nullable types, the compiler enforces null checks.
Non-blocking observation: This is a broader architectural consideration about the tradeoff between FP patterns (OneOf/Result types) and C#'s nullable reference types. The current approach works but requires discipline to ensure validation always precedes usage of these operators.
| if (!await OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(request)) | ||
| { | ||
| return Invalid(request, new AutomaticallyConfirmUsersPolicyIsNotEnabled()); | ||
| } |
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.
❌ Missing organization ability validation: The validator checks if the auto-confirm policy is enabled but does not verify the organization's plan supports this feature via the orgAbility cache (as mentioned in @eliykat's feedback).
Recommendation: Add validation to check request.Organization.UseAutoConfirmUsers before checking the policy. Organizations without proper plan entitlement should not be able to use this feature even if they enable the policy.
| /// <li>Creating the default collection if applicable.</li> | ||
| /// </ul> | ||
| /// | ||
| /// Each of these actions is performed independently of each other and not guaranteed to be performed in any order. |
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.
❌ Documentation inaccuracy: This statement "Each of these actions is performed independently of each other and not guaranteed to be performed in any order" is incorrect.
The actual implementation (AutomaticallyConfirmOrganizationUserCommand.cs:54-60) enforces explicit ordering:
CreateDefaultCollectionsAsyncexecutes before parallel tasks (line 54)- Within
SyncOrganizationKeysAsync,DeleteDeviceRegistrationAsyncmust complete beforePushSyncOrganizationKeysAsync(lines 69-70)
Recommendation: Update documentation to describe:
- Default collection creation happens first (to ensure it exists before sync)
- Device registration deletion happens before push notification
- Other side effects (logging, email) execute in parallel without ordering guarantees
| OrganizationUserId = id, | ||
| Key = model.Key, | ||
| DefaultUserCollectionName = model.DefaultUserCollectionName, | ||
| PerformedBy = new StandardUser(userId.Value, await _currentContext.OrganizationOwner(orgId)), |
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.
💭 Authorization semantic question: The StandardUser is created with await _currentContext.OrganizationOwner(orgId), which only returns true for Owners, not Admins.
However, the endpoint has [Authorize<ManageUsersRequirement>] which allows both Owners and Admins to manage users.
Questions:
- Is this intentional? Should only Owners be able to auto-confirm users?
- If Admins should also be allowed, should
IsOwnerbetruefor them, or is this property only for audit purposes?
The validator doesn't check PerformedBy.IsOwner, so this currently works but is semantically confusing. Consider either documenting this is intentional or using a different property name that doesn't imply Owner-only access.
| { | ||
| try | ||
| { | ||
| var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value); |
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.
OrganizationUser.UserId is non-null, there's no guarantee the User entity exists in the database.
Suggested fix:
var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value);
if (user == null)
{
logger.LogError("User {UserId} not found when sending confirmation email.", request.OrganizationUser.UserId.Value);
return;
}
await mailService.SendOrganizationConfirmedEmailAsync(request.Organization!.Name,
user.Email,
request.OrganizationUser.AccessSecretsManager);This is caught by the try-catch, but explicit null checking provides clearer error logging.
| })).ToList(); | ||
|
|
||
| var results = new List<HttpResponseMessage>(tenRequests.Count); | ||
| foreach (var request in tenRequests) |
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.
🎨 Test improvement: The current implementation processes requests sequentially rather than concurrently:
foreach (var request in tenRequests) // ⚠️ Sequential await
{
results.Add(await request);
}To truly test concurrent execution and race conditions:
var results = await Task.WhenAll(tenRequests);The current approach validates idempotency but doesn't test true concurrent request handling.
…ntent, however in CI, its slow enough for it to return 400 due to the user already being confirmed via validation.
| /// <li>Creating the default collection if applicable.</li> | ||
| /// </ul> | ||
| /// | ||
| /// Each of these actions is performed independently of each other and not guaranteed to be performed in any order. |
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.
❌ Documentation inaccuracy: This statement "Each of these actions is performed independently of each other and not guaranteed to be performed in any order" is incorrect.
The actual implementation enforces explicit ordering:
CreateDefaultCollectionsAsyncexecutes before parallel tasks (AutomaticallyConfirmOrganizationUserCommand.cs:54)- Within
SyncOrganizationKeysAsync,DeleteDeviceRegistrationAsyncmust complete beforePushSyncOrganizationKeysAsync(lines 69-70)
Recommendation: Update documentation to describe:
- Default collection creation happens first (to ensure it exists before sync)
- Device registration deletion happens before push notification
- Other side effects (logging, email) execute in parallel without ordering guarantees
| return Invalid(request, new UserIsNotUserType()); | ||
| } | ||
|
|
||
| if (!await OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(request)) |
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.
❌ Missing organization ability validation: The validator checks if the auto-confirm policy is enabled but does not verify the organization's plan supports this feature via the orgAbility cache (as mentioned in @eliykat's feedback).
Recommendation: Add validation to check request.Organization.UseAutoConfirmUsers (or similar ability property) before checking the policy. Organizations without proper plan entitlement should not be able to use this feature even if they enable the policy.
...ionFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs
Show resolved
Hide resolved
| DefaultUserCollectionName = _mockEncryptedString | ||
| })).ToList(); | ||
|
|
||
| var results = await Task.WhenAll(tenRequests); |
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.
👍 Excellent use of Task.WhenAll for true concurrent execution! This properly tests race conditions and idempotency.
🎨 Minor suggestion: Consider adding a comment explaining that the single default collection assertion validates the idempotency check worked correctly. Something like:
// Verify idempotency: despite 10 concurrent requests, only 1 default collection should be created
Assert.Single(collections.Where(c => c.Type == CollectionType.DefaultUserCollection));This helps future maintainers understand the assertion's purpose.
| private async Task<bool> OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync( | ||
| AutomaticallyConfirmOrganizationUserValidationRequest request) => | ||
| await policyRepository.GetByOrganizationIdTypeAsync(request.OrganizationId, | ||
| PolicyType.AutomaticUserConfirmation) is { Enabled: 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.
Should also check orgAbility.UsePolicies && orgAbility.UseAutomaticUserConfirmation, to ensure that the organization's current plan supports this functionality. (To protect against plan downgrades.)
| await _loginHelper.LoginAsync(_ownerEmail); | ||
|
|
||
| var userToConfirmEmail = $"org-user-to-confirm-{Guid.NewGuid()}@example.com"; | ||
| await _factory.LoginWithNewAccount(userToConfirmEmail); | ||
|
|
||
| await _loginHelper.LoginAsync(userToConfirmEmail); | ||
| var organizationUser = await OrganizationTestHelpers.CreateUserAsync( | ||
| _factory, | ||
| organization.Id, | ||
| userToConfirmEmail, | ||
| OrganizationUserType.User, | ||
| false, | ||
| new Permissions(), | ||
| OrganizationUserStatusType.Accepted); |
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.
Setup is a bit confused here:
userToConfirmappears to be the user who is doing the confirmation, not the user being confirmed- multiple calls to login methods are possibly redundant (e.g. you login as the owner but then immediately login as the user; you use both
_factory.LoginWithNewAccountand_loginHelper.LoginAsync).
Other tests in this file have similar issues.

🎟️ Tracking
PM-26636
📔 Objective
Add the automatic organization user confirmation feature that allows client applications to automatically confirm organization users. The implementation includes:
The feature is protected behind the AutomaticConfirmUsers feature flag and requires the ManageUsers authorization requirement.
⏰ Reminders before review
🦮 Reviewer guidelines
:+1:) or similar for great changes:memo:) or ℹ️ (:information_source:) for notes or general info:question:) for questions:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion:art:) for suggestions / improvements:x:) or:warning:) for more significant problems or concerns needing attention:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt:pick:) for minor or nitpick changes