1
0
mirror of https://github.com/bitwarden/server.git synced 2025-03-02 04:11:04 +01:00

Merge remote-tracking branch 'origin/main' into auth/pm-2996/add-auth-request-data-to-devices-response-model

This commit is contained in:
Patrick Pimentel 2024-12-09 12:19:39 -05:00
commit 1cf6d08616
No known key found for this signature in database
GPG Key ID: 4B27FC74C6422186
159 changed files with 14444 additions and 2599 deletions

View File

@ -21,10 +21,15 @@
}
},
"postCreateCommand": "bash .devcontainer/internal_dev/postCreateCommand.sh",
"forwardPorts": [1080, 1433],
"portsAttributes": {
"1080": {
"label": "Mail Catcher",
"onAutoForward": "notify"
},
"1433": {
"label": "SQL Server",
"onAutoForward": "notify"
}
}
}

View File

@ -70,6 +70,22 @@ Press <Enter> to continue."
sleep 5 # wait for DB container to start
dotnet run --project ./util/MsSqlMigratorUtility "$SQL_CONNECTION_STRING"
fi
read -r -p "Would you like to install the Stripe CLI? [y/N] " stripe_response
if [[ "$stripe_response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
install_stripe_cli
fi
}
# Install Stripe CLI
install_stripe_cli() {
echo "Installing Stripe CLI..."
# Add Stripe CLI GPG key so that apt can verify the packages authenticity.
# If Stripe ever changes the key, we'll need to update this. Visit https://docs.stripe.com/stripe-cli?install-method=apt if so
curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg >/dev/null
# Add Stripe CLI repository to apt sources
echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | sudo tee -a /etc/apt/sources.list.d/stripe.list >/dev/null
sudo apt update
sudo apt install -y stripe
}
# main

1
.github/CODEOWNERS vendored
View File

@ -38,7 +38,6 @@ src/Identity @bitwarden/team-auth-dev
# Key Management team
**/KeyManagement @bitwarden/team-key-management-dev
**/SecretsManager @bitwarden/team-secrets-manager-dev
**/Tools @bitwarden/team-tools-dev
# Vault team

View File

@ -26,7 +26,7 @@
},
{
"matchManagers": ["github-actions", "dockerfile", "docker-compose"],
"commitMessagePrefix": "[deps] DevOps:"
"commitMessagePrefix": "[deps] BRE:"
},
{
"matchPackageNames": ["DnsClient"],
@ -116,8 +116,8 @@
{
"matchPackageNames": ["CommandDotNet", "YamlDotNet"],
"description": "DevOps owned dependencies",
"commitMessagePrefix": "[deps] DevOps:",
"reviewers": ["team:dept-devops"]
"commitMessagePrefix": "[deps] BRE:",
"reviewers": ["team:dept-bre"]
},
{
"matchPackageNames": [

View File

@ -530,7 +530,9 @@ jobs:
self-host-build:
name: Trigger self-host build
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
runs-on: ubuntu-22.04
needs:
- build-docker

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2024.11.0</Version>
<Version>2024.12.0</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>

View File

@ -27,7 +27,11 @@ namespace Bit.Commercial.Core.AdminConsole.Services;
public class ProviderService : IProviderService
{
public static PlanType[] ProviderDisallowedOrganizationTypes = new[] { PlanType.Free, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019 };
private static readonly PlanType[] _resellerDisallowedOrganizationTypes = [
PlanType.Free,
PlanType.FamiliesAnnually,
PlanType.FamiliesAnnually2019
];
private readonly IDataProtector _dataProtector;
private readonly IMailService _mailService;
@ -690,13 +694,14 @@ public class ProviderService : IProviderService
throw new BadRequestException($"Multi-organization Enterprise Providers cannot manage organizations with the plan type {requestedType}. Only Enterprise (Monthly) and Enterprise (Annually) are allowed.");
}
break;
default:
throw new BadRequestException($"Unsupported provider type {providerType}.");
}
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
case ProviderType.Reseller:
if (_resellerDisallowedOrganizationTypes.Contains(requestedType))
{
throw new BadRequestException($"Providers cannot manage organizations with the requested plan type ({requestedType}). Only Teams and Enterprise accounts are allowed.");
}
break;
default:
throw new BadRequestException($"Unsupported provider type {providerType}.");
}
}
}

View File

@ -448,6 +448,7 @@ public class OrganizationsController : Controller
organization.UseTotp = model.UseTotp;
organization.UsersGetPremium = model.UsersGetPremium;
organization.UseSecretsManager = model.UseSecretsManager;
organization.UseRiskInsights = model.UseRiskInsights;
//secrets
organization.SmSeats = model.SmSeats;

View File

