using System.Net; using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.Models.Public.Response; using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors.ErrorMapper; using V2_UpdateUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.UpdateUser.v2; namespace Bit.Api.AdminConsole.Public.Controllers; [Route("public/members")] [Authorize("Organization")] public class MembersController : BaseAdminConsoleController { private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IGroupRepository _groupRepository; private readonly ICurrentContext _currentContext; private readonly V2_UpdateUserCommand.IUpdateOrganizationUserCommand _updateOrganizationUserCommand; private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand; private readonly IStripePaymentService _paymentService; private readonly IOrganizationRepository _organizationRepository; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand; private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommandV2; private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; private readonly IInviteOrganizationUsersCommand _inviteOrganizationUsersCommand; private readonly TimeProvider _timeProvider; public MembersController( IOrganizationUserRepository organizationUserRepository, IGroupRepository groupRepository, ICurrentContext currentContext, V2_UpdateUserCommand.IUpdateOrganizationUserCommand updateOrganizationUserCommand, IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand, IStripePaymentService paymentService, IOrganizationRepository organizationRepository, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IRemoveOrganizationUserCommand removeOrganizationUserCommand, IResendOrganizationInviteCommand resendOrganizationInviteCommand, IRevokeOrganizationUserCommand revokeOrganizationUserCommandV2, IRestoreOrganizationUserCommand restoreOrganizationUserCommand, IInviteOrganizationUsersCommand inviteOrganizationUsersCommand, TimeProvider timeProvider) { _organizationUserRepository = organizationUserRepository; _groupRepository = groupRepository; _currentContext = currentContext; _updateOrganizationUserCommand = updateOrganizationUserCommand; _updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand; _paymentService = paymentService; _organizationRepository = organizationRepository; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _removeOrganizationUserCommand = removeOrganizationUserCommand; _resendOrganizationInviteCommand = resendOrganizationInviteCommand; _revokeOrganizationUserCommandV2 = revokeOrganizationUserCommandV2; _restoreOrganizationUserCommand = restoreOrganizationUserCommand; _inviteOrganizationUsersCommand = inviteOrganizationUsersCommand; _timeProvider = timeProvider; } /// /// Retrieve a member. /// /// /// Retrieves the details of an existing member of the organization. You need only supply the /// unique member identifier that was returned upon member creation. /// /// The identifier of the member to be retrieved. [HttpGet("{id}")] [ProducesResponseType(typeof(MemberResponseModel), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task Get(Guid id) { var (orgUser, collections) = await _organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(id); if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId) { return new NotFoundResult(); } var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser), collections); return new JsonResult(response); } /// /// Retrieve a member's group ids /// /// /// Retrieves the unique identifiers for all groups that are associated with this member. You need only /// supply the unique member identifier that was returned upon member creation. /// /// The identifier of the member to be retrieved. [HttpGet("{id}/group-ids")] [ProducesResponseType(typeof(HashSet), (int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task GetGroupIds(Guid id) { var orgUser = await _organizationUserRepository.GetByIdAsync(id); if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId) { return new NotFoundResult(); } var groupIds = await _groupRepository.GetManyIdsByUserIdAsync(id); return new JsonResult(groupIds); } /// /// List all members. /// /// /// Returns a list of your organization's members. /// Member objects listed in this call include information about their associated collections. /// [HttpGet] [ProducesResponseType(typeof(ListResponseModel), (int)HttpStatusCode.OK)] public async Task List() { var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId!.Value, includeSharedCollections: true); var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails); var memberResponses = organizationUserUserDetails.Select(u => { return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, u.Collections); }); var response = new ListResponseModel(memberResponses); return new JsonResult(response); } /// /// Create a member. /// /// /// Creates a new member object by inviting a user to the organization. /// /// The request model. [HttpPost] [ProducesResponseType(typeof(MemberResponseModel), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] public async Task Post([FromBody] MemberCreateRequestModel model) { var hasStandaloneSecretsManager = false; var organization = await _organizationRepository.GetByIdAsync(_currentContext.OrganizationId!.Value); if (organization != null) { hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization); } var inviteRequest = model.ToInviteRequest(organization!, hasStandaloneSecretsManager, Guid.Empty, _timeProvider.GetUtcNow()); var inviteResult = await _inviteOrganizationUsersCommand.InviteImportedOrganizationUsersAsync(inviteRequest) switch { Success success => success, Failure { Error.Message: NoUsersToInviteError.Code } => throw new BadRequestException("This user has already been invited."), Failure failure => throw MapToBitException(failure.Error), _ => throw new InvalidOperationException() }; var user = inviteResult.Value.InvitedUsers.First(); var collections = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList(); var response = new MemberResponseModel(user, collections); return new JsonResult(response); } /// /// Update a member. /// /// /// Updates the specified member object. If a property is not provided, /// the value of the existing property will be reset. /// /// The identifier of the member to be updated. /// The request model. [HttpPut("{id}")] [ProducesResponseType(typeof(MemberResponseModel), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task Put(Guid id, [FromBody] MemberUpdateRequestModel model) { var existingUser = await _organizationUserRepository.GetByIdAsync(id); if (existingUser == null || existingUser.OrganizationId != _currentContext.OrganizationId) { return TypedResults.NotFound(); } existingUser.ExternalId = model.ExternalId; var organization = await _organizationRepository.GetByIdAsync(_currentContext.OrganizationId!.Value); var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList(); var request = new V2_UpdateUserCommand.UpdateOrganizationUserRequest( existingUser, organization!, model.Type!.Value, model.Permissions?.ToData(), existingUser.AccessSecretsManager, existingUser.AccessPam, associations, model.Groups, model.Email, model.Name, null, new SystemUser(EventSystemUser.PublicApi)); return await HandlePublic(await _updateOrganizationUserCommand.UpdateUserAsync(request), async _ => { MemberResponseModel response; if (existingUser.UserId.HasValue) { var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id); response = new MemberResponseModel(existingUserDetails!, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails!), associations); } else { response = new MemberResponseModel(existingUser, associations); } return TypedResults.Json(response); }); } /// /// Update a member's groups. /// /// /// Updates the specified member's group associations. /// /// The identifier of the member to be updated. /// The request model. [HttpPut("{id}/group-ids")] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task PutGroupIds(Guid id, [FromBody] UpdateGroupIdsRequestModel model) { var existingUser = await _organizationUserRepository.GetByIdAsync(id); if (existingUser == null || existingUser.OrganizationId != _currentContext.OrganizationId) { return new NotFoundResult(); } await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(existingUser, model.GroupIds); return new OkResult(); } /// /// Remove a member. /// /// /// Removes a member from the organization. This cannot be undone. The user account will still remain. /// /// The identifier of the member to be removed. [HttpDelete("{id}")] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task Remove(Guid id) { var user = await _organizationUserRepository.GetByIdAsync(id); if (user == null || user.OrganizationId != _currentContext.OrganizationId) { return new NotFoundResult(); } await _removeOrganizationUserCommand.RemoveUserAsync(_currentContext.OrganizationId!.Value, id, null); return new OkResult(); } /// /// Re-invite a member. /// /// /// Re-sends the invitation email to an organization member. /// /// The identifier of the member to re-invite. [HttpPost("{id}/reinvite")] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task PostReinvite(Guid id) { var existingUser = await _organizationUserRepository.GetByIdAsync(id); if (existingUser == null || existingUser.OrganizationId != _currentContext.OrganizationId) { return new NotFoundResult(); } await _resendOrganizationInviteCommand.ResendInviteAsync(_currentContext.OrganizationId!.Value, null, id); return new OkResult(); } /// /// Revoke a member's access to an organization. /// /// The ID of the member to be revoked. [HttpPost("{id}/revoke")] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task Revoke(Guid id) { var organizationUser = await _organizationUserRepository.GetByIdAsync(id); if (organizationUser == null || organizationUser.OrganizationId != _currentContext.OrganizationId) { return new NotFoundResult(); } var request = new RevokeOrganizationUsersRequest( _currentContext.OrganizationId!.Value, [id], new SystemUser(EventSystemUser.PublicApi), RevocationReason.Manual ); var results = await _revokeOrganizationUserCommandV2.RevokeUsersAsync(request); var result = results.Single(); return result.Result.Match( error => new BadRequestObjectResult(new ErrorResponseModel(error.Message)), _ => new OkResult() ); } /// /// Restore a member. /// /// /// Restores a previously revoked member of the organization. /// /// The identifier of the member to be restored. [HttpPost("{id}/restore")] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task Restore(Guid id) { var organizationUser = await _organizationUserRepository.GetByIdAsync(id); if (organizationUser == null || organizationUser.OrganizationId != _currentContext.OrganizationId) { return new NotFoundResult(); } await _restoreOrganizationUserCommand.RestoreUserAsync(organizationUser, EventSystemUser.PublicApi); return new OkResult(); } }