mirror of
https://github.com/bitwarden/server
synced 2025-12-10 21:33:41 +00:00
* Add new event types for collection management settings in EventType enum * Refactor collection management settings update process in OrganizationsController and IOrganizationService. Introduced UpdateCollectionManagementSettingsAsync method to streamline updates and logging for collection management settings. * Add unit tests for collection management settings updates in OrganizationsController and OrganizationService. Implemented tests to verify the successful update of collection management settings and the logging of specific events when settings are changed. Added error handling for cases where the organization is not found. * Refactor collection management settings handling in OrganizationsController and IOrganizationService. Updated the UpdateCollectionManagementSettingsAsync method to accept a single settings object, simplifying the parameter list and improving code readability. Introduced a new OrganizationCollectionManagementSettings model to encapsulate collection management settings. Adjusted related tests to reflect these changes. * Add Obsolete attribute to Organization_CollectionManagement_Updated event in EventType enum
1282 lines
51 KiB
C#
1282 lines
51 KiB
C#
// FIXME: Update this file to be null safe and then delete the line below
|
|
#nullable disable
|
|
|
|
using System.Text.Json;
|
|
using Bit.Core.AdminConsole.Entities;
|
|
using Bit.Core.AdminConsole.Enums;
|
|
using Bit.Core.AdminConsole.Enums.Provider;
|
|
using Bit.Core.AdminConsole.Models.Business;
|
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
|
using Bit.Core.AdminConsole.Repositories;
|
|
using Bit.Core.AdminConsole.Services;
|
|
using Bit.Core.AdminConsole.Utilities.DebuggingInstruments;
|
|
using Bit.Core.Auth.Enums;
|
|
using Bit.Core.Auth.Repositories;
|
|
using Bit.Core.Billing.Constants;
|
|
using Bit.Core.Billing.Enums;
|
|
using Bit.Core.Billing.Extensions;
|
|
using Bit.Core.Billing.Pricing;
|
|
using Bit.Core.Context;
|
|
using Bit.Core.Entities;
|
|
using Bit.Core.Enums;
|
|
using Bit.Core.Exceptions;
|
|
using Bit.Core.Models.Business;
|
|
using Bit.Core.Models.Data;
|
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
|
using Bit.Core.Platform.Push;
|
|
using Bit.Core.Repositories;
|
|
using Bit.Core.Settings;
|
|
using Bit.Core.Utilities;
|
|
using Microsoft.Extensions.Logging;
|
|
using Stripe;
|
|
using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;
|
|
|
|
namespace Bit.Core.Services;
|
|
|
|
public class OrganizationService : IOrganizationService
|
|
{
|
|
private readonly IOrganizationRepository _organizationRepository;
|
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
|
private readonly IGroupRepository _groupRepository;
|
|
private readonly IMailService _mailService;
|
|
private readonly IPushNotificationService _pushNotificationService;
|
|
private readonly IEventService _eventService;
|
|
private readonly IApplicationCacheService _applicationCacheService;
|
|
private readonly IPaymentService _paymentService;
|
|
private readonly IPolicyRepository _policyRepository;
|
|
private readonly IPolicyService _policyService;
|
|
private readonly ISsoUserRepository _ssoUserRepository;
|
|
private readonly IGlobalSettings _globalSettings;
|
|
private readonly ICurrentContext _currentContext;
|
|
private readonly ILogger<OrganizationService> _logger;
|
|
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
|
private readonly IProviderUserRepository _providerUserRepository;
|
|
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
|
|
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
|
private readonly IProviderRepository _providerRepository;
|
|
private readonly IFeatureService _featureService;
|
|
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
|
private readonly IPricingClient _pricingClient;
|
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
|
private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;
|
|
private readonly IStripeAdapter _stripeAdapter;
|
|
|
|
public OrganizationService(
|
|
IOrganizationRepository organizationRepository,
|
|
IOrganizationUserRepository organizationUserRepository,
|
|
IGroupRepository groupRepository,
|
|
IMailService mailService,
|
|
IPushNotificationService pushNotificationService,
|
|
IEventService eventService,
|
|
IApplicationCacheService applicationCacheService,
|
|
IPaymentService paymentService,
|
|
IPolicyRepository policyRepository,
|
|
IPolicyService policyService,
|
|
ISsoUserRepository ssoUserRepository,
|
|
IGlobalSettings globalSettings,
|
|
ICurrentContext currentContext,
|
|
ILogger<OrganizationService> logger,
|
|
IProviderOrganizationRepository providerOrganizationRepository,
|
|
IProviderUserRepository providerUserRepository,
|
|
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
|
|
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
|
IProviderRepository providerRepository,
|
|
IFeatureService featureService,
|
|
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
|
IPricingClient pricingClient,
|
|
IPolicyRequirementQuery policyRequirementQuery,
|
|
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,
|
|
IStripeAdapter stripeAdapter
|
|
)
|
|
{
|
|
_organizationRepository = organizationRepository;
|
|
_organizationUserRepository = organizationUserRepository;
|
|
_groupRepository = groupRepository;
|
|
_mailService = mailService;
|
|
_pushNotificationService = pushNotificationService;
|
|
_eventService = eventService;
|
|
_applicationCacheService = applicationCacheService;
|
|
_paymentService = paymentService;
|
|
_policyRepository = policyRepository;
|
|
_policyService = policyService;
|
|
_ssoUserRepository = ssoUserRepository;
|
|
_globalSettings = globalSettings;
|
|
_currentContext = currentContext;
|
|
_logger = logger;
|
|
_providerOrganizationRepository = providerOrganizationRepository;
|
|
_providerUserRepository = providerUserRepository;
|
|
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
|
|
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
|
_providerRepository = providerRepository;
|
|
_featureService = featureService;
|
|
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
|
_pricingClient = pricingClient;
|
|
_policyRequirementQuery = policyRequirementQuery;
|
|
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
|
|
_stripeAdapter = stripeAdapter;
|
|
}
|
|
|
|
public async Task ReinstateSubscriptionAsync(Guid organizationId)
|
|
{
|
|
var organization = await GetOrgById(organizationId);
|
|
if (organization == null)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
await _paymentService.ReinstateSubscriptionAsync(organization);
|
|
}
|
|
|
|
public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
|
|
{
|
|
var organization = await GetOrgById(organizationId);
|
|
if (organization == null)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
|
|
|
if (!plan.PasswordManager.HasAdditionalStorageOption)
|
|
{
|
|
throw new BadRequestException("Plan does not allow additional storage.");
|
|
}
|
|
|
|
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb,
|
|
plan.PasswordManager.StripeStoragePlanId);
|
|
await ReplaceAndUpdateCacheAsync(organization);
|
|
return secret;
|
|
}
|
|
|
|
public async Task UpdateSubscription(Guid organizationId, int seatAdjustment, int? maxAutoscaleSeats)
|
|
{
|
|
var organization = await GetOrgById(organizationId);
|
|
if (organization == null)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
var newSeatCount = organization.Seats + seatAdjustment;
|
|
if (maxAutoscaleSeats.HasValue && newSeatCount > maxAutoscaleSeats.Value)
|
|
{
|
|
throw new BadRequestException("Cannot set max seat autoscaling below seat count.");
|
|
}
|
|
|
|
if (seatAdjustment != 0)
|
|
{
|
|
await AdjustSeatsAsync(organization, seatAdjustment);
|
|
}
|
|
|
|
if (maxAutoscaleSeats != organization.MaxAutoscaleSeats)
|
|
{
|
|
await UpdateAutoscalingAsync(organization, maxAutoscaleSeats);
|
|
}
|
|
}
|
|
|
|
private async Task UpdateAutoscalingAsync(Organization organization, int? maxAutoscaleSeats)
|
|
{
|
|
if (maxAutoscaleSeats.HasValue &&
|
|
organization.Seats.HasValue &&
|
|
maxAutoscaleSeats.Value < organization.Seats.Value)
|
|
{
|
|
throw new BadRequestException($"Cannot set max seat autoscaling below current seat count.");
|
|
}
|
|
|
|
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
|
if (plan == null)
|
|
{
|
|
throw new BadRequestException("Existing plan not found.");
|
|
}
|
|
|
|
if (!plan.PasswordManager.AllowSeatAutoscale)
|
|
{
|
|
throw new BadRequestException("Your plan does not allow seat autoscaling.");
|
|
}
|
|
|
|
if (plan.PasswordManager.MaxSeats.HasValue && maxAutoscaleSeats.HasValue &&
|
|
maxAutoscaleSeats > plan.PasswordManager.MaxSeats)
|
|
{
|
|
throw new BadRequestException(string.Concat(
|
|
$"Your plan has a seat limit of {plan.PasswordManager.MaxSeats}, ",
|
|
$"but you have specified a max autoscale count of {maxAutoscaleSeats}.",
|
|
"Reduce your max autoscale seat count."));
|
|
}
|
|
|
|
organization.MaxAutoscaleSeats = maxAutoscaleSeats;
|
|
|
|
await ReplaceAndUpdateCacheAsync(organization);
|
|
}
|
|
|
|
public async Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment)
|
|
{
|
|
var organization = await GetOrgById(organizationId);
|
|
if (organization == null)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
return await AdjustSeatsAsync(organization, seatAdjustment);
|
|
}
|
|
|
|
private async Task<string> AdjustSeatsAsync(Organization organization, int seatAdjustment,
|
|
IEnumerable<string> ownerEmails = null)
|
|
{
|
|
if (organization.Seats == null)
|
|
{
|
|
throw new BadRequestException("Organization has no seat limit, no need to adjust seats");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
|
{
|
|
throw new BadRequestException("No payment method found.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
|
{
|
|
throw new BadRequestException("No subscription found.");
|
|
}
|
|
|
|
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
|
|
|
if (!plan.PasswordManager.HasAdditionalSeatsOption)
|
|
{
|
|
throw new BadRequestException("Plan does not allow additional seats.");
|
|
}
|
|
|
|
var newSeatTotal = organization.Seats.Value + seatAdjustment;
|
|
if (plan.PasswordManager.BaseSeats > newSeatTotal)
|
|
{
|
|
throw new BadRequestException($"Plan has a minimum of {plan.PasswordManager.BaseSeats} seats.");
|
|
}
|
|
|
|
if (newSeatTotal <= 0)
|
|
{
|
|
throw new BadRequestException("You must have at least 1 seat.");
|
|
}
|
|
|
|
var additionalSeats = newSeatTotal - plan.PasswordManager.BaseSeats;
|
|
if (plan.PasswordManager.MaxAdditionalSeats.HasValue &&
|
|
additionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value)
|
|
{
|
|
throw new BadRequestException($"Organization plan allows a maximum of " +
|
|
$"{plan.PasswordManager.MaxAdditionalSeats.Value} additional seats.");
|
|
}
|
|
|
|
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
|
|
{
|
|
var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
|
|
|
if (seatCounts.Total > newSeatTotal)
|
|
{
|
|
if (organization.UseAdminSponsoredFamilies || seatCounts.Sponsored > 0)
|
|
{
|
|
throw new BadRequestException(
|
|
$"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " +
|
|
$"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.");
|
|
}
|
|
else
|
|
{
|
|
throw new BadRequestException($"Your organization currently has {seatCounts.Total} seats filled. " +
|
|
$"Your new plan only has ({newSeatTotal}) seats. Remove some users.");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (organization.UseSecretsManager && organization.Seats + seatAdjustment < organization.SmSeats)
|
|
{
|
|
throw new BadRequestException("You cannot have more Secrets Manager seats than Password Manager seats.");
|
|
}
|
|
|
|
_logger.LogInformation("{Method}: Invoking _paymentService.AdjustSeatsAsync with {AdditionalSeats} additional seats for Organization ({OrganizationID})",
|
|
nameof(AdjustSeatsAsync), additionalSeats, organization.Id);
|
|
|
|
var paymentIntentClientSecret = await _paymentService.AdjustSeatsAsync(organization, plan, additionalSeats);
|
|
organization.Seats = (short?)newSeatTotal;
|
|
|
|
_logger.LogInformation("{Method}: Invoking _organizationRepository.ReplaceAsync with {Seats} seats for Organization ({OrganizationID})", nameof(AdjustSeatsAsync), organization.Seats, organization.Id); ;
|
|
|
|
await ReplaceAndUpdateCacheAsync(organization);
|
|
|
|
if (organization.Seats.HasValue && organization.MaxAutoscaleSeats.HasValue &&
|
|
organization.Seats == organization.MaxAutoscaleSeats)
|
|
{
|
|
try
|
|
{
|
|
if (ownerEmails == null)
|
|
{
|
|
ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,
|
|
OrganizationUserType.Owner)).Select(u => u.Email).Distinct();
|
|
}
|
|
|
|
await _mailService.SendOrganizationMaxSeatLimitReachedEmailAsync(organization,
|
|
organization.MaxAutoscaleSeats.Value, ownerEmails);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_logger.LogError(e, "Error encountered notifying organization owners of seat limit reached.");
|
|
}
|
|
}
|
|
|
|
return paymentIntentClientSecret;
|
|
}
|
|
|
|
public async Task VerifyBankAsync(Guid organizationId, int amount1, int amount2)
|
|
{
|
|
var organization = await GetOrgById(organizationId);
|
|
if (organization == null)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
|
{
|
|
throw new GatewayException("Not a gateway customer.");
|
|
}
|
|
|
|
var bankService = new BankAccountService();
|
|
var customer = await _stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId,
|
|
new CustomerGetOptions { Expand = new List<string> { "sources" } });
|
|
if (customer == null)
|
|
{
|
|
throw new GatewayException("Cannot find customer.");
|
|
}
|
|
|
|
var bankAccount = customer.Sources
|
|
.FirstOrDefault(s => s is BankAccount && ((BankAccount)s).Status != "verified") as BankAccount;
|
|
if (bankAccount == null)
|
|
{
|
|
throw new GatewayException("Cannot find an unverified bank account.");
|
|
}
|
|
|
|
try
|
|
{
|
|
var result = await bankService.VerifyAsync(organization.GatewayCustomerId, bankAccount.Id,
|
|
new BankAccountVerifyOptions { Amounts = new List<long> { amount1, amount2 } });
|
|
if (result.Status != "verified")
|
|
{
|
|
throw new GatewayException("Unable to verify account.");
|
|
}
|
|
}
|
|
catch (StripeException e)
|
|
{
|
|
throw new GatewayException(e.Message);
|
|
}
|
|
}
|
|
|
|
public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate)
|
|
{
|
|
var org = await GetOrgById(organizationId);
|
|
if (org != null)
|
|
{
|
|
org.ExpirationDate = expirationDate;
|
|
org.RevisionDate = DateTime.UtcNow;
|
|
await ReplaceAndUpdateCacheAsync(org);
|
|
}
|
|
}
|
|
|
|
public async Task UpdateAsync(Organization organization, bool updateBilling = false)
|
|
{
|
|
if (organization.Id == default(Guid))
|
|
{
|
|
throw new ApplicationException("Cannot create org this way. Call SignUpAsync.");
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(organization.Identifier))
|
|
{
|
|
var orgById = await _organizationRepository.GetByIdentifierAsync(organization.Identifier);
|
|
if (orgById != null && orgById.Id != organization.Id)
|
|
{
|
|
throw new BadRequestException("Identifier already in use by another organization.");
|
|
}
|
|
}
|
|
|
|
await ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated);
|
|
|
|
if (updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
|
{
|
|
var newDisplayName = organization.DisplayName();
|
|
|
|
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId,
|
|
new CustomerUpdateOptions
|
|
{
|
|
Email = organization.BillingEmail,
|
|
Description = organization.DisplayBusinessName(),
|
|
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
|
{
|
|
// This overwrites the existing custom fields for this organization
|
|
CustomFields = [
|
|
new CustomerInvoiceSettingsCustomFieldOptions
|
|
{
|
|
Name = organization.SubscriberType(),
|
|
Value = newDisplayName.Length <= 30
|
|
? newDisplayName
|
|
: newDisplayName[..30]
|
|
}]
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
public async Task<Organization> UpdateCollectionManagementSettingsAsync(Guid organizationId, OrganizationCollectionManagementSettings settings)
|
|
{
|
|
var existingOrganization = await _organizationRepository.GetByIdAsync(organizationId);
|
|
if (existingOrganization == null)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
// Create logging actions based on what will change
|
|
var loggingActions = CreateCollectionManagementLoggingActions(existingOrganization, settings);
|
|
|
|
existingOrganization.LimitCollectionCreation = settings.LimitCollectionCreation;
|
|
existingOrganization.LimitCollectionDeletion = settings.LimitCollectionDeletion;
|
|
existingOrganization.LimitItemDeletion = settings.LimitItemDeletion;
|
|
existingOrganization.AllowAdminAccessToAllCollectionItems = settings.AllowAdminAccessToAllCollectionItems;
|
|
existingOrganization.RevisionDate = DateTime.UtcNow;
|
|
|
|
await ReplaceAndUpdateCacheAsync(existingOrganization);
|
|
|
|
if (loggingActions.Any())
|
|
{
|
|
await Task.WhenAll(loggingActions.Select(action => action()));
|
|
}
|
|
|
|
await _pushNotificationService.PushSyncOrganizationCollectionManagementSettingsAsync(existingOrganization);
|
|
|
|
return existingOrganization;
|
|
}
|
|
|
|
public async Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type)
|
|
{
|
|
if (!type.ToString().Contains("Organization"))
|
|
{
|
|
throw new ArgumentException("Not an organization provider type.");
|
|
}
|
|
|
|
if (!organization.Use2fa)
|
|
{
|
|
throw new BadRequestException("Organization cannot use 2FA.");
|
|
}
|
|
|
|
var providers = organization.GetTwoFactorProviders();
|
|
if (providers is null || !providers.TryGetValue(type, out var provider))
|
|
{
|
|
return;
|
|
}
|
|
|
|
provider.Enabled = true;
|
|
organization.SetTwoFactorProviders(providers);
|
|
await UpdateAsync(organization);
|
|
}
|
|
|
|
public async Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type)
|
|
{
|
|
if (!type.ToString().Contains("Organization"))
|
|
{
|
|
throw new ArgumentException("Not an organization provider type.");
|
|
}
|
|
|
|
var providers = organization.GetTwoFactorProviders();
|
|
if (!providers?.ContainsKey(type) ?? true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
providers.Remove(type);
|
|
organization.SetTwoFactorProviders(providers);
|
|
await UpdateAsync(organization);
|
|
}
|
|
|
|
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId,
|
|
EventSystemUser? systemUser,
|
|
OrganizationUserInvite invite, string externalId)
|
|
{
|
|
// Ideally OrganizationUserInvite should represent a single user so that this doesn't have to be a runtime check
|
|
if (invite.Emails.Count() > 1)
|
|
{
|
|
throw new BadRequestException("This method can only be used to invite a single user.");
|
|
}
|
|
|
|
// Validate Collection associations
|
|
var invalidAssociations = invite.Collections?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));
|
|
if (invalidAssociations?.Any() ?? false)
|
|
{
|
|
throw new BadRequestException(
|
|
"The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.");
|
|
}
|
|
|
|
var results = await InviteUsersAsync(organizationId, invitingUserId, systemUser,
|
|
new (OrganizationUserInvite, string)[] { (invite, externalId) });
|
|
|
|
var result = results.FirstOrDefault();
|
|
if (result == null)
|
|
{
|
|
throw new BadRequestException("This user has already been invited.");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invite users to an organization.
|
|
/// </summary>
|
|
/// <param name="organizationId">The organization Id</param>
|
|
/// <param name="invitingUserId">The current authenticated user who is sending the invite. Only used when inviting via a client app; null if using SCIM or Public API.</param>
|
|
/// <param name="systemUser">The system user which is sending the invite. Only used when inviting via SCIM; null if using a client app or Public API</param>
|
|
/// <param name="invites">Details about the users being invited</param>
|
|
/// <returns></returns>
|
|
public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId,
|
|
EventSystemUser? systemUser,
|
|
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
|
|
{
|
|
var inviteTypes = new HashSet<OrganizationUserType>(invites.Where(i => i.invite.Type.HasValue)
|
|
.Select(i => i.invite.Type.Value));
|
|
|
|
// If authenticating via a client app, verify the inviting user has permissions
|
|
// cf. SCIM and Public API have superuser permissions here
|
|
if (invitingUserId.HasValue && inviteTypes.Count > 0)
|
|
{
|
|
foreach (var (invite, _) in invites)
|
|
{
|
|
await ValidateOrganizationUserUpdatePermissions(organizationId, invite.Type.Value, null,
|
|
invite.Permissions);
|
|
await ValidateOrganizationCustomPermissionsEnabledAsync(organizationId, invite.Type.Value);
|
|
}
|
|
}
|
|
|
|
var (organizationUsers, events) = await SaveUsersSendInvitesAsync(organizationId, invites);
|
|
|
|
if (systemUser.HasValue)
|
|
{
|
|
// Log SCIM event
|
|
await _eventService.LogOrganizationUserEventsAsync(events.Select(e =>
|
|
(e.Item1, e.Item2, systemUser.Value, e.Item3)));
|
|
}
|
|
else
|
|
{
|
|
// Log client app or Public Api event
|
|
await _eventService.LogOrganizationUserEventsAsync(events);
|
|
}
|
|
|
|
return organizationUsers;
|
|
}
|
|
|
|
private async
|
|
Task<(List<OrganizationUser> organizationUsers, List<(OrganizationUser, EventType, DateTime?)> events)>
|
|
SaveUsersSendInvitesAsync(Guid organizationId,
|
|
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
|
|
{
|
|
var organization = await GetOrgById(organizationId);
|
|
var initialSeatCount = organization.Seats;
|
|
if (organization == null || invites.Any(i => i.invite.Emails == null))
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
var existingEmails = new HashSet<string>(await _organizationUserRepository.SelectKnownEmailsAsync(
|
|
organizationId, invites.SelectMany(i => i.invite.Emails), false),
|
|
StringComparer.InvariantCultureIgnoreCase);
|
|
|
|
// Seat autoscaling
|
|
var initialSmSeatCount = organization.SmSeats;
|
|
var newSeatsRequired = 0;
|
|
if (organization.Seats.HasValue)
|
|
{
|
|
var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
|
var availableSeats = organization.Seats.Value - seatCounts.Total;
|
|
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
|
|
}
|
|
|
|
if (newSeatsRequired > 0)
|
|
{
|
|
var (canScale, failureReason) = await CanScaleAsync(organization, newSeatsRequired);
|
|
if (!canScale)
|
|
{
|
|
throw new BadRequestException(failureReason);
|
|
}
|
|
}
|
|
|
|
// Secrets Manager seat autoscaling
|
|
SecretsManagerSubscriptionUpdate smSubscriptionUpdate = null;
|
|
var inviteWithSmAccessCount = invites
|
|
.Where(i => i.invite.AccessSecretsManager)
|
|
.SelectMany(i => i.invite.Emails)
|
|
.Count(email => !existingEmails.Contains(email));
|
|
|
|
var additionalSmSeatsRequired =
|
|
await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount);
|
|
if (additionalSmSeatsRequired > 0)
|
|
{
|
|
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
|
smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, plan, true)
|
|
.AdjustSeats(additionalSmSeatsRequired);
|
|
}
|
|
|
|
var invitedAreAllOwners = invites.All(i => i.invite.Type == OrganizationUserType.Owner);
|
|
if (!invitedAreAllOwners &&
|
|
!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { },
|
|
includeProvider: true))
|
|
{
|
|
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
|
}
|
|
|
|
var orgUsersWithoutCollections = new List<OrganizationUser>();
|
|
var orgUsersWithCollections = new List<(OrganizationUser, IEnumerable<CollectionAccessSelection>)>();
|
|
var orgUserGroups = new List<(OrganizationUser, IEnumerable<Guid>)>();
|
|
var orgUserInvitedCount = 0;
|
|
var exceptions = new List<Exception>();
|
|
var events = new List<(OrganizationUser, EventType, DateTime?)>();
|
|
foreach (var (invite, externalId) in invites)
|
|
{
|
|
// Prevent duplicate invitations
|
|
foreach (var email in invite.Emails.Distinct())
|
|
{
|
|
try
|
|
{
|
|
// Make sure user is not already invited
|
|
if (existingEmails.Contains(email))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var orgUser = new OrganizationUser
|
|
{
|
|
OrganizationId = organizationId,
|
|
UserId = null,
|
|
Email = email.ToLowerInvariant(),
|
|
Key = null,
|
|
Type = invite.Type.Value,
|
|
Status = OrganizationUserStatusType.Invited,
|
|
AccessSecretsManager = invite.AccessSecretsManager,
|
|
ExternalId = externalId,
|
|
CreationDate = DateTime.UtcNow,
|
|
RevisionDate = DateTime.UtcNow,
|
|
};
|
|
|
|
if (invite.Type == OrganizationUserType.Custom)
|
|
{
|
|
orgUser.SetPermissions(invite.Permissions ?? new Permissions());
|
|
}
|
|
|
|
if (invite.Collections.Any())
|
|
{
|
|
orgUsersWithCollections.Add((orgUser, invite.Collections));
|
|
}
|
|
else
|
|
{
|
|
orgUsersWithoutCollections.Add(orgUser);
|
|
}
|
|
|
|
if (invite.Groups != null && invite.Groups.Any())
|
|
{
|
|
orgUserGroups.Add((orgUser, invite.Groups));
|
|
}
|
|
|
|
events.Add((orgUser, EventType.OrganizationUser_Invited, DateTime.UtcNow));
|
|
orgUserInvitedCount++;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
exceptions.Add(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (exceptions.Any())
|
|
{
|
|
throw new AggregateException("One or more errors occurred while inviting users.", exceptions);
|
|
}
|
|
|
|
var allOrgUsers = orgUsersWithoutCollections
|
|
.Concat(orgUsersWithCollections.Select(u => u.Item1))
|
|
.ToList();
|
|
|
|
try
|
|
{
|
|
await _organizationUserRepository.CreateManyAsync(orgUsersWithoutCollections);
|
|
foreach (var (orgUser, collections) in orgUsersWithCollections)
|
|
{
|
|
await _organizationUserRepository.CreateAsync(orgUser, collections);
|
|
}
|
|
|
|
foreach (var (orgUser, groups) in orgUserGroups)
|
|
{
|
|
await _organizationUserRepository.UpdateGroupsAsync(orgUser.Id, groups);
|
|
}
|
|
|
|
if (!await _currentContext.ManageUsers(organization.Id))
|
|
{
|
|
throw new BadRequestException("Cannot add seats. Cannot manage organization users.");
|
|
}
|
|
|
|
await AutoAddSeatsAsync(organization, newSeatsRequired);
|
|
|
|
if (additionalSmSeatsRequired > 0)
|
|
{
|
|
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(smSubscriptionUpdate);
|
|
}
|
|
|
|
await SendInvitesAsync(allOrgUsers, organization);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// Revert any added users.
|
|
var invitedOrgUserIds = allOrgUsers.Select(ou => ou.Id);
|
|
await _organizationUserRepository.DeleteManyAsync(invitedOrgUserIds);
|
|
var currentOrganization = await _organizationRepository.GetByIdAsync(organization.Id);
|
|
|
|
// Revert autoscaling
|
|
// Do this first so that SmSeats never exceed PM seats (due to current billing requirements)
|
|
if (initialSmSeatCount.HasValue && currentOrganization.SmSeats.HasValue &&
|
|
currentOrganization.SmSeats.Value != initialSmSeatCount.Value)
|
|
{
|
|
var plan = await _pricingClient.GetPlanOrThrow(currentOrganization.PlanType);
|
|
var smSubscriptionUpdateRevert = new SecretsManagerSubscriptionUpdate(currentOrganization, plan, false)
|
|
{
|
|
SmSeats = initialSmSeatCount.Value
|
|
};
|
|
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(smSubscriptionUpdateRevert);
|
|
}
|
|
|
|
if (initialSeatCount.HasValue && currentOrganization.Seats.HasValue &&
|
|
currentOrganization.Seats.Value != initialSeatCount.Value)
|
|
{
|
|
await AdjustSeatsAsync(organization, initialSeatCount.Value - currentOrganization.Seats.Value);
|
|
}
|
|
|
|
exceptions.Add(e);
|
|
}
|
|
|
|
if (exceptions.Any())
|
|
{
|
|
throw new AggregateException("One or more errors occurred while inviting users.", exceptions);
|
|
}
|
|
|
|
return (allOrgUsers, events);
|
|
}
|
|
|
|
public async Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId,
|
|
Guid? invitingUserId,
|
|
IEnumerable<Guid> organizationUsersId)
|
|
{
|
|
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
|
|
_logger.LogUserInviteStateDiagnostics(orgUsers);
|
|
|
|
var org = await GetOrgById(organizationId);
|
|
|
|
var result = new List<Tuple<OrganizationUser, string>>();
|
|
foreach (var orgUser in orgUsers)
|
|
{
|
|
if (orgUser.Status != OrganizationUserStatusType.Invited || orgUser.OrganizationId != organizationId)
|
|
{
|
|
result.Add(Tuple.Create(orgUser, "User invalid."));
|
|
continue;
|
|
}
|
|
|
|
await SendInviteAsync(orgUser, org, false);
|
|
result.Add(Tuple.Create(orgUser, ""));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
private async Task SendInvitesAsync(IEnumerable<OrganizationUser> orgUsers, Organization organization) =>
|
|
await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(orgUsers, organization));
|
|
|
|
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization, bool initOrganization) =>
|
|
await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(
|
|
users: [orgUser],
|
|
organization: organization,
|
|
initOrganization: initOrganization));
|
|
|
|
public async Task<(bool canScale, string failureReason)> CanScaleAsync(
|
|
Organization organization,
|
|
int seatsToAdd)
|
|
{
|
|
var failureReason = "";
|
|
if (_globalSettings.SelfHosted)
|
|
{
|
|
failureReason = "Cannot autoscale on self-hosted instance.";
|
|
return (false, failureReason);
|
|
}
|
|
|
|
if (seatsToAdd < 1)
|
|
{
|
|
return (true, failureReason);
|
|
}
|
|
|
|
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
|
|
|
if (provider is { Enabled: true })
|
|
{
|
|
if (provider.IsBillable())
|
|
{
|
|
return (false, "Seat limit has been reached. Please contact your provider to add more seats.");
|
|
}
|
|
|
|
if (provider.Type == ProviderType.Reseller)
|
|
{
|
|
return (false, "Seat limit has been reached. Contact your provider to purchase additional seats.");
|
|
}
|
|
}
|
|
|
|
var subscription = await _paymentService.GetSubscriptionAsync(organization);
|
|
if (subscription?.Subscription?.Status == StripeConstants.SubscriptionStatus.Canceled)
|
|
{
|
|
return (false, "You do not have an active subscription. Reinstate your subscription to make changes");
|
|
}
|
|
|
|
if (organization.Seats.HasValue &&
|
|
organization.MaxAutoscaleSeats.HasValue &&
|
|
organization.MaxAutoscaleSeats.Value < organization.Seats.Value + seatsToAdd)
|
|
{
|
|
return (false, $"Seat limit has been reached.");
|
|
}
|
|
|
|
return (true, failureReason);
|
|
}
|
|
|
|
public async Task AutoAddSeatsAsync(Organization organization, int seatsToAdd)
|
|
{
|
|
if (seatsToAdd < 1 || !organization.Seats.HasValue)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var (canScale, failureMessage) = await CanScaleAsync(organization, seatsToAdd);
|
|
if (!canScale)
|
|
{
|
|
throw new BadRequestException(failureMessage);
|
|
}
|
|
|
|
var providerOrg = await this._providerOrganizationRepository.GetByOrganizationId(organization.Id);
|
|
|
|
IEnumerable<string> ownerEmails;
|
|
if (providerOrg != null)
|
|
{
|
|
ownerEmails =
|
|
(await _providerUserRepository.GetManyDetailsByProviderAsync(providerOrg.ProviderId,
|
|
ProviderUserStatusType.Confirmed))
|
|
.Select(u => u.Email).Distinct();
|
|
}
|
|
else
|
|
{
|
|
ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,
|
|
OrganizationUserType.Owner)).Select(u => u.Email).Distinct();
|
|
}
|
|
|
|
var initialSeatCount = organization.Seats.Value;
|
|
|
|
await AdjustSeatsAsync(organization, seatsToAdd, ownerEmails);
|
|
|
|
if (!organization.OwnersNotifiedOfAutoscaling.HasValue)
|
|
{
|
|
await _mailService.SendOrganizationAutoscaledEmailAsync(organization, initialSeatCount,
|
|
ownerEmails);
|
|
organization.OwnersNotifiedOfAutoscaling = DateTime.UtcNow;
|
|
await _organizationRepository.UpsertAsync(organization);
|
|
}
|
|
}
|
|
|
|
|
|
public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey,
|
|
Guid? callingUserId)
|
|
{
|
|
// Org User must be the same as the calling user and the organization ID associated with the user must match passed org ID
|
|
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
|
|
if (!callingUserId.HasValue || orgUser == null || orgUser.UserId != callingUserId.Value ||
|
|
orgUser.OrganizationId != organizationId)
|
|
{
|
|
throw new BadRequestException("User not valid.");
|
|
}
|
|
|
|
// Make sure the organization has the ability to use password reset
|
|
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
|
if (org == null || !org.UseResetPassword)
|
|
{
|
|
throw new BadRequestException("Organization does not allow password reset enrollment.");
|
|
}
|
|
|
|
// Make sure the organization has the policy enabled
|
|
var resetPasswordPolicy =
|
|
await _policyRepository.GetByOrganizationIdTypeAsync(organizationId, PolicyType.ResetPassword);
|
|
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
|
|
{
|
|
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
|
}
|
|
|
|
// Block the user from withdrawal if auto enrollment is enabled
|
|
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
|
{
|
|
var resetPasswordPolicyRequirement =
|
|
await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(userId);
|
|
if (resetPasswordKey == null && resetPasswordPolicyRequirement.AutoEnrollEnabled(organizationId))
|
|
{
|
|
throw new BadRequestException(
|
|
"Due to an Enterprise Policy, you are not allowed to withdraw from account recovery.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (resetPasswordKey == null && resetPasswordPolicy.Data != null)
|
|
{
|
|
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data,
|
|
JsonHelpers.IgnoreCase);
|
|
|
|
if (data?.AutoEnrollEnabled ?? false)
|
|
{
|
|
throw new BadRequestException(
|
|
"Due to an Enterprise Policy, you are not allowed to withdraw from account recovery.");
|
|
}
|
|
}
|
|
}
|
|
|
|
orgUser.ResetPasswordKey = resetPasswordKey;
|
|
await _organizationUserRepository.ReplaceAsync(orgUser);
|
|
await _eventService.LogOrganizationUserEventAsync(orgUser,
|
|
resetPasswordKey != null
|
|
? EventType.OrganizationUser_ResetPassword_Enroll
|
|
: EventType.OrganizationUser_ResetPassword_Withdraw);
|
|
}
|
|
|
|
|
|
public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId)
|
|
{
|
|
await _ssoUserRepository.DeleteAsync(userId, organizationId);
|
|
if (organizationId.HasValue)
|
|
{
|
|
var organizationUser =
|
|
await _organizationUserRepository.GetByOrganizationAsync(organizationId.Value, userId);
|
|
if (organizationUser != null)
|
|
{
|
|
await _eventService.LogOrganizationUserEventAsync(organizationUser,
|
|
EventType.OrganizationUser_UnlinkedSso);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
|
|
{
|
|
try
|
|
{
|
|
await _organizationRepository.ReplaceAsync(org);
|
|
await _applicationCacheService.UpsertOrganizationAbilityAsync(org);
|
|
|
|
if (orgEvent.HasValue)
|
|
{
|
|
await _eventService.LogOrganizationEventAsync(org, orgEvent.Value);
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogError(exception, "An error occurred while calling {Method} for Organization ({OrganizationID})", nameof(ReplaceAndUpdateCacheAsync), org.Id);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task<Organization> GetOrgById(Guid id)
|
|
{
|
|
return await _organizationRepository.GetByIdAsync(id);
|
|
}
|
|
|
|
private static void ValidatePlan(Models.StaticStore.Plan plan, int additionalSeats, string productType)
|
|
{
|
|
if (plan is null)
|
|
{
|
|
throw new BadRequestException($"{productType} Plan was null.");
|
|
}
|
|
|
|
if (plan.Disabled)
|
|
{
|
|
throw new BadRequestException($"{productType} Plan not found.");
|
|
}
|
|
|
|
if (additionalSeats < 0)
|
|
{
|
|
throw new BadRequestException($"You can't subtract {productType} seats!");
|
|
}
|
|
}
|
|
|
|
public void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
|
|
{
|
|
ValidatePlan(plan, upgrade.AdditionalSeats, "Password Manager");
|
|
|
|
if (plan.PasswordManager.BaseSeats + upgrade.AdditionalSeats <= 0)
|
|
{
|
|
throw new BadRequestException($"You do not have any Password Manager seats!");
|
|
}
|
|
|
|
if (upgrade.AdditionalSeats < 0)
|
|
{
|
|
throw new BadRequestException($"You can't subtract Password Manager seats!");
|
|
}
|
|
|
|
if (!plan.PasswordManager.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0)
|
|
{
|
|
throw new BadRequestException("Plan does not allow additional storage.");
|
|
}
|
|
|
|
if (upgrade.AdditionalStorageGb < 0)
|
|
{
|
|
throw new BadRequestException("You can't subtract storage!");
|
|
}
|
|
|
|
if (!plan.PasswordManager.HasPremiumAccessOption && upgrade.PremiumAccessAddon)
|
|
{
|
|
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
|
|
}
|
|
|
|
if (!plan.PasswordManager.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0)
|
|
{
|
|
throw new BadRequestException("Plan does not allow additional users.");
|
|
}
|
|
|
|
if (plan.PasswordManager.HasAdditionalSeatsOption && plan.PasswordManager.MaxAdditionalSeats.HasValue &&
|
|
upgrade.AdditionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value)
|
|
{
|
|
throw new BadRequestException($"Selected plan allows a maximum of " +
|
|
$"{plan.PasswordManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
|
}
|
|
}
|
|
|
|
public void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
|
|
{
|
|
if (plan.SupportsSecretsManager == false)
|
|
{
|
|
throw new BadRequestException("Invalid Secrets Manager plan selected.");
|
|
}
|
|
|
|
ValidatePlan(plan, upgrade.AdditionalSmSeats.GetValueOrDefault(), "Secrets Manager");
|
|
|
|
if (plan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats <= 0)
|
|
{
|
|
throw new BadRequestException($"You do not have any Secrets Manager seats!");
|
|
}
|
|
|
|
if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0)
|
|
{
|
|
throw new BadRequestException("Plan does not allow additional Machine Accounts.");
|
|
}
|
|
|
|
if ((plan.ProductTier == ProductTierType.TeamsStarter &&
|
|
upgrade.AdditionalSmSeats.GetValueOrDefault() > plan.PasswordManager.BaseSeats) ||
|
|
(plan.ProductTier != ProductTierType.TeamsStarter &&
|
|
upgrade.AdditionalSmSeats.GetValueOrDefault() > upgrade.AdditionalSeats))
|
|
{
|
|
throw new BadRequestException("You cannot have more Secrets Manager seats than Password Manager seats.");
|
|
}
|
|
|
|
if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0)
|
|
{
|
|
throw new BadRequestException("You can't subtract Machine Accounts!");
|
|
}
|
|
|
|
switch (plan.SecretsManager.HasAdditionalSeatsOption)
|
|
{
|
|
case false when upgrade.AdditionalSmSeats > 0:
|
|
throw new BadRequestException("Plan does not allow additional users.");
|
|
case true when plan.SecretsManager.MaxAdditionalSeats.HasValue &&
|
|
upgrade.AdditionalSmSeats > plan.SecretsManager.MaxAdditionalSeats.Value:
|
|
throw new BadRequestException($"Selected plan allows a maximum of " +
|
|
$"{plan.SecretsManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
|
}
|
|
}
|
|
|
|
public async Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType,
|
|
OrganizationUserType? oldType, Permissions permissions)
|
|
{
|
|
if (await _currentContext.OrganizationOwner(organizationId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (oldType == OrganizationUserType.Owner || newType == OrganizationUserType.Owner)
|
|
{
|
|
throw new BadRequestException("Only an Owner can configure another Owner's account.");
|
|
}
|
|
|
|
if (await _currentContext.OrganizationAdmin(organizationId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!await _currentContext.ManageUsers(organizationId))
|
|
{
|
|
throw new BadRequestException("Your account does not have permission to manage users.");
|
|
}
|
|
|
|
if (oldType == OrganizationUserType.Admin || newType == OrganizationUserType.Admin)
|
|
{
|
|
throw new BadRequestException("Custom users can not manage Admins or Owners.");
|
|
}
|
|
|
|
if (newType == OrganizationUserType.Custom &&
|
|
!await ValidateCustomPermissionsGrant(organizationId, permissions))
|
|
{
|
|
throw new BadRequestException("Custom users can only grant the same custom permissions that they have.");
|
|
}
|
|
}
|
|
|
|
public async Task ValidateOrganizationCustomPermissionsEnabledAsync(Guid organizationId,
|
|
OrganizationUserType newType)
|
|
{
|
|
if (newType != OrganizationUserType.Custom)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
|
if (organization == null)
|
|
{
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
if (!organization.UseCustomPermissions)
|
|
{
|
|
throw new BadRequestException(
|
|
"To enable custom permissions the organization must be on an Enterprise plan.");
|
|
}
|
|
}
|
|
|
|
private async Task<bool> ValidateCustomPermissionsGrant(Guid organizationId, Permissions permissions)
|
|
{
|
|
if (permissions == null || await _currentContext.OrganizationAdmin(organizationId))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (permissions.ManageUsers && !await _currentContext.ManageUsers(organizationId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (permissions.AccessReports && !await _currentContext.AccessReports(organizationId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (permissions.ManageGroups && !await _currentContext.ManageGroups(organizationId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (permissions.ManagePolicies && !await _currentContext.ManagePolicies(organizationId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (permissions.ManageScim && !await _currentContext.ManageScim(organizationId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (permissions.ManageSso && !await _currentContext.ManageSso(organizationId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (permissions.AccessEventLogs && !await _currentContext.AccessEventLogs(organizationId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (permissions.AccessImportExport && !await _currentContext.AccessImportExport(organizationId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (permissions.EditAnyCollection && !await _currentContext.EditAnyCollection(organizationId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (permissions.ManageResetPassword && !await _currentContext.ManageResetPassword(organizationId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var org = _currentContext.GetOrganization(organizationId);
|
|
if (org == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (permissions.CreateNewCollections && !org.Permissions.CreateNewCollections)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (permissions.DeleteAnyCollection && !org.Permissions.DeleteAnyCollection)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
|
|
{
|
|
// Determine status to revert back to
|
|
var status = OrganizationUserStatusType.Invited;
|
|
if (organizationUser.UserId.HasValue && string.IsNullOrWhiteSpace(organizationUser.Email))
|
|
{
|
|
// Has UserId & Email is null, then Accepted
|
|
status = OrganizationUserStatusType.Accepted;
|
|
if (!string.IsNullOrWhiteSpace(organizationUser.Key))
|
|
{
|
|
// We have an org key for this user, user was confirmed
|
|
status = OrganizationUserStatusType.Confirmed;
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
private List<Func<Task>> CreateCollectionManagementLoggingActions(
|
|
Organization existingOrganization, OrganizationCollectionManagementSettings settings)
|
|
{
|
|
var loggingActions = new List<Func<Task>>();
|
|
|
|
if (existingOrganization.LimitCollectionCreation != settings.LimitCollectionCreation)
|
|
{
|
|
var eventType = settings.LimitCollectionCreation
|
|
? EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled
|
|
: EventType.Organization_CollectionManagement_LimitCollectionCreationDisabled;
|
|
loggingActions.Add(() => _eventService.LogOrganizationEventAsync(existingOrganization, eventType));
|
|
}
|
|
|
|
if (existingOrganization.LimitCollectionDeletion != settings.LimitCollectionDeletion)
|
|
{
|
|
var eventType = settings.LimitCollectionDeletion
|
|
? EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled
|
|
: EventType.Organization_CollectionManagement_LimitCollectionDeletionDisabled;
|
|
loggingActions.Add(() => _eventService.LogOrganizationEventAsync(existingOrganization, eventType));
|
|
}
|
|
|
|
if (existingOrganization.LimitItemDeletion != settings.LimitItemDeletion)
|
|
{
|
|
var eventType = settings.LimitItemDeletion
|
|
? EventType.Organization_CollectionManagement_LimitItemDeletionEnabled
|
|
: EventType.Organization_CollectionManagement_LimitItemDeletionDisabled;
|
|
loggingActions.Add(() => _eventService.LogOrganizationEventAsync(existingOrganization, eventType));
|
|
}
|
|
|
|
if (existingOrganization.AllowAdminAccessToAllCollectionItems != settings.AllowAdminAccessToAllCollectionItems)
|
|
{
|
|
var eventType = settings.AllowAdminAccessToAllCollectionItems
|
|
? EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled
|
|
: EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsDisabled;
|
|
loggingActions.Add(() => _eventService.LogOrganizationEventAsync(existingOrganization, eventType));
|
|
}
|
|
|
|
return loggingActions;
|
|
}
|
|
}
|