@ -80,6 +80,7 @@ public class OrganizationEditModel : OrganizationViewModel
Use2fa = org.Use2fa;
UseApi = org.UseApi;
UseSecretsManager = org.UseSecretsManager;
UseRiskInsights = org.UseRiskInsights;
UseResetPassword = org.UseResetPassword;
SelfHost = org.SelfHost;
UsersGetPremium = org.UsersGetPremium;
@ -144,6 +145,8 @@ public class OrganizationEditModel : OrganizationViewModel
public bool UseScim { get; set; }
[Display(Name = "Secrets Manager")]
public new bool UseSecretsManager { get; set; }
[Display(Name = "Risk Insights")]
public new bool UseRiskInsights { get; set; }
[Display(Name = "Self Host")]
public bool SelfHost { get; set; }
[Display(Name = "Users Get Premium")]
@ -284,6 +287,7 @@ public class OrganizationEditModel : OrganizationViewModel
existingOrganization.Use2fa = Use2fa;
existingOrganization.UseApi = UseApi;
existingOrganization.UseSecretsManager = UseSecretsManager;
existingOrganization.UseRiskInsights = UseRiskInsights;
existingOrganization.UseResetPassword = UseResetPassword;
existingOrganization.SelfHost = SelfHost;
existingOrganization.UsersGetPremium = UsersGetPremium;

View File

@ -69,4 +69,5 @@ public class OrganizationViewModel
public int ServiceAccountsCount { get; set; }
public int OccupiedSmSeatsCount { get; set; }
public bool UseSecretsManager => Organization.UseSecretsManager;
public bool UseRiskInsights => Organization.UseRiskInsights;
}

View File

@ -55,19 +55,11 @@
<dt class="col-sm-4 col-lg-3">Administrators manage all collections</dt>
<dd id="pm-manage-collections" class="col-sm-8 col-lg-9">@(Model.Organization.AllowAdminAccessToAllCollectionItems ? "On" : "Off")</dd>
@if (!FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreationDeletion ? "On" : "Off")</dd>
}
else
{
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreation ? "On" : "Off")</dd>
<dt class="col-sm-4 col-lg-3">Limit collection deletion to administrators</dt>
<dd id="pm-collection-deletion" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionDeletion ? "On" : "Off")</dd>
}
</dl>
<h2>Secrets Manager</h2>

View File

@ -94,7 +94,7 @@
</div>
</div>
<h2>Features</h2>
<div class="row mb-3">
<div class="row mb-4">
<div class="col-4">
<h3>General</h3>
<div class="form-check mb-2">
@ -146,7 +146,7 @@
<label class="form-check-label" asp-for="UseCustomPermissions"></label>
</div>
</div>
<div class="col-4">
<div class="col-3">
<h3>Password Manager</h3>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseTotp" disabled='@(canEditPlan ? null : "disabled")'>
@ -157,13 +157,20 @@
<label class="form-check-label" asp-for="UsersGetPremium"></label>
</div>
</div>
<div class="col-4">
<div class="col-3">
<h3>Secrets Manager</h3>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseSecretsManager" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseSecretsManager"></label>
</div>
</div>
<div class="col-2">
<h3>Access Insights</h3>
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseRiskInsights" disabled='@(canEditPlan ? null : "disabled")'>
<label class="form-check-label" asp-for="UseRiskInsights"></label>
</div>
</div>
</div>
}

View File

@ -9,10 +9,7 @@ namespace Bit.Admin.Models;
public class UserEditModel
{
public UserEditModel()
{
}
public UserEditModel() { }
public UserEditModel(
User user,
@ -21,10 +18,9 @@ public class UserEditModel
BillingInfo billingInfo,
BillingHistoryInfo billingHistoryInfo,
GlobalSettings globalSettings,
bool? domainVerified
)
bool? claimedAccount)
{
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, domainVerified);
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, claimedAccount);
BillingInfo = billingInfo;
BillingHistoryInfo = billingHistoryInfo;

View File

