mirror of
https://github.com/bitwarden/server
synced 2025-12-29 14:43:39 +00:00
Merge branch 'km/pm-10600' into km/pm-10600-full-notification-content
# Conflicts: # src/Core/Services/Implementations/MultiServicePushNotificationService.cs
This commit is contained in:
@@ -96,18 +96,6 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
/// </summary>
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
// Deprecated by https://bitwarden.atlassian.net/browse/PM-10863. This
|
||||
// was replaced with `LimitCollectionCreation` and
|
||||
// `LimitCollectionDeletion`.
|
||||
public bool LimitCollectionCreationDeletion
|
||||
{
|
||||
get => LimitCollectionCreation || LimitCollectionDeletion;
|
||||
set
|
||||
{
|
||||
LimitCollectionCreation = value;
|
||||
LimitCollectionDeletion = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, admins, owners, and some custom users can read/write all collections and items in the Admin Console.
|
||||
@@ -115,6 +103,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
/// </summary>
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Risk Insights is a reporting feature that provides insights into the security of an organization's vault.
|
||||
/// </summary>
|
||||
public bool UseRiskInsights { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
if (Id == default(Guid))
|
||||
@@ -314,11 +307,5 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
UseSecretsManager = license.UseSecretsManager;
|
||||
SmSeats = license.SmSeats;
|
||||
SmServiceAccounts = license.SmServiceAccounts;
|
||||
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||
{
|
||||
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
public enum EventSystemUser : byte
|
||||
{
|
||||
Unknown = 0,
|
||||
SCIM = 1,
|
||||
DomainVerification = 2,
|
||||
PublicApi = 3,
|
||||
TwoFactorDisabled = 4,
|
||||
}
|
||||
|
||||
10
src/Core/AdminConsole/Models/Data/IActingUser.cs
Normal file
10
src/Core/AdminConsole/Models/Data/IActingUser.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data;
|
||||
|
||||
public interface IActingUser
|
||||
{
|
||||
Guid? UserId { get; }
|
||||
bool IsOrganizationOwnerOrProvider { get; }
|
||||
EventSystemUser? SystemUserType { get; }
|
||||
}
|
||||
@@ -23,9 +23,8 @@ public class OrganizationAbility
|
||||
UsePolicies = organization.UsePolicies;
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@@ -42,7 +41,6 @@ public class OrganizationAbility
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ public class OrganizationUserOrganizationDetails
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
}
|
||||
|
||||
@@ -146,8 +146,6 @@ public class SelfHostedOrganizationDetails : Organization
|
||||
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
|
||||
LimitCollectionCreation = LimitCollectionCreation,
|
||||
LimitCollectionDeletion = LimitCollectionDeletion,
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
|
||||
Status = Status
|
||||
};
|
||||
|
||||
@@ -42,6 +42,6 @@ public class ProviderUserOrganizationDetails
|
||||
public PlanType PlanType { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
}
|
||||
|
||||
16
src/Core/AdminConsole/Models/Data/StandardUser.cs
Normal file
16
src/Core/AdminConsole/Models/Data/StandardUser.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data;
|
||||
|
||||
public class StandardUser : IActingUser
|
||||
{
|
||||
public StandardUser(Guid userId, bool isOrganizationOwner)
|
||||
{
|
||||
UserId = userId;
|
||||
IsOrganizationOwnerOrProvider = isOrganizationOwner;
|
||||
}
|
||||
|
||||
public Guid? UserId { get; }
|
||||
public bool IsOrganizationOwnerOrProvider { get; }
|
||||
public EventSystemUser? SystemUserType => throw new Exception($"{nameof(StandardUser)} does not have a {nameof(SystemUserType)}");
|
||||
}
|
||||
16
src/Core/AdminConsole/Models/Data/SystemUser.cs
Normal file
16
src/Core/AdminConsole/Models/Data/SystemUser.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data;
|
||||
|
||||
public class SystemUser : IActingUser
|
||||
{
|
||||
public SystemUser(EventSystemUser systemUser)
|
||||
{
|
||||
SystemUserType = systemUser;
|
||||
}
|
||||
|
||||
public Guid? UserId => throw new Exception($"{nameof(SystemUserType)} does not have a {nameof(UserId)}.");
|
||||
|
||||
public bool IsOrganizationOwnerOrProvider => false;
|
||||
public EventSystemUser? SystemUserType { get; }
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -12,124 +15,145 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
|
||||
public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
public class VerifyOrganizationDomainCommand(
|
||||
IOrganizationDomainRepository organizationDomainRepository,
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
IFeatureService featureService,
|
||||
ICurrentContext currentContext,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IMailService mailService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ILogger<VerifyOrganizationDomainCommand> logger)
|
||||
: IVerifyOrganizationDomainCommand
|
||||
{
|
||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||
private readonly IDnsResolverService _dnsResolverService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
||||
|
||||
public VerifyOrganizationDomainCommand(
|
||||
IOrganizationDomainRepository organizationDomainRepository,
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
ILogger<VerifyOrganizationDomainCommand> logger)
|
||||
{
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
_dnsResolverService = dnsResolverService;
|
||||
_eventService = eventService;
|
||||
_globalSettings = globalSettings;
|
||||
_policyService = policyService;
|
||||
_featureService = featureService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public async Task<OrganizationDomain> UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)
|
||||
{
|
||||
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain);
|
||||
if (currentContext.UserId is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(UserVerifyOrganizationDomainAsync)} can only be called by a user. " +
|
||||
$"Please call {nameof(SystemVerifyOrganizationDomainAsync)} for system users.");
|
||||
}
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
var actingUser = new StandardUser(currentContext.UserId.Value, await currentContext.OrganizationOwner(organizationDomain.OrganizationId));
|
||||
|
||||
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain, actingUser);
|
||||
|
||||
await eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
domainVerificationResult.VerifiedDate != null
|
||||
? EventType.OrganizationDomain_Verified
|
||||
: EventType.OrganizationDomain_NotVerified);
|
||||
|
||||
await _organizationDomainRepository.ReplaceAsync(domainVerificationResult);
|
||||
await organizationDomainRepository.ReplaceAsync(domainVerificationResult);
|
||||
|
||||
return domainVerificationResult;
|
||||
}
|
||||
|
||||
public async Task<OrganizationDomain> SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)
|
||||
{
|
||||
var actingUser = new SystemUser(EventSystemUser.DomainVerification);
|
||||
|
||||
organizationDomain.SetJobRunCount();
|
||||
|
||||
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain);
|
||||
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain, actingUser);
|
||||
|
||||
if (domainVerificationResult.VerifiedDate is not null)
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain");
|
||||
logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain");
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
await eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
EventType.OrganizationDomain_Verified,
|
||||
EventSystemUser.DomainVerification);
|
||||
}
|
||||
else
|
||||
{
|
||||
domainVerificationResult.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
||||
domainVerificationResult.SetNextRunDate(globalSettings.DomainVerification.VerificationInterval);
|
||||
|
||||
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
await eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||
EventType.OrganizationDomain_NotVerified,
|
||||
EventSystemUser.DomainVerification);
|
||||
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
"Verification for organization {OrgId} with domain {Domain} failed",
|
||||
domainVerificationResult.OrganizationId, domainVerificationResult.DomainName);
|
||||
}
|
||||
|
||||
await _organizationDomainRepository.ReplaceAsync(domainVerificationResult);
|
||||
await organizationDomainRepository.ReplaceAsync(domainVerificationResult);
|
||||
|
||||
return domainVerificationResult;
|
||||
}
|
||||
|
||||
private async Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain domain)
|
||||
private async Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain domain, IActingUser actingUser)
|
||||
{
|
||||
domain.SetLastCheckedDate();
|
||||
|
||||
if (domain.VerifiedDate is not null)
|
||||
{
|
||||
await _organizationDomainRepository.ReplaceAsync(domain);
|
||||
await organizationDomainRepository.ReplaceAsync(domain);
|
||||
throw new ConflictException("Domain has already been verified.");
|
||||
}
|
||||
|
||||
var claimedDomain =
|
||||
await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName);
|
||||
await organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName);
|
||||
|
||||
if (claimedDomain.Count > 0)
|
||||
{
|
||||
await _organizationDomainRepository.ReplaceAsync(domain);
|
||||
await organizationDomainRepository.ReplaceAsync(domain);
|
||||
throw new ConflictException("The domain is not available to be claimed.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt))
|
||||
if (await dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt))
|
||||
{
|
||||
domain.SetVerifiedDate();
|
||||
|
||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId);
|
||||
await DomainVerificationSideEffectsAsync(domain, actingUser);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("Error verifying Organization domain: {domain}. {errorMessage}",
|
||||
logger.LogError("Error verifying Organization domain: {domain}. {errorMessage}",
|
||||
domain.DomainName, e.Message);
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId)
|
||||
private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
await _policyService.SaveAsync(
|
||||
new Policy { OrganizationId = organizationId, Type = PolicyType.SingleOrg, Enabled = true }, null);
|
||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
|
||||
await SendVerifiedDomainUserEmailAsync(domain);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) =>
|
||||
await savePolicyCommand.SaveAsync(
|
||||
new PolicyUpdate
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Type = PolicyType.SingleOrg,
|
||||
Enabled = true,
|
||||
PerformedBy = actingUser
|
||||
});
|
||||
|
||||
private async Task SendVerifiedDomainUserEmailAsync(OrganizationDomain domain)
|
||||
{
|
||||
var orgUserUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(domain.OrganizationId);
|
||||
|
||||
var domainUserEmails = orgUserUsers
|
||||
.Where(ou => ou.Email.ToLower().EndsWith($"@{domain.DomainName.ToLower()}") &&
|
||||
ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Status != OrganizationUserStatusType.Invited)
|
||||
.Select(ou => ou.Email);
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(domain.OrganizationId);
|
||||
|
||||
await mailService.SendClaimedDomainUserEmailAsync(new ManagedUserDomainClaimedEmails(domainUserEmails, organization));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@@ -19,7 +23,10 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly IPushNotificationService _pushService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
public DeleteManagedOrganizationUserAccountCommand(
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
@@ -27,7 +34,11 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IUserRepository userRepository,
|
||||
ICurrentContext currentContext,
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||
IReferenceEventService referenceEventService,
|
||||
IPushNotificationService pushService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderUserRepository providerUserRepository)
|
||||
{
|
||||
_userService = userService;
|
||||
_eventService = eventService;
|
||||
@@ -36,6 +47,10 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
||||
_userRepository = userRepository;
|
||||
_currentContext = currentContext;
|
||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||
_referenceEventService = referenceEventService;
|
||||
_pushService = pushService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
}
|
||||
|
||||
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
|
||||
@@ -89,7 +104,8 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
||||
throw new NotFoundException("Member not found.");
|
||||
}
|
||||
|
||||
await _userService.DeleteAsync(user);
|
||||
await ValidateUserMembershipAndPremiumAsync(user);
|
||||
|
||||
results.Add((orgUserId, string.Empty));
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -98,6 +114,15 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
||||
}
|
||||
}
|
||||
|
||||
var orgUserResultsToDelete = results.Where(result => string.IsNullOrEmpty(result.ErrorMessage));
|
||||
var orgUsersToDelete = orgUsers.Where(orgUser => orgUserResultsToDelete.Any(result => orgUser.Id == result.OrganizationUserId));
|
||||
var usersToDelete = users.Where(user => orgUsersToDelete.Any(orgUser => orgUser.UserId == user.Id));
|
||||
|
||||
if (usersToDelete.Any())
|
||||
{
|
||||
await DeleteManyAsync(usersToDelete);
|
||||
}
|
||||
|
||||
await LogDeletedOrganizationUsersAsync(orgUsers, results);
|
||||
|
||||
return results;
|
||||
@@ -158,4 +183,59 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
||||
await _eventService.LogOrganizationUserEventsAsync(events);
|
||||
}
|
||||
}
|
||||
private async Task DeleteManyAsync(IEnumerable<User> users)
|
||||
{
|
||||
|
||||
await _userRepository.DeleteManyAsync(users);
|
||||
foreach (var user in users)
|
||||
{
|
||||
await _referenceEventService.RaiseEventAsync(
|
||||
new ReferenceEvent(ReferenceEventType.DeleteAccount, user, _currentContext));
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task ValidateUserMembershipAndPremiumAsync(User user)
|
||||
{
|
||||
// Check if user is the only owner of any organizations.
|
||||
var onlyOwnerCount = await _organizationUserRepository.GetCountByOnlyOwnerAsync(user.Id);
|
||||
if (onlyOwnerCount > 0)
|
||||
{
|
||||
throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.");
|
||||
}
|
||||
|
||||
var orgs = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed);
|
||||
if (orgs.Count == 1)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(orgs.First().OrganizationId);
|
||||
if (org != null && (!org.Enabled || string.IsNullOrWhiteSpace(org.GatewaySubscriptionId)))
|
||||
{
|
||||
var orgCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(org.Id);
|
||||
if (orgCount <= 1)
|
||||
{
|
||||
await _organizationRepository.DeleteAsync(org);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var onlyOwnerProviderCount = await _providerUserRepository.GetCountByOnlyOwnerAsync(user.Id);
|
||||
if (onlyOwnerProviderCount > 0)
|
||||
{
|
||||
throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(user.GatewaySubscriptionId))
|
||||
{
|
||||
try
|
||||
{
|
||||
await _userService.CancelPremiumAsync(user);
|
||||
}
|
||||
catch (GatewayException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,60 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
|
||||
public interface IRemoveOrganizationUserCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Removes a user from an organization.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization.</param>
|
||||
/// <param name="userId">The ID of the user to remove.</param>
|
||||
Task RemoveUserAsync(Guid organizationId, Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a user from an organization with a specified deleting user.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization.</param>
|
||||
/// <param name="organizationUserId">The ID of the organization user to remove.</param>
|
||||
/// <param name="deletingUserId">The ID of the user performing the removal operation.</param>
|
||||
Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a user from an organization using a system user.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization.</param>
|
||||
/// <param name="organizationUserId">The ID of the organization user to remove.</param>
|
||||
/// <param name="eventSystemUser">The system user performing the removal operation.</param>
|
||||
Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser);
|
||||
Task RemoveUserAsync(Guid organizationId, Guid userId);
|
||||
Task<List<Tuple<OrganizationUser, string>>> RemoveUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? deletingUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Removes multiple users from an organization with a specified deleting user.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization.</param>
|
||||
/// <param name="organizationUserIds">The collection of organization user IDs to remove.</param>
|
||||
/// <param name="deletingUserId">The ID of the user performing the removal operation.</param>
|
||||
/// <returns>
|
||||
/// A list of tuples containing the organization user id and the error message for each user that could not be removed, otherwise empty.
|
||||
/// </returns>
|
||||
Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(
|
||||
Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? deletingUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Removes multiple users from an organization using a system user.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization.</param>
|
||||
/// <param name="organizationUserIds">The collection of organization user IDs to remove.</param>
|
||||
/// <param name="eventSystemUser">The system user performing the removal operation.</param>
|
||||
/// <returns>
|
||||
/// A list of tuples containing the organization user id and the error message for each user that could not be removed, otherwise empty.
|
||||
/// </returns>
|
||||
Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(
|
||||
Guid organizationId, IEnumerable<Guid> organizationUserIds, EventSystemUser eventSystemUser);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a user from an organization when they have left voluntarily. This should only be called by the same user who is being removed.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">Organization to leave.</param>
|
||||
/// <param name="userId">User to leave.</param>
|
||||
Task UserLeaveAsync(Guid organizationId, Guid userId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.Models.Commands;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
|
||||
public interface IRevokeNonCompliantOrganizationUserCommand
|
||||
{
|
||||
Task<CommandResult> RevokeNonCompliantOrganizationUsersAsync(RevokeOrganizationUsersRequest request);
|
||||
}
|
||||
@@ -4,5 +4,5 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interface
|
||||
|
||||
public interface IUpdateOrganizationUserGroupsCommand
|
||||
{
|
||||
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId);
|
||||
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
||||
private readonly IPushRegistrationService _pushRegistrationService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public const string UserNotFoundErrorMessage = "User not found.";
|
||||
public const string UsersInvalidErrorMessage = "Users invalid.";
|
||||
public const string RemoveYourselfErrorMessage = "You cannot remove yourself.";
|
||||
public const string RemoveOwnerByNonOwnerErrorMessage = "Only owners can delete other owners.";
|
||||
public const string RemoveLastConfirmedOwnerErrorMessage = "Organization must have at least one confirmed owner.";
|
||||
public const string RemoveClaimedAccountErrorMessage = "Cannot remove member accounts claimed by the organization. To offboard a member, revoke or delete the account.";
|
||||
|
||||
public RemoveOrganizationUserCommand(
|
||||
IDeviceRepository deviceRepository,
|
||||
@@ -25,7 +35,10 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
||||
IPushNotificationService pushNotificationService,
|
||||
IPushRegistrationService pushRegistrationService,
|
||||
ICurrentContext currentContext,
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
|
||||
IFeatureService featureService,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_deviceRepository = deviceRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -34,14 +47,27 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
||||
_pushRegistrationService = pushRegistrationService;
|
||||
_currentContext = currentContext;
|
||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
|
||||
_featureService = featureService;
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
public async Task RemoveUserAsync(Guid organizationId, Guid userId)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
|
||||
ValidateRemoveUser(organizationId, organizationUser);
|
||||
|
||||
await RepositoryRemoveUserAsync(organizationUser, deletingUserId: null, eventSystemUser: null);
|
||||
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
|
||||
}
|
||||
|
||||
public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||
ValidateDeleteUser(organizationId, organizationUser);
|
||||
ValidateRemoveUser(organizationId, organizationUser);
|
||||
|
||||
await RepositoryDeleteUserAsync(organizationUser, deletingUserId);
|
||||
await RepositoryRemoveUserAsync(organizationUser, deletingUserId, eventSystemUser: null);
|
||||
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
|
||||
}
|
||||
@@ -49,108 +75,89 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
||||
public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||
ValidateDeleteUser(organizationId, organizationUser);
|
||||
ValidateRemoveUser(organizationId, organizationUser);
|
||||
|
||||
await RepositoryDeleteUserAsync(organizationUser, null);
|
||||
await RepositoryRemoveUserAsync(organizationUser, deletingUserId: null, eventSystemUser);
|
||||
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser);
|
||||
}
|
||||
|
||||
public async Task RemoveUserAsync(Guid organizationId, Guid userId)
|
||||
public async Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(
|
||||
Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? deletingUserId)
|
||||
{
|
||||
var result = await RemoveUsersInternalAsync(organizationId, organizationUserIds, deletingUserId, eventSystemUser: null);
|
||||
|
||||
var removedUsers = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList();
|
||||
if (removedUsers.Any())
|
||||
{
|
||||
DateTime? eventDate = _timeProvider.GetUtcNow().UtcDateTime;
|
||||
await _eventService.LogOrganizationUserEventsAsync(
|
||||
removedUsers.Select(ou => (ou, EventType.OrganizationUser_Removed, eventDate)));
|
||||
}
|
||||
|
||||
return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(
|
||||
Guid organizationId, IEnumerable<Guid> organizationUserIds, EventSystemUser eventSystemUser)
|
||||
{
|
||||
var result = await RemoveUsersInternalAsync(organizationId, organizationUserIds, deletingUserId: null, eventSystemUser);
|
||||
|
||||
var removedUsers = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList();
|
||||
if (removedUsers.Any())
|
||||
{
|
||||
DateTime? eventDate = _timeProvider.GetUtcNow().UtcDateTime;
|
||||
await _eventService.LogOrganizationUserEventsAsync(
|
||||
removedUsers.Select(ou => (ou, EventType.OrganizationUser_Removed, eventSystemUser, eventDate)));
|
||||
}
|
||||
|
||||
return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage));
|
||||
}
|
||||
|
||||
public async Task UserLeaveAsync(Guid organizationId, Guid userId)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
|
||||
ValidateDeleteUser(organizationId, organizationUser);
|
||||
ValidateRemoveUser(organizationId, organizationUser);
|
||||
|
||||
await RepositoryDeleteUserAsync(organizationUser, null);
|
||||
await RepositoryRemoveUserAsync(organizationUser, deletingUserId: null, eventSystemUser: null);
|
||||
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Left);
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<OrganizationUser, string>>> RemoveUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUsersId,
|
||||
Guid? deletingUserId)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
|
||||
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
|
||||
.ToList();
|
||||
|
||||
if (!filteredUsers.Any())
|
||||
{
|
||||
throw new BadRequestException("Users invalid.");
|
||||
}
|
||||
|
||||
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId))
|
||||
{
|
||||
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
||||
}
|
||||
|
||||
var deletingUserIsOwner = false;
|
||||
if (deletingUserId.HasValue)
|
||||
{
|
||||
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
|
||||
}
|
||||
|
||||
var result = new List<Tuple<OrganizationUser, string>>();
|
||||
var deletedUserIds = new List<Guid>();
|
||||
foreach (var orgUser in filteredUsers)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId)
|
||||
{
|
||||
throw new BadRequestException("You cannot remove yourself.");
|
||||
}
|
||||
|
||||
if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner)
|
||||
{
|
||||
throw new BadRequestException("Only owners can delete other owners.");
|
||||
}
|
||||
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
|
||||
|
||||
if (orgUser.UserId.HasValue)
|
||||
{
|
||||
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
|
||||
}
|
||||
result.Add(Tuple.Create(orgUser, ""));
|
||||
deletedUserIds.Add(orgUser.Id);
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
result.Add(Tuple.Create(orgUser, e.Message));
|
||||
}
|
||||
|
||||
await _organizationUserRepository.DeleteManyAsync(deletedUserIds);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ValidateDeleteUser(Guid organizationId, OrganizationUser orgUser)
|
||||
private void ValidateRemoveUser(Guid organizationId, OrganizationUser orgUser)
|
||||
{
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException("User not found.");
|
||||
throw new NotFoundException(UserNotFoundErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RepositoryDeleteUserAsync(OrganizationUser orgUser, Guid? deletingUserId)
|
||||
private async Task RepositoryRemoveUserAsync(OrganizationUser orgUser, Guid? deletingUserId, EventSystemUser? eventSystemUser)
|
||||
{
|
||||
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value)
|
||||
{
|
||||
throw new BadRequestException("You cannot remove yourself.");
|
||||
throw new BadRequestException(RemoveYourselfErrorMessage);
|
||||
}
|
||||
|
||||
if (orgUser.Type == OrganizationUserType.Owner)
|
||||
{
|
||||
if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(orgUser.OrganizationId))
|
||||
{
|
||||
throw new BadRequestException("Only owners can delete other owners.");
|
||||
throw new BadRequestException(RemoveOwnerByNonOwnerErrorMessage);
|
||||
}
|
||||
|
||||
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, new[] { orgUser.Id }, includeProvider: true))
|
||||
{
|
||||
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
||||
throw new BadRequestException(RemoveLastConfirmedOwnerErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null)
|
||||
{
|
||||
var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id });
|
||||
if (managementStatus.TryGetValue(orgUser.Id, out var isManaged) && isManaged)
|
||||
{
|
||||
throw new BadRequestException(RemoveClaimedAccountErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,4 +184,70 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
||||
organizationId.ToString());
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(userId);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<(OrganizationUser OrganizationUser, string ErrorMessage)>> RemoveUsersInternalAsync(
|
||||
Guid organizationId, IEnumerable<Guid> organizationUsersId, Guid? deletingUserId, EventSystemUser? eventSystemUser)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
|
||||
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId).ToList();
|
||||
|
||||
if (!filteredUsers.Any())
|
||||
{
|
||||
throw new BadRequestException(UsersInvalidErrorMessage);
|
||||
}
|
||||
|
||||
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId))
|
||||
{
|
||||
throw new BadRequestException(RemoveLastConfirmedOwnerErrorMessage);
|
||||
}
|
||||
|
||||
var deletingUserIsOwner = false;
|
||||
if (deletingUserId.HasValue)
|
||||
{
|
||||
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
|
||||
}
|
||||
|
||||
var managementStatus = _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null
|
||||
? await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, filteredUsers.Select(u => u.Id))
|
||||
: filteredUsers.ToDictionary(u => u.Id, u => false);
|
||||
var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>();
|
||||
foreach (var orgUser in filteredUsers)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId)
|
||||
{
|
||||
throw new BadRequestException(RemoveYourselfErrorMessage);
|
||||
}
|
||||
|
||||
if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner)
|
||||
{
|
||||
throw new BadRequestException(RemoveOwnerByNonOwnerErrorMessage);
|
||||
}
|
||||
|
||||
if (managementStatus.TryGetValue(orgUser.Id, out var isManaged) && isManaged)
|
||||
{
|
||||
throw new BadRequestException(RemoveClaimedAccountErrorMessage);
|
||||
}
|
||||
|
||||
result.Add((orgUser, string.Empty));
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
result.Add((orgUser, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
var organizationUsersToRemove = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList();
|
||||
if (organizationUsersToRemove.Any())
|
||||
{
|
||||
await _organizationUserRepository.DeleteManyAsync(organizationUsersToRemove.Select(ou => ou.Id));
|
||||
foreach (var orgUser in organizationUsersToRemove.Where(ou => ou.UserId.HasValue))
|
||||
{
|
||||
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId!.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
|
||||
public record RevokeOrganizationUsersRequest(
|
||||
Guid OrganizationId,
|
||||
IEnumerable<OrganizationUserUserDetails> OrganizationUsers,
|
||||
IActingUser ActionPerformedBy)
|
||||
{
|
||||
public RevokeOrganizationUsersRequest(Guid organizationId, OrganizationUserUserDetails organizationUser, IActingUser actionPerformedBy)
|
||||
: this(organizationId, [organizationUser], actionPerformedBy) { }
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Commands;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
public class RevokeNonCompliantOrganizationUserCommand(IOrganizationUserRepository organizationUserRepository,
|
||||
IEventService eventService,
|
||||
IHasConfirmedOwnersExceptQuery confirmedOwnersExceptQuery,
|
||||
TimeProvider timeProvider) : IRevokeNonCompliantOrganizationUserCommand
|
||||
{
|
||||
public const string ErrorCannotRevokeSelf = "You cannot revoke yourself.";
|
||||
public const string ErrorOnlyOwnersCanRevokeOtherOwners = "Only owners can revoke other owners.";
|
||||
public const string ErrorUserAlreadyRevoked = "User is already revoked.";
|
||||
public const string ErrorOrgMustHaveAtLeastOneOwner = "Organization must have at least one confirmed owner.";
|
||||
public const string ErrorInvalidUsers = "Invalid users.";
|
||||
public const string ErrorRequestedByWasNotValid = "Action was performed by an unexpected type.";
|
||||
|
||||
public async Task<CommandResult> RevokeNonCompliantOrganizationUsersAsync(RevokeOrganizationUsersRequest request)
|
||||
{
|
||||
var validationResult = await ValidateAsync(request);
|
||||
|
||||
if (validationResult.HasErrors)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
await organizationUserRepository.RevokeManyByIdAsync(request.OrganizationUsers.Select(x => x.Id));
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
switch (request.ActionPerformedBy)
|
||||
{
|
||||
case StandardUser:
|
||||
await eventService.LogOrganizationUserEventsAsync(
|
||||
request.OrganizationUsers.Select(x => GetRevokedUserEventTuple(x, now)));
|
||||
break;
|
||||
case SystemUser { SystemUserType: not null } loggableSystem:
|
||||
await eventService.LogOrganizationUserEventsAsync(
|
||||
request.OrganizationUsers.Select(x =>
|
||||
GetRevokedUserEventBySystemUserTuple(x, loggableSystem.SystemUserType.Value, now)));
|
||||
break;
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
private static (OrganizationUserUserDetails organizationUser, EventType eventType, DateTime? time) GetRevokedUserEventTuple(
|
||||
OrganizationUserUserDetails organizationUser, DateTimeOffset dateTimeOffset) =>
|
||||
new(organizationUser, EventType.OrganizationUser_Revoked, dateTimeOffset.UtcDateTime);
|
||||
|
||||
private static (OrganizationUserUserDetails organizationUser, EventType eventType, EventSystemUser eventSystemUser, DateTime? time) GetRevokedUserEventBySystemUserTuple(
|
||||
OrganizationUserUserDetails organizationUser, EventSystemUser systemUser, DateTimeOffset dateTimeOffset) => new(organizationUser,
|
||||
EventType.OrganizationUser_Revoked, systemUser, dateTimeOffset.UtcDateTime);
|
||||
|
||||
private async Task<CommandResult> ValidateAsync(RevokeOrganizationUsersRequest request)
|
||||
{
|
||||
if (!PerformedByIsAnExpectedType(request.ActionPerformedBy))
|
||||
{
|
||||
return new CommandResult(ErrorRequestedByWasNotValid);
|
||||
}
|
||||
|
||||
if (request.ActionPerformedBy is StandardUser user
|
||||
&& request.OrganizationUsers.Any(x => x.UserId == user.UserId))
|
||||
{
|
||||
return new CommandResult(ErrorCannotRevokeSelf);
|
||||
}
|
||||
|
||||
if (request.OrganizationUsers.Any(x => x.OrganizationId != request.OrganizationId))
|
||||
{
|
||||
return new CommandResult(ErrorInvalidUsers);
|
||||
}
|
||||
|
||||
if (!await confirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
|
||||
request.OrganizationId,
|
||||
request.OrganizationUsers.Select(x => x.Id)))
|
||||
{
|
||||
return new CommandResult(ErrorOrgMustHaveAtLeastOneOwner);
|
||||
}
|
||||
|
||||
return request.OrganizationUsers.Aggregate(new CommandResult(), (result, userToRevoke) =>
|
||||
{
|
||||
if (IsAlreadyRevoked(userToRevoke))
|
||||
{
|
||||
result.ErrorMessages.Add($"{ErrorUserAlreadyRevoked} Id: {userToRevoke.Id}");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (NonOwnersCannotRevokeOwners(userToRevoke, request.ActionPerformedBy))
|
||||
{
|
||||
result.ErrorMessages.Add($"{ErrorOnlyOwnersCanRevokeOtherOwners}");
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private static bool PerformedByIsAnExpectedType(IActingUser entity) => entity is SystemUser or StandardUser;
|
||||
|
||||
private static bool IsAlreadyRevoked(OrganizationUserUserDetails organizationUser) =>
|
||||
organizationUser is { Status: OrganizationUserStatusType.Revoked };
|
||||
|
||||
private static bool NonOwnersCannotRevokeOwners(OrganizationUserUserDetails organizationUser,
|
||||
IActingUser actingUser) =>
|
||||
actingUser is StandardUser { IsOrganizationOwnerOrProvider: false } && organizationUser.Type == OrganizationUserType.Owner;
|
||||
}
|
||||
@@ -9,25 +9,18 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
public class UpdateOrganizationUserGroupsCommand : IUpdateOrganizationUserGroupsCommand
|
||||
{
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public UpdateOrganizationUserGroupsCommand(
|
||||
IEventService eventService,
|
||||
IOrganizationService organizationService,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
_eventService = eventService;
|
||||
_organizationService = organizationService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
}
|
||||
|
||||
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId)
|
||||
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds)
|
||||
{
|
||||
if (loggedInUserId.HasValue)
|
||||
{
|
||||
await _organizationService.ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, null, organizationUser.GetPermissions());
|
||||
}
|
||||
await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupIds);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Services;
|
||||
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.Models.StaticStore;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
|
||||
public record SignUpOrganizationResponse(
|
||||
Organization Organization,
|
||||
OrganizationUser OrganizationUser,
|
||||
Collection DefaultCollection);
|
||||
|
||||
public interface ICloudOrganizationSignUpCommand
|
||||
{
|
||||
Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup);
|
||||
}
|
||||
|
||||
public class CloudOrganizationSignUpCommand(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IFeatureService featureService,
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IPaymentService paymentService,
|
||||
IPolicyService policyService,
|
||||
IReferenceEventService referenceEventService,
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IPushRegistrationService pushRegistrationService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
ICollectionRepository collectionRepository,
|
||||
IDeviceRepository deviceRepository) : ICloudOrganizationSignUpCommand
|
||||
{
|
||||
public async Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
|
||||
ValidatePasswordManagerPlan(plan, signup);
|
||||
|
||||
if (signup.UseSecretsManager)
|
||||
{
|
||||
if (signup.IsFromProvider)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"Organizations with a Managed Service Provider do not support Secrets Manager.");
|
||||
}
|
||||
ValidateSecretsManagerPlan(plan, signup);
|
||||
}
|
||||
|
||||
if (!signup.IsFromProvider)
|
||||
{
|
||||
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
|
||||
}
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
// Pre-generate the org id so that we can save it with the Stripe subscription
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
Name = signup.Name,
|
||||
BillingEmail = signup.BillingEmail,
|
||||
BusinessName = signup.BusinessName,
|
||||
PlanType = plan!.Type,
|
||||
Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats),
|
||||
MaxCollections = plan.PasswordManager.MaxCollections,
|
||||
MaxStorageGb = !plan.PasswordManager.BaseStorageGb.HasValue ?
|
||||
(short?)null : (short)(plan.PasswordManager.BaseStorageGb.Value + signup.AdditionalStorageGb),
|
||||
UsePolicies = plan.HasPolicies,
|
||||
UseSso = plan.HasSso,
|
||||
UseGroups = plan.HasGroups,
|
||||
UseEvents = plan.HasEvents,
|
||||
UseDirectory = plan.HasDirectory,
|
||||
UseTotp = plan.HasTotp,
|
||||
Use2fa = plan.Has2fa,
|
||||
UseApi = plan.HasApi,
|
||||
UseResetPassword = plan.HasResetPassword,
|
||||
SelfHost = plan.HasSelfHost,
|
||||
UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,
|
||||
UseCustomPermissions = plan.HasCustomPermissions,
|
||||
UseScim = plan.HasScim,
|
||||
Plan = plan.Name,
|
||||
Gateway = null,
|
||||
ReferenceData = signup.Owner.ReferenceData,
|
||||
Enabled = true,
|
||||
LicenseKey = CoreHelpers.SecureRandomString(20),
|
||||
PublicKey = signup.PublicKey,
|
||||
PrivateKey = signup.PrivateKey,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
Status = OrganizationStatusType.Created,
|
||||
UsePasswordManager = true,
|
||||
UseSecretsManager = signup.UseSecretsManager
|
||||
};
|
||||
|
||||
if (signup.UseSecretsManager)
|
||||
{
|
||||
organization.SmSeats = plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault();
|
||||
organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount +
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault();
|
||||
}
|
||||
|
||||
if (plan.Type == PlanType.Free && !signup.IsFromProvider)
|
||||
{
|
||||
var adminCount =
|
||||
await organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
|
||||
if (adminCount > 0)
|
||||
{
|
||||
throw new BadRequestException("You can only be an admin of one free organization.");
|
||||
}
|
||||
}
|
||||
else if (plan.Type != PlanType.Free)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||
{
|
||||
var sale = OrganizationSale.From(organization, signup);
|
||||
await organizationBillingService.Finalize(sale);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (signup.PaymentMethodType != null)
|
||||
{
|
||||
await paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
|
||||
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
||||
signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
||||
}
|
||||
else
|
||||
{
|
||||
await paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats,
|
||||
signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;
|
||||
var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true);
|
||||
await referenceEventService.RaiseEventAsync(
|
||||
new ReferenceEvent(ReferenceEventType.Signup, organization, currentContext)
|
||||
{
|
||||
PlanName = plan.Name,
|
||||
PlanType = plan.Type,
|
||||
Seats = returnValue.Item1.Seats,
|
||||
SignupInitiationPath = signup.InitiationPath,
|
||||
Storage = returnValue.Item1.MaxStorageGb,
|
||||
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
|
||||
});
|
||||
|
||||
return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser, returnValue.defaultCollection);
|
||||
}
|
||||
|
||||
public void ValidatePasswordManagerPlan(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(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.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidatePlan(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!");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
|
||||
{
|
||||
var anySingleOrgPolicies = await policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
|
||||
if (anySingleOrgPolicies)
|
||||
{
|
||||
throw new BadRequestException("You may not create an organization. You belong to an organization " +
|
||||
"which has a policy that prohibits you from being a member of any other organization.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(Organization organization,
|
||||
Guid ownerId, string ownerKey, string collectionName, bool withPayment)
|
||||
{
|
||||
try
|
||||
{
|
||||
await organizationRepository.CreateAsync(organization);
|
||||
await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
ApiKey = CoreHelpers.SecureRandomString(30),
|
||||
Type = OrganizationApiKeyType.Default,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
});
|
||||
await applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// ownerId == default if the org is created by a provider - in this case it's created without an
|
||||
// owner and the first owner is immediately invited afterwards
|
||||
OrganizationUser orgUser = null;
|
||||
if (ownerId != default)
|
||||
{
|
||||
orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = ownerId,
|
||||
Key = ownerKey,
|
||||
AccessSecretsManager = organization.UseSecretsManager,
|
||||
Type = OrganizationUserType.Owner,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
CreationDate = organization.CreationDate,
|
||||
RevisionDate = organization.CreationDate
|
||||
};
|
||||
orgUser.SetNewId();
|
||||
|
||||
await organizationUserRepository.CreateAsync(orgUser);
|
||||
|
||||
var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
|
||||
await pushRegistrationService.AddUserRegistrationOrganizationAsync(devices, organization.Id.ToString());
|
||||
await pushNotificationService.PushSyncOrgKeysAsync(ownerId);
|
||||
}
|
||||
|
||||
Collection defaultCollection = null;
|
||||
if (!string.IsNullOrWhiteSpace(collectionName))
|
||||
{
|
||||
defaultCollection = new Collection
|
||||
{
|
||||
Name = collectionName,
|
||||
OrganizationId = organization.Id,
|
||||
CreationDate = organization.CreationDate,
|
||||
RevisionDate = organization.CreationDate
|
||||
};
|
||||
|
||||
// Give the owner Can Manage access over the default collection
|
||||
List<CollectionAccessSelection> defaultOwnerAccess = null;
|
||||
if (orgUser != null)
|
||||
{
|
||||
defaultOwnerAccess =
|
||||
[new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }];
|
||||
}
|
||||
|
||||
await collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);
|
||||
}
|
||||
|
||||
return (organization, orgUser, defaultCollection);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (withPayment)
|
||||
{
|
||||
await paymentService.CancelAndRecoverChargesAsync(organization);
|
||||
}
|
||||
|
||||
if (organization.Id != default(Guid))
|
||||
{
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
||||
{
|
||||
var devices = await deviceRepository.GetManyByUserIdAsync(userId);
|
||||
return devices
|
||||
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||
.Select(d => d.Id.ToString());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
public interface ISavePolicyCommand
|
||||
{
|
||||
Task SaveAsync(PolicyUpdate policy);
|
||||
Task<Policy> SaveAsync(PolicyUpdate policy);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public class SavePolicyCommand : ISavePolicyCommand
|
||||
_policyValidators = policyValidatorsDict;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(PolicyUpdate policyUpdate)
|
||||
public async Task<Policy> SaveAsync(PolicyUpdate policyUpdate)
|
||||
{
|
||||
var org = await _applicationCacheService.GetOrganizationAbilityAsync(policyUpdate.OrganizationId);
|
||||
if (org == null)
|
||||
@@ -74,6 +74,8 @@ public class SavePolicyCommand : ISavePolicyCommand
|
||||
|
||||
await _policyRepository.UpsertAsync(policy);
|
||||
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
|
||||
|
||||
return policy;
|
||||
}
|
||||
|
||||
private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate policyUpdate)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
@@ -15,6 +16,7 @@ public record PolicyUpdate
|
||||
public PolicyType Type { get; set; }
|
||||
public string? Data { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public IActingUser? PerformedBy { get; set; }
|
||||
|
||||
public T GetDataModel<T>() where T : IPolicyDataModel, new()
|
||||
{
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@@ -18,6 +20,8 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
{
|
||||
public PolicyType Type => PolicyType.SingleOrg;
|
||||
private const string OrganizationNotFoundErrorMessage = "Organization not found.";
|
||||
private const string ClaimedDomainSingleOrganizationRequiredErrorMessage = "The Single organization policy is required for organizations that have enabled domain verification.";
|
||||
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IMailService _mailService;
|
||||
@@ -27,6 +31,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||
|
||||
public SingleOrgPolicyValidator(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@@ -36,7 +41,8 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_mailService = mailService;
|
||||
@@ -46,6 +52,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
_featureService = featureService;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||
}
|
||||
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
@@ -54,10 +61,61 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
{
|
||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
else
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RevokeNonCompliantUsersAsync(Guid organizationId, IActingUser performedBy)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization is null)
|
||||
{
|
||||
throw new NotFoundException(OrganizationNotFoundErrorMessage);
|
||||
}
|
||||
|
||||
var currentActiveRevocableOrganizationUsers =
|
||||
(await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId))
|
||||
.Where(ou => ou.Status != OrganizationUserStatusType.Invited &&
|
||||
ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != OrganizationUserType.Owner &&
|
||||
ou.Type != OrganizationUserType.Admin &&
|
||||
!(performedBy is StandardUser stdUser && stdUser.UserId == ou.UserId))
|
||||
.ToList();
|
||||
|
||||
if (currentActiveRevocableOrganizationUsers.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var allRevocableUserOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
|
||||
currentActiveRevocableOrganizationUsers.Select(ou => ou.UserId!.Value));
|
||||
var usersToRevoke = currentActiveRevocableOrganizationUsers.Where(ou =>
|
||||
allRevocableUserOrgs.Any(uo => uo.UserId == ou.UserId &&
|
||||
uo.OrganizationId != organizationId &&
|
||||
uo.Status != OrganizationUserStatusType.Invited)).ToList();
|
||||
|
||||
var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
|
||||
new RevokeOrganizationUsersRequest(organizationId, usersToRevoke, performedBy));
|
||||
|
||||
if (commandResult.HasErrors)
|
||||
{
|
||||
throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages));
|
||||
}
|
||||
|
||||
await Task.WhenAll(usersToRevoke.Select(x =>
|
||||
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email)));
|
||||
}
|
||||
|
||||
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
|
||||
{
|
||||
// Remove non-compliant users
|
||||
@@ -67,7 +125,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new NotFoundException("Organization not found.");
|
||||
throw new NotFoundException(OrganizationNotFoundErrorMessage);
|
||||
}
|
||||
|
||||
var removableOrgUsers = orgUsers.Where(ou =>
|
||||
@@ -76,18 +134,17 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
ou.Type != OrganizationUserType.Owner &&
|
||||
ou.Type != OrganizationUserType.Admin &&
|
||||
ou.UserId != savingUserId
|
||||
).ToList();
|
||||
).ToList();
|
||||
|
||||
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
|
||||
removableOrgUsers.Select(ou => ou.UserId!.Value));
|
||||
removableOrgUsers.Select(ou => ou.UserId!.Value));
|
||||
foreach (var orgUser in removableOrgUsers)
|
||||
{
|
||||
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
|
||||
&& ou.OrganizationId != org.Id
|
||||
&& ou.Status != OrganizationUserStatusType.Invited))
|
||||
&& ou.OrganizationId != org.Id
|
||||
&& ou.Status != OrganizationUserStatusType.Invited))
|
||||
{
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
|
||||
savingUserId);
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, savingUserId);
|
||||
|
||||
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
|
||||
org.DisplayName(), orgUser.Email);
|
||||
@@ -111,7 +168,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
|
||||
{
|
||||
return "The Single organization policy is required for organizations that have enabled domain verification.";
|
||||
return ClaimedDomainSingleOrganizationRequiredErrorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
@@ -21,6 +24,10 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||
|
||||
public const string NonCompliantMembersWillLoseAccessMessage = "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.";
|
||||
|
||||
public PolicyType Type => PolicyType.TwoFactorAuthentication;
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
@@ -31,7 +38,9 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
IOrganizationRepository organizationRepository,
|
||||
ICurrentContext currentContext,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IFeatureService featureService,
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_mailService = mailService;
|
||||
@@ -39,16 +48,65 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
_currentContext = currentContext;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_featureService = featureService;
|
||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||
}
|
||||
|
||||
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
|
||||
{
|
||||
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
else
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RevokeNonCompliantUsersAsync(Guid organizationId, IActingUser performedBy)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
var currentActiveRevocableOrganizationUsers =
|
||||
(await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId))
|
||||
.Where(ou => ou.Status != OrganizationUserStatusType.Invited &&
|
||||
ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != OrganizationUserType.Owner &&
|
||||
ou.Type != OrganizationUserType.Admin &&
|
||||
!(performedBy is StandardUser stdUser && stdUser.UserId == ou.UserId))
|
||||
.ToList();
|
||||
|
||||
if (currentActiveRevocableOrganizationUsers.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var organizationUsersTwoFactorEnabled =
|
||||
await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(currentActiveRevocableOrganizationUsers);
|
||||
|
||||
if (NonCompliantMembersWillLoseAccess(currentActiveRevocableOrganizationUsers, organizationUsersTwoFactorEnabled))
|
||||
{
|
||||
throw new BadRequestException(NonCompliantMembersWillLoseAccessMessage);
|
||||
}
|
||||
|
||||
var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
|
||||
new RevokeOrganizationUsersRequest(organizationId, currentActiveRevocableOrganizationUsers, performedBy));
|
||||
|
||||
if (commandResult.HasErrors)
|
||||
{
|
||||
throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages));
|
||||
}
|
||||
|
||||
await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x =>
|
||||
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email)));
|
||||
}
|
||||
|
||||
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
@@ -83,5 +141,12 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
}
|
||||
}
|
||||
|
||||
private static bool NonCompliantMembersWillLoseAccess(
|
||||
IEnumerable<OrganizationUserUserDetails> orgUserDetails,
|
||||
IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) =>
|
||||
orgUserDetails.Any(x =>
|
||||
!x.HasMasterPassword && !organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == x.Id)
|
||||
.isTwoFactorEnabled);
|
||||
|
||||
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
@@ -58,4 +58,6 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
/// Returns a list of OrganizationUsers with email domains that match one of the Organization's claimed domains.
|
||||
/// </summary>
|
||||
Task<ICollection<OrganizationUser>> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId);
|
||||
|
||||
Task RevokeManyByIdAsync(IEnumerable<Guid> organizationUserIds);
|
||||
}
|
||||
|
||||
@@ -20,13 +20,7 @@ public interface IOrganizationService
|
||||
Task AutoAddSeatsAsync(Organization organization, int seatsToAdd);
|
||||
Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment);
|
||||
Task VerifyBankAsync(Guid organizationId, int amount1, int amount2);
|
||||
/// <summary>
|
||||
/// Create a new organization in a cloud environment
|
||||
/// </summary>
|
||||
/// <returns>A tuple containing the new organization, the initial organizationUser (if any) and the default collection (if any)</returns>
|
||||
#nullable enable
|
||||
Task<(Organization organization, OrganizationUser? organizationUser, Collection? defaultCollection)> SignUpAsync(OrganizationSignup organizationSignup);
|
||||
|
||||
Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup);
|
||||
#nullable disable
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -9,8 +8,6 @@ namespace Bit.Core.AdminConsole.Services;
|
||||
|
||||
public interface IPolicyService
|
||||
{
|
||||
Task SaveAsync(Policy policy, Guid? savingUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Get the combined master password policy options for the specified user.
|
||||
/// </summary>
|
||||
|
||||
@@ -17,6 +17,7 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<OrganizationDomainService> _logger;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationDomainService(
|
||||
IOrganizationDomainRepository domainRepository,
|
||||
@@ -26,7 +27,8 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<OrganizationDomainService> logger,
|
||||
IGlobalSettings globalSettings)
|
||||
IGlobalSettings globalSettings,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_domainRepository = domainRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -36,6 +38,7 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
_timeProvider = timeProvider;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task ValidateOrganizationsDomainAsync()
|
||||
@@ -90,8 +93,16 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
//Send email to administrators
|
||||
if (adminEmails.Count > 0)
|
||||
{
|
||||
await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails,
|
||||
domain.OrganizationId.ToString(), domain.DomainName);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails,
|
||||
domain.OrganizationId.ToString(), domain.DomainName);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails,
|
||||
domain.OrganizationId.ToString(), domain.DomainName);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Expired domain: {domainName}", domain.DomainName);
|
||||
|
||||
@@ -14,9 +14,9 @@ using Bit.Core.Auth.Models.Business;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@@ -502,129 +502,6 @@ public class OrganizationService : IOrganizationService
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new organization in a cloud environment
|
||||
/// </summary>
|
||||
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(OrganizationSignup signup)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
|
||||
ValidatePasswordManagerPlan(plan, signup);
|
||||
|
||||
if (signup.UseSecretsManager)
|
||||
{
|
||||
if (signup.IsFromProvider)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"Organizations with a Managed Service Provider do not support Secrets Manager.");
|
||||
}
|
||||
ValidateSecretsManagerPlan(plan, signup);
|
||||
}
|
||||
|
||||
if (!signup.IsFromProvider)
|
||||
{
|
||||
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
|
||||
}
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
// Pre-generate the org id so that we can save it with the Stripe subscription..
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
Name = signup.Name,
|
||||
BillingEmail = signup.BillingEmail,
|
||||
BusinessName = signup.BusinessName,
|
||||
PlanType = plan!.Type,
|
||||
Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats),
|
||||
MaxCollections = plan.PasswordManager.MaxCollections,
|
||||
MaxStorageGb = !plan.PasswordManager.BaseStorageGb.HasValue ?
|
||||
(short?)null : (short)(plan.PasswordManager.BaseStorageGb.Value + signup.AdditionalStorageGb),
|
||||
UsePolicies = plan.HasPolicies,
|
||||
UseSso = plan.HasSso,
|
||||
UseGroups = plan.HasGroups,
|
||||
UseEvents = plan.HasEvents,
|
||||
UseDirectory = plan.HasDirectory,
|
||||
UseTotp = plan.HasTotp,
|
||||
Use2fa = plan.Has2fa,
|
||||
UseApi = plan.HasApi,
|
||||
UseResetPassword = plan.HasResetPassword,
|
||||
SelfHost = plan.HasSelfHost,
|
||||
UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,
|
||||
UseCustomPermissions = plan.HasCustomPermissions,
|
||||
UseScim = plan.HasScim,
|
||||
Plan = plan.Name,
|
||||
Gateway = null,
|
||||
ReferenceData = signup.Owner.ReferenceData,
|
||||
Enabled = true,
|
||||
LicenseKey = CoreHelpers.SecureRandomString(20),
|
||||
PublicKey = signup.PublicKey,
|
||||
PrivateKey = signup.PrivateKey,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
Status = OrganizationStatusType.Created,
|
||||
UsePasswordManager = true,
|
||||
UseSecretsManager = signup.UseSecretsManager
|
||||
};
|
||||
|
||||
if (signup.UseSecretsManager)
|
||||
{
|
||||
organization.SmSeats = plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault();
|
||||
organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount +
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault();
|
||||
}
|
||||
|
||||
if (plan.Type == PlanType.Free && !signup.IsFromProvider)
|
||||
{
|
||||
var adminCount =
|
||||
await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
|
||||
if (adminCount > 0)
|
||||
{
|
||||
throw new BadRequestException("You can only be an admin of one free organization.");
|
||||
}
|
||||
}
|
||||
else if (plan.Type != PlanType.Free)
|
||||
{
|
||||
var deprecateStripeSourcesAPI = _featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI);
|
||||
|
||||
if (deprecateStripeSourcesAPI)
|
||||
{
|
||||
var sale = OrganizationSale.From(organization, signup);
|
||||
await _organizationBillingService.Finalize(sale);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (signup.PaymentMethodType != null)
|
||||
{
|
||||
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
|
||||
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
||||
signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats,
|
||||
signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;
|
||||
var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true);
|
||||
await _referenceEventService.RaiseEventAsync(
|
||||
new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
|
||||
{
|
||||
PlanName = plan.Name,
|
||||
PlanType = plan.Type,
|
||||
Seats = returnValue.Item1.Seats,
|
||||
SignupInitiationPath = signup.InitiationPath,
|
||||
Storage = returnValue.Item1.MaxStorageGb,
|
||||
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
|
||||
});
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
|
||||
{
|
||||
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
|
||||
@@ -642,16 +519,29 @@ public class OrganizationService : IOrganizationService
|
||||
OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey,
|
||||
string privateKey)
|
||||
{
|
||||
var canUse = license.CanUse(_globalSettings, _licensingService, out var exception);
|
||||
if (license.LicenseType != LicenseType.Organization)
|
||||
{
|
||||
throw new BadRequestException("Premium licenses cannot be applied to an organization. " +
|
||||
"Upload this license from your personal account settings page.");
|
||||
}
|
||||
|
||||
var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license);
|
||||
var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception);
|
||||
|
||||
if (!canUse)
|
||||
{
|
||||
throw new BadRequestException(exception);
|
||||
}
|
||||
|
||||
if (license.PlanType != PlanType.Custom &&
|
||||
StaticStore.Plans.FirstOrDefault(p => p.Type == license.PlanType && !p.Disabled) == null)
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == license.PlanType);
|
||||
if (plan is null)
|
||||
{
|
||||
throw new BadRequestException("Plan not found.");
|
||||
throw new BadRequestException($"Server must be updated to support {license.Plan}.");
|
||||
}
|
||||
|
||||
if (license.PlanType != PlanType.Custom && plan.Disabled)
|
||||
{
|
||||
throw new BadRequestException($"Plan {plan.Name} is disabled.");
|
||||
}
|
||||
|
||||
var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync();
|
||||
@@ -704,14 +594,6 @@ public class OrganizationService : IOrganizationService
|
||||
SmServiceAccounts = license.SmServiceAccounts,
|
||||
};
|
||||
|
||||
// These fields are being removed from consideration when processing
|
||||
// licenses.
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||
{
|
||||
organization.LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
|
||||
organization.AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
|
||||
}
|
||||
|
||||
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
||||
|
||||
var dir = $"{_globalSettings.LicenseDirectory}/organization";
|
||||
@@ -1455,6 +1337,12 @@ public class OrganizationService : IOrganizationService
|
||||
return (false, $"Seat limit has been reached.");
|
||||
}
|
||||
|
||||
var subscription = await _paymentService.GetSubscriptionAsync(organization);
|
||||
if (subscription?.Subscription?.Status == StripeConstants.SubscriptionStatus.Canceled)
|
||||
{
|
||||
return (false, "Cannot autoscale with a canceled subscription.");
|
||||
}
|
||||
|
||||
return (true, failureReason);
|
||||
}
|
||||
|
||||
@@ -2332,10 +2220,13 @@ public class OrganizationService : IOrganizationService
|
||||
PolicyType.SingleOrg, OrganizationUserStatusType.Revoked);
|
||||
var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId);
|
||||
|
||||
var singleOrgCompliant = true;
|
||||
var belongsToOtherOrgCompliant = true;
|
||||
var twoFactorCompliant = true;
|
||||
|
||||
if (hasOtherOrgs && singleOrgPolicyApplies)
|
||||
{
|
||||
throw new BadRequestException("You cannot restore this user until " +
|
||||
"they leave or remove all other organizations.");
|
||||
singleOrgCompliant = false;
|
||||
}
|
||||
|
||||
// Enforce Single Organization Policy of other organizations user is a member of
|
||||
@@ -2343,8 +2234,7 @@ public class OrganizationService : IOrganizationService
|
||||
PolicyType.SingleOrg);
|
||||
if (anySingleOrgPolicies)
|
||||
{
|
||||
throw new BadRequestException("You cannot restore this user because they are a member of " +
|
||||
"another organization which forbids it");
|
||||
belongsToOtherOrgCompliant = false;
|
||||
}
|
||||
|
||||
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
||||
@@ -2354,10 +2244,28 @@ public class OrganizationService : IOrganizationService
|
||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
||||
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
||||
{
|
||||
throw new BadRequestException("You cannot restore this user until they enable " +
|
||||
"two-step login on their user account.");
|
||||
twoFactorCompliant = false;
|
||||
}
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(userId);
|
||||
|
||||
if (!singleOrgCompliant && !twoFactorCompliant)
|
||||
{
|
||||
throw new BadRequestException(user.Email + " is not compliant with the single organization and two-step login polciy");
|
||||
}
|
||||
else if (!singleOrgCompliant)
|
||||
{
|
||||
throw new BadRequestException(user.Email + " is not compliant with the single organization policy");
|
||||
}
|
||||
else if (!belongsToOtherOrgCompliant)
|
||||
{
|
||||
throw new BadRequestException(user.Email + " belongs to an organization that doesn't allow them to join multiple organizations");
|
||||
}
|
||||
else if (!twoFactorCompliant)
|
||||
{
|
||||
throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
|
||||
}
|
||||
}
|
||||
|
||||
static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -22,100 +13,20 @@ namespace Bit.Core.AdminConsole.Services.Implementations;
|
||||
public class PolicyService : IPolicyService
|
||||
{
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
|
||||
public PolicyService(
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IEventService eventService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IMailService mailService,
|
||||
GlobalSettings globalSettings,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_eventService = eventService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_policyRepository = policyRepository;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_mailService = mailService;
|
||||
_globalSettings = globalSettings;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_featureService = featureService;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Policy policy, Guid? savingUserId)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.Pm13322AddPolicyDefinitions))
|
||||
{
|
||||
// Transitional mapping - this will be moved to callers once the feature flag is removed
|
||||
var policyUpdate = new PolicyUpdate
|
||||
{
|
||||
OrganizationId = policy.OrganizationId,
|
||||
Type = policy.Type,
|
||||
Enabled = policy.Enabled,
|
||||
Data = policy.Data
|
||||
};
|
||||
|
||||
await _savePolicyCommand.SaveAsync(policyUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new BadRequestException("Organization not found");
|
||||
}
|
||||
|
||||
if (!org.UsePolicies)
|
||||
{
|
||||
throw new BadRequestException("This organization cannot use policies.");
|
||||
}
|
||||
|
||||
// FIXME: This method will throw a bunch of errors based on if the
|
||||
// policy that is being applied requires some other policy that is
|
||||
// not enabled. It may be advisable to refactor this into a domain
|
||||
// object and get this kind of stuff out of the service.
|
||||
await HandleDependentPoliciesAsync(policy, org);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (policy.Id == default(Guid))
|
||||
{
|
||||
policy.CreationDate = now;
|
||||
}
|
||||
|
||||
policy.RevisionDate = now;
|
||||
|
||||
// We can exit early for disable operations, because they are
|
||||
// simpler.
|
||||
if (!policy.Enabled)
|
||||
{
|
||||
await SetPolicyConfiguration(policy);
|
||||
return;
|
||||
}
|
||||
|
||||
await EnablePolicyAsync(policy, org, savingUserId);
|
||||
}
|
||||
|
||||
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
|
||||
@@ -181,178 +92,4 @@ public class PolicyService : IPolicyService
|
||||
|
||||
return new[] { OrganizationUserType.Owner, OrganizationUserType.Admin };
|
||||
}
|
||||
|
||||
private async Task DependsOnSingleOrgAsync(Organization org)
|
||||
{
|
||||
var singleOrg = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.SingleOrg);
|
||||
if (singleOrg?.Enabled != true)
|
||||
{
|
||||
throw new BadRequestException("Single Organization policy not enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredBySsoAsync(Organization org)
|
||||
{
|
||||
var requireSso = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.RequireSso);
|
||||
if (requireSso?.Enabled == true)
|
||||
{
|
||||
throw new BadRequestException("Single Sign-On Authentication policy is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredByKeyConnectorAsync(Organization org)
|
||||
{
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
|
||||
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector)
|
||||
{
|
||||
throw new BadRequestException("Key Connector is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredByAccountRecoveryAsync(Organization org)
|
||||
{
|
||||
var requireSso = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.ResetPassword);
|
||||
if (requireSso?.Enabled == true)
|
||||
{
|
||||
throw new BadRequestException("Account recovery policy is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredByVaultTimeoutAsync(Organization org)
|
||||
{
|
||||
var vaultTimeout = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.MaximumVaultTimeout);
|
||||
if (vaultTimeout?.Enabled == true)
|
||||
{
|
||||
throw new BadRequestException("Maximum Vault Timeout policy is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredBySsoTrustedDeviceEncryptionAsync(Organization org)
|
||||
{
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
|
||||
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption)
|
||||
{
|
||||
throw new BadRequestException("Trusted device encryption is on and requires this policy.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleDependentPoliciesAsync(Policy policy, Organization org)
|
||||
{
|
||||
switch (policy.Type)
|
||||
{
|
||||
case PolicyType.SingleOrg:
|
||||
if (!policy.Enabled)
|
||||
{
|
||||
await HasVerifiedDomainsAsync(org);
|
||||
await RequiredBySsoAsync(org);
|
||||
await RequiredByVaultTimeoutAsync(org);
|
||||
await RequiredByKeyConnectorAsync(org);
|
||||
await RequiredByAccountRecoveryAsync(org);
|
||||
}
|
||||
break;
|
||||
|
||||
case PolicyType.RequireSso:
|
||||
if (policy.Enabled)
|
||||
{
|
||||
await DependsOnSingleOrgAsync(org);
|
||||
}
|
||||
else
|
||||
{
|
||||
await RequiredByKeyConnectorAsync(org);
|
||||
await RequiredBySsoTrustedDeviceEncryptionAsync(org);
|
||||
}
|
||||
break;
|
||||
|
||||
case PolicyType.ResetPassword:
|
||||
if (!policy.Enabled || policy.GetDataModel<ResetPasswordDataModel>()?.AutoEnrollEnabled == false)
|
||||
{
|
||||
await RequiredBySsoTrustedDeviceEncryptionAsync(org);
|
||||
}
|
||||
|
||||
if (policy.Enabled)
|
||||
{
|
||||
await DependsOnSingleOrgAsync(org);
|
||||
}
|
||||
break;
|
||||
|
||||
case PolicyType.MaximumVaultTimeout:
|
||||
if (policy.Enabled)
|
||||
{
|
||||
await DependsOnSingleOrgAsync(org);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HasVerifiedDomainsAsync(Organization org)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(org.Id))
|
||||
{
|
||||
throw new BadRequestException("The Single organization policy is required for organizations that have enabled domain verification.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetPolicyConfiguration(Policy policy)
|
||||
{
|
||||
await _policyRepository.UpsertAsync(policy);
|
||||
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
|
||||
}
|
||||
|
||||
private async Task EnablePolicyAsync(Policy policy, Organization org, Guid? savingUserId)
|
||||
{
|
||||
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
|
||||
if (!currentPolicy?.Enabled ?? true)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(policy.OrganizationId);
|
||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
|
||||
var removableOrgUsers = orgUsers.Where(ou =>
|
||||
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
|
||||
ou.UserId != savingUserId);
|
||||
switch (policy.Type)
|
||||
{
|
||||
case PolicyType.TwoFactorAuthentication:
|
||||
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
|
||||
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
|
||||
{
|
||||
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id).twoFactorIsEnabled;
|
||||
if (!userTwoFactorEnabled)
|
||||
{
|
||||
if (!orgUser.HasMasterPassword)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
|
||||
}
|
||||
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id,
|
||||
savingUserId);
|
||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||
org.DisplayName(), orgUser.Email);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PolicyType.SingleOrg:
|
||||
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
|
||||
removableOrgUsers.Select(ou => ou.UserId.Value));
|
||||
foreach (var orgUser in removableOrgUsers)
|
||||
{
|
||||
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
|
||||
&& ou.OrganizationId != org.Id
|
||||
&& ou.Status != OrganizationUserStatusType.Invited))
|
||||
{
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id,
|
||||
savingUserId);
|
||||
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
|
||||
org.DisplayName(), orgUser.Email);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await SetPolicyConfiguration(policy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
|
||||
#nullable enable
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@@ -17,25 +18,25 @@ public class SsoConfigService : ISsoConfigService
|
||||
{
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
|
||||
public SsoConfigService(
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyService policyService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IEventService eventService)
|
||||
IEventService eventService,
|
||||
ISavePolicyCommand savePolicyCommand)
|
||||
{
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_policyRepository = policyRepository;
|
||||
_policyService = policyService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_eventService = eventService;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(SsoConfig config, Organization organization)
|
||||
@@ -63,25 +64,29 @@ public class SsoConfigService : ISsoConfigService
|
||||
// Automatically enable account recovery, SSO required, and single org policies if trusted device encryption is selected
|
||||
if (config.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption)
|
||||
{
|
||||
var singleOrgPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.SingleOrg) ??
|
||||
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.SingleOrg };
|
||||
|
||||
singleOrgPolicy.Enabled = true;
|
||||
await _savePolicyCommand.SaveAsync(new()
|
||||
{
|
||||
OrganizationId = config.OrganizationId,
|
||||
Type = PolicyType.SingleOrg,
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
await _policyService.SaveAsync(singleOrgPolicy, null);
|
||||
var resetPasswordPolicy = new PolicyUpdate
|
||||
{
|
||||
OrganizationId = config.OrganizationId,
|
||||
Type = PolicyType.ResetPassword,
|
||||
Enabled = true,
|
||||
};
|
||||
resetPasswordPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });
|
||||
await _savePolicyCommand.SaveAsync(resetPasswordPolicy);
|
||||
|
||||
var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ??
|
||||
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, };
|
||||
|
||||
resetPolicy.Enabled = true;
|
||||
resetPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });
|
||||
await _policyService.SaveAsync(resetPolicy, null);
|
||||
|
||||
var ssoRequiredPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso) ??
|
||||
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.RequireSso, };
|
||||
|
||||
ssoRequiredPolicy.Enabled = true;
|
||||
await _policyService.SaveAsync(ssoRequiredPolicy, null);
|
||||
await _savePolicyCommand.SaveAsync(new()
|
||||
{
|
||||
OrganizationId = config.OrganizationId,
|
||||
Type = PolicyType.RequireSso,
|
||||
Enabled = true
|
||||
});
|
||||
}
|
||||
|
||||
await LogEventsAsync(config, oldConfig);
|
||||
|
||||
@@ -329,7 +329,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
{
|
||||
// We validate open registration on send of initial email and here b/c a user could technically start the
|
||||
// account creation process while open registration is enabled and then finish it after it has been
|
||||
// disabled by the self hosted admin.Ï
|
||||
// disabled by the self hosted admin.
|
||||
if (_globalSettings.DisableUserRegistration)
|
||||
{
|
||||
throw new BadRequestException(_disabledUserRegistrationExceptionMsg);
|
||||
|
||||
@@ -5,12 +5,12 @@ using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.KeyManagement.UserKey.Implementations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -5,5 +5,7 @@ public class BillingException(
|
||||
string message = null,
|
||||
Exception innerException = null) : Exception(message, innerException)
|
||||
{
|
||||
public string Response { get; } = response ?? "Something went wrong with your request. Please contact support.";
|
||||
public const string DefaultMessage = "Something went wrong with your request. Please contact support.";
|
||||
|
||||
public string Response { get; } = response ?? DefaultMessage;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ public static class StripeConstants
|
||||
public static class ErrorCodes
|
||||
{
|
||||
public const string CustomerTaxLocationInvalid = "customer_tax_location_invalid";
|
||||
public const string PaymentMethodMicroDepositVerificationAttemptsExceeded = "payment_method_microdeposit_verification_attempts_exceeded";
|
||||
public const string PaymentMethodMicroDepositVerificationDescriptorCodeMismatch = "payment_method_microdeposit_verification_descriptor_code_mismatch";
|
||||
public const string PaymentMethodMicroDepositVerificationTimeout = "payment_method_microdeposit_verification_timeout";
|
||||
public const string TaxIdInvalid = "tax_id_invalid";
|
||||
}
|
||||
|
||||
|
||||
24
src/Core/Billing/Entities/OrganizationInstallation.cs
Normal file
24
src/Core/Billing/Entities/OrganizationInstallation.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Billing.Entities;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public class OrganizationInstallation : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid OrganizationId { get; set; }
|
||||
public Guid InstallationId { get; set; }
|
||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
public DateTime? RevisionDate { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
if (Id == default)
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Caches.Implementations;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Implementations;
|
||||
|
||||
@@ -15,5 +16,6 @@ public static class ServiceCollectionExtensions
|
||||
services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>();
|
||||
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
|
||||
services.AddTransient<ISubscriberService, SubscriberService>();
|
||||
services.AddLicenseServices();
|
||||
}
|
||||
}
|
||||
|
||||
151
src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs
Normal file
151
src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Extensions;
|
||||
|
||||
public static class LicenseExtensions
|
||||
{
|
||||
public static DateTime CalculateFreshExpirationDate(this Organization org, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
if (subscriptionInfo?.Subscription == null)
|
||||
{
|
||||
if (org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue)
|
||||
{
|
||||
return org.ExpirationDate.Value;
|
||||
}
|
||||
|
||||
return DateTime.UtcNow.AddDays(7);
|
||||
}
|
||||
|
||||
var subscription = subscriptionInfo.Subscription;
|
||||
|
||||
if (subscription.TrialEndDate > DateTime.UtcNow)
|
||||
{
|
||||
return subscription.TrialEndDate.Value;
|
||||
}
|
||||
|
||||
if (org.ExpirationDate.HasValue && org.ExpirationDate.Value < DateTime.UtcNow)
|
||||
{
|
||||
return org.ExpirationDate.Value;
|
||||
}
|
||||
|
||||
if (subscription.PeriodEndDate.HasValue && subscription.PeriodDuration > TimeSpan.FromDays(180))
|
||||
{
|
||||
return subscription.PeriodEndDate
|
||||
.Value
|
||||
.AddDays(Bit.Core.Constants.OrganizationSelfHostSubscriptionGracePeriodDays);
|
||||
}
|
||||
|
||||
return org.ExpirationDate?.AddMonths(11) ?? DateTime.UtcNow.AddYears(1);
|
||||
}
|
||||
|
||||
public static DateTime CalculateFreshRefreshDate(this Organization org, SubscriptionInfo subscriptionInfo, DateTime expirationDate)
|
||||
{
|
||||
if (subscriptionInfo?.Subscription == null ||
|
||||
subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow ||
|
||||
org.ExpirationDate < DateTime.UtcNow)
|
||||
{
|
||||
return expirationDate;
|
||||
}
|
||||
|
||||
return subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180) ||
|
||||
DateTime.UtcNow - expirationDate > TimeSpan.FromDays(30)
|
||||
? DateTime.UtcNow.AddDays(30)
|
||||
: expirationDate;
|
||||
}
|
||||
|
||||
public static DateTime CalculateFreshExpirationDateWithoutGracePeriod(this Organization org, SubscriptionInfo subscriptionInfo, DateTime expirationDate)
|
||||
{
|
||||
if (subscriptionInfo?.Subscription is null)
|
||||
{
|
||||
return expirationDate;
|
||||
}
|
||||
|
||||
var subscription = subscriptionInfo.Subscription;
|
||||
|
||||
if (subscription.TrialEndDate <= DateTime.UtcNow &&
|
||||
org.ExpirationDate >= DateTime.UtcNow &&
|
||||
subscription.PeriodEndDate.HasValue &&
|
||||
subscription.PeriodDuration > TimeSpan.FromDays(180))
|
||||
{
|
||||
return subscription.PeriodEndDate.Value;
|
||||
}
|
||||
|
||||
return expirationDate;
|
||||
}
|
||||
|
||||
public static T GetValue<T>(this ClaimsPrincipal principal, string claimType)
|
||||
{
|
||||
var claim = principal.FindFirst(claimType);
|
||||
|
||||
if (claim is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
// Handle Guid
|
||||
if (typeof(T) == typeof(Guid))
|
||||
{
|
||||
return Guid.TryParse(claim.Value, out var guid)
|
||||
? (T)(object)guid
|
||||
: default;
|
||||
}
|
||||
|
||||
// Handle DateTime
|
||||
if (typeof(T) == typeof(DateTime))
|
||||
{
|
||||
return DateTime.TryParse(claim.Value, out var dateTime)
|
||||
? (T)(object)dateTime
|
||||
: default;
|
||||
}
|
||||
|
||||
// Handle TimeSpan
|
||||
if (typeof(T) == typeof(TimeSpan))
|
||||
{
|
||||
return TimeSpan.TryParse(claim.Value, out var timeSpan)
|
||||
? (T)(object)timeSpan
|
||||
: default;
|
||||
}
|
||||
|
||||
// Check for Nullable Types
|
||||
var underlyingType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
|
||||
// Handle Enums
|
||||
if (underlyingType.IsEnum)
|
||||
{
|
||||
if (Enum.TryParse(underlyingType, claim.Value, true, out var enumValue))
|
||||
{
|
||||
return (T)enumValue; // Cast back to T
|
||||
}
|
||||
|
||||
return default; // Return default value for non-nullable enums or null for nullable enums
|
||||
}
|
||||
|
||||
// Handle other Nullable Types (e.g., int?, bool?)
|
||||
if (underlyingType == typeof(int))
|
||||
{
|
||||
return int.TryParse(claim.Value, out var intValue)
|
||||
? (T)(object)intValue
|
||||
: default;
|
||||
}
|
||||
|
||||
if (underlyingType == typeof(bool))
|
||||
{
|
||||
return bool.TryParse(claim.Value, out var boolValue)
|
||||
? (T)(object)boolValue
|
||||
: default;
|
||||
}
|
||||
|
||||
if (underlyingType == typeof(double))
|
||||
{
|
||||
return double.TryParse(claim.Value, out var doubleValue)
|
||||
? (T)(object)doubleValue
|
||||
: default;
|
||||
}
|
||||
|
||||
// Fallback to Convert.ChangeType for other types including strings
|
||||
return (T)Convert.ChangeType(claim.Value, underlyingType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Licenses.Services;
|
||||
using Bit.Core.Billing.Licenses.Services.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Extensions;
|
||||
|
||||
public static class LicenseServiceCollectionExtensions
|
||||
{
|
||||
public static void AddLicenseServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<ILicenseClaimsFactory<Organization>, OrganizationLicenseClaimsFactory>();
|
||||
services.AddTransient<ILicenseClaimsFactory<User>, UserLicenseClaimsFactory>();
|
||||
}
|
||||
}
|
||||
58
src/Core/Billing/Licenses/LicenseConstants.cs
Normal file
58
src/Core/Billing/Licenses/LicenseConstants.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace Bit.Core.Billing.Licenses;
|
||||
|
||||
public static class OrganizationLicenseConstants
|
||||
{
|
||||
public const string LicenseType = nameof(LicenseType);
|
||||
public const string LicenseKey = nameof(LicenseKey);
|
||||
public const string InstallationId = nameof(InstallationId);
|
||||
public const string Id = nameof(Id);
|
||||
public const string Name = nameof(Name);
|
||||
public const string BusinessName = nameof(BusinessName);
|
||||
public const string BillingEmail = nameof(BillingEmail);
|
||||
public const string Enabled = nameof(Enabled);
|
||||
public const string Plan = nameof(Plan);
|
||||
public const string PlanType = nameof(PlanType);
|
||||
public const string Seats = nameof(Seats);
|
||||
public const string MaxCollections = nameof(MaxCollections);
|
||||
public const string UsePolicies = nameof(UsePolicies);
|
||||
public const string UseSso = nameof(UseSso);
|
||||
public const string UseKeyConnector = nameof(UseKeyConnector);
|
||||
public const string UseScim = nameof(UseScim);
|
||||
public const string UseGroups = nameof(UseGroups);
|
||||
public const string UseEvents = nameof(UseEvents);
|
||||
public const string UseDirectory = nameof(UseDirectory);
|
||||
public const string UseTotp = nameof(UseTotp);
|
||||
public const string Use2fa = nameof(Use2fa);
|
||||
public const string UseApi = nameof(UseApi);
|
||||
public const string UseResetPassword = nameof(UseResetPassword);
|
||||
public const string MaxStorageGb = nameof(MaxStorageGb);
|
||||
public const string SelfHost = nameof(SelfHost);
|
||||
public const string UsersGetPremium = nameof(UsersGetPremium);
|
||||
public const string UseCustomPermissions = nameof(UseCustomPermissions);
|
||||
public const string Issued = nameof(Issued);
|
||||
public const string UsePasswordManager = nameof(UsePasswordManager);
|
||||
public const string UseSecretsManager = nameof(UseSecretsManager);
|
||||
public const string SmSeats = nameof(SmSeats);
|
||||
public const string SmServiceAccounts = nameof(SmServiceAccounts);
|
||||
public const string LimitCollectionCreationDeletion = nameof(LimitCollectionCreationDeletion);
|
||||
public const string AllowAdminAccessToAllCollectionItems = nameof(AllowAdminAccessToAllCollectionItems);
|
||||
public const string Expires = nameof(Expires);
|
||||
public const string Refresh = nameof(Refresh);
|
||||
public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod);
|
||||
public const string Trial = nameof(Trial);
|
||||
}
|
||||
|
||||
public static class UserLicenseConstants
|
||||
{
|
||||
public const string LicenseType = nameof(LicenseType);
|
||||
public const string LicenseKey = nameof(LicenseKey);
|
||||
public const string Id = nameof(Id);
|
||||
public const string Name = nameof(Name);
|
||||
public const string Email = nameof(Email);
|
||||
public const string Premium = nameof(Premium);
|
||||
public const string MaxStorageGb = nameof(MaxStorageGb);
|
||||
public const string Issued = nameof(Issued);
|
||||
public const string Expires = nameof(Expires);
|
||||
public const string Refresh = nameof(Refresh);
|
||||
public const string Trial = nameof(Trial);
|
||||
}
|
||||
10
src/Core/Billing/Licenses/Models/LicenseContext.cs
Normal file
10
src/Core/Billing/Licenses/Models/LicenseContext.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Models;
|
||||
|
||||
public class LicenseContext
|
||||
{
|
||||
public Guid? InstallationId { get; init; }
|
||||
public required SubscriptionInfo SubscriptionInfo { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Billing.Licenses.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Services;
|
||||
|
||||
public interface ILicenseClaimsFactory<in T>
|
||||
{
|
||||
Task<List<Claim>> GenerateClaims(T entity, LicenseContext licenseContext);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Licenses.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Services.Implementations;
|
||||
|
||||
public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organization>
|
||||
{
|
||||
public Task<List<Claim>> GenerateClaims(Organization entity, LicenseContext licenseContext)
|
||||
{
|
||||
var subscriptionInfo = licenseContext.SubscriptionInfo;
|
||||
var expires = entity.CalculateFreshExpirationDate(subscriptionInfo);
|
||||
var refresh = entity.CalculateFreshRefreshDate(subscriptionInfo, expires);
|
||||
var expirationWithoutGracePeriod = entity.CalculateFreshExpirationDateWithoutGracePeriod(subscriptionInfo, expires);
|
||||
var trial = IsTrialing(entity, subscriptionInfo);
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(nameof(OrganizationLicenseConstants.LicenseType), LicenseType.Organization.ToString()),
|
||||
new Claim(nameof(OrganizationLicenseConstants.LicenseKey), entity.LicenseKey),
|
||||
new(nameof(OrganizationLicenseConstants.InstallationId), licenseContext.InstallationId.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Id), entity.Id.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Name), entity.Name),
|
||||
new(nameof(OrganizationLicenseConstants.BillingEmail), entity.BillingEmail),
|
||||
new(nameof(OrganizationLicenseConstants.Enabled), entity.Enabled.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Plan), entity.Plan),
|
||||
new(nameof(OrganizationLicenseConstants.PlanType), entity.PlanType.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Seats), entity.Seats.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.MaxCollections), entity.MaxCollections.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UsePolicies), entity.UsePolicies.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseSso), entity.UseSso.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseKeyConnector), entity.UseKeyConnector.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseScim), entity.UseScim.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseGroups), entity.UseGroups.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseEvents), entity.UseEvents.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseDirectory), entity.UseDirectory.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseTotp), entity.UseTotp.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Use2fa), entity.Use2fa.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseApi), entity.UseApi.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseResetPassword), entity.UseResetPassword.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.SelfHost), entity.SelfHost.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UsersGetPremium), entity.UsersGetPremium.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseCustomPermissions), entity.UseCustomPermissions.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.UsePasswordManager), entity.UsePasswordManager.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseSecretsManager), entity.UseSecretsManager.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.SmSeats), entity.SmSeats.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.SmServiceAccounts), entity.SmServiceAccounts.ToString()),
|
||||
// LimitCollectionCreationDeletion was split and removed from the
|
||||
// license. Left here with an assignment from the new values for
|
||||
// backwards compatibility.
|
||||
new(nameof(OrganizationLicenseConstants.LimitCollectionCreationDeletion),
|
||||
(entity.LimitCollectionCreation || entity.LimitCollectionDeletion).ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.AllowAdminAccessToAllCollectionItems), entity.AllowAdminAccessToAllCollectionItems.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Expires), expires.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()),
|
||||
};
|
||||
|
||||
if (entity.BusinessName is not null)
|
||||
{
|
||||
claims.Add(new Claim(nameof(OrganizationLicenseConstants.BusinessName), entity.BusinessName));
|
||||
}
|
||||
|
||||
return Task.FromResult(claims);
|
||||
}
|
||||
|
||||
private static bool IsTrialing(Organization org, SubscriptionInfo subscriptionInfo) =>
|
||||
subscriptionInfo?.Subscription is null
|
||||
? org.PlanType != PlanType.Custom || !org.ExpirationDate.HasValue
|
||||
: subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Billing.Licenses.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Services.Implementations;
|
||||
|
||||
public class UserLicenseClaimsFactory : ILicenseClaimsFactory<User>
|
||||
{
|
||||
public Task<List<Claim>> GenerateClaims(User entity, LicenseContext licenseContext)
|
||||
{
|
||||
var subscriptionInfo = licenseContext.SubscriptionInfo;
|
||||
|
||||
var expires = subscriptionInfo.UpcomingInvoice?.Date?.AddDays(7) ?? entity.PremiumExpirationDate?.AddDays(7);
|
||||
var refresh = subscriptionInfo.UpcomingInvoice?.Date ?? entity.PremiumExpirationDate;
|
||||
var trial = (subscriptionInfo.Subscription?.TrialEndDate.HasValue ?? false) &&
|
||||
subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow;
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(nameof(UserLicenseConstants.LicenseType), LicenseType.User.ToString()),
|
||||
new(nameof(UserLicenseConstants.LicenseKey), entity.LicenseKey),
|
||||
new(nameof(UserLicenseConstants.Id), entity.Id.ToString()),
|
||||
new(nameof(UserLicenseConstants.Name), entity.Name),
|
||||
new(nameof(UserLicenseConstants.Email), entity.Email),
|
||||
new(nameof(UserLicenseConstants.Premium), entity.Premium.ToString()),
|
||||
new(nameof(UserLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()),
|
||||
new(nameof(UserLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(UserLicenseConstants.Expires), expires.ToString()),
|
||||
new(nameof(UserLicenseConstants.Refresh), refresh.ToString()),
|
||||
new(nameof(UserLicenseConstants.Trial), trial.ToString()),
|
||||
};
|
||||
|
||||
return Task.FromResult(claims);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.Billing.Repositories;
|
||||
|
||||
public interface IOrganizationInstallationRepository : IRepository<OrganizationInstallation, Guid>
|
||||
{
|
||||
Task<OrganizationInstallation> GetByInstallationIdAsync(Guid installationId);
|
||||
Task<ICollection<OrganizationInstallation>> GetByOrganizationIdAsync(Guid organizationId);
|
||||
}
|
||||
@@ -141,13 +141,13 @@ public interface ISubscriberService
|
||||
TaxInformation taxInformation);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the subscriber's pending bank account using the provided <paramref name="microdeposits"/>.
|
||||
/// Verifies the subscriber's pending bank account using the provided <paramref name="descriptorCode"/>.
|
||||
/// </summary>
|
||||
/// <param name="subscriber">The subscriber to verify the bank account for.</param>
|
||||
/// <param name="microdeposits">Deposits made to the subscriber's bank account in order to ensure they have access to it.
|
||||
/// <param name="descriptorCode">The code attached to a deposit made to the subscriber's bank account in order to ensure they have access to it.
|
||||
/// <a href="https://docs.stripe.com/payments/ach-debit/set-up-payment">Learn more.</a></param>
|
||||
/// <returns></returns>
|
||||
Task VerifyBankAccount(
|
||||
ISubscriber subscriber,
|
||||
(long, long) microdeposits);
|
||||
string descriptorCode);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
@@ -650,41 +651,53 @@ public class SubscriberService(
|
||||
|
||||
public async Task VerifyBankAccount(
|
||||
ISubscriber subscriber,
|
||||
(long, long) microdeposits)
|
||||
string descriptorCode)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subscriber);
|
||||
|
||||
var setupIntentId = await setupIntentCache.Get(subscriber.Id);
|
||||
|
||||
if (string.IsNullOrEmpty(setupIntentId))
|
||||
{
|
||||
logger.LogError("No setup intent ID exists to verify for subscriber with ID ({SubscriberID})", subscriber.Id);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var (amount1, amount2) = microdeposits;
|
||||
|
||||
await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId, new SetupIntentVerifyMicrodepositsOptions
|
||||
try
|
||||
{
|
||||
Amounts = [amount1, amount2]
|
||||
});
|
||||
await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId,
|
||||
new SetupIntentVerifyMicrodepositsOptions { DescriptorCode = descriptorCode });
|
||||
|
||||
var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId);
|
||||
var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId);
|
||||
|
||||
await stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId, new PaymentMethodAttachOptions
|
||||
{
|
||||
Customer = subscriber.GatewayCustomerId
|
||||
});
|
||||
await stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
|
||||
new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId });
|
||||
|
||||
await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId,
|
||||
new CustomerUpdateOptions
|
||||
{
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId,
|
||||
new CustomerUpdateOptions
|
||||
{
|
||||
DefaultPaymentMethod = setupIntent.PaymentMethodId
|
||||
}
|
||||
});
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = setupIntent.PaymentMethodId
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (StripeException stripeException)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stripeException.StripeError?.Code))
|
||||
{
|
||||
var message = stripeException.StripeError.Code switch
|
||||
{
|
||||
StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationAttemptsExceeded => "You have exceeded the number of allowed verification attempts. Please contact support.",
|
||||
StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationDescriptorCodeMismatch => "The verification code you provided does not match the one sent to your bank account. Please try again.",
|
||||
StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationTimeout => "Your bank account was not verified within the required time period. Please contact support.",
|
||||
_ => BillingException.DefaultMessage
|
||||
};
|
||||
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
|
||||
logger.LogError(stripeException, "An unhandled Stripe exception was thrown while verifying subscriber's ({SubscriberID}) bank account", subscriber.Id);
|
||||
throw new BillingException();
|
||||
}
|
||||
}
|
||||
|
||||
#region Shared Utilities
|
||||
@@ -768,8 +781,9 @@ public class SubscriberService(
|
||||
{
|
||||
var metadata = customer.Metadata ?? new Dictionary<string, string>();
|
||||
|
||||
if (metadata.ContainsKey(BraintreeCustomerIdKey))
|
||||
if (metadata.TryGetValue(BraintreeCustomerIdKey, out var value))
|
||||
{
|
||||
metadata[BraintreeCustomerIdOldKey] = value;
|
||||
metadata[BraintreeCustomerIdKey] = null;
|
||||
|
||||
await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
|
||||
@@ -57,7 +57,7 @@ public static class AuthConstants
|
||||
public static readonly RangeConstant ARGON2_ITERATIONS = new(2, 10, 3);
|
||||
public static readonly RangeConstant ARGON2_MEMORY = new(15, 1024, 64);
|
||||
public static readonly RangeConstant ARGON2_PARALLELISM = new(1, 16, 4);
|
||||
|
||||
public static readonly string NewDeviceVerificationExceptionCacheKeyFormat = "NewDeviceVerificationException_{0}";
|
||||
}
|
||||
|
||||
public class RangeConstant
|
||||
@@ -123,12 +123,12 @@ public static class FeatureFlagKeys
|
||||
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
||||
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||
public const string SSHAgent = "ssh-agent";
|
||||
public const string SSHVersionCheckQAOverride = "ssh-version-check-qa-override";
|
||||
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
||||
public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor";
|
||||
public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2";
|
||||
public const string MembersTwoFAQueryOptimization = "ac-1698-members-two-fa-query-optimization";
|
||||
public const string NativeCarouselFlow = "native-carousel-flow";
|
||||
public const string NativeCreateAccountFlow = "native-create-account-flow";
|
||||
public const string AccountDeprovisioning = "pm-10308-account-deprovisioning";
|
||||
@@ -144,8 +144,6 @@ public static class FeatureFlagKeys
|
||||
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
||||
public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises";
|
||||
public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions";
|
||||
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
public const string NewDeviceVerification = "new-device-verification";
|
||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||
@@ -154,7 +152,17 @@ public static class FeatureFlagKeys
|
||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
||||
public const string SecurityTasks = "security-tasks";
|
||||
public const string PM14401_ScaleMSPOnClientOrganizationUpdate = "PM-14401-scale-msp-on-client-organization-update";
|
||||
public const string PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission";
|
||||
public const string DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship";
|
||||
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
|
||||
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
||||
public const string InlineMenuTotp = "inline-menu-totp";
|
||||
public const string PM12443RemovePagingLogic = "pm-12443-remove-paging-logic";
|
||||
public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor";
|
||||
public const string PromoteProviderServiceUserTool = "pm-15128-promote-provider-service-user-tool";
|
||||
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
||||
public const string AuthenticatorSynciOS = "enable-authenticator-sync-ios";
|
||||
public const string AuthenticatorSyncAndroid = "enable-authenticator-sync-android";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
@@ -170,7 +178,6 @@ public static class FeatureFlagKeys
|
||||
return new Dictionary<string, string>()
|
||||
{
|
||||
{ DuoRedirect, "true" },
|
||||
{ CipherKeyEncryption, "true" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.37" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.47" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.7" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.64" />
|
||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||
@@ -34,9 +34,9 @@
|
||||
<PackageReference Include="DnsClient" Version="1.8.0" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
||||
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
||||
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.45.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.46.0" />
|
||||
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.1" />
|
||||
@@ -44,7 +44,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.10" />
|
||||
<PackageReference Include="Quartz" Version="3.9.0" />
|
||||
<PackageReference Include="Quartz" Version="3.13.1" />
|
||||
<PackageReference Include="SendGrid" Version="9.29.3" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
@@ -54,7 +54,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="4.0.0" />
|
||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||
<PackageReference Include="Braintree" Version="5.27.0" />
|
||||
<PackageReference Include="Braintree" Version="5.28.0" />
|
||||
<PackageReference Include="Stripe.net" Version="45.14.0" />
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||
|
||||
@@ -25,6 +25,7 @@ public enum PushType : byte
|
||||
AuthRequestResponse = 16,
|
||||
|
||||
SyncOrganizations = 17,
|
||||
SyncOrganizationStatusChanged = 18,
|
||||
|
||||
SyncNotification = 18,
|
||||
SyncNotification = 19,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Quartz;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Quartz;
|
||||
using Quartz.Spi;
|
||||
|
||||
namespace Bit.Core.Jobs;
|
||||
@@ -14,7 +15,8 @@ public class JobFactory : IJobFactory
|
||||
|
||||
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
|
||||
{
|
||||
return _container.GetService(bundle.JobDetail.JobType) as IJob;
|
||||
var scope = _container.CreateScope();
|
||||
return scope.ServiceProvider.GetService(bundle.JobDetail.JobType) as IJob;
|
||||
}
|
||||
|
||||
public void ReturnJob(IJob job)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Commands.Interfaces;
|
||||
|
||||
public interface IRegenerateUserAsymmetricKeysCommand
|
||||
{
|
||||
Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys,
|
||||
ICollection<OrganizationUser> usersOrganizationAccounts,
|
||||
ICollection<EmergencyAccessDetails> designatedEmergencyAccess);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Commands;
|
||||
|
||||
public class RegenerateUserAsymmetricKeysCommand : IRegenerateUserAsymmetricKeysCommand
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ILogger<RegenerateUserAsymmetricKeysCommand> _logger;
|
||||
private readonly IUserAsymmetricKeysRepository _userAsymmetricKeysRepository;
|
||||
private readonly IPushNotificationService _pushService;
|
||||
|
||||
public RegenerateUserAsymmetricKeysCommand(
|
||||
ICurrentContext currentContext,
|
||||
IUserAsymmetricKeysRepository userAsymmetricKeysRepository,
|
||||
IPushNotificationService pushService,
|
||||
ILogger<RegenerateUserAsymmetricKeysCommand> logger)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_logger = logger;
|
||||
_userAsymmetricKeysRepository = userAsymmetricKeysRepository;
|
||||
_pushService = pushService;
|
||||
}
|
||||
|
||||
public async Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys,
|
||||
ICollection<OrganizationUser> usersOrganizationAccounts,
|
||||
ICollection<EmergencyAccessDetails> designatedEmergencyAccess)
|
||||
{
|
||||
var userId = _currentContext.UserId;
|
||||
if (!userId.HasValue ||
|
||||
userAsymmetricKeys.UserId != userId.Value ||
|
||||
usersOrganizationAccounts.Any(ou => ou.UserId != userId) ||
|
||||
designatedEmergencyAccess.Any(dea => dea.GranteeId != userId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var inOrganizations = usersOrganizationAccounts.Any(ou =>
|
||||
ou.Status is OrganizationUserStatusType.Confirmed or OrganizationUserStatusType.Revoked);
|
||||
var hasDesignatedEmergencyAccess = designatedEmergencyAccess.Any(x =>
|
||||
x.Status is EmergencyAccessStatusType.Confirmed or EmergencyAccessStatusType.RecoveryApproved
|
||||
or EmergencyAccessStatusType.RecoveryInitiated);
|
||||
|
||||
_logger.LogInformation(
|
||||
"User asymmetric keys regeneration requested. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}",
|
||||
userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType);
|
||||
|
||||
// For now, don't regenerate asymmetric keys for user's with organization membership and designated emergency access.
|
||||
if (inOrganizations || hasDesignatedEmergencyAccess)
|
||||
{
|
||||
throw new BadRequestException("Key regeneration not supported for this user.");
|
||||
}
|
||||
|
||||
await _userAsymmetricKeysRepository.RegenerateUserAsymmetricKeysAsync(userAsymmetricKeys);
|
||||
_logger.LogInformation(
|
||||
"User's asymmetric keys regenerated. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}",
|
||||
userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType);
|
||||
|
||||
await _pushService.PushSyncSettingsAsync(userId.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Bit.Core.KeyManagement.Commands;
|
||||
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.KeyManagement;
|
||||
|
||||
public static class KeyManagementServiceCollectionExtensions
|
||||
{
|
||||
public static void AddKeyManagementServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddKeyManagementCommands();
|
||||
}
|
||||
|
||||
private static void AddKeyManagementCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Data;
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
public class RotateUserKeyData
|
||||
{
|
||||
9
src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs
Normal file
9
src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
#nullable enable
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
public class UserAsymmetricKeys
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public required string PublicKey { get; set; }
|
||||
public required string UserKeyEncryptedPrivateKey { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#nullable enable
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Repositories;
|
||||
|
||||
public interface IUserAsymmetricKeysRepository
|
||||
{
|
||||
Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserKey;
|
||||
namespace Bit.Core.KeyManagement.UserKey;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for rotation of a user key and updating database with re-encrypted data
|
||||
@@ -1,13 +1,13 @@
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserKey.Implementations;
|
||||
namespace Bit.Core.KeyManagement.UserKey.Implementations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class RotateUserKeyCommand : IRotateUserKeyCommand
|
||||
@@ -0,0 +1,23 @@
|
||||
{{#>TitleContactUsHtmlLayout}}
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="display: table; width:100%; padding: 30px; text-align: left;" align="center">
|
||||
<tr>
|
||||
<td display="display: table-cell">
|
||||
As a member of {{OrganizationName}}, your Bitwarden account is claimed and owned by your organization.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;">
|
||||
<b>Here's what that means:</b>
|
||||
<ul>
|
||||
<li>Your administrators can delete your account at any time</li>
|
||||
<li>You cannot leave the organization</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;">
|
||||
For more information, please refer to the following help article: <a href="https://bitwarden.com/help/claimed-accounts">Claimed Accounts</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/TitleContactUsHtmlLayout}}
|
||||
@@ -0,0 +1,7 @@
|
||||
As a member of {{OrganizationName}}, your Bitwarden account is claimed and owned by your organization.
|
||||
|
||||
Here's what that means:
|
||||
- Your administrators can delete your account at any time
|
||||
- You cannot leave the organization
|
||||
|
||||
For more information, please refer to the following help article: Claimed Accounts (https://bitwarden.com/help/claimed-accounts)
|
||||
@@ -0,0 +1,14 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top" align="left">
|
||||
Your user account has been revoked from the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{OrganizationName}}</b> organization because your account is part of multiple organizations. Before you can re-join {{OrganizationName}}, you must first leave all other organizations.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top" align="left">
|
||||
To leave an organization, first log into the <a href="https://vault.bitwarden.com/#/login">web app</a>, select the three dot menu next to the organization name, and select Leave.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
||||
@@ -0,0 +1,5 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Your user account has been revoked from the {{OrganizationName}} organization because your account is part of multiple organizations. Before you can rejoin {{OrganizationName}}, you must first leave all other organizations.
|
||||
|
||||
To leave an organization, first log in the web app (https://vault.bitwarden.com/#/login), select the three dot menu next to the organization name, and select Leave.
|
||||
{{/BasicTextLayout}}
|
||||
@@ -0,0 +1,15 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top" align="left">
|
||||
Your user account has been revoked from the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{OrganizationName}}</b> organization because you do not have two-step login configured. Before you can re-join {{OrganizationName}}, you need to set up two-step login on your user account.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top" align="left">
|
||||
Learn how to enable two-step login on your user account at
|
||||
<a target="_blank" href="https://help.bitwarden.com/article/setup-two-step-login/" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">https://help.bitwarden.com/article/setup-two-step-login/</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
||||
@@ -0,0 +1,7 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Your user account has been removed from the {{OrganizationName}} organization because you do not have two-step login
|
||||
configured. Before you can re-join this organization you need to set up two-step login on your user account.
|
||||
|
||||
Learn how to enable two-step login on your user account at
|
||||
https://help.bitwarden.com/article/setup-two-step-login/
|
||||
{{/BasicTextLayout}}
|
||||
@@ -0,0 +1,27 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;" valign="top">
|
||||
The domain {{DomainName}} in your Bitwarden organization could not be claimed.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
Check the corresponding record in your domain host. Then reclaim this domain in Bitwarden to use it for your organization.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||
The domain will be removed from your organization in 7 days if it is not claimed.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
Manage Domains
|
||||
</a>
|
||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
||||
@@ -0,0 +1,10 @@
|
||||
{{#>BasicTextLayout}}
|
||||
The domain {{DomainName}} in your Bitwarden organization could not be claimed.
|
||||
|
||||
Check the corresponding record in your domain host. Then reclaim this domain in Bitwarden to use it for your organization.
|
||||
|
||||
The domain will be removed from your organization in 7 days if it is not claimed.
|
||||
|
||||
{{Url}}
|
||||
|
||||
{{/BasicTextLayout}}
|
||||
@@ -12,6 +12,7 @@ public interface ILicense
|
||||
bool Trial { get; set; }
|
||||
string Hash { get; set; }
|
||||
string Signature { get; set; }
|
||||
string Token { get; set; }
|
||||
byte[] SignatureBytes { get; }
|
||||
byte[] GetDataBytes(bool forHash = false);
|
||||
byte[] ComputeHash();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -55,7 +57,7 @@ public class OrganizationLicense : ILicense
|
||||
SmServiceAccounts = org.SmServiceAccounts;
|
||||
|
||||
// Deprecated. Left for backwards compatibility with old license versions.
|
||||
LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion;
|
||||
LimitCollectionCreationDeletion = org.LimitCollectionCreation || org.LimitCollectionDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems;
|
||||
//
|
||||
|
||||
@@ -151,6 +153,7 @@ public class OrganizationLicense : ILicense
|
||||
public LicenseType? LicenseType { get; set; }
|
||||
public string Hash { get; set; }
|
||||
public string Signature { get; set; }
|
||||
public string Token { get; set; }
|
||||
[JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature);
|
||||
|
||||
/// <summary>
|
||||
@@ -176,6 +179,7 @@ public class OrganizationLicense : ILicense
|
||||
!p.Name.Equals(nameof(Signature)) &&
|
||||
!p.Name.Equals(nameof(SignatureBytes)) &&
|
||||
!p.Name.Equals(nameof(LicenseType)) &&
|
||||
!p.Name.Equals(nameof(Token)) &&
|
||||
// UsersGetPremium was added in Version 2
|
||||
(Version >= 2 || !p.Name.Equals(nameof(UsersGetPremium))) &&
|
||||
// UseEvents was added in Version 3
|
||||
@@ -236,8 +240,65 @@ public class OrganizationLicense : ILicense
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception)
|
||||
public bool CanUse(
|
||||
IGlobalSettings globalSettings,
|
||||
ILicensingService licensingService,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
out string exception)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null)
|
||||
{
|
||||
return ObsoleteCanUse(globalSettings, licensingService, out exception);
|
||||
}
|
||||
|
||||
var errorMessages = new StringBuilder();
|
||||
|
||||
var enabled = claimsPrincipal.GetValue<bool>(nameof(Enabled));
|
||||
if (!enabled)
|
||||
{
|
||||
errorMessages.AppendLine("Your cloud-hosted organization is currently disabled.");
|
||||
}
|
||||
|
||||
var installationId = claimsPrincipal.GetValue<Guid>(nameof(InstallationId));
|
||||
if (installationId != globalSettings.Installation.Id)
|
||||
{
|
||||
errorMessages.AppendLine("The installation ID does not match the current installation.");
|
||||
}
|
||||
|
||||
var selfHost = claimsPrincipal.GetValue<bool>(nameof(SelfHost));
|
||||
if (!selfHost)
|
||||
{
|
||||
errorMessages.AppendLine("The license does not allow for on-premise hosting of organizations.");
|
||||
}
|
||||
|
||||
var licenseType = claimsPrincipal.GetValue<LicenseType>(nameof(LicenseType));
|
||||
if (licenseType != Enums.LicenseType.Organization)
|
||||
{
|
||||
errorMessages.AppendLine("Premium licenses cannot be applied to an organization. " +
|
||||
"Upload this license from your personal account settings page.");
|
||||
}
|
||||
|
||||
if (errorMessages.Length > 0)
|
||||
{
|
||||
exception = $"Invalid license. {errorMessages.ToString().TrimEnd()}";
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do not extend this method. It is only here for backwards compatibility with old licenses.
|
||||
/// Instead, extend the CanUse method using the ClaimsPrincipal.
|
||||
/// </summary>
|
||||
/// <param name="globalSettings"></param>
|
||||
/// <param name="licensingService"></param>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
private bool ObsoleteCanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception)
|
||||
{
|
||||
// Do not extend this method. It is only here for backwards compatibility with old licenses.
|
||||
var errorMessages = new StringBuilder();
|
||||
|
||||
if (!Enabled)
|
||||
@@ -291,101 +352,177 @@ public class OrganizationLicense : ILicense
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool VerifyData(Organization organization, IGlobalSettings globalSettings)
|
||||
public bool VerifyData(
|
||||
Organization organization,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
IGlobalSettings globalSettings)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Token))
|
||||
{
|
||||
return ObsoleteVerifyData(organization, globalSettings);
|
||||
}
|
||||
|
||||
var issued = claimsPrincipal.GetValue<DateTime>(nameof(Issued));
|
||||
var expires = claimsPrincipal.GetValue<DateTime>(nameof(Expires));
|
||||
var installationId = claimsPrincipal.GetValue<Guid>(nameof(InstallationId));
|
||||
var licenseKey = claimsPrincipal.GetValue<string>(nameof(LicenseKey));
|
||||
var enabled = claimsPrincipal.GetValue<bool>(nameof(Enabled));
|
||||
var planType = claimsPrincipal.GetValue<PlanType>(nameof(PlanType));
|
||||
var seats = claimsPrincipal.GetValue<int?>(nameof(Seats));
|
||||
var maxCollections = claimsPrincipal.GetValue<short?>(nameof(MaxCollections));
|
||||
var useGroups = claimsPrincipal.GetValue<bool>(nameof(UseGroups));
|
||||
var useDirectory = claimsPrincipal.GetValue<bool>(nameof(UseDirectory));
|
||||
var useTotp = claimsPrincipal.GetValue<bool>(nameof(UseTotp));
|
||||
var selfHost = claimsPrincipal.GetValue<bool>(nameof(SelfHost));
|
||||
var name = claimsPrincipal.GetValue<string>(nameof(Name));
|
||||
var usersGetPremium = claimsPrincipal.GetValue<bool>(nameof(UsersGetPremium));
|
||||
var useEvents = claimsPrincipal.GetValue<bool>(nameof(UseEvents));
|
||||
var use2fa = claimsPrincipal.GetValue<bool>(nameof(Use2fa));
|
||||
var useApi = claimsPrincipal.GetValue<bool>(nameof(UseApi));
|
||||
var usePolicies = claimsPrincipal.GetValue<bool>(nameof(UsePolicies));
|
||||
var useSso = claimsPrincipal.GetValue<bool>(nameof(UseSso));
|
||||
var useResetPassword = claimsPrincipal.GetValue<bool>(nameof(UseResetPassword));
|
||||
var useKeyConnector = claimsPrincipal.GetValue<bool>(nameof(UseKeyConnector));
|
||||
var useScim = claimsPrincipal.GetValue<bool>(nameof(UseScim));
|
||||
var useCustomPermissions = claimsPrincipal.GetValue<bool>(nameof(UseCustomPermissions));
|
||||
var useSecretsManager = claimsPrincipal.GetValue<bool>(nameof(UseSecretsManager));
|
||||
var usePasswordManager = claimsPrincipal.GetValue<bool>(nameof(UsePasswordManager));
|
||||
var smSeats = claimsPrincipal.GetValue<int?>(nameof(SmSeats));
|
||||
var smServiceAccounts = claimsPrincipal.GetValue<int?>(nameof(SmServiceAccounts));
|
||||
|
||||
return issued <= DateTime.UtcNow &&
|
||||
expires >= DateTime.UtcNow &&
|
||||
installationId == globalSettings.Installation.Id &&
|
||||
licenseKey == organization.LicenseKey &&
|
||||
enabled == organization.Enabled &&
|
||||
planType == organization.PlanType &&
|
||||
seats == organization.Seats &&
|
||||
maxCollections == organization.MaxCollections &&
|
||||
useGroups == organization.UseGroups &&
|
||||
useDirectory == organization.UseDirectory &&
|
||||
useTotp == organization.UseTotp &&
|
||||
selfHost == organization.SelfHost &&
|
||||
name == organization.Name &&
|
||||
usersGetPremium == organization.UsersGetPremium &&
|
||||
useEvents == organization.UseEvents &&
|
||||
use2fa == organization.Use2fa &&
|
||||
useApi == organization.UseApi &&
|
||||
usePolicies == organization.UsePolicies &&
|
||||
useSso == organization.UseSso &&
|
||||
useResetPassword == organization.UseResetPassword &&
|
||||
useKeyConnector == organization.UseKeyConnector &&
|
||||
useScim == organization.UseScim &&
|
||||
useCustomPermissions == organization.UseCustomPermissions &&
|
||||
useSecretsManager == organization.UseSecretsManager &&
|
||||
usePasswordManager == organization.UsePasswordManager &&
|
||||
smSeats == organization.SmSeats &&
|
||||
smServiceAccounts == organization.SmServiceAccounts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do not extend this method. It is only here for backwards compatibility with old licenses.
|
||||
/// Instead, extend the VerifyData method using the ClaimsPrincipal.
|
||||
/// </summary>
|
||||
/// <param name="organization"></param>
|
||||
/// <param name="globalSettings"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotSupportedException"></exception>
|
||||
private bool ObsoleteVerifyData(Organization organization, IGlobalSettings globalSettings)
|
||||
{
|
||||
// Do not extend this method. It is only here for backwards compatibility with old licenses.
|
||||
if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ValidLicenseVersion)
|
||||
if (!ValidLicenseVersion)
|
||||
{
|
||||
var valid =
|
||||
globalSettings.Installation.Id == InstallationId &&
|
||||
organization.LicenseKey != null && organization.LicenseKey.Equals(LicenseKey) &&
|
||||
organization.Enabled == Enabled &&
|
||||
organization.PlanType == PlanType &&
|
||||
organization.Seats == Seats &&
|
||||
organization.MaxCollections == MaxCollections &&
|
||||
organization.UseGroups == UseGroups &&
|
||||
organization.UseDirectory == UseDirectory &&
|
||||
organization.UseTotp == UseTotp &&
|
||||
organization.SelfHost == SelfHost &&
|
||||
organization.Name.Equals(Name);
|
||||
throw new NotSupportedException($"Version {Version} is not supported.");
|
||||
}
|
||||
|
||||
if (valid && Version >= 2)
|
||||
{
|
||||
valid = organization.UsersGetPremium == UsersGetPremium;
|
||||
}
|
||||
var valid =
|
||||
globalSettings.Installation.Id == InstallationId &&
|
||||
organization.LicenseKey != null && organization.LicenseKey.Equals(LicenseKey) &&
|
||||
organization.Enabled == Enabled &&
|
||||
organization.PlanType == PlanType &&
|
||||
organization.Seats == Seats &&
|
||||
organization.MaxCollections == MaxCollections &&
|
||||
organization.UseGroups == UseGroups &&
|
||||
organization.UseDirectory == UseDirectory &&
|
||||
organization.UseTotp == UseTotp &&
|
||||
organization.SelfHost == SelfHost &&
|
||||
organization.Name.Equals(Name);
|
||||
|
||||
if (valid && Version >= 3)
|
||||
{
|
||||
valid = organization.UseEvents == UseEvents;
|
||||
}
|
||||
if (valid && Version >= 2)
|
||||
{
|
||||
valid = organization.UsersGetPremium == UsersGetPremium;
|
||||
}
|
||||
|
||||
if (valid && Version >= 4)
|
||||
{
|
||||
valid = organization.Use2fa == Use2fa;
|
||||
}
|
||||
if (valid && Version >= 3)
|
||||
{
|
||||
valid = organization.UseEvents == UseEvents;
|
||||
}
|
||||
|
||||
if (valid && Version >= 5)
|
||||
{
|
||||
valid = organization.UseApi == UseApi;
|
||||
}
|
||||
if (valid && Version >= 4)
|
||||
{
|
||||
valid = organization.Use2fa == Use2fa;
|
||||
}
|
||||
|
||||
if (valid && Version >= 6)
|
||||
{
|
||||
valid = organization.UsePolicies == UsePolicies;
|
||||
}
|
||||
if (valid && Version >= 5)
|
||||
{
|
||||
valid = organization.UseApi == UseApi;
|
||||
}
|
||||
|
||||
if (valid && Version >= 7)
|
||||
{
|
||||
valid = organization.UseSso == UseSso;
|
||||
}
|
||||
if (valid && Version >= 6)
|
||||
{
|
||||
valid = organization.UsePolicies == UsePolicies;
|
||||
}
|
||||
|
||||
if (valid && Version >= 8)
|
||||
{
|
||||
valid = organization.UseResetPassword == UseResetPassword;
|
||||
}
|
||||
if (valid && Version >= 7)
|
||||
{
|
||||
valid = organization.UseSso == UseSso;
|
||||
}
|
||||
|
||||
if (valid && Version >= 9)
|
||||
{
|
||||
valid = organization.UseKeyConnector == UseKeyConnector;
|
||||
}
|
||||
if (valid && Version >= 8)
|
||||
{
|
||||
valid = organization.UseResetPassword == UseResetPassword;
|
||||
}
|
||||
|
||||
if (valid && Version >= 10)
|
||||
{
|
||||
valid = organization.UseScim == UseScim;
|
||||
}
|
||||
if (valid && Version >= 9)
|
||||
{
|
||||
valid = organization.UseKeyConnector == UseKeyConnector;
|
||||
}
|
||||
|
||||
if (valid && Version >= 11)
|
||||
{
|
||||
valid = organization.UseCustomPermissions == UseCustomPermissions;
|
||||
}
|
||||
if (valid && Version >= 10)
|
||||
{
|
||||
valid = organization.UseScim == UseScim;
|
||||
}
|
||||
|
||||
/*Version 12 added ExpirationWithoutDatePeriod, but that property is informational only and is not saved
|
||||
if (valid && Version >= 11)
|
||||
{
|
||||
valid = organization.UseCustomPermissions == UseCustomPermissions;
|
||||
}
|
||||
|
||||
/*Version 12 added ExpirationWithoutDatePeriod, but that property is informational only and is not saved
|
||||
to the Organization object. It's validated as part of the hash but does not need to be validated here.
|
||||
*/
|
||||
|
||||
if (valid && Version >= 13)
|
||||
{
|
||||
valid = organization.UseSecretsManager == UseSecretsManager &&
|
||||
organization.UsePasswordManager == UsePasswordManager &&
|
||||
organization.SmSeats == SmSeats &&
|
||||
organization.SmServiceAccounts == SmServiceAccounts;
|
||||
}
|
||||
if (valid && Version >= 13)
|
||||
{
|
||||
valid = organization.UseSecretsManager == UseSecretsManager &&
|
||||
organization.UsePasswordManager == UsePasswordManager &&
|
||||
organization.SmSeats == SmSeats &&
|
||||
organization.SmServiceAccounts == SmServiceAccounts;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* Version 14 added LimitCollectionCreationDeletion and Version
|
||||
* 15 added AllowAdminAccessToAllCollectionItems, however they
|
||||
* are no longer used and are intentionally excluded from
|
||||
* validation.
|
||||
*/
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Version {Version} is not supported.");
|
||||
return valid;
|
||||
}
|
||||
|
||||
public bool VerifySignature(X509Certificate2 certificate)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
@@ -70,6 +72,7 @@ public class UserLicense : ILicense
|
||||
public LicenseType? LicenseType { get; set; }
|
||||
public string Hash { get; set; }
|
||||
public string Signature { get; set; }
|
||||
public string Token { get; set; }
|
||||
[JsonIgnore]
|
||||
public byte[] SignatureBytes => Convert.FromBase64String(Signature);
|
||||
|
||||
@@ -84,6 +87,7 @@ public class UserLicense : ILicense
|
||||
!p.Name.Equals(nameof(Signature)) &&
|
||||
!p.Name.Equals(nameof(SignatureBytes)) &&
|
||||
!p.Name.Equals(nameof(LicenseType)) &&
|
||||
!p.Name.Equals(nameof(Token)) &&
|
||||
(
|
||||
!forHash ||
|
||||
(
|
||||
@@ -113,8 +117,47 @@ public class UserLicense : ILicense
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanUse(User user, out string exception)
|
||||
public bool CanUse(User user, ClaimsPrincipal claimsPrincipal, out string exception)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null)
|
||||
{
|
||||
return ObsoleteCanUse(user, out exception);
|
||||
}
|
||||
|
||||
var errorMessages = new StringBuilder();
|
||||
|
||||
if (!user.EmailVerified)
|
||||
{
|
||||
errorMessages.AppendLine("The user's email is not verified.");
|
||||
}
|
||||
|
||||
var email = claimsPrincipal.GetValue<string>(nameof(Email));
|
||||
if (!email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
errorMessages.AppendLine("The user's email does not match the license email.");
|
||||
}
|
||||
|
||||
if (errorMessages.Length > 0)
|
||||
{
|
||||
exception = $"Invalid license. {errorMessages.ToString().TrimEnd()}";
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do not extend this method. It is only here for backwards compatibility with old licenses.
|
||||
/// Instead, extend the CanUse method using the ClaimsPrincipal.
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotSupportedException"></exception>
|
||||
private bool ObsoleteCanUse(User user, out string exception)
|
||||
{
|
||||
// Do not extend this method. It is only here for backwards compatibility with old licenses.
|
||||
var errorMessages = new StringBuilder();
|
||||
|
||||
if (Issued > DateTime.UtcNow)
|
||||
@@ -152,22 +195,46 @@ public class UserLicense : ILicense
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool VerifyData(User user)
|
||||
public bool VerifyData(User user, ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null)
|
||||
{
|
||||
return ObsoleteVerifyData(user);
|
||||
}
|
||||
|
||||
var licenseKey = claimsPrincipal.GetValue<string>(nameof(LicenseKey));
|
||||
var premium = claimsPrincipal.GetValue<bool>(nameof(Premium));
|
||||
var email = claimsPrincipal.GetValue<string>(nameof(Email));
|
||||
|
||||
return licenseKey == user.LicenseKey &&
|
||||
premium == user.Premium &&
|
||||
email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do not extend this method. It is only here for backwards compatibility with old licenses.
|
||||
/// Instead, extend the VerifyData method using the ClaimsPrincipal.
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotSupportedException"></exception>
|
||||
private bool ObsoleteVerifyData(User user)
|
||||
{
|
||||
// Do not extend this method. It is only here for backwards compatibility with old licenses.
|
||||
if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Version == 1)
|
||||
if (Version != 1)
|
||||
{
|
||||
return
|
||||
user.LicenseKey != null && user.LicenseKey.Equals(LicenseKey) &&
|
||||
user.Premium == Premium &&
|
||||
user.Email.Equals(Email, StringComparison.InvariantCultureIgnoreCase);
|
||||
throw new NotSupportedException($"Version {Version} is not supported.");
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Version {Version} is not supported.");
|
||||
return
|
||||
user.LicenseKey != null && user.LicenseKey.Equals(LicenseKey) &&
|
||||
user.Premium == Premium &&
|
||||
user.Email.Equals(Email, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public bool VerifySignature(X509Certificate2 certificate)
|
||||
|
||||
12
src/Core/Models/Commands/CommandResult.cs
Normal file
12
src/Core/Models/Commands/CommandResult.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Bit.Core.Models.Commands;
|
||||
|
||||
public class CommandResult(IEnumerable<string> errors)
|
||||
{
|
||||
public CommandResult(string error) : this([error]) { }
|
||||
|
||||
public bool Success => ErrorMessages.Count == 0;
|
||||
public bool HasErrors => ErrorMessages.Count > 0;
|
||||
public List<string> ErrorMessages { get; } = errors.ToList();
|
||||
|
||||
public CommandResult() : this(Array.Empty<string>()) { }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations;
|
||||
|
||||
public record ManagedUserDomainClaimedEmails(IEnumerable<string> EmailList, Organization Organization);
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Models.Mail;
|
||||
|
||||
public class ClaimedDomainUserNotificationViewModel : BaseTitleContactUsMailModel
|
||||
{
|
||||
public string OrganizationName { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Models.Mail;
|
||||
|
||||
public class OrganizationUserRevokedForPolicySingleOrgViewModel : BaseMailModel
|
||||
{
|
||||
public string OrganizationName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Models.Mail;
|
||||
|
||||
public class OrganizationUserRevokedForPolicyTwoFactorViewModel : BaseMailModel
|
||||
{
|
||||
public string OrganizationName { get; set; }
|
||||
}
|
||||
@@ -8,10 +8,8 @@ public class ProviderInitiateDeleteModel : BaseMailModel
|
||||
Token,
|
||||
ProviderNameUrlEncoded);
|
||||
|
||||
public string WebVaultUrl { get; set; }
|
||||
public string Token { get; set; }
|
||||
public Guid ProviderId { get; set; }
|
||||
public string SiteName { get; set; }
|
||||
public string ProviderName { get; set; }
|
||||
public string ProviderNameUrlEncoded { get; set; }
|
||||
public string ProviderBillingEmail { get; set; }
|
||||
|
||||
@@ -67,3 +67,9 @@ public class AuthRequestPushNotification
|
||||
public Guid UserId { get; set; }
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationStatusPushNotification
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#nullable enable
|
||||
using Bit.Core.NotificationCenter.Authorization;
|
||||
using Bit.Core.NotificationCenter.Commands;
|
||||
using Bit.Core.NotificationCenter.Commands.Interfaces;
|
||||
using Bit.Core.NotificationCenter.Queries;
|
||||
using Bit.Core.NotificationCenter.Queries.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.NotificationCenter;
|
||||
|
||||
public static class NotificationCenterServiceCollectionExtensions
|
||||
{
|
||||
public static void AddNotificationCenterServices(this IServiceCollection services)
|
||||
{
|
||||
// Authorization Handlers
|
||||
services.AddScoped<IAuthorizationHandler, NotificationAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, NotificationStatusAuthorizationHandler>();
|
||||
// Commands
|
||||
services.AddScoped<ICreateNotificationCommand, CreateNotificationCommand>();
|
||||
services.AddScoped<ICreateNotificationStatusCommand, CreateNotificationStatusCommand>();
|
||||
services.AddScoped<IMarkNotificationDeletedCommand, MarkNotificationDeletedCommand>();
|
||||
services.AddScoped<IMarkNotificationReadCommand, MarkNotificationReadCommand>();
|
||||
services.AddScoped<IUpdateNotificationCommand, UpdateNotificationCommand>();
|
||||
// Queries
|
||||
services.AddScoped<IGetNotificationStatusDetailsForUserQuery, GetNotificationStatusDetailsForUserQuery>();
|
||||
services.AddScoped<IGetNotificationStatusForUserQuery, GetNotificationStatusForUserQuery>();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.NotificationCenter.Models.Data;
|
||||
using Bit.Core.NotificationCenter.Models.Filter;
|
||||
using Bit.Core.NotificationCenter.Queries.Interfaces;
|
||||
@@ -21,8 +22,8 @@ public class GetNotificationStatusDetailsForUserQuery : IGetNotificationStatusDe
|
||||
_notificationRepository = notificationRepository;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<NotificationStatusDetails>> GetByUserIdStatusFilterAsync(
|
||||
NotificationStatusFilter statusFilter)
|
||||
public async Task<PagedResult<NotificationStatusDetails>> GetByUserIdStatusFilterAsync(
|
||||
NotificationStatusFilter statusFilter, PageOptions pageOptions)
|
||||
{
|
||||
if (!_currentContext.UserId.HasValue)
|
||||
{
|
||||
@@ -33,6 +34,6 @@ public class GetNotificationStatusDetailsForUserQuery : IGetNotificationStatusDe
|
||||
|
||||
// Note: only returns the user's notifications - no authorization check needed
|
||||
return await _notificationRepository.GetByUserIdAndStatusAsync(_currentContext.UserId.Value, clientType,
|
||||
statusFilter);
|
||||
statusFilter, pageOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.NotificationCenter.Models.Data;
|
||||
using Bit.Core.NotificationCenter.Models.Filter;
|
||||
|
||||
@@ -6,5 +7,6 @@ namespace Bit.Core.NotificationCenter.Queries.Interfaces;
|
||||
|
||||
public interface IGetNotificationStatusDetailsForUserQuery
|
||||
{
|
||||
Task<IEnumerable<NotificationStatusDetails>> GetByUserIdStatusFilterAsync(NotificationStatusFilter statusFilter);
|
||||
Task<PagedResult<NotificationStatusDetails>> GetByUserIdStatusFilterAsync(NotificationStatusFilter statusFilter,
|
||||
PageOptions pageOptions);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.NotificationCenter.Entities;
|
||||
using Bit.Core.NotificationCenter.Models.Data;
|
||||
using Bit.Core.NotificationCenter.Models.Filter;
|
||||
@@ -22,10 +23,13 @@ public interface INotificationRepository : IRepository<Notification, Guid>
|
||||
/// If both <see cref="NotificationStatusFilter.Read"/> and <see cref="NotificationStatusFilter.Deleted"/>
|
||||
/// are not set, includes notifications without a status.
|
||||
/// </param>
|
||||
/// <param name="pageOptions">
|
||||
/// Pagination options.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Ordered by priority (highest to lowest) and creation date (descending).
|
||||
/// Paged results ordered by priority (descending, highest to lowest) and creation date (descending).
|
||||
/// Includes all fields from <see cref="Notification"/> and <see cref="NotificationStatus"/>
|
||||
/// </returns>
|
||||
Task<IEnumerable<NotificationStatusDetails>> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType,
|
||||
NotificationStatusFilter? statusFilter);
|
||||
Task<PagedResult<NotificationStatusDetails>> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType,
|
||||
NotificationStatusFilter? statusFilter, PageOptions pageOptions);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
@@ -251,6 +252,17 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||
{
|
||||
var message = new OrganizationStatusPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Enabled = organization.Enabled
|
||||
};
|
||||
|
||||
await SendPayloadToOrganizationAsync(organization.Id, PushType.SyncOrganizationStatusChanged, message, false);
|
||||
}
|
||||
|
||||
private string GetContextIdentifier(bool excludeCurrentContext)
|
||||
{
|
||||
if (!excludeCurrentContext)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
@@ -12,15 +14,18 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
|
||||
private readonly IInstallationRepository _installationRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
|
||||
public CloudGetOrganizationLicenseQuery(
|
||||
IInstallationRepository installationRepository,
|
||||
IPaymentService paymentService,
|
||||
ILicensingService licensingService)
|
||||
ILicensingService licensingService,
|
||||
IProviderRepository providerRepository)
|
||||
{
|
||||
_installationRepository = installationRepository;
|
||||
_paymentService = paymentService;
|
||||
_licensingService = licensingService;
|
||||
_providerRepository = providerRepository;
|
||||
}
|
||||
|
||||
public async Task<OrganizationLicense> GetLicenseAsync(Organization organization, Guid installationId,
|
||||
@@ -32,7 +37,22 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
|
||||
throw new BadRequestException("Invalid installation id");
|
||||
}
|
||||
|
||||
var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization);
|
||||
return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version);
|
||||
var subscriptionInfo = await GetSubscriptionAsync(organization);
|
||||
|
||||
return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version)
|
||||
{
|
||||
Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<SubscriptionInfo> GetSubscriptionAsync(Organization organization)
|
||||
{
|
||||
if (organization is not { Status: OrganizationStatusType.Managed })
|
||||
{
|
||||
return await _paymentService.GetSubscriptionAsync(organization);
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||
return await _paymentService.GetSubscriptionAsync(provider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
|
||||
throw new BadRequestException("License is already in use by another organization.");
|
||||
}
|
||||
|
||||
var canUse = license.CanUse(_globalSettings, _licensingService, out var exception) &&
|
||||
var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license);
|
||||
var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception) &&
|
||||
selfHostedOrganization.CanUseLicense(license, out exception);
|
||||
|
||||
if (!canUse)
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
@@ -50,12 +51,16 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddOrganizationGroupCommands();
|
||||
services.AddOrganizationLicenseCommandsQueries();
|
||||
services.AddOrganizationDomainCommandsQueries();
|
||||
services.AddOrganizationSignUpCommands();
|
||||
services.AddOrganizationAuthCommands();
|
||||
services.AddOrganizationUserCommands();
|
||||
services.AddOrganizationUserCommandsQueries();
|
||||
services.AddBaseOrganizationSubscriptionCommandsQueries();
|
||||
}
|
||||
|
||||
private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) =>
|
||||
services.AddScoped<ICloudOrganizationSignUpCommand, CloudOrganizationSignUpCommand>();
|
||||
|
||||
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ICreateOrganizationConnectionCommand, CreateOrganizationConnectionCommand>();
|
||||
@@ -91,6 +96,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
private static void AddOrganizationUserCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IRemoveOrganizationUserCommand, RemoveOrganizationUserCommand>();
|
||||
services.AddScoped<IRevokeNonCompliantOrganizationUserCommand, RevokeNonCompliantOrganizationUserCommand>();
|
||||
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
|
||||
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
||||
services.AddScoped<IDeleteManagedOrganizationUserAccountCommand, DeleteManagedOrganizationUserAccountCommand>();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
#nullable enable
|
||||
@@ -32,4 +32,5 @@ public interface IUserRepository : IRepository<User, Guid>
|
||||
/// <param name="updateDataActions">Registered database calls to update re-encrypted data.</param>
|
||||
Task UpdateUserKeyAndEncryptedDataAsync(User user,
|
||||
IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions);
|
||||
Task DeleteManyAsync(IEnumerable<User> users);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
@@ -13,5 +14,12 @@ public interface ILicensingService
|
||||
byte[] SignLicense(ILicense license);
|
||||
Task<OrganizationLicense> ReadOrganizationLicenseAsync(Organization organization);
|
||||
Task<OrganizationLicense> ReadOrganizationLicenseAsync(Guid organizationId);
|
||||
ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license);
|
||||
|
||||
Task<string> CreateOrganizationTokenAsync(
|
||||
Organization organization,
|
||||
Guid installationId,
|
||||
SubscriptionInfo subscriptionInfo);
|
||||
|
||||
Task<string> CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Mail;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
@@ -35,6 +36,8 @@ public interface IMailService
|
||||
Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable<string> adminEmails, bool hasAccessSecretsManager = false);
|
||||
Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false);
|
||||
Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email);
|
||||
Task SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(string organizationName, string email);
|
||||
Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email);
|
||||
Task SendPasswordlessSignInAsync(string returnUrl, string token, string email);
|
||||
Task SendInvoiceUpcoming(
|
||||
string email,
|
||||
@@ -82,6 +85,7 @@ public interface IMailService
|
||||
Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip);
|
||||
Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip);
|
||||
Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
|
||||
Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
|
||||
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||
Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||
Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier);
|
||||
@@ -91,5 +95,6 @@ public interface IMailService
|
||||
Task SendRequestSMAccessToAdminEmailAsync(IEnumerable<string> adminEmails, string organizationName, string userRequestingAccess, string emailContent);
|
||||
Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate, string organizationId,
|
||||
string organizationName);
|
||||
Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.NotificationCenter.Entities;
|
||||
using Bit.Core.Tools.Entities;
|
||||
@@ -32,4 +33,5 @@ public interface IPushNotificationService
|
||||
|
||||
Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
|
||||
string deviceId = null, ClientType? clientType = null);
|
||||
Task PushSyncOrganizationStatusAsync(Organization organization);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ public interface IUserService
|
||||
Task SendOTPAsync(User user);
|
||||
Task<bool> VerifyOTPAsync(User user, string token);
|
||||
Task<bool> VerifySecretAsync(User user, string secret, bool isSettingMFA = false);
|
||||
|
||||
Task ResendNewDeviceVerificationEmail(string email, string secret);
|
||||
|
||||
void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Azure.Storage.Queues;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
@@ -231,4 +232,15 @@ public class AzureQueuePushNotificationService : IPushNotificationService
|
||||
// Noop
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||
{
|
||||
var message = new OrganizationStatusPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Enabled = organization.Enabled
|
||||
};
|
||||
await SendMessageAsync(PushType.SyncOrganizationStatusChanged, message, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Bit.Core.Auth.Models.Mail;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.Mail;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Models.Mail.FamiliesForEnterprise;
|
||||
using Bit.Core.Models.Mail.Provider;
|
||||
@@ -25,8 +26,7 @@ public class HandlebarsMailService : IMailService
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IMailDeliveryService _mailDeliveryService;
|
||||
private readonly IMailEnqueuingService _mailEnqueuingService;
|
||||
private readonly Dictionary<string, HandlebarsTemplate<object, object>> _templateCache =
|
||||
new Dictionary<string, HandlebarsTemplate<object, object>>();
|
||||
private readonly Dictionary<string, HandlebarsTemplate<object, object>> _templateCache = new();
|
||||
|
||||
private bool _registeredHelpersAndPartials = false;
|
||||
|
||||
@@ -295,6 +295,20 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(string organizationName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email);
|
||||
var model = new OrganizationUserRevokedForPolicyTwoFactorViewModel
|
||||
{
|
||||
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName
|
||||
};
|
||||
await AddMessageContentAsync(message, "AdminConsole.OrganizationUserRevokedForTwoFactorPolicy", model);
|
||||
message.Category = "OrganizationUserRevokedForTwoFactorPolicy";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendWelcomeEmailAsync(User user)
|
||||
{
|
||||
var message = CreateDefaultMessage("Welcome to Bitwarden!", user.Email);
|
||||
@@ -447,6 +461,22 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList)
|
||||
{
|
||||
await EnqueueMailAsync(emailList.EmailList.Select(email =>
|
||||
CreateMessage(email, emailList.Organization)));
|
||||
return;
|
||||
|
||||
MailQueueMessage CreateMessage(string emailAddress, Organization org) =>
|
||||
new(CreateDefaultMessage($"Your Bitwarden account is claimed by {org.DisplayName()}", emailAddress),
|
||||
"AdminConsole.DomainClaimedByOrganization",
|
||||
new ClaimedDomainUserNotificationViewModel
|
||||
{
|
||||
TitleFirst = $"Hey {emailAddress}, your account is owned by {org.DisplayName()}",
|
||||
OrganizationName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false)
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip)
|
||||
{
|
||||
var message = CreateDefaultMessage($"New Device Logged In From {deviceType}", email);
|
||||
@@ -496,6 +526,20 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email);
|
||||
var model = new OrganizationUserRevokedForPolicySingleOrgViewModel
|
||||
{
|
||||
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName
|
||||
};
|
||||
await AddMessageContentAsync(message, "AdminConsole.OrganizationUserRevokedForSingleOrgPolicy", model);
|
||||
message.Category = "OrganizationUserRevokedForSingleOrgPolicy";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendEnqueuedMailMessageAsync(IMailQueueMessage queueMessage)
|
||||
{
|
||||
var message = CreateDefaultMessage(queueMessage.Subject, queueMessage.ToEmails);
|
||||
@@ -1024,6 +1068,19 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
|
||||
{
|
||||
var message = CreateDefaultMessage("Domain not claimed", adminEmails);
|
||||
var model = new OrganizationDomainUnverifiedViewModel
|
||||
{
|
||||
Url = $"{_globalSettings.BaseServiceUri.VaultWithHash}/organizations/{organizationId}/settings/domain-verification",
|
||||
DomainName = domainName
|
||||
};
|
||||
await AddMessageContentAsync(message, "OrganizationDomainUnclaimed", model);
|
||||
message.Category = "UnclaimedOrganizationDomain";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
||||
IEnumerable<string> ownerEmails)
|
||||
{
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Licenses.Models;
|
||||
using Bit.Core.Billing.Licenses.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
@@ -19,27 +26,33 @@ public class LicensingService : ILicensingService
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ILogger<LicensingService> _logger;
|
||||
private readonly ILicenseClaimsFactory<Organization> _organizationLicenseClaimsFactory;
|
||||
private readonly ILicenseClaimsFactory<User> _userLicenseClaimsFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private IDictionary<Guid, DateTime> _userCheckCache = new Dictionary<Guid, DateTime>();
|
||||
|
||||
public LicensingService(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IMailService mailService,
|
||||
IWebHostEnvironment environment,
|
||||
ILogger<LicensingService> logger,
|
||||
IGlobalSettings globalSettings)
|
||||
IGlobalSettings globalSettings,
|
||||
ILicenseClaimsFactory<Organization> organizationLicenseClaimsFactory,
|
||||
IFeatureService featureService,
|
||||
ILicenseClaimsFactory<User> userLicenseClaimsFactory)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_mailService = mailService;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_organizationLicenseClaimsFactory = organizationLicenseClaimsFactory;
|
||||
_featureService = featureService;
|
||||
_userLicenseClaimsFactory = userLicenseClaimsFactory;
|
||||
|
||||
var certThumbprint = environment.IsDevelopment() ?
|
||||
"207E64A231E8AA32AAF68A61037C075EBEBD553F" :
|
||||
@@ -104,13 +117,13 @@ public class LicensingService : ILicensingService
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!license.VerifyData(org, _globalSettings))
|
||||
if (!license.VerifyData(org, GetClaimsPrincipalFromLicense(license), _globalSettings))
|
||||
{
|
||||
await DisableOrganizationAsync(org, license, "Invalid data.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!license.VerifySignature(_certificate))
|
||||
if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate))
|
||||
{
|
||||
await DisableOrganizationAsync(org, license, "Invalid signature.");
|
||||
continue;
|
||||
@@ -203,13 +216,14 @@ public class LicensingService : ILicensingService
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!license.VerifyData(user))
|
||||
var claimsPrincipal = GetClaimsPrincipalFromLicense(license);
|
||||
if (!license.VerifyData(user, claimsPrincipal))
|
||||
{
|
||||
await DisablePremiumAsync(user, license, "Invalid data.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!license.VerifySignature(_certificate))
|
||||
if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate))
|
||||
{
|
||||
await DisablePremiumAsync(user, license, "Invalid signature.");
|
||||
return false;
|
||||
@@ -234,7 +248,21 @@ public class LicensingService : ILicensingService
|
||||
|
||||
public bool VerifyLicense(ILicense license)
|
||||
{
|
||||
return license.VerifySignature(_certificate);
|
||||
if (string.IsNullOrWhiteSpace(license.Token))
|
||||
{
|
||||
return license.VerifySignature(_certificate);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = GetClaimsPrincipalFromLicense(license);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Invalid token.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] SignLicense(ILicense license)
|
||||
@@ -272,4 +300,101 @@ public class LicensingService : ILicensingService
|
||||
using var fs = File.OpenRead(filePath);
|
||||
return await JsonSerializer.DeserializeAsync<OrganizationLicense>(fs);
|
||||
}
|
||||
|
||||
public ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(license.Token))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var audience = license switch
|
||||
{
|
||||
OrganizationLicense orgLicense => $"organization:{orgLicense.Id}",
|
||||
UserLicense userLicense => $"user:{userLicense.Id}",
|
||||
_ => throw new ArgumentException("Unsupported license type.", nameof(license)),
|
||||
};
|
||||
|
||||
var token = license.Token;
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var validationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new X509SecurityKey(_certificate),
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = "bitwarden",
|
||||
ValidateAudience = true,
|
||||
ValidAudience = audience,
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.Zero,
|
||||
RequireExpirationTime = true
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
return tokenHandler.ValidateToken(token, validationParameters, out _);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Token exceptions thrown are interpreted by the client as Identity errors and cause the user to logout
|
||||
// Mask them by rethrowing as BadRequestException
|
||||
throw new BadRequestException($"Invalid license. {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var licenseContext = new LicenseContext
|
||||
{
|
||||
InstallationId = installationId,
|
||||
SubscriptionInfo = subscriptionInfo,
|
||||
};
|
||||
|
||||
var claims = await _organizationLicenseClaimsFactory.GenerateClaims(organization, licenseContext);
|
||||
var audience = $"organization:{organization.Id}";
|
||||
|
||||
return GenerateToken(claims, audience);
|
||||
}
|
||||
|
||||
public async Task<string> CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var licenseContext = new LicenseContext { SubscriptionInfo = subscriptionInfo };
|
||||
var claims = await _userLicenseClaimsFactory.GenerateClaims(user, licenseContext);
|
||||
var audience = $"user:{user.Id}";
|
||||
|
||||
return GenerateToken(claims, audience);
|
||||
}
|
||||
|
||||
private string GenerateToken(List<Claim> claims, string audience)
|
||||
{
|
||||
if (claims.All(claim => claim.Type != JwtClaimTypes.JwtId))
|
||||
{
|
||||
claims.Add(new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()));
|
||||
}
|
||||
|
||||
var securityKey = new RsaSecurityKey(_certificate.GetRSAPrivateKey());
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(claims),
|
||||
Issuer = "bitwarden",
|
||||
Audience = audience,
|
||||
NotBefore = DateTime.UtcNow,
|
||||
Expires = DateTime.UtcNow.AddYears(1), // Org expiration is a claim
|
||||
SigningCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256Signature)
|
||||
};
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
return tokenHandler.WriteToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.NotificationCenter.Entities;
|
||||
using Bit.Core.Settings;
|
||||
@@ -145,6 +146,12 @@ public class MultiServicePushNotificationService : IPushNotificationService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncOrganizationStatusAsync(organization));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushNotificationAsync(Notification notification)
|
||||
{
|
||||
PushToServices((s) => s.PushNotificationAsync(notification));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
@@ -239,4 +240,15 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
|
||||
// Noop
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||
{
|
||||
var message = new OrganizationStatusPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Enabled = organization.Enabled
|
||||
};
|
||||
|
||||
await SendMessageAsync(PushType.SyncOrganizationStatusChanged, message, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.IdentityServer;
|
||||
@@ -277,4 +278,15 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||
{
|
||||
var message = new OrganizationStatusPushNotification
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Enabled = organization.Enabled
|
||||
};
|
||||
|
||||
await SendPayloadToOrganizationAsync(organization.Id, PushType.SyncOrganizationStatusChanged, message, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@@ -14,6 +16,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
@@ -67,6 +70,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPremiumUserBillingService _premiumUserBillingService;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||
|
||||
public UserService(
|
||||
IUserRepository userRepository,
|
||||
@@ -101,7 +105,8 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
IPremiumUserBillingService premiumUserBillingService,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||
: base(
|
||||
store,
|
||||
optionsAccessor,
|
||||
@@ -142,6 +147,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
_featureService = featureService;
|
||||
_premiumUserBillingService = premiumUserBillingService;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||
}
|
||||
|
||||
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
||||
@@ -908,7 +914,9 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
throw new BadRequestException("Invalid license.");
|
||||
}
|
||||
|
||||
if (!license.CanUse(user, out var exceptionMessage))
|
||||
var claimsPrincipal = _licenseService.GetClaimsPrincipalFromLicense(license);
|
||||
|
||||
if (!license.CanUse(user, claimsPrincipal, out var exceptionMessage))
|
||||
{
|
||||
throw new BadRequestException(exceptionMessage);
|
||||
}
|
||||
@@ -987,7 +995,9 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
throw new BadRequestException("Invalid license.");
|
||||
}
|
||||
|
||||
if (!license.CanUse(user, out var exceptionMessage))
|
||||
var claimsPrincipal = _licenseService.GetClaimsPrincipalFromLicense(license);
|
||||
|
||||
if (!license.CanUse(user, claimsPrincipal, out var exceptionMessage))
|
||||
{
|
||||
throw new BadRequestException(exceptionMessage);
|
||||
}
|
||||
@@ -1111,7 +1121,9 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<UserLicense> GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null,
|
||||
public async Task<UserLicense> GenerateLicenseAsync(
|
||||
User user,
|
||||
SubscriptionInfo subscriptionInfo = null,
|
||||
int? version = null)
|
||||
{
|
||||
if (user == null)
|
||||
@@ -1124,8 +1136,13 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
subscriptionInfo = await _paymentService.GetSubscriptionAsync(user);
|
||||
}
|
||||
|
||||
return subscriptionInfo == null ? new UserLicense(user, _licenseService) :
|
||||
new UserLicense(user, subscriptionInfo, _licenseService);
|
||||
var userLicense = subscriptionInfo == null
|
||||
? new UserLicense(user, _licenseService)
|
||||
: new UserLicense(user, subscriptionInfo, _licenseService);
|
||||
|
||||
userLicense.Token = await _licenseService.CreateUserTokenAsync(user, subscriptionInfo);
|
||||
|
||||
return userLicense;
|
||||
}
|
||||
|
||||
public override async Task<bool> CheckPasswordAsync(User user, string password)
|
||||
@@ -1344,13 +1361,27 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
|
||||
{
|
||||
var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication);
|
||||
var organizationsManagingUser = await GetOrganizationsManagingUserAsync(user.Id);
|
||||
|
||||
var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
|
||||
{
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id);
|
||||
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
|
||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||
organization.DisplayName(), user.Email);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && organizationsManagingUser.Any(o => o.Id == p.OrganizationId))
|
||||
{
|
||||
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
|
||||
new RevokeOrganizationUsersRequest(
|
||||
p.OrganizationId,
|
||||
[new OrganizationUserUserDetails { UserId = user.Id, OrganizationId = p.OrganizationId }],
|
||||
new SystemUser(EventSystemUser.TwoFactorDisabled)));
|
||||
await _mailService.SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization.DisplayName(), user.Email);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id);
|
||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||
organization.DisplayName(), user.Email);
|
||||
}
|
||||
|
||||
}).ToArray();
|
||||
|
||||
await Task.WhenAll(removeOrgUserTasks);
|
||||
@@ -1376,7 +1407,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
|
||||
public async Task SendOTPAsync(User user)
|
||||
{
|
||||
if (user.Email == null)
|
||||
if (string.IsNullOrEmpty(user.Email))
|
||||
{
|
||||
throw new BadRequestException("No user email.");
|
||||
}
|
||||
@@ -1419,6 +1450,20 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
return isVerified;
|
||||
}
|
||||
|
||||
public async Task ResendNewDeviceVerificationEmail(string email, string secret)
|
||||
{
|
||||
var user = await _userRepository.GetByEmailAsync(email);
|
||||
if (user == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (await VerifySecretAsync(user, secret))
|
||||
{
|
||||
await SendOTPAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendAppropriateWelcomeEmailAsync(User user, string initiationPath)
|
||||
{
|
||||
var isFromMarketingWebsite = initiationPath.Contains("Secrets Manager trial");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Settings;
|
||||
@@ -53,4 +54,19 @@ public class NoopLicensingService : ILicensingService
|
||||
{
|
||||
return Task.FromResult<OrganizationLicense>(null);
|
||||
}
|
||||
|
||||
public ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task<string> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
|
||||
public Task<string> CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user