server/src/Api/Controllers/OrganizationsController.cs
Rui Tomé f5a8cf5c9c
[AC-1081] Merge feature/billing-obfuscation (#2665)
* [EC-1014] Create Organization Status (Pending/Created) (#2610)

* [EC-427] Add columns 'Type' and 'BillingPhone' to Provider table

* [EC-427] Provider table Type and BillingPhone MySql migrations

* [EC-427] Provider table Type and BillingPhone Postgres migrations

* [EC-427] Add mysql migration script

* [EC-427] Add mysql migration script

* [EC-427] Updated Provider sql script to include default column value

* [EC-427] Removed default value from Provider.Type column

* [EC-427] Changed migration script to include a default value constraint instead of updating the null type

* [EC-427] Updated Sql project Provider table script

* [EC-427] Changed migration script to use 'Create OR Alter' for views and sprocs

* [EC-427] Added default values for 'BillingPhone' and 'Type' fields on sprocs [dbo].[Provider_Create] and [dbo].[Provider_Update]

* [EC-427] Adjusting metadata in migration script

* [EC-427] Updated Provider sprocs SQL script files

* [EC-427] Fixed migration script

* [EC-427] Added sqlite migration

* [EC-427] Add missing Provider_Update sproc default value

* [EC-427] Added missing GO action to migration script

* [EC-428] Redirect to Edit after creating Provider

* Revert "[EC-428] Redirect to Edit after creating Provider"

This reverts commit 6347bca1ed85681710379dbffc09e25b99b93ae6.

* [EC-1014] Create OrganizationStatusType and add Status column to Organizations table

* [EC-1014] Added EF migrations

* [EC-1014] dotnet format

* [EC-1014] Changed Organization.Status from SMALLINT to TINYINT

* [EC-1014] Set Organization.Status default value = 1

* [EC-1014] Setting Organization.Status default value as 1

* [EC-459 / EC-428] Admin panel: Add Provider Type to list and creation flow (#2593)

* [EC-427] Add columns 'Type' and 'BillingPhone' to Provider table

* [EC-427] Provider table Type and BillingPhone MySql migrations

* [EC-427] Provider table Type and BillingPhone Postgres migrations

* [EC-427] Add mysql migration script

* [EC-427] Add mysql migration script

* [EC-427] Updated Provider sql script to include default column value

* [EC-427] Removed default value from Provider.Type column

* [EC-427] Changed migration script to include a default value constraint instead of updating the null type

* [EC-427] Updated Sql project Provider table script

* [EC-427] Changed migration script to use 'Create OR Alter' for views and sprocs

* [EC-427] Added default values for 'BillingPhone' and 'Type' fields on sprocs [dbo].[Provider_Create] and [dbo].[Provider_Update]

* [EC-427] Adjusting metadata in migration script

* [EC-427] Updated Provider sprocs SQL script files

* [EC-427] Fixed migration script

* [EC-427] Added sqlite migration

* [EC-427] Add missing Provider_Update sproc default value

* [EC-427] Added missing GO action to migration script

* [EC-459] Added Type column to Providers list

* [EC-428] Added Type, BusinessName and BillingEmail to CreateProviderModel

* [EC-428] Updated Create Provider view to include new fields

* [EC-428] Updated ProviderService to not create a ProviderUser for the type Reseller

* [EC-428] Added custom validation for Provider fields depending on selected Type

* [EC-428] Redirect to Edit after creating Provider

* [EC-428] Setting Provider status as Created for Resellers

* [EC-428] Redirect on Provider creation depending if self host server

* [EC-428] Split ProviderService.CreateAsync into two methods: CreateMspAsync and CreateResellerAsync

* [EC-428] Created ICreateProviderCommand and added service for injection on Admin.Startup

* [EC-428] Modified Provider views to use DisplayName attribute values

* [EC-428] Moved ICreateProviderCommand to Core project

* [EC-428] Adding ICreateProviderCommand injection next to IProviderService

* [EC-428] Moved CreateProviderCommand to Commercial.Core project

* [EC-459] Added Type column to Providers list

* [EC-428] Added Type, BusinessName and BillingEmail to CreateProviderModel

* [EC-428] Updated Create Provider view to include new fields

* [EC-428] Updated ProviderService to not create a ProviderUser for the type Reseller

* [EC-428] Added custom validation for Provider fields depending on selected Type

* [EC-428] Redirect to Edit after creating Provider

* [EC-428] Setting Provider status as Created for Resellers

* [EC-428] Redirect on Provider creation depending if self host server

* [EC-428] Split ProviderService.CreateAsync into two methods: CreateMspAsync and CreateResellerAsync

* [EC-428] Created ICreateProviderCommand and added service for injection on Admin.Startup

* [EC-428] Modified Provider views to use DisplayName attribute values

* [EC-428] Moved ICreateProviderCommand to Core project

* [EC-428] Adding ICreateProviderCommand injection next to IProviderService

* [EC-428] Moved CreateProviderCommand to Commercial.Core project

* [EC-428] Moved CreateProviderCommand to namespace Bit.Commercial.Core.Providers

* [EC-429] Provider details screen updated with Type, BillingPhone and Organization details (#2666)

* [EC-430] Admin portal: Update organization information screen (#2672)

* [EC-430] Added ProviderOrganizationProviderDetailsView to get Provider details for an Organization

* [EC-430] Added Provider information to Organization Edit/View on Admin panel

* [EC-430] Remove "Add to Reseller" button

* [EC-430] Removed unused property OrganizationEditModel.ClientOwnerEmail

* [EC-430] Replaced IProviderOrganizationRepository.GetProviderDetailsByOrganizationAsync with IProviderRepository.GetByOrganizationIdAsync

* [EC-430] Deleted ProviderOrganizationProviderDetails and ProviderOrganizationProviderDetailsReadByOrganizationIdQuery

* [EC-429] Only show Create/Add Existing Organization buttons for Reseller providers (#2723)

* [EC-432] Add existing Organizations to Provider (#2683)

* [EC-432] Added ProviderOrganizationUnassignedOrganizationDetails_Search stored procedure

* [EC-432] Added IProviderOrganizationRepository.SearchAsync

* [EC-432] Created controller ProviderOrganizationsController to assign Organizations to a Provider

* [EC-432] Filter existing organizations by plans Enterprise or Team

* [EC-432] Existing Organization name links to edit page

* [EC-432] EF filtering out existing organizations by plan type enterprise or teams

* [EC-432] Creating multiple ProviderOrganization records

* [EC-432] Added ProviderOrganizationUnassignedOrganizationDetails_Search stored procedure

* [EC-432] Added IProviderOrganizationRepository.SearchAsync

* [EC-432] Created controller ProviderOrganizationsController to assign Organizations to a Provider

* [EC-432] Filter existing organizations by plans Enterprise or Team

* [EC-432] Existing Organization name links to edit page

* [EC-432] EF filtering out existing organizations by plan type enterprise or teams

* [EC-432] Creating multiple ProviderOrganization records

* [EC-432] Renamed migration script and added missing sproc

* [EC-432] Saving multiple events for the created ProviderOrganizations

* [EC-432] Included unit testing for ProviderService.AddOrganizations and EventService.LogProviderOrganizationEventsAsync

* [EC-432] Removed async from NoopEventService.LogProviderOrganizationEventsAsync

* [EC-432] Remove unused dependency setup in ProviderServiceTests.AddOrganizations_Success

* [EC-432] Renamed AddOrganizations to AddOrganizationsToReseller and removed addingUserId and key arguments

* [EC-432] Added DisplayName attributes to ProviderOrganizationViewModel and used them in the view

* [EC-432] Reverted changes to input fields

* [EC-432] Moved unassigned organizations search to Organizations repo

* [EC-432] Moved AddExistingOrganization action to ProvidersController

* [EC-432] dotnet format

* [EC-432] Fixed unit test issues

* [EC-432] Removed unnecessary Html.DisplayNameFor for labels

* [EC-432] Renamed OrganizationSearchViewModel to OrganizationUnassignedToProviderSearchViewModel

* [EC-432] Modified IEventService.LogProviderOrganizationEventsAsync to receive an IEnumerable as parameter

* [EC-432] Updated IProviderOrganizationRepository and replaced CreateWithManyOrganizations method with CreateManyAsync

* [EC-432] Deleted ProviderOrganization_CreateWithManyOrganizations

* [AC-432] Simplified Organization_UnassignedToProviderSearch query

* [AC-432] Removed unnecessary setup

* [EC-432] Checking if stored procedure exists before creating

* [EC-432] Renamed migration file to recent date

* [EC-435] Admin Portal: Add new Organization creation flow UI (#2707)

* [EC-435] Created _OrganizationForm partial view. Added actions for creating an Organization assigned to a provider

* [EC-435] Remove logic for creating an organization

* [EC-435] Created partial view _OrganizationFormScripts

* [EC-435] Remove unused ReferenceEventType

* [EC-435] Added TODO comment on Organization Create

* [EC-435] Checking if Provider type is Reseller on creating new assigned organization

* [EC-435] Setting the Organization plan type as TeamsMonthly by default when adding to a provider

* [EC-435] Removing unused buttons

* [EC-435] Switched hidden fields to form submit route value

* [EC-435] Moved _OrganizationForm and _OrganizationFormScripts to Shared folder

* [EC-435] Moved Create organization actions from OrganizationsController to ProvidersController

* [EC-435] Fixing bug on saving Organization that would have BillingEmail as null

* [EC-435] Added null check to Provider

* [EC-435] Moved trial buttons script logic to Edit view

* [AC-431] Add new organization invite process (#2737)

* [EC-435] Created _OrganizationForm partial view. Added actions for creating an Organization assigned to a provider

* [EC-435] Remove logic for creating an organization

* [EC-435] Created partial view _OrganizationFormScripts

* [EC-435] Remove unused ReferenceEventType

* [EC-435] Added TODO comment on Organization Create

* [EC-435] Checking if Provider type is Reseller on creating new assigned organization

* [EC-435] Setting the Organization plan type as TeamsMonthly by default when adding to a provider

* [EC-435] Removing unused buttons

* [EC-435] Switched hidden fields to form submit route value

* [EC-435] Moved _OrganizationForm and _OrganizationFormScripts to Shared folder

* [EC-435] Moved Create organization actions from OrganizationsController to ProvidersController

* [AC-431] Added new ReferenceEventType OrganizationCreatedByAdmin

* [AC-431] Added method IOrganizationService.CreateOrganization

* [AC-431] Creating new Organization with Pending status and assigning to Provider

* [AC-431] Added method to IMailService to send invitation to initialize org

* [AC-431] Added methods CreatePendingOrganization and InitPendingOrganization to IOrganizationService

* [AC-431] Org invite includes initOrganization parameter

* [AC-431] Modified existing Accept organization user action to initialize org

* [AC-431] Updated ProvidersController method name

* [AC-431] Created OrganizationUserInitInvitedViewModel to link to 'accept-init-organization' url

* [AC-431] Added action AcceptInit to OrganizationUsersController

* [AC-431] Resend owner invite

* [AC-431] dotnet format

* [AC-431] Removed unused parameter 'addingUserId' from IProviderService.AddOrganization

* [AC-431] Removed setting manual values for CreationDate and RevisionDate

* [AC-431] Updated OrganizationService.InitPendingOrganization to throw exceptions when the Organization does not meet the required criteria

* [AC-431] Modified OrganizationUserInitInvitedViewModel to inherit properties from OrganizationUserInvitedViewModel

* [AC-431] Removed unecessary parameter check

* [AC-431] Moved method description to IOrganizationService.InitPendingOrganization

* [AC-431] Moved ApplicationCacheService.UpsertOrganizationAbilityAsync and ReferenceEventService.RaiseEventAsync to OrganizationService

* [AC-431] Creating collection after creating organization

* [EC-435] Fixing bug on saving Organization that would have BillingEmail as null

* [AC-431] Deleted OrganizationUserInitInvitedViewModel and added parameter InitOrganization to OrganizationUserInvitedViewModel.cs

* [AC-431] Checking if the user has any existing SingleOrg policies before initializing an Org

* [AC-431] Remove commented code

* [EC-435] Added null check to Provider

* [EC-435] Moved trial buttons script logic to Edit view

* [AC-431] Added EncryptedString attribute to OrganizationUserAcceptInitRequestModel.CollectionName

* [AC-431] Refactored plan check condition

* [AC-431] Remove duplicate _applicationCacheService.UpsertOrganizationAbilityAsync call

* [AC-431] Removed IMailService.SendOrganizationInitInviteEmailAsync

* [AC-431] Added parameters ClaimsPrincipal and IUserService to IOrganizationService.CreatePendingOrganization

* [AC-434] Hide Billing screen for Reseller clients (#2783)

* [AC-434] Added ProviderType to ProfileOrganizationResponseModel

* [AC-434] Migration script

* [AC-434] Fixed indentation on migration script

* [AC-434] Hiding sensitive subscription data if the user does not have permissions

* [AC-434] Fixed missing dependency in unit test

* [AC-434] Altered BillingSubscription.Amount and BillingSubscriptionUpcomingInvoice.Amount to nullable

* [AC-434] Replaced CurrentContext.ManageBilling with ViewBillingHistory, ViewSubscription, EditSubscription and EditPaymentMethods

* [AC-434] Reverted change on BillingSubscription.Amount and now setting Subscription.Items = null when User does not have permission

* [AC-434] Added ProviderOrganizationProviderDetails_ReadByUserId

* [AC-434] Added IProviderOrganizationRepository.GetManyByUserAsync

* [AC-434] Added CurrentContext.GetOrganizationProviderDetails

* [AC-434] Remove unneeded join Organization table

* [AC-1255] Search Existing Organizations by partial Email (#2830)

* [AC-1255] Added email search field input validation

* [AC-1255] Reverted added email pattern

* [AC-1255] Modified Organization search by Email to search using substring

* [AC-1276] Displaying an Organizations pending owners if the Organization is in a Pending status (#2834)

* [AC-432] Checking that an existing Organization is not assigned to any Provider before being assigned (#2840)

* [AC-432] Checking if any of the selected Organizations is already assigned to a Provider

* [AC-432] Changed ProviderOrganization_ReadByOrganizationIds to only get count

* [AC-432] Replaced IProviderOrganizationRepository.GetCountByOrganizationIdsAsync with call to IProviderOrganizationRepository.GetByOrganizationId

* [AC-432] undo new line

* [AC-432] Fixed unit test

* Revert "[AC-432] Replaced IProviderOrganizationRepository.GetCountByOrganizationIdsAsync with call to IProviderOrganizationRepository.GetByOrganizationId"

This reverts commit ee6e095e883d933aa0d4c6beec0d4a93777ee2b9.

# Conflicts:
#	util/Migrator/DbScripts/2023-03-22_00_ProviderAddExistingOrganizations.sql

* [AC-432] Created new migration script for ProviderOrganization_ReadCountByOrganizationIds
2023-04-14 11:13:16 +01:00

719 lines
26 KiB
C#

using System.Text.Json;
using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response;
using Bit.Api.Models.Response.Organizations;
using Bit.Api.SecretsManager;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations.Policies;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers;
[Route("organizations")]
[Authorize("Application")]
public class OrganizationsController : Controller
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPolicyRepository _policyRepository;
private readonly IProviderRepository _providerRepository;
private readonly IOrganizationService _organizationService;
private readonly IUserService _userService;
private readonly IPaymentService _paymentService;
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 IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
private readonly GlobalSettings _globalSettings;
public OrganizationsController(
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IPolicyRepository policyRepository,
IProviderRepository providerRepository,
IOrganizationService organizationService,
IUserService userService,
IPaymentService paymentService,
ICurrentContext currentContext,
ISsoConfigRepository ssoConfigRepository,
ISsoConfigService ssoConfigService,
IGetOrganizationApiKeyQuery getOrganizationApiKeyQuery,
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
IOrganizationApiKeyRepository organizationApiKeyRepository,
IUpdateOrganizationLicenseCommand updateOrganizationLicenseCommand,
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
GlobalSettings globalSettings)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_policyRepository = policyRepository;
_providerRepository = providerRepository;
_organizationService = organizationService;
_userService = userService;
_paymentService = paymentService;
_currentContext = currentContext;
_ssoConfigRepository = ssoConfigRepository;
_ssoConfigService = ssoConfigService;
_getOrganizationApiKeyQuery = getOrganizationApiKeyQuery;
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
_organizationApiKeyRepository = organizationApiKeyRepository;
_updateOrganizationLicenseCommand = updateOrganizationLicenseCommand;
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
_globalSettings = globalSettings;
}
[HttpGet("{id}")]
public async Task<OrganizationResponseModel> 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();
}
return new OrganizationResponseModel(organization);
}
[HttpGet("{id}/billing")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<BillingResponseModel> GetBilling(string id)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.ViewBillingHistory(orgIdGuid))
{
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
if (organization == null)
{
throw new NotFoundException();
}
var billingInfo = await _paymentService.GetBillingAsync(organization);
return new BillingResponseModel(billingInfo);
}
[HttpGet("{id}/subscription")]
public async Task<OrganizationSubscriptionResponseModel> GetSubscription(string id)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.ViewSubscription(orgIdGuid))
{
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
if (organization == null)
{
throw new NotFoundException();
}
if (!_globalSettings.SelfHosted && organization.Gateway != null)
{
var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization);
if (subscriptionInfo == null)
{
throw new NotFoundException();
}
var hideSensitiveData = !await _currentContext.EditSubscription(orgIdGuid);
return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, hideSensitiveData);
}
else
{
return new OrganizationSubscriptionResponseModel(organization);
}
}
[HttpGet("{id}/license")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<OrganizationLicense> GetLicense(string id, [FromQuery] Guid installationId)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.OrganizationOwner(orgIdGuid))
{
throw new NotFoundException();
}
var org = await _organizationRepository.GetByIdAsync(new Guid(id));
var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(org, installationId);
if (license == null)
{
throw new NotFoundException();
}
return license;
}
[HttpGet("")]
public async Task<ListResponseModel<ProfileOrganizationResponseModel>> GetUser()
{
var userId = _userService.GetProperUserId(User).Value;
var organizations = await _organizationUserRepository.GetManyDetailsByUserAsync(userId,
OrganizationUserStatusType.Confirmed);
var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o));
return new ListResponseModel<ProfileOrganizationResponseModel>(responses);
}
[HttpGet("{identifier}/auto-enroll-status")]
public async Task<OrganizationAutoEnrollStatusResponseModel> 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();
}
var resetPasswordPolicy =
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled || resetPasswordPolicy.Data == null)
{
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, false);
}
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase);
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
}
[HttpPost("")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<OrganizationResponseModel> Post([FromBody] OrganizationCreateRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var organizationSignup = model.ToOrganizationSignup(user);
var result = await _organizationService.SignUpAsync(organizationSignup);
return new OrganizationResponseModel(result.Item1);
}
[HttpPut("{id}")]
[HttpPost("{id}")]
public async Task<OrganizationResponseModel> Put(string id, [FromBody] OrganizationUpdateRequestModel model)
{
var orgIdGuid = new Guid(id);
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
if (organization == null)
{
throw new NotFoundException();
}
var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.BusinessName ||
model.BillingEmail != organization.BillingEmail);
var hasRequiredPermissions = updateBilling
? await _currentContext.EditSubscription(orgIdGuid)
: await _currentContext.OrganizationOwner(orgIdGuid);
if (!hasRequiredPermissions)
{
throw new NotFoundException();
}
await _organizationService.UpdateAsync(model.ToOrganization(organization, _globalSettings), updateBilling);
return new OrganizationResponseModel(organization);
}
[HttpPost("{id}/payment")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostPayment(string id, [FromBody] PaymentRequestModel model)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.EditPaymentMethods(orgIdGuid))
{
throw new NotFoundException();
}
await _organizationService.ReplacePaymentMethodAsync(orgIdGuid, model.PaymentToken,
model.PaymentMethodType.Value, new TaxInfo
{
BillingAddressLine1 = model.Line1,
BillingAddressLine2 = model.Line2,
BillingAddressState = model.State,
BillingAddressCity = model.City,
BillingAddressPostalCode = model.PostalCode,
BillingAddressCountry = model.Country,
TaxIdNumber = model.TaxId,
});
}
[HttpPost("{id}/upgrade")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<PaymentResponseModel> PostUpgrade(string id, [FromBody] OrganizationUpgradeRequestModel model)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.EditSubscription(orgIdGuid))
{
throw new NotFoundException();
}
var result = await _organizationService.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade());
return new PaymentResponseModel { Success = result.Item1, PaymentIntentClientSecret = result.Item2 };
}
[HttpPost("{id}/subscription")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostSubscription(string id, [FromBody] OrganizationSubscriptionUpdateRequestModel model)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.EditSubscription(orgIdGuid))
{
throw new NotFoundException();
}
await _organizationService.UpdateSubscription(orgIdGuid, model.SeatAdjustment, model.MaxAutoscaleSeats);
}
[HttpPost("{id}/seat")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<PaymentResponseModel> PostSeat(string id, [FromBody] OrganizationSeatRequestModel model)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.EditSubscription(orgIdGuid))
{
throw new NotFoundException();
}
var result = await _organizationService.AdjustSeatsAsync(orgIdGuid, model.SeatAdjustment.Value);
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
}
[HttpPost("{id}/storage")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<PaymentResponseModel> 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}/verify-bank")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostVerifyBank(string id, [FromBody] OrganizationVerifyBankRequestModel model)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.EditSubscription(orgIdGuid))
{
throw new NotFoundException();
}
await _organizationService.VerifyBankAsync(orgIdGuid, model.Amount1.Value, model.Amount2.Value);
}
[HttpPost("{id}/cancel")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostCancel(string id)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.EditSubscription(orgIdGuid))
{
throw new NotFoundException();
}
await _organizationService.CancelSubscriptionAsync(orgIdGuid);
}
[HttpPost("{id}/reinstate")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostReinstate(string id)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.EditSubscription(orgIdGuid))
{
throw new NotFoundException();
}
await _organizationService.ReinstateSubscriptionAsync(orgIdGuid);
}
[HttpPost("{id}/leave")]
public async Task Leave(string id)
{
var orgGuidId = new Guid(id);
if (!await _currentContext.OrganizationUser(orgGuidId))
{
throw new NotFoundException();
}
var user = await _userService.GetUserByPrincipalAsync(User);
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgGuidId);
if (ssoConfig?.GetData()?.KeyConnectorEnabled == true &&
user.UsesKeyConnector)
{
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
}
await _organizationService.DeleteUserAsync(orgGuidId, user.Id);
}
[HttpDelete("{id}")]
[HttpPost("{id}/delete")]
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.");
}
else
{
await _organizationService.DeleteAsync(organization);
}
}
[HttpPost("{id}/import")]
public async Task Import(string id, [FromBody] ImportOrganizationUsersRequestModel model)
{
if (!_globalSettings.SelfHosted && !model.LargeImport &&
(model.Groups.Count() > 2000 || model.Users.Count(u => !u.Deleted) > 2000))
{
throw new BadRequestException("You cannot import this much data at once.");
}
var orgIdGuid = new Guid(id);
if (!await _currentContext.OrganizationAdmin(orgIdGuid))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
await _organizationService.ImportAsync(
orgIdGuid,
userId.Value,
model.Groups.Select(g => g.ToImportedGroup(orgIdGuid)),
model.Users.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()),
model.Users.Where(u => u.Deleted).Select(u => u.ExternalId),
model.OverwriteExisting);
}
[HttpPost("{id}/api-key")]
public async Task<ApiKeyResponseModel> 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 plan = StaticStore.GetPlan(organization.PlanType);
if (plan.Product != ProductType.Enterprise)
{
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<ListResponseModel<OrganizationApiKeyInformation>> ApiKeyInformation(Guid id,
[FromRoute] OrganizationApiKeyType? type)
{
if (!await HasApiKeyAccessAsync(id, type))
{
throw new NotFoundException();
}
var apiKeys = await _organizationApiKeyRepository.GetManyByOrganizationIdTypeAsync(id, type);
return new ListResponseModel<OrganizationApiKeyInformation>(
apiKeys.Select(k => new OrganizationApiKeyInformation(k)));
}
[HttpPost("{id}/rotate-api-key")]
public async Task<ApiKeyResponseModel> 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<bool> HasApiKeyAccessAsync(Guid orgId, OrganizationApiKeyType? type)
{
return type switch
{
OrganizationApiKeyType.Scim => await _currentContext.ManageScim(orgId),
_ => await _currentContext.OrganizationOwner(orgId),
};
}
[HttpGet("{id}/tax")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<TaxInfoResponseModel> GetTaxInfo(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 taxInfo = await _paymentService.GetTaxInfoAsync(organization);
return new TaxInfoResponseModel(taxInfo);
}
[HttpPut("{id}/tax")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PutTaxInfo(string id, [FromBody] OrganizationTaxInfoUpdateRequestModel 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 taxInfo = new TaxInfo
{
TaxIdNumber = model.TaxId,
BillingAddressLine1 = model.Line1,
BillingAddressLine2 = model.Line2,
BillingAddressCity = model.City,
BillingAddressState = model.State,
BillingAddressPostalCode = model.PostalCode,
BillingAddressCountry = model.Country,
};
await _paymentService.SaveTaxInfoAsync(organization, taxInfo);
}
[HttpGet("{id}/keys")]
public async Task<OrganizationKeysResponseModel> GetKeys(string id)
{
var org = await _organizationRepository.GetByIdAsync(new Guid(id));
if (org == null)
{
throw new NotFoundException();
}
return new OrganizationKeysResponseModel(org);
}
[HttpPost("{id}/keys")]
public async Task<OrganizationKeysResponseModel> PostKeys(string id, [FromBody] OrganizationKeysRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var org = await _organizationService.UpdateOrganizationKeysAsync(new Guid(id), model.PublicKey,
model.EncryptedPrivateKey);
return new OrganizationKeysResponseModel(org);
}
[HttpGet("{id:guid}/sso")]
public async Task<OrganizationSsoResponseModel> 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<OrganizationSsoResponseModel> 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);
}
// This is a temporary endpoint to self-enroll in secrets manager
[SecretsManager]
[SelfHosted(NotSelfHostedOnly = true)]
[HttpPost("{id}/enroll-secrets-manager")]
public async Task EnrollSecretsManager(Guid id, [FromBody] OrganizationEnrollSecretsManagerRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
if (!await _currentContext.OrganizationAdmin(id))
{
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization == null)
{
throw new NotFoundException();
}
organization.UseSecretsManager = model.Enabled;
await _organizationService.UpdateAsync(organization);
// Turn on Secrets Manager for the user
if (model.Enabled)
{
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(id, userId);
if (orgUser != null)
{
orgUser.AccessSecretsManager = true;
await _organizationUserRepository.ReplaceAsync(orgUser);
}
}
}
}