@ -14,7 +14,7 @@ public class UserViewModel
public bool Premium { get; }
public short? MaxStorageGb { get; }
public bool EmailVerified { get; }
public bool? DomainVerified { get; }
public bool? ClaimedAccount { get; }
public bool TwoFactorEnabled { get; }
public DateTime AccountRevisionDate { get; }
public DateTime RevisionDate { get; }
@ -36,7 +36,7 @@ public class UserViewModel
bool premium,
short? maxStorageGb,
bool emailVerified,
bool? domainVerified,
bool? claimedAccount,
bool twoFactorEnabled,
DateTime accountRevisionDate,
DateTime revisionDate,
@ -58,7 +58,7 @@ public class UserViewModel
Premium = premium;
MaxStorageGb = maxStorageGb;
EmailVerified = emailVerified;
DomainVerified = domainVerified;
ClaimedAccount = claimedAccount;
TwoFactorEnabled = twoFactorEnabled;
AccountRevisionDate = accountRevisionDate;
RevisionDate = revisionDate;
@ -79,7 +79,7 @@ public class UserViewModel
users.Select(user => MapViewModel(user, lookup, false));
public static UserViewModel MapViewModel(User user,
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? domainVerified) =>
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? claimedAccount) =>
new(
user.Id,
user.Name,
@ -89,7 +89,7 @@ public class UserViewModel
user.Premium,
user.MaxStorageGb,
user.EmailVerified,
domainVerified,
claimedAccount,
IsTwoFactorEnabled(user, lookup),
user.AccountRevisionDate,
user.RevisionDate,
@ -106,7 +106,7 @@ public class UserViewModel
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) =>
MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>(), false);
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers, bool? domainVerified) =>
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers, bool? claimedAccount) =>
new(
user.Id,
user.Name,
@ -116,7 +116,7 @@ public class UserViewModel
user.Premium,
user.MaxStorageGb,
user.EmailVerified,
domainVerified,
claimedAccount,
isTwoFactorEnabled,
user.AccountRevisionDate,
user.RevisionDate,

View File

@ -12,9 +12,10 @@
<dt class="col-sm-4 col-lg-3">Email Verified</dt>
<dd class="col-sm-8 col-lg-9">@(Model.EmailVerified ? "Yes" : "No")</dd>
@if(Model.DomainVerified.HasValue){
<dt class="col-sm-4 col-lg-3">Domain Verified</dt>
<dd class="col-sm-8 col-lg-9">@(Model.DomainVerified.Value == true ? "Yes" : "No")</dd>
@if(Model.ClaimedAccount.HasValue)
{
<dt class="col-sm-4 col-lg-3">Claimed Account</dt>
<dd class="col-sm-8 col-lg-9">@(Model.ClaimedAccount.Value ? "Yes" : "No")</dd>
}
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>

View File

@ -539,7 +539,7 @@ public class OrganizationUsersController : Controller
var userId = _userService.GetProperUserId(User);
var result = await _removeOrganizationUserCommand.RemoveUsersAsync(orgId, model.Ids, userId.Value);
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
}
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]

View File

@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums;
@ -52,11 +53,11 @@ public class OrganizationsController : Controller
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly IFeatureService _featureService;
private readonly GlobalSettings _globalSettings;
private readonly IPushNotificationService _pushNotificationService;
private readonly IProviderRepository _providerRepository;
private readonly IProviderBillingService _providerBillingService;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
public OrganizationsController(
IOrganizationRepository organizationRepository,
@ -73,11 +74,11 @@ public class OrganizationsController : Controller
IOrganizationApiKeyRepository organizationApiKeyRepository,
IFeatureService featureService,
GlobalSettings globalSettings,
IPushNotificationService pushNotificationService,
IProviderRepository providerRepository,
IProviderBillingService providerBillingService,
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -93,11 +94,11 @@ public class OrganizationsController : Controller
_organizationApiKeyRepository = organizationApiKeyRepository;
_featureService = featureService;
_globalSettings = globalSettings;
_pushNotificationService = pushNotificationService;
_providerRepository = providerRepository;
_providerBillingService = providerBillingService;
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
}
[HttpGet("{id}")]
@ -175,8 +176,8 @@ public class OrganizationsController : Controller
}
var organizationSignup = model.ToOrganizationSignup(user);
var result = await _organizationService.SignUpAsync(organizationSignup);
return new OrganizationResponseModel(result.Item1);
var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);
return new OrganizationResponseModel(result.Organization);
}
[HttpPost("create-without-payment")]
@ -190,8 +191,8 @@ public class OrganizationsController : Controller
}
var organizationSignup = model.ToOrganizationSignup(user);
var result = await _organizationService.SignUpAsync(organizationSignup);
return new OrganizationResponseModel(result.Item1);
var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);
return new OrganizationResponseModel(result.Organization);
}
[HttpPut("{id}")]
@ -525,14 +526,6 @@ public class OrganizationsController : Controller
[HttpPut("{id}/collection-management")]
public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)
{
if (
_globalSettings.SelfHosted &&
!_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
)
{
throw new BadRequestException("Only allowed when not self hosted.");
}
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization == null)
{

View File

@ -6,8 +6,8 @@ using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context;
using Bit.Core.Enums;
@ -28,7 +28,6 @@ namespace Bit.Api.AdminConsole.Controllers;
public class PoliciesController : Controller
{
private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IUserService _userService;
private readonly ICurrentContext _currentContext;
@ -37,10 +36,10 @@ public class PoliciesController : Controller
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IFeatureService _featureService;
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
private readonly ISavePolicyCommand _savePolicyCommand;
public PoliciesController(
IPolicyRepository policyRepository,
IPolicyService policyService,
IOrganizationUserRepository organizationUserRepository,
IUserService userService,
ICurrentContext currentContext,
@ -48,10 +47,10 @@ public class PoliciesController : Controller
IDataProtectionProvider dataProtectionProvider,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IFeatureService featureService,
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
ISavePolicyCommand savePolicyCommand)
{
_policyRepository = policyRepository;
_policyService = policyService;
_organizationUserRepository = organizationUserRepository;
_userService = userService;
_currentContext = currentContext;
@ -62,6 +61,7 @@ public class PoliciesController : Controller
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_featureService = featureService;
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
_savePolicyCommand = savePolicyCommand;
}
[HttpGet("{type}")]
@ -178,25 +178,20 @@ public class PoliciesController : Controller
}
[HttpPut("{type}")]
public async Task<PolicyResponseModel> Put(string orgId, int type, [FromBody] PolicyRequestModel model)
public async Task<PolicyResponseModel> Put(Guid orgId, PolicyType type, [FromBody] PolicyRequestModel model)
{
var orgIdGuid = new Guid(orgId);
if (!await _currentContext.ManagePolicies(orgIdGuid))
if (!await _currentContext.ManagePolicies(orgId))
{
throw new NotFoundException();
}
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(new Guid(orgId), (PolicyType)type);
if (policy == null)
if (type != model.Type)
{
policy = model.ToPolicy(orgIdGuid);
}
else
{
policy = model.ToPolicy(policy);
throw new BadRequestException("Mismatched policy type");
}
var userId = _userService.GetProperUserId(User);
await _policyService.SaveAsync(policy, userId);
var policyUpdate = await model.ToPolicyUpdateAsync(orgId, _currentContext);
var policy = await _savePolicyCommand.SaveAsync(policyUpdate);
return new PolicyResponseModel(policy);
}
}

