-
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?
Changes from all commits
14f3e88
4645222
6a29377
89b8d59
f60f0f5
0ba770d
e5f07de
5527ca6
27ce4b8
2069c9f
cb81d29
24a39d8
a1b2bac
7307454
1303b22
4bb8734
68afb17
aee2734
031c080
805eba9
ce596b2
6e5188f
d6a5813
9d47bf4
929ac41
f648953
f800119
2e80a4d
9b009b2
e1eeaf9
3af7ba4
cd1eca4
34a05f4
72fc706
781d6e6
e5ec8d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| ๏ปฟusing Bit.Core.AdminConsole.Utilities.v2; | ||
| using Bit.Core.AdminConsole.Utilities.v2.Results; | ||
| using Bit.Core.Models.Api; | ||
| using Microsoft.AspNetCore.Mvc; | ||
|
|
||
| namespace Bit.Api.AdminConsole.Controllers; | ||
|
|
||
| public abstract class BaseAdminConsoleController : Controller | ||
| { | ||
| protected static IResult Handle<T>(CommandResult<T> commandResult, Func<T, IActionResult> resultSelector) => | ||
| commandResult.Match<IResult>( | ||
| error => error switch | ||
| { | ||
| BadRequestError badRequest => TypedResults.BadRequest(new ErrorResponseModel(badRequest.Message)), | ||
| NotFoundError notFound => TypedResults.NotFound(new ErrorResponseModel(notFound.Message)), | ||
| InternalError internalError => TypedResults.Json( | ||
| new ErrorResponseModel(internalError.Message), | ||
| statusCode: StatusCodes.Status500InternalServerError), | ||
| _ => TypedResults.Json( | ||
| new ErrorResponseModel(error.Message), | ||
| statusCode: StatusCodes.Status500InternalServerError | ||
| ) | ||
| }, | ||
| success => Results.Ok(resultSelector(success)) | ||
| ); | ||
|
|
||
| protected static IResult Handle(BulkCommandResult commandResult) => | ||
| commandResult.Result.Match<IResult>( | ||
| error => error switch | ||
| { | ||
| NotFoundError notFoundError => TypedResults.NotFound(new ErrorResponseModel(notFoundError.Message)), | ||
| _ => TypedResults.BadRequest(new ErrorResponseModel(error.Message)) | ||
| }, | ||
| _ => TypedResults.NoContent() | ||
| ); | ||
|
Comment on lines
+27
to
+35
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not generally how we'd handle a I see that you're currently using this on the (non-bulk) |
||
|
|
||
| protected static IResult Handle(CommandResult commandResult) => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same feedback as above for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this one, its a Also, for this one, would the NoContent be more accurate as its a 200 code (success) but gives the additional information of telling the client that there's no body to look for? (I know we don't do this else where but is a good API practice.) I'll change for consistency if that's better. |
||
| commandResult.Match<IResult>( | ||
| error => error switch | ||
| { | ||
| BadRequestError badRequest => TypedResults.BadRequest(new ErrorResponseModel(badRequest.Message)), | ||
| NotFoundError notFound => TypedResults.NotFound(new ErrorResponseModel(notFound.Message)), | ||
| InternalError internalError => TypedResults.Json( | ||
| new ErrorResponseModel(internalError.Message), | ||
| statusCode: StatusCodes.Status500InternalServerError), | ||
| _ => TypedResults.Json( | ||
| new ErrorResponseModel(error.Message), | ||
| statusCode: StatusCodes.Status500InternalServerError | ||
| ) | ||
| }, | ||
| _ => TypedResults.NoContent() | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,8 +11,10 @@ | |
| using Bit.Api.Vault.AuthorizationHandlers.Collections; | ||
| using Bit.Core; | ||
| using Bit.Core.AdminConsole.Enums; | ||
| using Bit.Core.AdminConsole.Models.Data; | ||
| using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; | ||
| using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery; | ||
| using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser; | ||
| using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; | ||
| using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; | ||
| using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; | ||
|
|
@@ -43,7 +45,7 @@ namespace Bit.Api.AdminConsole.Controllers; | |
|
|
||
| [Route("organizations/{orgId}/users")] | ||
| [Authorize("Application")] | ||
| public class OrganizationUsersController : Controller | ||
| public class OrganizationUsersController : BaseAdminConsoleController | ||
| { | ||
| private readonly IOrganizationRepository _organizationRepository; | ||
| private readonly IOrganizationUserRepository _organizationUserRepository; | ||
|
|
@@ -68,6 +70,8 @@ public class OrganizationUsersController : Controller | |
| private readonly IFeatureService _featureService; | ||
| private readonly IPricingClient _pricingClient; | ||
| private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand; | ||
| private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand; | ||
| private readonly TimeProvider _timeProvider; | ||
| private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand; | ||
| private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; | ||
| private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand; | ||
|
|
@@ -101,7 +105,9 @@ public OrganizationUsersController(IOrganizationRepository organizationRepositor | |
| IInitPendingOrganizationCommand initPendingOrganizationCommand, | ||
| IRevokeOrganizationUserCommand revokeOrganizationUserCommand, | ||
| IResendOrganizationInviteCommand resendOrganizationInviteCommand, | ||
| IAdminRecoverAccountCommand adminRecoverAccountCommand) | ||
| IAdminRecoverAccountCommand adminRecoverAccountCommand, | ||
| IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand, | ||
| TimeProvider timeProvider) | ||
| { | ||
| _organizationRepository = organizationRepository; | ||
| _organizationUserRepository = organizationUserRepository; | ||
|
|
@@ -126,6 +132,8 @@ public OrganizationUsersController(IOrganizationRepository organizationRepositor | |
| _featureService = featureService; | ||
| _pricingClient = pricingClient; | ||
| _resendOrganizationInviteCommand = resendOrganizationInviteCommand; | ||
| _automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand; | ||
| _timeProvider = timeProvider; | ||
| _confirmOrganizationUserCommand = confirmOrganizationUserCommand; | ||
| _restoreOrganizationUserCommand = restoreOrganizationUserCommand; | ||
| _initPendingOrganizationCommand = initPendingOrganizationCommand; | ||
|
|
@@ -591,14 +599,7 @@ public async Task<IResult> DeleteAccount(Guid orgId, Guid id) | |
| return TypedResults.Unauthorized(); | ||
| } | ||
|
|
||
| var commandResult = await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUserId.Value); | ||
|
|
||
| return commandResult.Result.Match<IResult>( | ||
| error => error is NotFoundError | ||
| ? TypedResults.NotFound(new ErrorResponseModel(error.Message)) | ||
| : TypedResults.BadRequest(new ErrorResponseModel(error.Message)), | ||
| TypedResults.Ok | ||
| ); | ||
| return Handle(await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUserId.Value)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ (non-blocking) assigning the result to a |
||
| } | ||
|
|
||
| [HttpPost("{id}/delete-account")] | ||
|
|
@@ -738,6 +739,32 @@ public async Task PatchBulkEnableSecretsManagerAsync(Guid orgId, | |
| await BulkEnableSecretsManagerAsync(orgId, model); | ||
| } | ||
|
|
||
| [HttpPost("{id}/auto-confirm")] | ||
| [Authorize<ManageUsersRequirement>] | ||
| [RequireFeature(FeatureFlagKeys.AutomaticConfirmUsers)] | ||
| public async Task<IResult> AutomaticallyConfirmOrganizationUserAsync([FromRoute] Guid orgId, | ||
| [FromRoute] Guid id, | ||
| [FromBody] OrganizationUserConfirmRequestModel model) | ||
| { | ||
| var userId = _userService.GetProperUserId(User); | ||
|
|
||
| if (userId is null || userId.Value == Guid.Empty) | ||
| { | ||
| return TypedResults.Unauthorized(); | ||
| } | ||
|
|
||
| return Handle(await _automaticallyConfirmOrganizationUserCommand.AutomaticallyConfirmOrganizationUserAsync( | ||
| new AutomaticallyConfirmOrganizationUserRequest | ||
| { | ||
| OrganizationId = orgId, | ||
| OrganizationUserId = id, | ||
| Key = model.Key, | ||
| DefaultUserCollectionName = model.DefaultUserCollectionName, | ||
| PerformedBy = new StandardUser(userId.Value, await _currentContext.OrganizationOwner(orgId)), | ||
| PerformedOn = _timeProvider.GetUtcNow() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Passing in the date from the controller level seems unexpected to me. It's not information from the request or otherwise from the API layer (like claims in |
||
| })); | ||
| } | ||
|
|
||
| private async Task RestoreOrRevokeUserAsync( | ||
| Guid orgId, | ||
| Guid id, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| ๏ปฟusing Bit.Core.Entities; | ||
| using Bit.Core.Enums; | ||
|
|
||
| namespace Bit.Core.AdminConsole.Models.Data.OrganizationUsers; | ||
|
|
||
| public class AcceptedOrganizationUser : OrganizationUser | ||
| { | ||
| public AcceptedOrganizationUser(OrganizationUser organizationUser, string key) : this(organizationUser) | ||
| { | ||
| ArgumentException.ThrowIfNullOrWhiteSpace(key); | ||
| Key = key; | ||
| } | ||
|
|
||
| public AcceptedOrganizationUser(OrganizationUser organizationUser) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(organizationUser); | ||
| ArgumentNullException.ThrowIfNull(organizationUser.UserId); | ||
|
|
||
| if (organizationUser.Status != OrganizationUserStatusType.Accepted) | ||
| { | ||
| throw new ArgumentException("The organization user must be accepted", nameof(organizationUser)); | ||
| } | ||
|
|
||
| Id = organizationUser.Id; | ||
| OrganizationId = organizationUser.OrganizationId; | ||
| UserId = organizationUser.UserId.Value; | ||
| Email = organizationUser.Email; | ||
| Key = organizationUser.Key; | ||
| Type = organizationUser.Type; | ||
| ExternalId = organizationUser.ExternalId; | ||
| CreationDate = organizationUser.CreationDate; | ||
| RevisionDate = organizationUser.RevisionDate; | ||
| Permissions = organizationUser.Permissions; | ||
| ResetPasswordKey = organizationUser.ResetPasswordKey; | ||
| AccessSecretsManager = organizationUser.AccessSecretsManager; | ||
| } | ||
|
|
||
| public new Guid UserId { get; init; } | ||
| } |
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.
๐ Thanks for starting this!