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:
commit
1cf6d08616
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
1
.github/CODEOWNERS
vendored
@ -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
|
||||
|
6
.github/renovate.json
vendored
6
.github/renovate.json
vendored
@ -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": [
|
||||
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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)]
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
20
src/Api/Tools/Authorization/VaultExportOperations.cs
Normal file
20
src/Api/Tools/Authorization/VaultExportOperations.cs
Normal 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) };
|
||||
}
|
@ -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));
|
||||
|
@ -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; }
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -96,18 +96,6 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
/// </summary>
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
// Deprecated by https://bitwarden.atlassian.net/browse/PM-10863. This
|
||||
// was replaced with `LimitCollectionCreation` and
|
||||
// `LimitCollectionDeletion`.
|
||||
public bool LimitCollectionCreationDeletion
|
||||
{
|
||||
get => LimitCollectionCreation || LimitCollectionDeletion;
|
||||
set
|
||||
{
|
||||
LimitCollectionCreation = value;
|
||||
LimitCollectionDeletion = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, admins, owners, and some custom users can read/write all collections and items in the Admin Console.
|
||||
@ -115,6 +103,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
/// </summary>
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Risk Insights is a reporting feature that provides insights into the security of an organization's vault.
|
||||
/// </summary>
|
||||
public bool UseRiskInsights { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
if (Id == default(Guid))
|
||||
@ -314,11 +307,5 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
UseSecretsManager = license.UseSecretsManager;
|
||||
SmSeats = license.SmSeats;
|
||||
SmServiceAccounts = license.SmServiceAccounts;
|
||||
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||
{
|
||||
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
public enum EventSystemUser : byte
|
||||
{
|
||||
Unknown = 0,
|
||||
SCIM = 1,
|
||||
DomainVerification = 2,
|
||||
PublicApi = 3,
|
||||
|
10
src/Core/AdminConsole/Models/Data/IActingUser.cs
Normal file
10
src/Core/AdminConsole/Models/Data/IActingUser.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data;
|
||||
|
||||
public interface IActingUser
|
||||
{
|
||||
Guid? UserId { get; }
|
||||
bool IsOrganizationOwnerOrProvider { get; }
|
||||
EventSystemUser? SystemUserType { get; }
|
||||
}
|
@ -23,9 +23,8 @@ public class OrganizationAbility
|
||||
UsePolicies = organization.UsePolicies;
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@ -42,7 +41,6 @@ public class OrganizationAbility
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
}
|
||||
|
@ -56,7 +56,6 @@ public class OrganizationUserOrganizationDetails
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
}
|
||||
|
@ -146,8 +146,6 @@ public class SelfHostedOrganizationDetails : Organization
|
||||
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
|
||||
LimitCollectionCreation = LimitCollectionCreation,
|
||||
LimitCollectionDeletion = LimitCollectionDeletion,
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
|
||||
Status = Status
|
||||
};
|
||||
|
@ -42,6 +42,6 @@ public class ProviderUserOrganizationDetails
|
||||
public PlanType PlanType { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
}
|
||||
|
16
src/Core/AdminConsole/Models/Data/StandardUser.cs
Normal file
16
src/Core/AdminConsole/Models/Data/StandardUser.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data;
|
||||
|
||||
public class StandardUser : IActingUser
|
||||
{
|
||||
public StandardUser(Guid userId, bool isOrganizationOwner)
|
||||
{
|
||||
UserId = userId;
|
||||
IsOrganizationOwnerOrProvider = isOrganizationOwner;
|
||||
}
|
||||
|
||||
public Guid? UserId { get; }
|
||||
public bool IsOrganizationOwnerOrProvider { get; }
|
||||
public EventSystemUser? SystemUserType => throw new Exception($"{nameof(StandardUser)} does not have a {nameof(SystemUserType)}");
|
||||
}
|
16
src/Core/AdminConsole/Models/Data/SystemUser.cs
Normal file
16
src/Core/AdminConsole/Models/Data/SystemUser.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data;
|
||||
|
||||
public class SystemUser : IActingUser
|
||||
{
|
||||
public SystemUser(EventSystemUser systemUser)
|
||||
{
|
||||
SystemUserType = systemUser;
|
||||
}
|
||||
|
||||
public Guid? UserId => throw new Exception($"{nameof(SystemUserType)} does not have a {nameof(UserId)}.");
|
||||
|
||||
public bool IsOrganizationOwnerOrProvider => false;
|
||||
public EventSystemUser? SystemUserType { get; }
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -12,124 +15,145 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
|
||||
public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -19,7 +23,10 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly IPushNotificationService _pushService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
public DeleteManagedOrganizationUserAccountCommand(
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
@ -27,7 +34,11 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IUserRepository userRepository,
|
||||
ICurrentContext currentContext,
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||
IReferenceEventService referenceEventService,
|
||||
IPushNotificationService pushService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderUserRepository providerUserRepository)
|
||||
{
|
||||
_userService = userService;
|
||||
_eventService = eventService;
|
||||
@ -36,6 +47,10 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
||||
_userRepository = userRepository;
|
||||
_currentContext = currentContext;
|
||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||
_referenceEventService = referenceEventService;
|
||||
_pushService = pushService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
}
|
||||
|
||||
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
|
||||
@ -89,7 +104,8 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
||||
throw new NotFoundException("Member not found.");
|
||||
}
|
||||
|
||||
await _userService.DeleteAsync(user);
|
||||
await ValidateUserMembershipAndPremiumAsync(user);
|
||||
|
||||
results.Add((orgUserId, string.Empty));
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -98,6 +114,15 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
||||
}
|
||||
}
|
||||
|
||||
var orgUserResultsToDelete = results.Where(result => string.IsNullOrEmpty(result.ErrorMessage));
|
||||
var orgUsersToDelete = orgUsers.Where(orgUser => orgUserResultsToDelete.Any(result => orgUser.Id == result.OrganizationUserId));
|
||||
var usersToDelete = users.Where(user => orgUsersToDelete.Any(orgUser => orgUser.UserId == user.Id));
|
||||
|
||||
if (usersToDelete.Any())
|
||||
{
|
||||
await DeleteManyAsync(usersToDelete);
|
||||
}
|
||||
|
||||
await LogDeletedOrganizationUsersAsync(orgUsers, results);
|
||||
|
||||
return results;
|
||||
@ -158,4 +183,59 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
|
||||
await _eventService.LogOrganizationUserEventsAsync(events);
|
||||
}
|
||||
}
|
||||
private async Task DeleteManyAsync(IEnumerable<User> users)
|
||||
{
|
||||
|
||||
await _userRepository.DeleteManyAsync(users);
|
||||
foreach (var user in users)
|
||||
{
|
||||
await _referenceEventService.RaiseEventAsync(
|
||||
new ReferenceEvent(ReferenceEventType.DeleteAccount, user, _currentContext));
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task ValidateUserMembershipAndPremiumAsync(User user)
|
||||
{
|
||||
// Check if user is the only owner of any organizations.
|
||||
var onlyOwnerCount = await _organizationUserRepository.GetCountByOnlyOwnerAsync(user.Id);
|
||||
if (onlyOwnerCount > 0)
|
||||
{
|
||||
throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.");
|
||||
}
|
||||
|
||||
var orgs = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed);
|
||||
if (orgs.Count == 1)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(orgs.First().OrganizationId);
|
||||
if (org != null && (!org.Enabled || string.IsNullOrWhiteSpace(org.GatewaySubscriptionId)))
|
||||
{
|
||||
var orgCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(org.Id);
|
||||
if (orgCount <= 1)
|
||||
{
|
||||
await _organizationRepository.DeleteAsync(org);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var onlyOwnerProviderCount = await _providerUserRepository.GetCountByOnlyOwnerAsync(user.Id);
|
||||
if (onlyOwnerProviderCount > 0)
|
||||
{
|
||||
throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(user.GatewaySubscriptionId))
|
||||
{
|
||||
try
|
||||
{
|
||||
await _userService.CancelPremiumAsync(user);
|
||||
}
|
||||
catch (GatewayException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
|
||||
public record RevokeOrganizationUsersRequest(
|
||||
Guid OrganizationId,
|
||||
IEnumerable<OrganizationUserUserDetails> OrganizationUsers,
|
||||
IActingUser ActionPerformedBy)
|
||||
{
|
||||
public RevokeOrganizationUsersRequest(Guid organizationId, OrganizationUserUserDetails organizationUser, IActingUser actionPerformedBy)
|
||||
: this(organizationId, [organizationUser], actionPerformedBy) { }
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Commands;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
public class RevokeNonCompliantOrganizationUserCommand(IOrganizationUserRepository organizationUserRepository,
|
||||
IEventService eventService,
|
||||
IHasConfirmedOwnersExceptQuery confirmedOwnersExceptQuery,
|
||||
TimeProvider timeProvider) : IRevokeNonCompliantOrganizationUserCommand
|
||||
{
|
||||
public const string ErrorCannotRevokeSelf = "You cannot revoke yourself.";
|
||||
public const string ErrorOnlyOwnersCanRevokeOtherOwners = "Only owners can revoke other owners.";
|
||||
public const string ErrorUserAlreadyRevoked = "User is already revoked.";
|
||||
public const string ErrorOrgMustHaveAtLeastOneOwner = "Organization must have at least one confirmed owner.";
|
||||
public const string ErrorInvalidUsers = "Invalid users.";
|
||||
public const string ErrorRequestedByWasNotValid = "Action was performed by an unexpected type.";
|
||||
|
||||
public async Task<CommandResult> RevokeNonCompliantOrganizationUsersAsync(RevokeOrganizationUsersRequest request)
|
||||
{
|
||||
var validationResult = await ValidateAsync(request);
|
||||
|
||||
if (validationResult.HasErrors)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
await organizationUserRepository.RevokeManyByIdAsync(request.OrganizationUsers.Select(x => x.Id));
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
switch (request.ActionPerformedBy)
|
||||
{
|
||||
case StandardUser:
|
||||
await eventService.LogOrganizationUserEventsAsync(
|
||||
request.OrganizationUsers.Select(x => GetRevokedUserEventTuple(x, now)));
|
||||
break;
|
||||
case SystemUser { SystemUserType: not null } loggableSystem:
|
||||
await eventService.LogOrganizationUserEventsAsync(
|
||||
request.OrganizationUsers.Select(x =>
|
||||
GetRevokedUserEventBySystemUserTuple(x, loggableSystem.SystemUserType.Value, now)));
|
||||
break;
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
private static (OrganizationUserUserDetails organizationUser, EventType eventType, DateTime? time) GetRevokedUserEventTuple(
|
||||
OrganizationUserUserDetails organizationUser, DateTimeOffset dateTimeOffset) =>
|
||||
new(organizationUser, EventType.OrganizationUser_Revoked, dateTimeOffset.UtcDateTime);
|
||||
|
||||
private static (OrganizationUserUserDetails organizationUser, EventType eventType, EventSystemUser eventSystemUser, DateTime? time) GetRevokedUserEventBySystemUserTuple(
|
||||
OrganizationUserUserDetails organizationUser, EventSystemUser systemUser, DateTimeOffset dateTimeOffset) => new(organizationUser,
|
||||
EventType.OrganizationUser_Revoked, systemUser, dateTimeOffset.UtcDateTime);
|
||||
|
||||
private async Task<CommandResult> ValidateAsync(RevokeOrganizationUsersRequest request)
|
||||
{
|
||||
if (!PerformedByIsAnExpectedType(request.ActionPerformedBy))
|
||||
{
|
||||
return new CommandResult(ErrorRequestedByWasNotValid);
|
||||
}
|
||||
|
||||
if (request.ActionPerformedBy is StandardUser user
|
||||
&& request.OrganizationUsers.Any(x => x.UserId == user.UserId))
|
||||
{
|
||||
return new CommandResult(ErrorCannotRevokeSelf);
|
||||
}
|
||||
|
||||
if (request.OrganizationUsers.Any(x => x.OrganizationId != request.OrganizationId))
|
||||
{
|
||||
return new CommandResult(ErrorInvalidUsers);
|
||||
}
|
||||
|
||||
if (!await confirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
|
||||
request.OrganizationId,
|
||||
request.OrganizationUsers.Select(x => x.Id)))
|
||||
{
|
||||
return new CommandResult(ErrorOrgMustHaveAtLeastOneOwner);
|
||||
}
|
||||
|
||||
return request.OrganizationUsers.Aggregate(new CommandResult(), (result, userToRevoke) =>
|
||||
{
|
||||
if (IsAlreadyRevoked(userToRevoke))
|
||||
{
|
||||
result.ErrorMessages.Add($"{ErrorUserAlreadyRevoked} Id: {userToRevoke.Id}");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (NonOwnersCannotRevokeOwners(userToRevoke, request.ActionPerformedBy))
|
||||
{
|
||||
result.ErrorMessages.Add($"{ErrorOnlyOwnersCanRevokeOtherOwners}");
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private static bool PerformedByIsAnExpectedType(IActingUser entity) => entity is SystemUser or StandardUser;
|
||||
|
||||
private static bool IsAlreadyRevoked(OrganizationUserUserDetails organizationUser) =>
|
||||
organizationUser is { Status: OrganizationUserStatusType.Revoked };
|
||||
|
||||
private static bool NonOwnersCannotRevokeOwners(OrganizationUserUserDetails organizationUser,
|
||||
IActingUser actingUser) =>
|
||||
actingUser is StandardUser { IsOrganizationOwnerOrProvider: false } && organizationUser.Type == OrganizationUserType.Owner;
|
||||
}
|
@ -0,0 +1,368 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
|
||||
public record SignUpOrganizationResponse(
|
||||
Organization Organization,
|
||||
OrganizationUser OrganizationUser,
|
||||
Collection DefaultCollection);
|
||||
|
||||
public interface ICloudOrganizationSignUpCommand
|
||||
{
|
||||
Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup);
|
||||
}
|
||||
|
||||
public class CloudOrganizationSignUpCommand(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IFeatureService featureService,
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IPaymentService paymentService,
|
||||
IPolicyService policyService,
|
||||
IReferenceEventService referenceEventService,
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IPushRegistrationService pushRegistrationService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
ICollectionRepository collectionRepository,
|
||||
IDeviceRepository deviceRepository) : ICloudOrganizationSignUpCommand
|
||||
{
|
||||
public async Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
|
||||
ValidatePasswordManagerPlan(plan, signup);
|
||||
|
||||
if (signup.UseSecretsManager)
|
||||
{
|
||||
if (signup.IsFromProvider)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"Organizations with a Managed Service Provider do not support Secrets Manager.");
|
||||
}
|
||||
ValidateSecretsManagerPlan(plan, signup);
|
||||
}
|
||||
|
||||
if (!signup.IsFromProvider)
|
||||
{
|
||||
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
|
||||
}
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
// Pre-generate the org id so that we can save it with the Stripe subscription
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
Name = signup.Name,
|
||||
BillingEmail = signup.BillingEmail,
|
||||
BusinessName = signup.BusinessName,
|
||||
PlanType = plan!.Type,
|
||||
Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats),
|
||||
MaxCollections = plan.PasswordManager.MaxCollections,
|
||||
MaxStorageGb = !plan.PasswordManager.BaseStorageGb.HasValue ?
|
||||
(short?)null : (short)(plan.PasswordManager.BaseStorageGb.Value + signup.AdditionalStorageGb),
|
||||
UsePolicies = plan.HasPolicies,
|
||||
UseSso = plan.HasSso,
|
||||
UseGroups = plan.HasGroups,
|
||||
UseEvents = plan.HasEvents,
|
||||
UseDirectory = plan.HasDirectory,
|
||||
UseTotp = plan.HasTotp,
|
||||
Use2fa = plan.Has2fa,
|
||||
UseApi = plan.HasApi,
|
||||
UseResetPassword = plan.HasResetPassword,
|
||||
SelfHost = plan.HasSelfHost,
|
||||
UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,
|
||||
UseCustomPermissions = plan.HasCustomPermissions,
|
||||
UseScim = plan.HasScim,
|
||||
Plan = plan.Name,
|
||||
Gateway = null,
|
||||
ReferenceData = signup.Owner.ReferenceData,
|
||||
Enabled = true,
|
||||
LicenseKey = CoreHelpers.SecureRandomString(20),
|
||||
PublicKey = signup.PublicKey,
|
||||
PrivateKey = signup.PrivateKey,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
Status = OrganizationStatusType.Created,
|
||||
UsePasswordManager = true,
|
||||
UseSecretsManager = signup.UseSecretsManager
|
||||
};
|
||||
|
||||
if (signup.UseSecretsManager)
|
||||
{
|
||||
organization.SmSeats = plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault();
|
||||
organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount +
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault();
|
||||
}
|
||||
|
||||
if (plan.Type == PlanType.Free && !signup.IsFromProvider)
|
||||
{
|
||||
var adminCount =
|
||||
await organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
|
||||
if (adminCount > 0)
|
||||
{
|
||||
throw new BadRequestException("You can only be an admin of one free organization.");
|
||||
}
|
||||
}
|
||||
else if (plan.Type != PlanType.Free)
|
||||
{
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||
{
|
||||
var sale = OrganizationSale.From(organization, signup);
|
||||
await organizationBillingService.Finalize(sale);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (signup.PaymentMethodType != null)
|
||||
{
|
||||
await paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
|
||||
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
|
||||
signup.PremiumAccessAddon, signup.TaxInfo, signup.IsFromProvider, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
||||
}
|
||||
else
|
||||
{
|
||||
await paymentService.PurchaseOrganizationNoPaymentMethod(organization, plan, signup.AdditionalSeats,
|
||||
signup.PremiumAccessAddon, signup.AdditionalSmSeats.GetValueOrDefault(),
|
||||
signup.AdditionalServiceAccounts.GetValueOrDefault(), signup.IsFromSecretsManagerTrial);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;
|
||||
var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true);
|
||||
await referenceEventService.RaiseEventAsync(
|
||||
new ReferenceEvent(ReferenceEventType.Signup, organization, currentContext)
|
||||
{
|
||||
PlanName = plan.Name,
|
||||
PlanType = plan.Type,
|
||||
Seats = returnValue.Item1.Seats,
|
||||
SignupInitiationPath = signup.InitiationPath,
|
||||
Storage = returnValue.Item1.MaxStorageGb,
|
||||
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
|
||||
});
|
||||
|
||||
return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser, returnValue.defaultCollection);
|
||||
}
|
||||
|
||||
public void ValidatePasswordManagerPlan(Plan plan, OrganizationUpgrade upgrade)
|
||||
{
|
||||
ValidatePlan(plan, upgrade.AdditionalSeats, "Password Manager");
|
||||
|
||||
if (plan.PasswordManager.BaseSeats + upgrade.AdditionalSeats <= 0)
|
||||
{
|
||||
throw new BadRequestException($"You do not have any Password Manager seats!");
|
||||
}
|
||||
|
||||
if (upgrade.AdditionalSeats < 0)
|
||||
{
|
||||
throw new BadRequestException($"You can't subtract Password Manager seats!");
|
||||
}
|
||||
|
||||
if (!plan.PasswordManager.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0)
|
||||
{
|
||||
throw new BadRequestException("Plan does not allow additional storage.");
|
||||
}
|
||||
|
||||
if (upgrade.AdditionalStorageGb < 0)
|
||||
{
|
||||
throw new BadRequestException("You can't subtract storage!");
|
||||
}
|
||||
|
||||
if (!plan.PasswordManager.HasPremiumAccessOption && upgrade.PremiumAccessAddon)
|
||||
{
|
||||
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
|
||||
}
|
||||
|
||||
if (!plan.PasswordManager.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0)
|
||||
{
|
||||
throw new BadRequestException("Plan does not allow additional users.");
|
||||
}
|
||||
|
||||
if (plan.PasswordManager.HasAdditionalSeatsOption && plan.PasswordManager.MaxAdditionalSeats.HasValue &&
|
||||
upgrade.AdditionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value)
|
||||
{
|
||||
throw new BadRequestException($"Selected plan allows a maximum of " +
|
||||
$"{plan.PasswordManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
||||
}
|
||||
}
|
||||
|
||||
public void ValidateSecretsManagerPlan(Plan plan, OrganizationUpgrade upgrade)
|
||||
{
|
||||
if (plan.SupportsSecretsManager == false)
|
||||
{
|
||||
throw new BadRequestException("Invalid Secrets Manager plan selected.");
|
||||
}
|
||||
|
||||
ValidatePlan(plan, upgrade.AdditionalSmSeats.GetValueOrDefault(), "Secrets Manager");
|
||||
|
||||
if (plan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats <= 0)
|
||||
{
|
||||
throw new BadRequestException($"You do not have any Secrets Manager seats!");
|
||||
}
|
||||
|
||||
if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0)
|
||||
{
|
||||
throw new BadRequestException("Plan does not allow additional Machine Accounts.");
|
||||
}
|
||||
|
||||
if ((plan.ProductTier == ProductTierType.TeamsStarter &&
|
||||
upgrade.AdditionalSmSeats.GetValueOrDefault() > plan.PasswordManager.BaseSeats) ||
|
||||
(plan.ProductTier != ProductTierType.TeamsStarter &&
|
||||
upgrade.AdditionalSmSeats.GetValueOrDefault() > upgrade.AdditionalSeats))
|
||||
{
|
||||
throw new BadRequestException("You cannot have more Secrets Manager seats than Password Manager seats.");
|
||||
}
|
||||
|
||||
if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0)
|
||||
{
|
||||
throw new BadRequestException("You can't subtract Machine Accounts!");
|
||||
}
|
||||
|
||||
switch (plan.SecretsManager.HasAdditionalSeatsOption)
|
||||
{
|
||||
case false when upgrade.AdditionalSmSeats > 0:
|
||||
throw new BadRequestException("Plan does not allow additional users.");
|
||||
case true when plan.SecretsManager.MaxAdditionalSeats.HasValue &&
|
||||
upgrade.AdditionalSmSeats > plan.SecretsManager.MaxAdditionalSeats.Value:
|
||||
throw new BadRequestException($"Selected plan allows a maximum of " +
|
||||
$"{plan.SecretsManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidatePlan(Plan plan, int additionalSeats, string productType)
|
||||
{
|
||||
if (plan is null)
|
||||
{
|
||||
throw new BadRequestException($"{productType} Plan was null.");
|
||||
}
|
||||
|
||||
if (plan.Disabled)
|
||||
{
|
||||
throw new BadRequestException($"{productType} Plan not found.");
|
||||
}
|
||||
|
||||
if (additionalSeats < 0)
|
||||
{
|
||||
throw new BadRequestException($"You can't subtract {productType} seats!");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
|
||||
{
|
||||
var anySingleOrgPolicies = await policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg);
|
||||
if (anySingleOrgPolicies)
|
||||
{
|
||||
throw new BadRequestException("You may not create an organization. You belong to an organization " +
|
||||
"which has a policy that prohibits you from being a member of any other organization.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(Organization organization,
|
||||
Guid ownerId, string ownerKey, string collectionName, bool withPayment)
|
||||
{
|
||||
try
|
||||
{
|
||||
await organizationRepository.CreateAsync(organization);
|
||||
await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
ApiKey = CoreHelpers.SecureRandomString(30),
|
||||
Type = OrganizationApiKeyType.Default,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
});
|
||||
await applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// ownerId == default if the org is created by a provider - in this case it's created without an
|
||||
// owner and the first owner is immediately invited afterwards
|
||||
OrganizationUser orgUser = null;
|
||||
if (ownerId != default)
|
||||
{
|
||||
orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = ownerId,
|
||||
Key = ownerKey,
|
||||
AccessSecretsManager = organization.UseSecretsManager,
|
||||
Type = OrganizationUserType.Owner,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
CreationDate = organization.CreationDate,
|
||||
RevisionDate = organization.CreationDate
|
||||
};
|
||||
orgUser.SetNewId();
|
||||
|
||||
await organizationUserRepository.CreateAsync(orgUser);
|
||||
|
||||
var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
|
||||
await pushRegistrationService.AddUserRegistrationOrganizationAsync(devices, organization.Id.ToString());
|
||||
await pushNotificationService.PushSyncOrgKeysAsync(ownerId);
|
||||
}
|
||||
|
||||
Collection defaultCollection = null;
|
||||
if (!string.IsNullOrWhiteSpace(collectionName))
|
||||
{
|
||||
defaultCollection = new Collection
|
||||
{
|
||||
Name = collectionName,
|
||||
OrganizationId = organization.Id,
|
||||
CreationDate = organization.CreationDate,
|
||||
RevisionDate = organization.CreationDate
|
||||
};
|
||||
|
||||
// Give the owner Can Manage access over the default collection
|
||||
List<CollectionAccessSelection> defaultOwnerAccess = null;
|
||||
if (orgUser != null)
|
||||
{
|
||||
defaultOwnerAccess =
|
||||
[new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }];
|
||||
}
|
||||
|
||||
await collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);
|
||||
}
|
||||
|
||||
return (organization, orgUser, defaultCollection);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (withPayment)
|
||||
{
|
||||
await paymentService.CancelAndRecoverChargesAsync(organization);
|
||||
}
|
||||
|
||||
if (organization.Id != default(Guid))
|
||||
{
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
||||
{
|
||||
var devices = await deviceRepository.GetManyByUserIdAsync(userId);
|
||||
return devices
|
||||
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||
.Select(d => d.Id.ToString());
|
||||
}
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
public interface ISavePolicyCommand
|
||||
{
|
||||
Task SaveAsync(PolicyUpdate policy);
|
||||
Task<Policy> SaveAsync(PolicyUpdate policy);
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ public class SavePolicyCommand : ISavePolicyCommand
|
||||
_policyValidators = policyValidatorsDict;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(PolicyUpdate policyUpdate)
|
||||
public async Task<Policy> SaveAsync(PolicyUpdate policyUpdate)
|
||||
{
|
||||
var org = await _applicationCacheService.GetOrganizationAbilityAsync(policyUpdate.OrganizationId);
|
||||
if (org == null)
|
||||
@ -74,6 +74,8 @@ public class SavePolicyCommand : ISavePolicyCommand
|
||||
|
||||
await _policyRepository.UpsertAsync(policy);
|
||||
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
|
||||
|
||||
return policy;
|
||||
}
|
||||
|
||||
private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate policyUpdate)
|
||||
|
@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
@ -15,6 +16,7 @@ public record PolicyUpdate
|
||||
public PolicyType Type { get; set; }
|
||||
public string? Data { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public IActingUser? PerformedBy { get; set; }
|
||||
|
||||
public T GetDataModel<T>() where T : IPolicyDataModel, new()
|
||||
{
|
||||
|
@ -2,8 +2,10 @@
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@ -18,6 +20,8 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
|
||||
public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
{
|
||||
public PolicyType Type => PolicyType.SingleOrg;
|
||||
private const string OrganizationNotFoundErrorMessage = "Organization not found.";
|
||||
private const string ClaimedDomainSingleOrganizationRequiredErrorMessage = "The Single organization policy is required for organizations that have enabled domain verification.";
|
||||
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IMailService _mailService;
|
||||
@ -27,6 +31,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||
|
||||
public SingleOrgPolicyValidator(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -36,7 +41,8 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_mailService = mailService;
|
||||
@ -46,6 +52,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
_featureService = featureService;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||
}
|
||||
|
||||
public IEnumerable<PolicyType> RequiredPolicies => [];
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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("");
|
||||
}
|
||||
|
@ -58,4 +58,6 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
/// Returns a list of OrganizationUsers with email domains that match one of the Organization's claimed domains.
|
||||
/// </summary>
|
||||
Task<ICollection<OrganizationUser>> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId);
|
||||
|
||||
Task RevokeManyByIdAsync(IEnumerable<Guid> organizationUserIds);
|
||||
}
|
||||
|
@ -20,13 +20,7 @@ public interface IOrganizationService
|
||||
Task AutoAddSeatsAsync(Organization organization, int seatsToAdd);
|
||||
Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment);
|
||||
Task VerifyBankAsync(Guid organizationId, int amount1, int amount2);
|
||||
/// <summary>
|
||||
/// Create a new organization in a cloud environment
|
||||
/// </summary>
|
||||
/// <returns>A tuple containing the new organization, the initial organizationUser (if any) and the default collection (if any)</returns>
|
||||
#nullable enable
|
||||
Task<(Organization organization, OrganizationUser? organizationUser, Collection? defaultCollection)> SignUpAsync(OrganizationSignup organizationSignup);
|
||||
|
||||
Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup);
|
||||
#nullable disable
|
||||
/// <summary>
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -9,8 +8,6 @@ namespace Bit.Core.AdminConsole.Services;
|
||||
|
||||
public interface IPolicyService
|
||||
{
|
||||
Task SaveAsync(Policy policy, Guid? savingUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Get the combined master password policy options for the specified user.
|
||||
/// </summary>
|
||||
|
@ -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)
|
||||
|
@ -1,17 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -22,100 +13,20 @@ namespace Bit.Core.AdminConsole.Services.Implementations;
|
||||
public class PolicyService : IPolicyService
|
||||
{
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
|
||||
public PolicyService(
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IEventService eventService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IMailService mailService,
|
||||
GlobalSettings globalSettings,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_eventService = eventService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_policyRepository = policyRepository;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_mailService = mailService;
|
||||
_globalSettings = globalSettings;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_featureService = featureService;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Policy policy, Guid? savingUserId)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.Pm13322AddPolicyDefinitions))
|
||||
{
|
||||
// Transitional mapping - this will be moved to callers once the feature flag is removed
|
||||
var policyUpdate = new PolicyUpdate
|
||||
{
|
||||
OrganizationId = policy.OrganizationId,
|
||||
Type = policy.Type,
|
||||
Enabled = policy.Enabled,
|
||||
Data = policy.Data
|
||||
};
|
||||
|
||||
await _savePolicyCommand.SaveAsync(policyUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new BadRequestException("Organization not found");
|
||||
}
|
||||
|
||||
if (!org.UsePolicies)
|
||||
{
|
||||
throw new BadRequestException("This organization cannot use policies.");
|
||||
}
|
||||
|
||||
// FIXME: This method will throw a bunch of errors based on if the
|
||||
// policy that is being applied requires some other policy that is
|
||||
// not enabled. It may be advisable to refactor this into a domain
|
||||
// object and get this kind of stuff out of the service.
|
||||
await HandleDependentPoliciesAsync(policy, org);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (policy.Id == default(Guid))
|
||||
{
|
||||
policy.CreationDate = now;
|
||||
}
|
||||
|
||||
policy.RevisionDate = now;
|
||||
|
||||
// We can exit early for disable operations, because they are
|
||||
// simpler.
|
||||
if (!policy.Enabled)
|
||||
{
|
||||
await SetPolicyConfiguration(policy);
|
||||
return;
|
||||
}
|
||||
|
||||
await EnablePolicyAsync(policy, org, savingUserId);
|
||||
}
|
||||
|
||||
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
|
||||
@ -181,178 +92,4 @@ public class PolicyService : IPolicyService
|
||||
|
||||
return new[] { OrganizationUserType.Owner, OrganizationUserType.Admin };
|
||||
}
|
||||
|
||||
private async Task DependsOnSingleOrgAsync(Organization org)
|
||||
{
|
||||
var singleOrg = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.SingleOrg);
|
||||
if (singleOrg?.Enabled != true)
|
||||
{
|
||||
throw new BadRequestException("Single Organization policy not enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredBySsoAsync(Organization org)
|
||||
{
|
||||
var requireSso = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.RequireSso);
|
||||
if (requireSso?.Enabled == true)
|
||||
{
|
||||
throw new BadRequestException("Single Sign-On Authentication policy is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredByKeyConnectorAsync(Organization org)
|
||||
{
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
|
||||
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector)
|
||||
{
|
||||
throw new BadRequestException("Key Connector is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredByAccountRecoveryAsync(Organization org)
|
||||
{
|
||||
var requireSso = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.ResetPassword);
|
||||
if (requireSso?.Enabled == true)
|
||||
{
|
||||
throw new BadRequestException("Account recovery policy is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredByVaultTimeoutAsync(Organization org)
|
||||
{
|
||||
var vaultTimeout = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.MaximumVaultTimeout);
|
||||
if (vaultTimeout?.Enabled == true)
|
||||
{
|
||||
throw new BadRequestException("Maximum Vault Timeout policy is enabled.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequiredBySsoTrustedDeviceEncryptionAsync(Organization org)
|
||||
{
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
|
||||
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption)
|
||||
{
|
||||
throw new BadRequestException("Trusted device encryption is on and requires this policy.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleDependentPoliciesAsync(Policy policy, Organization org)
|
||||
{
|
||||
switch (policy.Type)
|
||||
{
|
||||
case PolicyType.SingleOrg:
|
||||
if (!policy.Enabled)
|
||||
{
|
||||
await HasVerifiedDomainsAsync(org);
|
||||
await RequiredBySsoAsync(org);
|
||||
await RequiredByVaultTimeoutAsync(org);
|
||||
await RequiredByKeyConnectorAsync(org);
|
||||
await RequiredByAccountRecoveryAsync(org);
|
||||
}
|
||||
break;
|
||||
|
||||
case PolicyType.RequireSso:
|
||||
if (policy.Enabled)
|
||||
{
|
||||
await DependsOnSingleOrgAsync(org);
|
||||
}
|
||||
else
|
||||
{
|
||||
await RequiredByKeyConnectorAsync(org);
|
||||
await RequiredBySsoTrustedDeviceEncryptionAsync(org);
|
||||
}
|
||||
break;
|
||||
|
||||
case PolicyType.ResetPassword:
|
||||
if (!policy.Enabled || policy.GetDataModel<ResetPasswordDataModel>()?.AutoEnrollEnabled == false)
|
||||
{
|
||||
await RequiredBySsoTrustedDeviceEncryptionAsync(org);
|
||||
}
|
||||
|
||||
if (policy.Enabled)
|
||||
{
|
||||
await DependsOnSingleOrgAsync(org);
|
||||
}
|
||||
break;
|
||||
|
||||
case PolicyType.MaximumVaultTimeout:
|
||||
if (policy.Enabled)
|
||||
{
|
||||
await DependsOnSingleOrgAsync(org);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HasVerifiedDomainsAsync(Organization org)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(org.Id))
|
||||
{
|
||||
throw new BadRequestException("The Single organization policy is required for organizations that have enabled domain verification.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetPolicyConfiguration(Policy policy)
|
||||
{
|
||||
await _policyRepository.UpsertAsync(policy);
|
||||
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
|
||||
}
|
||||
|
||||
private async Task EnablePolicyAsync(Policy policy, Organization org, Guid? savingUserId)
|
||||
{
|
||||
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
|
||||
if (!currentPolicy?.Enabled ?? true)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(policy.OrganizationId);
|
||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
|
||||
var removableOrgUsers = orgUsers.Where(ou =>
|
||||
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
|
||||
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
|
||||
ou.UserId != savingUserId);
|
||||
switch (policy.Type)
|
||||
{
|
||||
case PolicyType.TwoFactorAuthentication:
|
||||
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
|
||||
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
|
||||
{
|
||||
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id).twoFactorIsEnabled;
|
||||
if (!userTwoFactorEnabled)
|
||||
{
|
||||
if (!orgUser.HasMasterPassword)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
|
||||
}
|
||||
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id,
|
||||
savingUserId);
|
||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||
org.DisplayName(), orgUser.Email);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PolicyType.SingleOrg:
|
||||
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
|
||||
removableOrgUsers.Select(ou => ou.UserId.Value));
|
||||
foreach (var orgUser in removableOrgUsers)
|
||||
{
|
||||
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
|
||||
&& ou.OrganizationId != org.Id
|
||||
&& ou.Status != OrganizationUserStatusType.Invited))
|
||||
{
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id,
|
||||
savingUserId);
|
||||
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
|
||||
org.DisplayName(), orgUser.Email);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await SetPolicyConfiguration(policy);
|
||||
}
|
||||
}
|
||||
|
@ -1,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);
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Caches.Implementations;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Implementations;
|
||||
|
||||
@ -15,5 +16,6 @@ public static class ServiceCollectionExtensions
|
||||
services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>();
|
||||
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
|
||||
services.AddTransient<ISubscriberService, SubscriberService>();
|
||||
services.AddLicenseServices();
|
||||
}
|
||||
}
|
||||
|
151
src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs
Normal file
151
src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs
Normal file
@ -0,0 +1,151 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Extensions;
|
||||
|
||||
public static class LicenseExtensions
|
||||
{
|
||||
public static DateTime CalculateFreshExpirationDate(this Organization org, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
if (subscriptionInfo?.Subscription == null)
|
||||
{
|
||||
if (org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue)
|
||||
{
|
||||
return org.ExpirationDate.Value;
|
||||
}
|
||||
|
||||
return DateTime.UtcNow.AddDays(7);
|
||||
}
|
||||
|
||||
var subscription = subscriptionInfo.Subscription;
|
||||
|
||||
if (subscription.TrialEndDate > DateTime.UtcNow)
|
||||
{
|
||||
return subscription.TrialEndDate.Value;
|
||||
}
|
||||
|
||||
if (org.ExpirationDate.HasValue && org.ExpirationDate.Value < DateTime.UtcNow)
|
||||
{
|
||||
return org.ExpirationDate.Value;
|
||||
}
|
||||
|
||||
if (subscription.PeriodEndDate.HasValue && subscription.PeriodDuration > TimeSpan.FromDays(180))
|
||||
{
|
||||
return subscription.PeriodEndDate
|
||||
.Value
|
||||
.AddDays(Bit.Core.Constants.OrganizationSelfHostSubscriptionGracePeriodDays);
|
||||
}
|
||||
|
||||
return org.ExpirationDate?.AddMonths(11) ?? DateTime.UtcNow.AddYears(1);
|
||||
}
|
||||
|
||||
public static DateTime CalculateFreshRefreshDate(this Organization org, SubscriptionInfo subscriptionInfo, DateTime expirationDate)
|
||||
{
|
||||
if (subscriptionInfo?.Subscription == null ||
|
||||
subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow ||
|
||||
org.ExpirationDate < DateTime.UtcNow)
|
||||
{
|
||||
return expirationDate;
|
||||
}
|
||||
|
||||
return subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180) ||
|
||||
DateTime.UtcNow - expirationDate > TimeSpan.FromDays(30)
|
||||
? DateTime.UtcNow.AddDays(30)
|
||||
: expirationDate;
|
||||
}
|
||||
|
||||
public static DateTime CalculateFreshExpirationDateWithoutGracePeriod(this Organization org, SubscriptionInfo subscriptionInfo, DateTime expirationDate)
|
||||
{
|
||||
if (subscriptionInfo?.Subscription is null)
|
||||
{
|
||||
return expirationDate;
|
||||
}
|
||||
|
||||
var subscription = subscriptionInfo.Subscription;
|
||||
|
||||
if (subscription.TrialEndDate <= DateTime.UtcNow &&
|
||||
org.ExpirationDate >= DateTime.UtcNow &&
|
||||
subscription.PeriodEndDate.HasValue &&
|
||||
subscription.PeriodDuration > TimeSpan.FromDays(180))
|
||||
{
|
||||
return subscription.PeriodEndDate.Value;
|
||||
}
|
||||
|
||||
return expirationDate;
|
||||
}
|
||||
|
||||
public static T GetValue<T>(this ClaimsPrincipal principal, string claimType)
|
||||
{
|
||||
var claim = principal.FindFirst(claimType);
|
||||
|
||||
if (claim is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
// Handle Guid
|
||||
if (typeof(T) == typeof(Guid))
|
||||
{
|
||||
return Guid.TryParse(claim.Value, out var guid)
|
||||
? (T)(object)guid
|
||||
: default;
|
||||
}
|
||||
|
||||
// Handle DateTime
|
||||
if (typeof(T) == typeof(DateTime))
|
||||
{
|
||||
return DateTime.TryParse(claim.Value, out var dateTime)
|
||||
? (T)(object)dateTime
|
||||
: default;
|
||||
}
|
||||
|
||||
// Handle TimeSpan
|
||||
if (typeof(T) == typeof(TimeSpan))
|
||||
{
|
||||
return TimeSpan.TryParse(claim.Value, out var timeSpan)
|
||||
? (T)(object)timeSpan
|
||||
: default;
|
||||
}
|
||||
|
||||
// Check for Nullable Types
|
||||
var underlyingType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
|
||||
// Handle Enums
|
||||
if (underlyingType.IsEnum)
|
||||
{
|
||||
if (Enum.TryParse(underlyingType, claim.Value, true, out var enumValue))
|
||||
{
|
||||
return (T)enumValue; // Cast back to T
|
||||
}
|
||||
|
||||
return default; // Return default value for non-nullable enums or null for nullable enums
|
||||
}
|
||||
|
||||
// Handle other Nullable Types (e.g., int?, bool?)
|
||||
if (underlyingType == typeof(int))
|
||||
{
|
||||
return int.TryParse(claim.Value, out var intValue)
|
||||
? (T)(object)intValue
|
||||
: default;
|
||||
}
|
||||
|
||||
if (underlyingType == typeof(bool))
|
||||
{
|
||||
return bool.TryParse(claim.Value, out var boolValue)
|
||||
? (T)(object)boolValue
|
||||
: default;
|
||||
}
|
||||
|
||||
if (underlyingType == typeof(double))
|
||||
{
|
||||
return double.TryParse(claim.Value, out var doubleValue)
|
||||
? (T)(object)doubleValue
|
||||
: default;
|
||||
}
|
||||
|
||||
// Fallback to Convert.ChangeType for other types including strings
|
||||
return (T)Convert.ChangeType(claim.Value, underlyingType);
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Licenses.Services;
|
||||
using Bit.Core.Billing.Licenses.Services.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Extensions;
|
||||
|
||||
public static class LicenseServiceCollectionExtensions
|
||||
{
|
||||
public static void AddLicenseServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<ILicenseClaimsFactory<Organization>, OrganizationLicenseClaimsFactory>();
|
||||
services.AddTransient<ILicenseClaimsFactory<User>, UserLicenseClaimsFactory>();
|
||||
}
|
||||
}
|
58
src/Core/Billing/Licenses/LicenseConstants.cs
Normal file
58
src/Core/Billing/Licenses/LicenseConstants.cs
Normal file
@ -0,0 +1,58 @@
|
||||
namespace Bit.Core.Billing.Licenses;
|
||||
|
||||
public static class OrganizationLicenseConstants
|
||||
{
|
||||
public const string LicenseType = nameof(LicenseType);
|
||||
public const string LicenseKey = nameof(LicenseKey);
|
||||
public const string InstallationId = nameof(InstallationId);
|
||||
public const string Id = nameof(Id);
|
||||
public const string Name = nameof(Name);
|
||||
public const string BusinessName = nameof(BusinessName);
|
||||
public const string BillingEmail = nameof(BillingEmail);
|
||||
public const string Enabled = nameof(Enabled);
|
||||
public const string Plan = nameof(Plan);
|
||||
public const string PlanType = nameof(PlanType);
|
||||
public const string Seats = nameof(Seats);
|
||||
public const string MaxCollections = nameof(MaxCollections);
|
||||
public const string UsePolicies = nameof(UsePolicies);
|
||||
public const string UseSso = nameof(UseSso);
|
||||
public const string UseKeyConnector = nameof(UseKeyConnector);
|
||||
public const string UseScim = nameof(UseScim);
|
||||
public const string UseGroups = nameof(UseGroups);
|
||||
public const string UseEvents = nameof(UseEvents);
|
||||
public const string UseDirectory = nameof(UseDirectory);
|
||||
public const string UseTotp = nameof(UseTotp);
|
||||
public const string Use2fa = nameof(Use2fa);
|
||||
public const string UseApi = nameof(UseApi);
|
||||
public const string UseResetPassword = nameof(UseResetPassword);
|
||||
public const string MaxStorageGb = nameof(MaxStorageGb);
|
||||
public const string SelfHost = nameof(SelfHost);
|
||||
public const string UsersGetPremium = nameof(UsersGetPremium);
|
||||
public const string UseCustomPermissions = nameof(UseCustomPermissions);
|
||||
public const string Issued = nameof(Issued);
|
||||
public const string UsePasswordManager = nameof(UsePasswordManager);
|
||||
public const string UseSecretsManager = nameof(UseSecretsManager);
|
||||
public const string SmSeats = nameof(SmSeats);
|
||||
public const string SmServiceAccounts = nameof(SmServiceAccounts);
|
||||
public const string LimitCollectionCreationDeletion = nameof(LimitCollectionCreationDeletion);
|
||||
public const string AllowAdminAccessToAllCollectionItems = nameof(AllowAdminAccessToAllCollectionItems);
|
||||
public const string Expires = nameof(Expires);
|
||||
public const string Refresh = nameof(Refresh);
|
||||
public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod);
|
||||
public const string Trial = nameof(Trial);
|
||||
}
|
||||
|
||||
public static class UserLicenseConstants
|
||||
{
|
||||
public const string LicenseType = nameof(LicenseType);
|
||||
public const string LicenseKey = nameof(LicenseKey);
|
||||
public const string Id = nameof(Id);
|
||||
public const string Name = nameof(Name);
|
||||
public const string Email = nameof(Email);
|
||||
public const string Premium = nameof(Premium);
|
||||
public const string MaxStorageGb = nameof(MaxStorageGb);
|
||||
public const string Issued = nameof(Issued);
|
||||
public const string Expires = nameof(Expires);
|
||||
public const string Refresh = nameof(Refresh);
|
||||
public const string Trial = nameof(Trial);
|
||||
}
|
10
src/Core/Billing/Licenses/Models/LicenseContext.cs
Normal file
10
src/Core/Billing/Licenses/Models/LicenseContext.cs
Normal file
@ -0,0 +1,10 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Models;
|
||||
|
||||
public class LicenseContext
|
||||
{
|
||||
public Guid? InstallationId { get; init; }
|
||||
public required SubscriptionInfo SubscriptionInfo { get; init; }
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Billing.Licenses.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Services;
|
||||
|
||||
public interface ILicenseClaimsFactory<in T>
|
||||
{
|
||||
Task<List<Claim>> GenerateClaims(T entity, LicenseContext licenseContext);
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Licenses.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Services.Implementations;
|
||||
|
||||
public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organization>
|
||||
{
|
||||
public Task<List<Claim>> GenerateClaims(Organization entity, LicenseContext licenseContext)
|
||||
{
|
||||
var subscriptionInfo = licenseContext.SubscriptionInfo;
|
||||
var expires = entity.CalculateFreshExpirationDate(subscriptionInfo);
|
||||
var refresh = entity.CalculateFreshRefreshDate(subscriptionInfo, expires);
|
||||
var expirationWithoutGracePeriod = entity.CalculateFreshExpirationDateWithoutGracePeriod(subscriptionInfo, expires);
|
||||
var trial = IsTrialing(entity, subscriptionInfo);
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(nameof(OrganizationLicenseConstants.LicenseType), LicenseType.Organization.ToString()),
|
||||
new Claim(nameof(OrganizationLicenseConstants.LicenseKey), entity.LicenseKey),
|
||||
new(nameof(OrganizationLicenseConstants.InstallationId), licenseContext.InstallationId.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Id), entity.Id.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Name), entity.Name),
|
||||
new(nameof(OrganizationLicenseConstants.BillingEmail), entity.BillingEmail),
|
||||
new(nameof(OrganizationLicenseConstants.Enabled), entity.Enabled.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Plan), entity.Plan),
|
||||
new(nameof(OrganizationLicenseConstants.PlanType), entity.PlanType.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Seats), entity.Seats.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.MaxCollections), entity.MaxCollections.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UsePolicies), entity.UsePolicies.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseSso), entity.UseSso.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseKeyConnector), entity.UseKeyConnector.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseScim), entity.UseScim.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseGroups), entity.UseGroups.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseEvents), entity.UseEvents.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseDirectory), entity.UseDirectory.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseTotp), entity.UseTotp.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Use2fa), entity.Use2fa.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseApi), entity.UseApi.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseResetPassword), entity.UseResetPassword.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.SelfHost), entity.SelfHost.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UsersGetPremium), entity.UsersGetPremium.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseCustomPermissions), entity.UseCustomPermissions.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.UsePasswordManager), entity.UsePasswordManager.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseSecretsManager), entity.UseSecretsManager.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.SmSeats), entity.SmSeats.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.SmServiceAccounts), entity.SmServiceAccounts.ToString()),
|
||||
// LimitCollectionCreationDeletion was split and removed from the
|
||||
// license. Left here with an assignment from the new values for
|
||||
// backwards compatibility.
|
||||
new(nameof(OrganizationLicenseConstants.LimitCollectionCreationDeletion),
|
||||
(entity.LimitCollectionCreation || entity.LimitCollectionDeletion).ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.AllowAdminAccessToAllCollectionItems), entity.AllowAdminAccessToAllCollectionItems.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Expires), expires.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()),
|
||||
};
|
||||
|
||||
if (entity.BusinessName is not null)
|
||||
{
|
||||
claims.Add(new Claim(nameof(OrganizationLicenseConstants.BusinessName), entity.BusinessName));
|
||||
}
|
||||
|
||||
return Task.FromResult(claims);
|
||||
}
|
||||
|
||||
private static bool IsTrialing(Organization org, SubscriptionInfo subscriptionInfo) =>
|
||||
subscriptionInfo?.Subscription is null
|
||||
? org.PlanType != PlanType.Custom || !org.ExpirationDate.HasValue
|
||||
: subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Billing.Licenses.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Services.Implementations;
|
||||
|
||||
public class UserLicenseClaimsFactory : ILicenseClaimsFactory<User>
|
||||
{
|
||||
public Task<List<Claim>> GenerateClaims(User entity, LicenseContext licenseContext)
|
||||
{
|
||||
var subscriptionInfo = licenseContext.SubscriptionInfo;
|
||||
|
||||
var expires = subscriptionInfo.UpcomingInvoice?.Date?.AddDays(7) ?? entity.PremiumExpirationDate?.AddDays(7);
|
||||
var refresh = subscriptionInfo.UpcomingInvoice?.Date ?? entity.PremiumExpirationDate;
|
||||
var trial = (subscriptionInfo.Subscription?.TrialEndDate.HasValue ?? false) &&
|
||||
subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow;
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(nameof(UserLicenseConstants.LicenseType), LicenseType.User.ToString()),
|
||||
new(nameof(UserLicenseConstants.LicenseKey), entity.LicenseKey),
|
||||
new(nameof(UserLicenseConstants.Id), entity.Id.ToString()),
|
||||
new(nameof(UserLicenseConstants.Name), entity.Name),
|
||||
new(nameof(UserLicenseConstants.Email), entity.Email),
|
||||
new(nameof(UserLicenseConstants.Premium), entity.Premium.ToString()),
|
||||
new(nameof(UserLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()),
|
||||
new(nameof(UserLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(UserLicenseConstants.Expires), expires.ToString()),
|
||||
new(nameof(UserLicenseConstants.Refresh), refresh.ToString()),
|
||||
new(nameof(UserLicenseConstants.Trial), trial.ToString()),
|
||||
};
|
||||
|
||||
return Task.FromResult(claims);
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -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" />
|
||||
|
@ -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}}
|
@ -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)
|
@ -0,0 +1,14 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top" align="left">
|
||||
Your user account has been revoked from the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{OrganizationName}}</b> organization because your account is part of multiple organizations. Before you can re-join {{OrganizationName}}, you must first leave all other organizations.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top" align="left">
|
||||
To leave an organization, first log into the <a href="https://vault.bitwarden.com/#/login">web app</a>, select the three dot menu next to the organization name, and select Leave.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,5 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Your user account has been revoked from the {{OrganizationName}} organization because your account is part of multiple organizations. Before you can rejoin {{OrganizationName}}, you must first leave all other organizations.
|
||||
|
||||
To leave an organization, first log in the web app (https://vault.bitwarden.com/#/login), select the three dot menu next to the organization name, and select Leave.
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,15 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top" align="left">
|
||||
Your user account has been revoked from the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{OrganizationName}}</b> organization because you do not have two-step login configured. Before you can re-join {{OrganizationName}}, you need to set up two-step login on your user account.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top" align="left">
|
||||
Learn how to enable two-step login on your user account at
|
||||
<a target="_blank" href="https://help.bitwarden.com/article/setup-two-step-login/" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">https://help.bitwarden.com/article/setup-two-step-login/</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,7 @@
|
||||
{{#>BasicTextLayout}}
|
||||
Your user account has been removed from the {{OrganizationName}} organization because you do not have two-step login
|
||||
configured. Before you can re-join this organization you need to set up two-step login on your user account.
|
||||
|
||||
Learn how to enable two-step login on your user account at
|
||||
https://help.bitwarden.com/article/setup-two-step-login/
|
||||
{{/BasicTextLayout}}
|
@ -12,6 +12,7 @@ public interface ILicense
|
||||
bool Trial { get; set; }
|
||||
string Hash { get; set; }
|
||||
string Signature { get; set; }
|
||||
string Token { get; set; }
|
||||
byte[] SignatureBytes { get; }
|
||||
byte[] GetDataBytes(bool forHash = false);
|
||||
byte[] ComputeHash();
|
||||
|
@ -1,10 +1,12 @@
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -55,7 +57,7 @@ public class OrganizationLicense : ILicense
|
||||
SmServiceAccounts = org.SmServiceAccounts;
|
||||
|
||||
// Deprecated. Left for backwards compatibility with old license versions.
|
||||
LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion;
|
||||
LimitCollectionCreationDeletion = org.LimitCollectionCreation || org.LimitCollectionDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems;
|
||||
//
|
||||
|
||||
@ -151,6 +153,7 @@ public class OrganizationLicense : ILicense
|
||||
public LicenseType? LicenseType { get; set; }
|
||||
public string Hash { get; set; }
|
||||
public string Signature { get; set; }
|
||||
public string Token { get; set; }
|
||||
[JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature);
|
||||
|
||||
/// <summary>
|
||||
@ -176,6 +179,7 @@ public class OrganizationLicense : ILicense
|
||||
!p.Name.Equals(nameof(Signature)) &&
|
||||
!p.Name.Equals(nameof(SignatureBytes)) &&
|
||||
!p.Name.Equals(nameof(LicenseType)) &&
|
||||
!p.Name.Equals(nameof(Token)) &&
|
||||
// UsersGetPremium was added in Version 2
|
||||
(Version >= 2 || !p.Name.Equals(nameof(UsersGetPremium))) &&
|
||||
// UseEvents was added in Version 3
|
||||
@ -236,8 +240,65 @@ public class OrganizationLicense : ILicense
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception)
|
||||
public bool CanUse(
|
||||
IGlobalSettings globalSettings,
|
||||
ILicensingService licensingService,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
out string exception)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null)
|
||||
{
|
||||
return ObsoleteCanUse(globalSettings, licensingService, out exception);
|
||||
}
|
||||
|
||||
var errorMessages = new StringBuilder();
|
||||
|
||||
var enabled = claimsPrincipal.GetValue<bool>(nameof(Enabled));
|
||||
if (!enabled)
|
||||
{
|
||||
errorMessages.AppendLine("Your cloud-hosted organization is currently disabled.");
|
||||
}
|
||||
|
||||
var installationId = claimsPrincipal.GetValue<Guid>(nameof(InstallationId));
|
||||
if (installationId != globalSettings.Installation.Id)
|
||||
{
|
||||
errorMessages.AppendLine("The installation ID does not match the current installation.");
|
||||
}
|
||||
|
||||
var selfHost = claimsPrincipal.GetValue<bool>(nameof(SelfHost));
|
||||
if (!selfHost)
|
||||
{
|
||||
errorMessages.AppendLine("The license does not allow for on-premise hosting of organizations.");
|
||||
}
|
||||
|
||||
var licenseType = claimsPrincipal.GetValue<LicenseType>(nameof(LicenseType));
|
||||
if (licenseType != Enums.LicenseType.Organization)
|
||||
{
|
||||
errorMessages.AppendLine("Premium licenses cannot be applied to an organization. " +
|
||||
"Upload this license from your personal account settings page.");
|
||||
}
|
||||
|
||||
if (errorMessages.Length > 0)
|
||||
{
|
||||
exception = $"Invalid license. {errorMessages.ToString().TrimEnd()}";
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do not extend this method. It is only here for backwards compatibility with old licenses.
|
||||
/// Instead, extend the CanUse method using the ClaimsPrincipal.
|
||||
/// </summary>
|
||||
/// <param name="globalSettings"></param>
|
||||
/// <param name="licensingService"></param>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
private bool ObsoleteCanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception)
|
||||
{
|
||||
// Do not extend this method. It is only here for backwards compatibility with old licenses.
|
||||
var errorMessages = new StringBuilder();
|
||||
|
||||
if (!Enabled)
|
||||
@ -291,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())
|
||||
|
@ -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())
|
||||
|
12
src/Core/Models/Commands/CommandResult.cs
Normal file
12
src/Core/Models/Commands/CommandResult.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace Bit.Core.Models.Commands;
|
||||
|
||||
public class CommandResult(IEnumerable<string> errors)
|
||||
{
|
||||
public CommandResult(string error) : this([error]) { }
|
||||
|
||||
public bool Success => ErrorMessages.Count == 0;
|
||||
public bool HasErrors => ErrorMessages.Count > 0;
|
||||
public List<string> ErrorMessages { get; } = errors.ToList();
|
||||
|
||||
public CommandResult() : this(Array.Empty<string>()) { }
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations;
|
||||
|
||||
public record ManagedUserDomainClaimedEmails(IEnumerable<string> EmailList, Organization Organization);
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Models.Mail;
|
||||
|
||||
public class ClaimedDomainUserNotificationViewModel : BaseTitleContactUsMailModel
|
||||
{
|
||||
public string OrganizationName { get; init; }
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Models.Mail;
|
||||
|
||||
public class OrganizationUserRevokedForPolicySingleOrgViewModel : BaseMailModel
|
||||
{
|
||||
public string OrganizationName { get; set; }
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Models.Mail;
|
||||
|
||||
public class OrganizationUserRevokedForPolicyTwoFactorViewModel : BaseMailModel
|
||||
{
|
||||
public string OrganizationName { get; set; }
|
||||
}
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,8 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
|
||||
throw new BadRequestException("License is already in use by another organization.");
|
||||
}
|
||||
|
||||
var canUse = license.CanUse(_globalSettings, _licensingService, out var exception) &&
|
||||
var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license);
|
||||
var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception) &&
|
||||
selfHostedOrganization.CanUseLicense(license, out exception);
|
||||
|
||||
if (!canUse)
|
||||
|
@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
@ -50,12 +51,16 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddOrganizationGroupCommands();
|
||||
services.AddOrganizationLicenseCommandsQueries();
|
||||
services.AddOrganizationDomainCommandsQueries();
|
||||
services.AddOrganizationSignUpCommands();
|
||||
services.AddOrganizationAuthCommands();
|
||||
services.AddOrganizationUserCommands();
|
||||
services.AddOrganizationUserCommandsQueries();
|
||||
services.AddBaseOrganizationSubscriptionCommandsQueries();
|
||||
}
|
||||
|
||||
private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) =>
|
||||
services.AddScoped<ICloudOrganizationSignUpCommand, CloudOrganizationSignUpCommand>();
|
||||
|
||||
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ICreateOrganizationConnectionCommand, CreateOrganizationConnectionCommand>();
|
||||
@ -91,6 +96,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
private static void AddOrganizationUserCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IRemoveOrganizationUserCommand, RemoveOrganizationUserCommand>();
|
||||
services.AddScoped<IRevokeNonCompliantOrganizationUserCommand, RevokeNonCompliantOrganizationUserCommand>();
|
||||
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
|
||||
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
||||
services.AddScoped<IDeleteManagedOrganizationUserAccountCommand, DeleteManagedOrganizationUserAccountCommand>();
|
||||
|
@ -32,4 +32,5 @@ public interface IUserRepository : IRepository<User, Guid>
|
||||
/// <param name="updateDataActions">Registered database calls to update re-encrypted data.</param>
|
||||
Task UpdateUserKeyAndEncryptedDataAsync(User user,
|
||||
IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions);
|
||||
Task DeleteManyAsync(IEnumerable<User> users);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
@ -13,5 +14,12 @@ public interface ILicensingService
|
||||
byte[] SignLicense(ILicense license);
|
||||
Task<OrganizationLicense> ReadOrganizationLicenseAsync(Organization organization);
|
||||
Task<OrganizationLicense> ReadOrganizationLicenseAsync(Guid organizationId);
|
||||
ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license);
|
||||
|
||||
Task<string> CreateOrganizationTokenAsync(
|
||||
Organization organization,
|
||||
Guid installationId,
|
||||
SubscriptionInfo subscriptionInfo);
|
||||
|
||||
Task<string> CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Mail;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
@ -35,6 +36,8 @@ public interface IMailService
|
||||
Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable<string> adminEmails, bool hasAccessSecretsManager = false);
|
||||
Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false);
|
||||
Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email);
|
||||
Task SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(string organizationName, string email);
|
||||
Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email);
|
||||
Task SendPasswordlessSignInAsync(string returnUrl, string token, string email);
|
||||
Task SendInvoiceUpcoming(
|
||||
string email,
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user