// FIXME: Update this file to be null safe and then delete the line below #nullable disable using System.Text.Json; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.Organizations; using Bit.Api.Auth.Models.Response.Organizations; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tokens; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Bit.Api.AdminConsole.Controllers; [Route("organizations")] [Authorize("Application")] public class OrganizationsController : Controller { private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IPolicyQuery _policyQuery; private readonly IOrganizationService _organizationService; private readonly IUserService _userService; private readonly ICurrentContext _currentContext; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigService _ssoConfigService; private readonly IGetOrganizationApiKeyQuery _getOrganizationApiKeyQuery; private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand; private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly IFeatureService _featureService; private readonly GlobalSettings _globalSettings; private readonly IProviderRepository _providerRepository; private readonly IProviderBillingService _providerBillingService; private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand; private readonly IOrganizationDeleteCommand _organizationDeleteCommand; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IPricingClient _pricingClient; private readonly IOrganizationUpdateKeysCommand _organizationUpdateKeysCommand; private readonly IOrganizationUpdateCommand _organizationUpdateCommand; public OrganizationsController( IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IPolicyQuery policyQuery, IOrganizationService organizationService, IUserService userService, ICurrentContext currentContext, ISsoConfigRepository ssoConfigRepository, ISsoConfigService ssoConfigService, IGetOrganizationApiKeyQuery getOrganizationApiKeyQuery, IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand, ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand, IOrganizationApiKeyRepository organizationApiKeyRepository, IFeatureService featureService, GlobalSettings globalSettings, IProviderRepository providerRepository, IProviderBillingService providerBillingService, IDataProtectorTokenFactory orgDeleteTokenDataFactory, IRemoveOrganizationUserCommand removeOrganizationUserCommand, ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand, IOrganizationDeleteCommand organizationDeleteCommand, IPolicyRequirementQuery policyRequirementQuery, IPricingClient pricingClient, IOrganizationUpdateKeysCommand organizationUpdateKeysCommand, IOrganizationUpdateCommand organizationUpdateCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _policyQuery = policyQuery; _organizationService = organizationService; _userService = userService; _currentContext = currentContext; _ssoConfigRepository = ssoConfigRepository; _ssoConfigService = ssoConfigService; _getOrganizationApiKeyQuery = getOrganizationApiKeyQuery; _rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand; _createOrganizationApiKeyCommand = createOrganizationApiKeyCommand; _organizationApiKeyRepository = organizationApiKeyRepository; _featureService = featureService; _globalSettings = globalSettings; _providerRepository = providerRepository; _providerBillingService = providerBillingService; _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; _removeOrganizationUserCommand = removeOrganizationUserCommand; _cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand; _organizationDeleteCommand = organizationDeleteCommand; _policyRequirementQuery = policyRequirementQuery; _pricingClient = pricingClient; _organizationUpdateKeysCommand = organizationUpdateKeysCommand; _organizationUpdateCommand = organizationUpdateCommand; } [HttpGet("{id}")] public async Task Get(string id) { var orgIdGuid = new Guid(id); if (!await _currentContext.OrganizationOwner(orgIdGuid)) { throw new NotFoundException(); } var organization = await _organizationRepository.GetByIdAsync(orgIdGuid); if (organization == null) { throw new NotFoundException(); } var plan = await _pricingClient.GetPlan(organization.PlanType); return new OrganizationResponseModel(organization, plan); } [HttpGet("")] public async Task> GetUser() { var userId = _userService.GetProperUserId(User).Value; var organizations = await _organizationUserRepository.GetManyDetailsByUserAsync(userId, OrganizationUserStatusType.Confirmed); var organizationsClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(userId); var organizationIdsClaimingActiveUser = organizationsClaimingActiveUser.Select(o => o.Id); var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsClaimingActiveUser)); return new ListResponseModel(responses); } [HttpGet("{identifier}/auto-enroll-status")] public async Task GetAutoEnrollStatus(string identifier) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) { throw new UnauthorizedAccessException(); } var organization = await _organizationRepository.GetByIdentifierAsync(identifier); if (organization == null) { throw new NotFoundException(); } var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id); if (organizationUser == null) { throw new NotFoundException(); } if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) { var resetPasswordPolicyRequirement = await _policyRequirementQuery.GetAsync(user.Id); return new OrganizationAutoEnrollStatusResponseModel(organization.Id, resetPasswordPolicyRequirement.AutoEnrollEnabled(organization.Id)); } var resetPasswordPolicy = await _policyQuery.RunAsync(organization.Id, PolicyType.ResetPassword); if (!resetPasswordPolicy.Enabled || resetPasswordPolicy.Data == null) { return new OrganizationAutoEnrollStatusResponseModel(organization.Id, false); } var data = JsonSerializer.Deserialize(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase); return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false); } [HttpPost("")] [SelfHosted(NotSelfHostedOnly = true)] public async Task Post([FromBody] OrganizationCreateRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) { throw new UnauthorizedAccessException(); } var organizationSignup = model.ToOrganizationSignup(user); var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup); var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType); return new OrganizationResponseModel(result.Organization, plan); } [HttpPost("create-without-payment")] [SelfHosted(NotSelfHostedOnly = true)] public async Task CreateWithoutPaymentAsync([FromBody] OrganizationNoPaymentCreateRequest model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) { throw new UnauthorizedAccessException(); } var organizationSignup = model.ToOrganizationSignup(user); var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup); var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType); return new OrganizationResponseModel(result.Organization, plan); } [HttpPut("{organizationId:guid}")] public async Task Put(Guid organizationId, [FromBody] OrganizationUpdateRequestModel model) { // If billing email is being changed, require subscription editing permissions. // Otherwise, organization owner permissions are sufficient. var requiresBillingPermission = model.BillingEmail is not null; var authorized = requiresBillingPermission ? await _currentContext.EditSubscription(organizationId) : await _currentContext.OrganizationOwner(organizationId); if (!authorized) { return TypedResults.Unauthorized(); } var commandRequest = model.ToCommandRequest(organizationId); var updatedOrganization = await _organizationUpdateCommand.UpdateAsync(commandRequest); var plan = await _pricingClient.GetPlan(updatedOrganization.PlanType); return TypedResults.Ok(new OrganizationResponseModel(updatedOrganization, plan)); } [HttpPost("{id}")] [Obsolete("This endpoint is deprecated. Use PUT method instead")] public async Task PostPut(Guid id, [FromBody] OrganizationUpdateRequestModel model) { return await Put(id, model); } [HttpPost("{id}/storage")] [SelfHosted(NotSelfHostedOnly = true)] public async Task PostStorage(string id, [FromBody] StorageRequestModel model) { var orgIdGuid = new Guid(id); if (!await _currentContext.EditSubscription(orgIdGuid)) { throw new NotFoundException(); } var result = await _organizationService.AdjustStorageAsync(orgIdGuid, model.StorageGbAdjustment.Value); return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result }; } [HttpPost("{id}/leave")] public async Task Leave(Guid id) { if (!await _currentContext.OrganizationUser(id)) { throw new NotFoundException(); } var user = await _userService.GetUserByPrincipalAsync(User); var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id); if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector && user.UsesKeyConnector) { throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving."); } if ((await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id)) { throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details."); } await _removeOrganizationUserCommand.UserLeaveAsync(id, user.Id); } [HttpDelete("{id}")] public async Task Delete(string id, [FromBody] SecretVerificationRequestModel model) { var orgIdGuid = new Guid(id); if (!await _currentContext.OrganizationOwner(orgIdGuid)) { throw new NotFoundException(); } var organization = await _organizationRepository.GetByIdAsync(orgIdGuid); if (organization == null) { throw new NotFoundException(); } var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) { throw new UnauthorizedAccessException(); } if (!await _userService.VerifySecretAsync(user, model.Secret)) { await Task.Delay(2000); throw new BadRequestException(string.Empty, "User verification failed."); } if (organization.IsValidClient()) { var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); if (provider.IsBillable()) { await _providerBillingService.ScaleSeats( provider, organization.PlanType, -organization.Seats ?? 0); } } await _organizationDeleteCommand.DeleteAsync(organization); } [HttpPost("{id}/delete")] [Obsolete("This endpoint is deprecated. Use DELETE method instead")] public async Task PostDelete(string id, [FromBody] SecretVerificationRequestModel model) { await Delete(id, model); } [HttpPost("{id}/delete-recover-token")] [AllowAnonymous] public async Task PostDeleteRecoverToken(Guid id, [FromBody] OrganizationVerifyDeleteRecoverRequestModel model) { var organization = await _organizationRepository.GetByIdAsync(id); if (organization == null) { throw new NotFoundException(); } if (!_orgDeleteTokenDataFactory.TryUnprotect(model.Token, out var data) || !data.IsValid(organization)) { throw new BadRequestException("Invalid token."); } if (organization.IsValidClient()) { var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); if (provider.IsBillable()) { await _providerBillingService.ScaleSeats( provider, organization.PlanType, -organization.Seats ?? 0); } } await _organizationDeleteCommand.DeleteAsync(organization); } [HttpPost("{id}/api-key")] public async Task ApiKey(string id, [FromBody] OrganizationApiKeyRequestModel model) { var orgIdGuid = new Guid(id); if (!await HasApiKeyAccessAsync(orgIdGuid, model.Type)) { throw new NotFoundException(); } var organization = await _organizationRepository.GetByIdAsync(orgIdGuid); if (organization == null) { throw new NotFoundException(); } if (model.Type == OrganizationApiKeyType.BillingSync || model.Type == OrganizationApiKeyType.Scim) { // Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types var productTier = organization.PlanType.GetProductTier(); if (productTier is not ProductTierType.Enterprise and not ProductTierType.Teams) { throw new NotFoundException(); } } var organizationApiKey = await _getOrganizationApiKeyQuery .GetOrganizationApiKeyAsync(organization.Id, model.Type) ?? await _createOrganizationApiKeyCommand.CreateAsync(organization.Id, model.Type); var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) { throw new UnauthorizedAccessException(); } if (model.Type != OrganizationApiKeyType.Scim && !await _userService.VerifySecretAsync(user, model.Secret)) { await Task.Delay(2000); throw new BadRequestException("MasterPasswordHash", "Invalid password."); } else { var response = new ApiKeyResponseModel(organizationApiKey); return response; } } [HttpGet("{id}/api-key-information/{type?}")] public async Task> ApiKeyInformation(Guid id, [FromRoute] OrganizationApiKeyType? type) { if (!await HasApiKeyAccessAsync(id, type)) { throw new NotFoundException(); } var apiKeys = await _organizationApiKeyRepository.GetManyByOrganizationIdTypeAsync(id, type); return new ListResponseModel( apiKeys.Select(k => new OrganizationApiKeyInformation(k))); } [HttpPost("{id}/rotate-api-key")] public async Task RotateApiKey(string id, [FromBody] OrganizationApiKeyRequestModel model) { var orgIdGuid = new Guid(id); if (!await HasApiKeyAccessAsync(orgIdGuid, model.Type)) { throw new NotFoundException(); } var organization = await _organizationRepository.GetByIdAsync(orgIdGuid); if (organization == null) { throw new NotFoundException(); } var organizationApiKey = await _getOrganizationApiKeyQuery .GetOrganizationApiKeyAsync(organization.Id, model.Type) ?? await _createOrganizationApiKeyCommand.CreateAsync(organization.Id, model.Type); var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) { throw new UnauthorizedAccessException(); } if (model.Type != OrganizationApiKeyType.Scim && !await _userService.VerifySecretAsync(user, model.Secret)) { await Task.Delay(2000); throw new BadRequestException("MasterPasswordHash", "Invalid password."); } else { await _rotateOrganizationApiKeyCommand.RotateApiKeyAsync(organizationApiKey); var response = new ApiKeyResponseModel(organizationApiKey); return response; } } private async Task HasApiKeyAccessAsync(Guid orgId, OrganizationApiKeyType? type) { return type switch { OrganizationApiKeyType.Scim => await _currentContext.ManageScim(orgId), _ => await _currentContext.OrganizationOwner(orgId), }; } [HttpGet("{id}/public-key")] public async Task GetPublicKey(string id) { var org = await _organizationRepository.GetByIdAsync(new Guid(id)); if (org == null) { throw new NotFoundException(); } return new OrganizationPublicKeyResponseModel(org); } [Obsolete("TDL-136 Renamed to public-key (2023.8), left for backwards compatibility with older clients.")] [HttpGet("{id}/keys")] public async Task GetKeys(string id) { return await GetPublicKey(id); } [HttpPost("{id}/keys")] public async Task PostKeys(Guid id, [FromBody] OrganizationKeysRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) { throw new UnauthorizedAccessException(); } var org = await _organizationUpdateKeysCommand.UpdateOrganizationKeysAsync(id, model.PublicKey, model.EncryptedPrivateKey); return new OrganizationKeysResponseModel(org); } [HttpGet("{id:guid}/sso")] public async Task GetSso(Guid id) { if (!await _currentContext.ManageSso(id)) { throw new NotFoundException(); } var organization = await _organizationRepository.GetByIdAsync(id); if (organization == null) { throw new NotFoundException(); } var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id); return new OrganizationSsoResponseModel(organization, _globalSettings, ssoConfig); } [HttpPost("{id:guid}/sso")] public async Task PostSso(Guid id, [FromBody] OrganizationSsoRequestModel model) { if (!await _currentContext.ManageSso(id)) { throw new NotFoundException(); } var organization = await _organizationRepository.GetByIdAsync(id); if (organization == null) { throw new NotFoundException(); } var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id); ssoConfig = ssoConfig == null ? model.ToSsoConfig(id) : model.ToSsoConfig(ssoConfig); organization.Identifier = model.Identifier; await _ssoConfigService.SaveAsync(ssoConfig, organization); await _organizationService.UpdateAsync(organization); return new OrganizationSsoResponseModel(organization, _globalSettings, ssoConfig); } [HttpPut("{id}/collection-management")] public async Task PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model) { if (!await _currentContext.OrganizationOwner(id)) { throw new NotFoundException(); } var organization = await _organizationService.UpdateCollectionManagementSettingsAsync(id, model.ToSettings()); var plan = await _pricingClient.GetPlan(organization.PlanType); return new OrganizationResponseModel(organization, plan); } [HttpGet("{id}/plan-type")] public async Task GetPlanType(string id) { var orgIdGuid = new Guid(id); var organization = await _organizationRepository.GetByIdAsync(orgIdGuid); if (organization == null) { throw new NotFoundException(); } return organization.PlanType; } }