View File

@ -1,7 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.Context;
namespace Bit.Api.AdminConsole.Models.Request;
@ -13,19 +15,12 @@ public class PolicyRequestModel
public bool? Enabled { get; set; }
public Dictionary<string, object> Data { get; set; }
public Policy ToPolicy(Guid orgId)
public async Task<PolicyUpdate> ToPolicyUpdateAsync(Guid organizationId, ICurrentContext currentContext) => new()
{
return ToPolicy(new Policy
{
Type = Type.Value,
OrganizationId = orgId
});
}
public Policy ToPolicy(Policy existingPolicy)
{
existingPolicy.Enabled = Enabled.GetValueOrDefault();
existingPolicy.Data = Data != null ? JsonSerializer.Serialize(Data) : null;
return existingPolicy;
}
Type = Type!.Value,
OrganizationId = organizationId,
Data = Data != null ? JsonSerializer.Serialize(Data) : null,
Enabled = Enabled.GetValueOrDefault(),
PerformedBy = new StandardUser(currentContext.UserId!.Value, await currentContext.OrganizationOwner(organizationId))
};
}

View File

@ -57,9 +57,8 @@ public class OrganizationResponseModel : ResponseModel
MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;
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; }
@ -103,9 +102,8 @@ public class OrganizationResponseModel : ResponseModel
public int? MaxAutoscaleSmServiceAccounts { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deperectated: https://bitwarden.atlassian.net/browse/PM-10863
public bool LimitCollectionCreationDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; }
}
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel

View File

@ -67,10 +67,9 @@ public class ProfileOrganizationResponseModel : ResponseModel
AccessSecretsManager = organization.AccessSecretsManager;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId);
UseRiskInsights = organization.UseRiskInsights;
if (organization.SsoConfig != null)
{
@ -129,8 +128,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
public bool AccessSecretsManager { 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; }
/// <summary>
/// Indicates if the organization manages the user.
@ -143,4 +140,5 @@ public class ProfileOrganizationResponseModel : ResponseModel
/// False if the Account Deprovisioning feature flag is disabled.
/// </returns>
public bool UserIsManagedByOrganization { get; set; }
public bool UseRiskInsights { get; set; }
}

View File

@ -46,8 +46,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
// https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights;
}
}

View File

@ -1,6 +1,5 @@
using System.Net;
using Bit.Api.AdminConsole.Public.Models.Request;
using Bit.Api.AdminConsole.Public.Models.Response;
using Bit.Api.Models.Public.Response;
using Bit.Core.Context;
using Bit.Core.Enums;
@ -38,7 +37,7 @@ public class OrganizationController : Controller
/// </remarks>
/// <param name="model">The request model.</param>
[HttpPost("import")]
[ProducesResponseType(typeof(MemberResponseModel), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(OkResult), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> Import([FromBody] OrganizationImportRequestModel model)
{

View File

@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Public.Models.Request;
using Bit.Api.AdminConsole.Public.Models.Response;
using Bit.Api.Models.Public.Response;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Context;
@ -18,15 +19,18 @@ public class PoliciesController : Controller
private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService;
private readonly ICurrentContext _currentContext;
private readonly ISavePolicyCommand _savePolicyCommand;
public PoliciesController(
IPolicyRepository policyRepository,
IPolicyService policyService,
ICurrentContext currentContext)
ICurrentContext currentContext,
ISavePolicyCommand savePolicyCommand)
{
_policyRepository = policyRepository;
_policyService = policyService;
_currentContext = currentContext;
_savePolicyCommand = savePolicyCommand;
}
/// <summary>
@ -80,17 +84,9 @@ public class PoliciesController : Controller
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Put(PolicyType type, [FromBody] PolicyUpdateRequestModel model)
{
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(
_currentContext.OrganizationId.Value, type);
if (policy == null)
{
policy = model.ToPolicy(_currentContext.OrganizationId.Value, type);
}
else
{
policy = model.ToPolicy(policy);
}
await _policyService.SaveAsync(policy, null);
var policyUpdate = model.ToPolicyUpdate(_currentContext.OrganizationId!.Value, type);
var policy = await _savePolicyCommand.SaveAsync(policyUpdate);
var response = new PolicyResponseModel(policy);
return new JsonResult(response);
}

View File

@ -1,26 +1,19 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.Enums;
namespace Bit.Api.AdminConsole.Public.Models.Request;
public class PolicyUpdateRequestModel : PolicyBaseModel
{
public Policy ToPolicy(Guid orgId, PolicyType type)
public PolicyUpdate ToPolicyUpdate(Guid organizationId, PolicyType type) => new()
{
return ToPolicy(new Policy
{
OrganizationId = orgId,
Enabled = Enabled.GetValueOrDefault(),
Type = type,
OrganizationId = organizationId,
Data = Data != null ? JsonSerializer.Serialize(Data) : null,
Type = type
});
}
public virtual Policy ToPolicy(Policy existingPolicy)
{
existingPolicy.Enabled = Enabled.GetValueOrDefault();
existingPolicy.Data = Data != null ? JsonSerializer.Serialize(Data) : null;
return existingPolicy;
}
Enabled = Enabled.GetValueOrDefault(),
PerformedBy = new SystemUser(EventSystemUser.PublicApi)
};
}

View File

@ -1,8 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Api.Models.Public.Response;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Newtonsoft.Json;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Bit.Api.AdminConsole.Public.Models.Response;
@ -11,6 +12,9 @@ namespace Bit.Api.AdminConsole.Public.Models.Response;
/// </summary>
public class PolicyResponseModel : PolicyBaseModel, IResponseModel
{
[JsonConstructor]
public PolicyResponseModel() { }
public PolicyResponseModel(Policy policy)
{
if (policy == null)

View File

@ -1,5 +1,4 @@
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Services;
namespace Bit.Api.Models.Request.Organizations;
@ -8,22 +7,12 @@ public class OrganizationCollectionManagementUpdateRequestModel
{
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
public bool LimitCreateDeleteOwnerAdmin { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService)
{
if (featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
existingOrganization.LimitCollectionCreation = LimitCollectionCreation;
existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion;
}
else
{
existingOrganization.LimitCollectionCreationDeletion = LimitCreateDeleteOwnerAdmin || LimitCollectionCreation || LimitCollectionDeletion;
}
existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems;
return existingOrganization;
}

View File

@ -0,0 +1,38 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
using Bit.Core.Context;
using Bit.Core.Enums;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Api.Tools.Authorization;
public class VaultExportAuthorizationHandler(ICurrentContext currentContext)
: AuthorizationHandler<VaultExportOperationRequirement, OrganizationScope>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
VaultExportOperationRequirement requirement, OrganizationScope organizationScope)
{
var org = currentContext.GetOrganization(organizationScope);
var authorized = requirement switch
{
not null when requirement == VaultExportOperations.ExportWholeVault =>
CanExportWholeVault(org),
not null when requirement == VaultExportOperations.ExportManagedCollections =>
CanExportManagedCollections(org),
_ => false
};
if (authorized)
{
context.Succeed(requirement);
}
return Task.FromResult(0);
}
private bool CanExportWholeVault(CurrentContextOrganization organization) => organization is
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
{ Type: OrganizationUserType.Custom, Permissions.AccessImportExport: true };
private bool CanExportManagedCollections(CurrentContextOrganization organization) => organization is not null;
}

View File

@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Bit.Api.Tools.Authorization;
public class VaultExportOperationRequirement : OperationAuthorizationRequirement;
public static class VaultExportOperations
{
/// <summary>
/// Exporting the entire organization vault.
/// </summary>
public static readonly VaultExportOperationRequirement ExportWholeVault =
new() { Name = nameof(ExportWholeVault) };
/// <summary>
/// Exporting only the organization items that the user has Can Manage permissions for
/// </summary>
public static readonly VaultExportOperationRequirement ExportManagedCollections =
new() { Name = nameof(ExportManagedCollections) };
}

View File

@ -1,11 +1,17 @@
using Bit.Api.Models.Response;
using Bit.Api.Tools.Authorization;
using Bit.Api.Tools.Models.Response;
using Bit.Api.Vault.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Queries;
using Bit.Core.Vault.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -21,24 +27,41 @@ public class OrganizationExportController : Controller
private readonly ICollectionService _collectionService;
private readonly ICipherService _cipherService;
private readonly GlobalSettings _globalSettings;
private readonly IFeatureService _featureService;
private readonly IAuthorizationService _authorizationService;
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
private readonly ICollectionRepository _collectionRepository;
public OrganizationExportController(
ICurrentContext currentContext,
ICipherService cipherService,
ICollectionService collectionService,
IUserService userService,
GlobalSettings globalSettings)
GlobalSettings globalSettings,
IFeatureService featureService,
IAuthorizationService authorizationService,
IOrganizationCiphersQuery organizationCiphersQuery,
ICollectionRepository collectionRepository)
{
_currentContext = currentContext;
_cipherService = cipherService;
_collectionService = collectionService;
_userService = userService;
_globalSettings = globalSettings;
_featureService = featureService;
_authorizationService = authorizationService;
_organizationCiphersQuery = organizationCiphersQuery;
_collectionRepository = collectionRepository;
}
[HttpGet("export")]
public async Task<IActionResult> Export(Guid organizationId)
{
if (_featureService.IsEnabled(FeatureFlagKeys.PM11360RemoveProviderExportPermission))
{
return await Export_vNext(organizationId);
}
var userId = _userService.GetProperUserId(User).Value;
IEnumerable<Collection> orgCollections = await _collectionService.GetOrganizationCollectionsAsync(organizationId);
@ -65,6 +88,35 @@ public class OrganizationExportController : Controller
return Ok(organizationExportListResponseModel);
}
private async Task<IActionResult> Export_vNext(Guid organizationId)
{
var canExportAll = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId),
VaultExportOperations.ExportWholeVault);
if (canExportAll.Succeeded)
{
var allOrganizationCiphers = await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId);
var allCollections = await _collectionRepository.GetManyByOrganizationIdAsync(organizationId);
return Ok(new OrganizationExportResponseModel(allOrganizationCiphers, allCollections, _globalSettings));
}
var canExportManaged = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId),
VaultExportOperations.ExportManagedCollections);
if (canExportManaged.Succeeded)
{
var userId = _userService.GetProperUserId(User)!.Value;
var allUserCollections = await _collectionRepository.GetManyByUserIdAsync(userId);
var managedOrgCollections = allUserCollections.Where(c => c.OrganizationId == organizationId && c.Manage).ToList();
var managedCiphers =
await _organizationCiphersQuery.GetOrganizationCiphersByCollectionIds(organizationId, managedOrgCollections.Select(c => c.Id));
return Ok(new OrganizationExportResponseModel(managedCiphers, managedOrgCollections, _globalSettings));
}
// Unauthorized
throw new NotFoundException();
}
private ListResponseModel<CollectionResponseModel> GetOrganizationCollectionsResponse(IEnumerable<Collection> orgCollections)
{
var collections = orgCollections.Select(c => new CollectionResponseModel(c));

View File

@ -1,6 +1,9 @@
using Bit.Api.Models.Response;
using Bit.Api.Vault.Models.Response;
using Bit.Core.Entities;
using Bit.Core.Models.Api;
using Bit.Core.Settings;
using Bit.Core.Vault.Models.Data;
namespace Bit.Api.Tools.Models.Response;
@ -10,6 +13,13 @@ public class OrganizationExportResponseModel : ResponseModel
{
}
public OrganizationExportResponseModel(IEnumerable<CipherOrganizationDetailsWithCollections> ciphers,
IEnumerable<Collection> collections, GlobalSettings globalSettings) : this()
{
Ciphers = ciphers.Select(c => new CipherMiniDetailsResponseModel(c, globalSettings));
Collections = collections.Select(c => new CollectionResponseModel(c));
}
public IEnumerable<CollectionResponseModel> Collections { get; set; }
public IEnumerable<CipherMiniDetailsResponseModel> Ciphers { get; set; }

View File

@ -1,4 +1,5 @@
using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Api.Tools.Authorization;
using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
using Bit.Core.IdentityServer;
using Bit.Core.Settings;
@ -99,5 +100,6 @@ public static class ServiceCollectionExtensions
services.AddScoped<IAuthorizationHandler, BulkCollectionAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, CollectionAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, GroupAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, VaultExportAuthorizationHandler>();
}
}

View File

@ -1,6 +1,5 @@
#nullable enable
using System.Diagnostics;
using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -124,8 +123,8 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
return true;
}
if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
var organizationAbility = await GetOrganizationAbilityAsync(org);
var userIsMemberOfOrg = org is not null;
var limitCollectionCreationEnabled = await GetOrganizationAbilityAsync(org) is { LimitCollectionCreation: true };
var userIsOrgOwnerOrAdmin = org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
@ -134,15 +133,6 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
{
return true;
}
}
else
{
// If the limit collection management setting is disabled, allow any user to create collections
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
{
return true;
}
}
// Allow provider users to create collections if they are a provider for the target organization
return await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId);
@ -267,8 +257,6 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
return true;
}
if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
var userIsMemberOfOrg = org is not null;
var limitCollectionDeletionEnabled = await GetOrganizationAbilityAsync(org) is { LimitCollectionDeletion: true };
var userIsOrgOwnerOrAdmin = org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
@ -277,20 +265,6 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
{
return true;
}
}
else
{
// If LimitCollectionCreationDeletion is false, AllowAdminAccessToAllCollectionItems setting is irrelevant.
// Ensure acting user has manage permissions for all collections being deleted
// If LimitCollectionCreationDeletion is true, only Owners and Admins can delete collections they manage
var organizationAbility = await GetOrganizationAbilityAsync(org);
var canDeleteManagedCollections = organizationAbility is { LimitCollectionCreationDeletion: false } ||
org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
if (canDeleteManagedCollections && await CanManageCollectionsAsync(resources, org))
{
return true;
}
}
// Allow providers to delete collections if they are a provider for the target organization
return await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId);

View File

@ -166,5 +166,12 @@ public class CipherMiniDetailsResponseModel : CipherMiniResponseModel
CollectionIds = cipher.CollectionIds ?? new List<Guid>();
}
public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher,
GlobalSettings globalSettings, string obj = "cipherMiniDetails")
: base(cipher, globalSettings, cipher.OrganizationUseTotp, obj)
{
CollectionIds = cipher.CollectionIds ?? new List<Guid>();
}
public IEnumerable<Guid> CollectionIds { get; set; }
}

View File

@ -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;
}
}
}

View File

@ -2,6 +2,7 @@
public enum EventSystemUser : byte
{
Unknown = 0,
SCIM = 1,
DomainVerification = 2,
PublicApi = 3,

View 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; }
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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
};

View File

@ -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; }
}

View 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)}");
}

View 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; }
}

View File

@ -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
{
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(
public class VerifyOrganizationDomainCommand(
IOrganizationDomainRepository organizationDomainRepository,
IDnsResolverService dnsResolverService,
IEventService eventService,
IGlobalSettings globalSettings,
IPolicyService policyService,
IFeatureService featureService,
ICurrentContext currentContext,
ISavePolicyCommand savePolicyCommand,
IMailService mailService,
IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
ILogger<VerifyOrganizationDomainCommand> logger)
: IVerifyOrganizationDomainCommand
{
_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));
}
}

View File

@ -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) { }
}
}
}

View File

@ -1,14 +1,53 @@
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);
}

View File

@ -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);
}

View File

@ -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,79 @@ 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 organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
ValidateDeleteUser(organizationId, organizationUser);
var result = await RemoveUsersInternalAsync(organizationId, organizationUserIds, deletingUserId, eventSystemUser: null);
await RepositoryDeleteUserAsync(organizationUser, null);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
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)));
}
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.");
return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage));
}
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId))
public async Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(
Guid organizationId, IEnumerable<Guid> organizationUserIds, EventSystemUser eventSystemUser)
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
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)));
}
var deletingUserIsOwner = false;
if (deletingUserId.HasValue)
{
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage));
}
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 +174,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;
}
}

View File

@ -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) { }
}

View File

@ -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;
}

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -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)

View File

@ -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()
{

View File

@ -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 => [];
@ -53,10 +60,54 @@ public class SingleOrgPolicyValidator : IPolicyValidator
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
{
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 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)
{
@ -67,7 +118,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 =>
@ -86,8 +137,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
&& 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 +161,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;
}
}

View File

@ -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,15 +48,64 @@ 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 })
{
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)
{
@ -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("");
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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>

View File

@ -16,7 +16,6 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
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 +501,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,7 +518,8 @@ 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);
var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license);
var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception);
if (!canUse)
{
throw new BadRequestException(exception);
@ -704,14 +581,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";
@ -2332,10 +2201,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 +2215,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 +2225,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)

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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();
}
}

View 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);
}
}

View File

@ -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>();
}
}

View 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);
}

View 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; }
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -129,7 +129,6 @@ public static class FeatureFlagKeys
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";
@ -146,7 +145,6 @@ public static class FeatureFlagKeys
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";
@ -155,7 +153,12 @@ 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 SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor";
public static List<string> GetAllKeys()
{

View File

@ -36,7 +36,7 @@
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
<PackageReference Include="MailKit" Version="4.8.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" />

View File

@ -0,0 +1,24 @@
{{#>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>This account should only be used to store items related to {{OrganizationName}}</li>
<li>Admins managing your Bitwarden organization manage your email address and other account settings</li>
<li>Admins can also revoke or delete your account at any time</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}}

View File

@ -0,0 +1,8 @@
As a member of {{OrganizationName}}, your Bitwarden account is claimed and owned by your organization.
Here's what that means:
- This account should only be used to store items related to {{OrganizationName}}
- Your admins managing your Bitwarden organization manages your email address and other account settings
- Your admins can also revoke or delete your account at any time
For more information, please refer to the following help article: Claimed Accounts (https://bitwarden.com/help/claimed-accounts)

View File

@ -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}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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();

View File

@ -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,15 +352,94 @@ 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)
{
throw new NotSupportedException($"Version {Version} is not supported.");
}
var valid =
globalSettings.Installation.Id == InstallationId &&
organization.LicenseKey != null && organization.LicenseKey.Equals(LicenseKey) &&
@ -385,9 +525,6 @@ public class OrganizationLicense : ILicense
return valid;
}
throw new NotSupportedException($"Version {Version} is not supported.");
}
public bool VerifySignature(X509Certificate2 certificate)
{
using (var rsa = certificate.GetRSAPublicKey())

View File

@ -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,24 +195,48 @@ 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)
{
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);
}
throw new NotSupportedException($"Version {Version} is not supported.");
}
public bool VerifySignature(X509Certificate2 certificate)
{
using (var rsa = certificate.GetRSAPublicKey())

View 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>()) { }
}

View File

@ -0,0 +1,5 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.Models.Data.Organizations;
public record ManagedUserDomainClaimedEmails(IEnumerable<string> EmailList, Organization Organization);

View File

@ -0,0 +1,6 @@
namespace Bit.Core.Models.Mail;
public class ClaimedDomainUserNotificationViewModel : BaseTitleContactUsMailModel
{
public string OrganizationName { get; init; }
}

View File

@ -0,0 +1,6 @@
namespace Bit.Core.Models.Mail;
public class OrganizationUserRevokedForPolicySingleOrgViewModel : BaseMailModel
{
public string OrganizationName { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace Bit.Core.Models.Mail;
public class OrganizationUserRevokedForPolicyTwoFactorViewModel : BaseMailModel
{
public string OrganizationName { get; set; }
}

View File

@ -33,6 +33,10 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
}
var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization);
return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version);
return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version)
{
Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo)
};
}
}

View File

@ -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)

View File

@ -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>();

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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,
@ -91,5 +94,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);
}

View File

@ -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}, here is a heads up on your claimed account:",
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);

View File

@ -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;
@ -233,10 +247,24 @@ public class LicensingService : ILicensingService
}
public bool VerifyLicense(ILicense license)
{
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)
{
if (_globalSettings.SelfHosted || !_certificate.HasPrivateKey)
@ -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);
}
}

View File

@ -908,7 +908,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 +989,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 +1115,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 +1130,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)

View File

@ -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);
}
}

View File

@ -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;
@ -79,6 +80,12 @@ public class NoopMailService : IMailService
return Task.FromResult(0);
}
public Task SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(string organizationName, string email) =>
Task.CompletedTask;
public Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) =>
Task.CompletedTask;
public Task SendTwoFactorEmailAsync(string email, string token)
{
return Task.FromResult(0);
@ -303,5 +310,6 @@ public class NoopMailService : IMailService
{
return Task.FromResult(0);
}
public Task SendClaimedDomainUserEmailAsync(ManagedUserDomainClaimedEmails emailList) => Task.CompletedTask;
}

View File

@ -27,4 +27,14 @@ public interface IOrganizationCiphersQuery
/// </summary>
/// <exception cref="FeatureUnavailableException"></exception>
Task<IEnumerable<CipherOrganizationDetails>> GetUnassignedOrganizationCiphers(Guid organizationId);
/// <summary>
/// Returns ciphers belonging to the organization that are in the specified collections.
/// </summary>
/// <remarks>
/// Note that the <see cref="CipherOrganizationDetailsWithCollections.CollectionIds"/> will include all collections
/// the cipher belongs to even if it is not in the <paramref name="collectionIds"/> parameter.
/// </remarks>
public Task<IEnumerable<CipherOrganizationDetailsWithCollections>> GetOrganizationCiphersByCollectionIds(
Guid organizationId, IEnumerable<Guid> collectionIds);
}

View File

@ -52,4 +52,13 @@ public class OrganizationCiphersQuery : IOrganizationCiphersQuery
{
return await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId);
}
/// <inheritdoc />
public async Task<IEnumerable<CipherOrganizationDetailsWithCollections>> GetOrganizationCiphersByCollectionIds(
Guid organizationId, IEnumerable<Guid> collectionIds)
{
var managedCollectionIds = collectionIds.ToHashSet();
var allOrganizationCiphers = await GetAllOrganizationCiphers(organizationId);
return allOrganizationCiphers.Where(c => c.CollectionIds.Intersect(managedCollectionIds).Any());
}
}

View File

@ -557,4 +557,14 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
return results.ToList();
}
}
public async Task RevokeManyByIdAsync(IEnumerable<Guid> organizationUserIds)
{
await using var connection = new SqlConnection(ConnectionString);
await connection.ExecuteAsync(
"[dbo].[OrganizationUser_SetStatusForUsersById]",
new { OrganizationUserIds = JsonSerializer.Serialize(organizationUserIds), Status = OrganizationUserStatusType.Revoked },
commandType: CommandType.StoredProcedure);
}
}

Some files were not shown because too many files have changed in this diff Show More