mirror of
https://github.com/bitwarden/server.git
synced 2025-06-20 02:48:03 -05:00
Merge branch 'km/signing-api-changes' into km/signing-upgrade-rotation
This commit is contained in:
commit
16f69887ad
@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"swashbuckle.aspnetcore.cli": {
|
"swashbuckle.aspnetcore.cli": {
|
||||||
"version": "7.2.0",
|
"version": "7.3.2",
|
||||||
"commands": ["swagger"]
|
"commands": ["swagger"]
|
||||||
},
|
},
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
|
28
.github/workflows/build.yml
vendored
28
.github/workflows/build.yml
vendored
@ -350,14 +350,6 @@ jobs:
|
|||||||
cd docker-stub/US; zip -r ../../docker-stub-US.zip *; cd ../..
|
cd docker-stub/US; zip -r ../../docker-stub-US.zip *; cd ../..
|
||||||
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../..
|
cd docker-stub/EU; zip -r ../../docker-stub-EU.zip *; cd ../..
|
||||||
|
|
||||||
- name: Make Docker stub checksums
|
|
||||||
if: |
|
|
||||||
github.event_name != 'pull_request'
|
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
|
||||||
run: |
|
|
||||||
sha256sum docker-stub-US.zip > docker-stub-US-sha256.txt
|
|
||||||
sha256sum docker-stub-EU.zip > docker-stub-EU-sha256.txt
|
|
||||||
|
|
||||||
- name: Upload Docker stub US artifact
|
- name: Upload Docker stub US artifact
|
||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request'
|
github.event_name != 'pull_request'
|
||||||
@ -378,26 +370,6 @@ jobs:
|
|||||||
path: docker-stub-EU.zip
|
path: docker-stub-EU.zip
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload Docker stub US checksum artifact
|
|
||||||
if: |
|
|
||||||
github.event_name != 'pull_request'
|
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
|
||||||
with:
|
|
||||||
name: docker-stub-US-sha256.txt
|
|
||||||
path: docker-stub-US-sha256.txt
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload Docker stub EU checksum artifact
|
|
||||||
if: |
|
|
||||||
github.event_name != 'pull_request'
|
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
|
||||||
with:
|
|
||||||
name: docker-stub-EU-sha256.txt
|
|
||||||
path: docker-stub-EU-sha256.txt
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Build Public API Swagger
|
- name: Build Public API Swagger
|
||||||
run: |
|
run: |
|
||||||
cd ./src/Api
|
cd ./src/Api
|
||||||
|
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@ -17,6 +17,9 @@ on:
|
|||||||
env:
|
env:
|
||||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
name: Setup
|
name: Setup
|
||||||
@ -65,9 +68,7 @@ jobs:
|
|||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: ${{ needs.setup.outputs.branch-name }}
|
branch: ${{ needs.setup.outputs.branch-name }}
|
||||||
artifacts: "docker-stub-US.zip,
|
artifacts: "docker-stub-US.zip,
|
||||||
docker-stub-US-sha256.txt,
|
|
||||||
docker-stub-EU.zip,
|
docker-stub-EU.zip,
|
||||||
docker-stub-EU-sha256.txt,
|
|
||||||
swagger.json"
|
swagger.json"
|
||||||
|
|
||||||
- name: Dry Run - Download latest release Docker stubs
|
- name: Dry Run - Download latest release Docker stubs
|
||||||
@ -78,9 +79,7 @@ jobs:
|
|||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
branch: main
|
branch: main
|
||||||
artifacts: "docker-stub-US.zip,
|
artifacts: "docker-stub-US.zip,
|
||||||
docker-stub-US-sha256.txt,
|
|
||||||
docker-stub-EU.zip,
|
docker-stub-EU.zip,
|
||||||
docker-stub-EU-sha256.txt,
|
|
||||||
swagger.json"
|
swagger.json"
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
@ -88,9 +87,7 @@ jobs:
|
|||||||
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
|
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
|
||||||
with:
|
with:
|
||||||
artifacts: "docker-stub-US.zip,
|
artifacts: "docker-stub-US.zip,
|
||||||
docker-stub-US-sha256.txt,
|
|
||||||
docker-stub-EU.zip,
|
docker-stub-EU.zip,
|
||||||
docker-stub-EU-sha256.txt,
|
|
||||||
swagger.json"
|
swagger.json"
|
||||||
commit: ${{ github.sha }}
|
commit: ${{ github.sha }}
|
||||||
tag: "v${{ needs.setup.outputs.release_version }}"
|
tag: "v${{ needs.setup.outputs.release_version }}"
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2025.6.0</Version>
|
<Version>2025.6.1</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -550,6 +550,15 @@ public class ProviderBillingService(
|
|||||||
[
|
[
|
||||||
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
|
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
|
||||||
|
{
|
||||||
|
options.TaxIdData.Add(new CustomerTaxIdDataOptions
|
||||||
|
{
|
||||||
|
Type = StripeConstants.TaxIdType.EUVAT,
|
||||||
|
Value = $"ES{taxInfo.TaxIdNumber}"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(provider.DiscountId))
|
if (!string.IsNullOrEmpty(provider.DiscountId))
|
||||||
|
@ -499,9 +499,9 @@ public class AccountController : Controller
|
|||||||
// Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one
|
// Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one
|
||||||
if (orgUser == null && organization.Seats.HasValue)
|
if (orgUser == null && organization.Seats.HasValue)
|
||||||
{
|
{
|
||||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
var occupiedSeats = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
var initialSeatCount = organization.Seats.Value;
|
var initialSeatCount = organization.Seats.Value;
|
||||||
var availableSeats = initialSeatCount - occupiedSeats;
|
var availableSeats = initialSeatCount - occupiedSeats.Total;
|
||||||
if (availableSeats < 1)
|
if (availableSeats < 1)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -242,10 +242,32 @@ public class OrganizationsController : Controller
|
|||||||
Seats = organization.Seats
|
Seats = organization.Seats
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (model.PlanType.HasValue)
|
||||||
|
{
|
||||||
|
var freePlan = await _pricingClient.GetPlanOrThrow(model.PlanType.Value);
|
||||||
|
var isDowngradingToFree = organization.PlanType != PlanType.Free && model.PlanType.Value == PlanType.Free;
|
||||||
|
if (isDowngradingToFree)
|
||||||
|
{
|
||||||
|
if (model.Seats.HasValue && model.Seats.Value > freePlan.PasswordManager.MaxSeats)
|
||||||
|
{
|
||||||
|
TempData["Error"] = $"Organizations with more than {freePlan.PasswordManager.MaxSeats} seats cannot be downgraded to the Free plan";
|
||||||
|
return RedirectToAction("Edit", new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.MaxCollections > freePlan.PasswordManager.MaxCollections)
|
||||||
|
{
|
||||||
|
TempData["Error"] = $"Organizations with more than {freePlan.PasswordManager.MaxCollections} collections cannot be downgraded to the Free plan. Your organization currently has {organization.MaxCollections} collections.";
|
||||||
|
return RedirectToAction("Edit", new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
model.MaxStorageGb = null;
|
||||||
|
model.ExpirationDate = null;
|
||||||
|
model.Enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
UpdateOrganization(organization, model);
|
UpdateOrganization(organization, model);
|
||||||
|
|
||||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||||
|
|
||||||
if (organization.UseSecretsManager && !plan.SupportsSecretsManager)
|
if (organization.UseSecretsManager && !plan.SupportsSecretsManager)
|
||||||
{
|
{
|
||||||
TempData["Error"] = "Plan does not support Secrets Manager";
|
TempData["Error"] = "Plan does not support Secrets Manager";
|
||||||
|
@ -521,7 +521,9 @@ public class OrganizationUsersController : Controller
|
|||||||
.Concat(readonlyCollectionAccess)
|
.Concat(readonlyCollectionAccess)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), userId,
|
var existingUserType = organizationUser.Type;
|
||||||
|
|
||||||
|
await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), existingUserType, userId,
|
||||||
collectionsToSave, groupsToSave);
|
collectionsToSave, groupsToSave);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,9 +177,10 @@ public class MembersController : Controller
|
|||||||
{
|
{
|
||||||
return new NotFoundResult();
|
return new NotFoundResult();
|
||||||
}
|
}
|
||||||
|
var existingUserType = existingUser.Type;
|
||||||
var updatedUser = model.ToOrganizationUser(existingUser);
|
var updatedUser = model.ToOrganizationUser(existingUser);
|
||||||
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList();
|
var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList();
|
||||||
await _updateOrganizationUserCommand.UpdateUserAsync(updatedUser, null, associations, model.Groups);
|
await _updateOrganizationUserCommand.UpdateUserAsync(updatedUser, existingUserType, null, associations, model.Groups);
|
||||||
MemberResponseModel response = null;
|
MemberResponseModel response = null;
|
||||||
if (existingUser.UserId.HasValue)
|
if (existingUser.UserId.HasValue)
|
||||||
{
|
{
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
||||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
|
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -3,8 +3,7 @@ using Bit.Api.AdminConsole.Models.Response;
|
|||||||
using Bit.Api.Auth.Models.Request;
|
using Bit.Api.Auth.Models.Request;
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||||
using Bit.Api.KeyManagement.Models.Response;
|
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Api.KeyManagement.Queries;
|
|
||||||
using Bit.Api.KeyManagement.Validators;
|
using Bit.Api.KeyManagement.Validators;
|
||||||
using Bit.Api.Models.Request.Accounts;
|
using Bit.Api.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
@ -402,9 +401,9 @@ public class AccountsController : Controller
|
|||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
var accountKeysResponse = new PrivateKeysResponseModel(await _userAccountKeysQuery.Run(user));
|
var accountKeys = await _userAccountKeysQuery.Run(user);
|
||||||
|
|
||||||
var response = new ProfileResponseModel(user, accountKeysResponse, organizationUserDetails, providerUserDetails,
|
var response = new ProfileResponseModel(user, accountKeys, organizationUserDetails, providerUserDetails,
|
||||||
providerUserOrganizationDetails, twoFactorEnabled,
|
providerUserOrganizationDetails, twoFactorEnabled,
|
||||||
hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
||||||
return response;
|
return response;
|
||||||
@ -437,9 +436,9 @@ public class AccountsController : Controller
|
|||||||
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
var userAccountKeysData = await _userAccountKeysQuery.Run(user);
|
var userAccountKeys = await _userAccountKeysQuery.Run(user);
|
||||||
|
|
||||||
var response = new ProfileResponseModel(user, new PrivateKeysResponseModel(userAccountKeysData), null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
var response = new ProfileResponseModel(user, userAccountKeys, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,7 +456,7 @@ public class AccountsController : Controller
|
|||||||
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
var accountKeys = new PrivateKeysResponseModel(await _userAccountKeysQuery.Run(user));
|
var accountKeys = await _userAccountKeysQuery.Run(user);
|
||||||
|
|
||||||
var response = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
var response = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
||||||
return response;
|
return response;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using Bit.Api.KeyManagement.Models.Response;
|
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Api.KeyManagement.Queries;
|
|
||||||
using Bit.Api.Models.Request;
|
using Bit.Api.Models.Request;
|
||||||
using Bit.Api.Models.Request.Accounts;
|
using Bit.Api.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
@ -61,7 +60,7 @@ public class AccountsController(
|
|||||||
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
var accountKeys = new PrivateKeysResponseModel(await userAccountKeysQuery.Run(user));
|
var accountKeys = await userAccountKeysQuery.Run(user);
|
||||||
|
|
||||||
var profile = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled,
|
var profile = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled,
|
||||||
userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
||||||
|
@ -4,6 +4,7 @@ using Bit.Api.AdminConsole.Models.Request.Organizations;
|
|||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
using Bit.Api.Billing.Queries.Organizations;
|
using Bit.Api.Billing.Queries.Organizations;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
@ -280,17 +281,36 @@ public class OrganizationBillingController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
|
||||||
if (organization == null)
|
if (organization == null)
|
||||||
{
|
{
|
||||||
return Error.NotFound();
|
return Error.NotFound();
|
||||||
}
|
}
|
||||||
|
var existingPlan = organization.PlanType;
|
||||||
var organizationSignup = model.ToOrganizationSignup(user);
|
var organizationSignup = model.ToOrganizationSignup(user);
|
||||||
var sale = OrganizationSale.From(organization, organizationSignup);
|
var sale = OrganizationSale.From(organization, organizationSignup);
|
||||||
var plan = await pricingClient.GetPlanOrThrow(model.PlanType);
|
var plan = await pricingClient.GetPlanOrThrow(model.PlanType);
|
||||||
sale.Organization.PlanType = plan.Type;
|
sale.Organization.PlanType = plan.Type;
|
||||||
sale.Organization.Plan = plan.Name;
|
sale.Organization.Plan = plan.Name;
|
||||||
sale.SubscriptionSetup.SkipTrial = true;
|
sale.SubscriptionSetup.SkipTrial = true;
|
||||||
|
if (existingPlan == PlanType.Free && organization.GatewaySubscriptionId is not null)
|
||||||
|
{
|
||||||
|
sale.Organization.UseTotp = plan.HasTotp;
|
||||||
|
sale.Organization.UseGroups = plan.HasGroups;
|
||||||
|
sale.Organization.UseDirectory = plan.HasDirectory;
|
||||||
|
sale.Organization.SelfHost = plan.HasSelfHost;
|
||||||
|
sale.Organization.UsersGetPremium = plan.UsersGetPremium;
|
||||||
|
sale.Organization.UseEvents = plan.HasEvents;
|
||||||
|
sale.Organization.Use2fa = plan.Has2fa;
|
||||||
|
sale.Organization.UseApi = plan.HasApi;
|
||||||
|
sale.Organization.UsePolicies = plan.HasPolicies;
|
||||||
|
sale.Organization.UseSso = plan.HasSso;
|
||||||
|
sale.Organization.UseResetPassword = plan.HasResetPassword;
|
||||||
|
sale.Organization.UseKeyConnector = plan.HasKeyConnector;
|
||||||
|
sale.Organization.UseScim = plan.HasScim;
|
||||||
|
sale.Organization.UseCustomPermissions = plan.HasCustomPermissions;
|
||||||
|
sale.Organization.UseOrganizationDomains = plan.HasOrganizationDomains;
|
||||||
|
sale.Organization.MaxCollections = plan.PasswordManager.MaxCollections;
|
||||||
|
}
|
||||||
|
|
||||||
if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken))
|
if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken))
|
||||||
{
|
{
|
||||||
|
@ -8,7 +8,7 @@ using Bit.Core.Utilities;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Bit.Api.Tools.Controllers;
|
namespace Bit.Api.Dirt.Controllers;
|
||||||
|
|
||||||
[Route("hibp")]
|
[Route("hibp")]
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
using Bit.Api.Tools.Models;
|
using Bit.Api.Dirt.Models;
|
||||||
using Bit.Api.Tools.Models.Response;
|
using Bit.Api.Dirt.Models.Response;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Dirt.Reports.Entities;
|
||||||
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Tools.Entities;
|
|
||||||
using Bit.Core.Tools.Models.Data;
|
|
||||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
|
||||||
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
|
||||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Bit.Api.Tools.Controllers;
|
namespace Bit.Api.Dirt.Controllers;
|
||||||
|
|
||||||
[Route("reports")]
|
[Route("reports")]
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
@ -47,7 +47,7 @@ public class ReportsController : Controller
|
|||||||
[HttpGet("member-cipher-details/{orgId}")]
|
[HttpGet("member-cipher-details/{orgId}")]
|
||||||
public async Task<IEnumerable<MemberCipherDetailsResponseModel>> GetMemberCipherDetails(Guid orgId)
|
public async Task<IEnumerable<MemberCipherDetailsResponseModel>> GetMemberCipherDetails(Guid orgId)
|
||||||
{
|
{
|
||||||
// Using the AccessReports permission here until new permissions
|
// Using the AccessReports permission here until new permissions
|
||||||
// are needed for more control over reports
|
// are needed for more control over reports
|
||||||
if (!await _currentContext.AccessReports(orgId))
|
if (!await _currentContext.AccessReports(orgId))
|
||||||
{
|
{
|
||||||
@ -84,7 +84,7 @@ public class ReportsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains the organization member info, the cipher ids associated with the member,
|
/// Contains the organization member info, the cipher ids associated with the member,
|
||||||
/// and details on their collections, groups, and permissions
|
/// and details on their collections, groups, and permissions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request to the MemberAccessCipherDetailsQuery</param>
|
/// <param name="request">Request to the MemberAccessCipherDetailsQuery</param>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Api.Tools.Models;
|
namespace Bit.Api.Dirt.Models;
|
||||||
|
|
||||||
public class PasswordHealthReportApplicationModel
|
public class PasswordHealthReportApplicationModel
|
||||||
{
|
{
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
using Bit.Core.Tools.Models.Data;
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
|
||||||
namespace Bit.Api.Tools.Models.Response;
|
namespace Bit.Api.Dirt.Models.Response;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains the collections and group collections a user has access to including
|
/// Contains the collections and group collections a user has access to including
|
||||||
/// the permission level for the collection and group collection.
|
/// the permission level for the collection and group collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MemberAccessReportResponseModel
|
public class MemberAccessReportResponseModel
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using Bit.Core.Tools.Models.Data;
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
|
||||||
namespace Bit.Api.Tools.Models.Response;
|
namespace Bit.Api.Dirt.Models.Response;
|
||||||
|
|
||||||
public class MemberCipherDetailsResponseModel
|
public class MemberCipherDetailsResponseModel
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Api.KeyManagement.Models.Response;
|
using Bit.Api.KeyManagement.Models.Response;
|
||||||
using Bit.Api.KeyManagement.Queries;
|
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -10,10 +10,17 @@ namespace Bit.Api.KeyManagement.Controllers;
|
|||||||
|
|
||||||
[Route("users")]
|
[Route("users")]
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class UsersController(
|
public class UsersController : Controller
|
||||||
IUserRepository _userRepository,
|
|
||||||
IUserAccountKeysQuery _userAccountKeysQuery) : Controller
|
|
||||||
{
|
{
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||||
|
|
||||||
|
public UsersController(IUserRepository userRepository, IUserAccountKeysQuery userAccountKeysQuery)
|
||||||
|
{
|
||||||
|
_userRepository = userRepository;
|
||||||
|
_userAccountKeysQuery = userAccountKeysQuery;
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/public-key")]
|
[HttpGet("{id}/public-key")]
|
||||||
public async Task<UserKeyResponseModel> GetPublicKeyAsync(string id)
|
public async Task<UserKeyResponseModel> GetPublicKeyAsync(string id)
|
||||||
{
|
{
|
||||||
@ -23,10 +30,9 @@ public class UsersController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/keys")]
|
[HttpGet("{id}/keys")]
|
||||||
public async Task<PublicKeysResponseModel> GetAccountKeysAsync(string id)
|
public async Task<PublicKeysResponseModel> GetAccountKeysAsync([FromRoute] Guid id)
|
||||||
{
|
{
|
||||||
var guidId = new Guid(id);
|
var user = await _userRepository.GetByIdAsync(id) ?? throw new NotFoundException();
|
||||||
var user = await _userRepository.GetByIdAsync(guidId) ?? throw new NotFoundException();
|
|
||||||
var accountKeys = await _userAccountKeysQuery.Run(user) ?? throw new NotFoundException("User account keys not found.");
|
var accountKeys = await _userAccountKeysQuery.Run(user) ?? throw new NotFoundException("User account keys not found.");
|
||||||
return new PublicKeysResponseModel(accountKeys);
|
return new PublicKeysResponseModel(accountKeys);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ namespace Bit.Api.KeyManagement.Models.Response;
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This response model is used to return the the asymmetric encryption keys,
|
/// This response model is used to return the asymmetric encryption keys,
|
||||||
/// and signature keys of an entity. This includes the private keys of the key pairs,
|
/// and signature keys of an entity. This includes the private keys of the key pairs,
|
||||||
/// (private key, signing key), and the public keys of the key pairs (unsigned public key,
|
/// (private key, signing key), and the public keys of the key pairs (unsigned public key,
|
||||||
/// signed public key, verification key).
|
/// signed public key, verification key).
|
||||||
@ -18,10 +18,7 @@ public class PrivateKeysResponseModel : ResponseModel
|
|||||||
public PrivateKeysResponseModel(UserAccountKeysData accountKeys) : base("privateKeys")
|
public PrivateKeysResponseModel(UserAccountKeysData accountKeys) : base("privateKeys")
|
||||||
{
|
{
|
||||||
PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairModel(accountKeys.PublicKeyEncryptionKeyPairData);
|
PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairModel(accountKeys.PublicKeyEncryptionKeyPairData);
|
||||||
if (accountKeys == null)
|
ArgumentNullException.ThrowIfNull(accountKeys);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(accountKeys));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accountKeys.SignatureKeyPairData != null)
|
if (accountKeys.SignatureKeyPairData != null)
|
||||||
{
|
{
|
||||||
|
@ -16,10 +16,7 @@ public class PublicKeysResponseModel : ResponseModel
|
|||||||
: base("publicKeys")
|
: base("publicKeys")
|
||||||
{
|
{
|
||||||
PublicKey = accountKeys.PublicKeyEncryptionKeyPairData.PublicKey;
|
PublicKey = accountKeys.PublicKeyEncryptionKeyPairData.PublicKey;
|
||||||
if (accountKeys == null)
|
ArgumentNullException.ThrowIfNull(accountKeys);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(accountKeys));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accountKeys.SignatureKeyPairData != null)
|
if (accountKeys.SignatureKeyPairData != null)
|
||||||
{
|
{
|
||||||
|
@ -15,11 +15,7 @@ public class SignatureKeyPairResponseModel : ResponseModel
|
|||||||
public SignatureKeyPairResponseModel(SignatureKeyPairData signatureKeyPair)
|
public SignatureKeyPairResponseModel(SignatureKeyPairData signatureKeyPair)
|
||||||
: base("signatureKeyPair")
|
: base("signatureKeyPair")
|
||||||
{
|
{
|
||||||
if (signatureKeyPair == null)
|
ArgumentNullException.ThrowIfNull(signatureKeyPair);
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(signatureKeyPair));
|
|
||||||
}
|
|
||||||
|
|
||||||
WrappedSigningKey = signatureKeyPair.WrappedSigningKey;
|
WrappedSigningKey = signatureKeyPair.WrappedSigningKey;
|
||||||
VerifyingKey = signatureKeyPair.VerifyingKey;
|
VerifyingKey = signatureKeyPair.VerifyingKey;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Api.KeyManagement.Queries;
|
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||||
|
using Bit.Core.KeyManagement.Queries;
|
||||||
|
|
||||||
namespace Bit.Api.KeyManagement;
|
namespace Bit.Api.KeyManagement;
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Api.AdminConsole.Public.Models.Response;
|
using Bit.Api.AdminConsole.Public.Models.Response;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
namespace Bit.Api.Models.Public.Response;
|
namespace Bit.Api.Models.Public.Response;
|
||||||
@ -20,6 +21,7 @@ public class CollectionResponseModel : CollectionBaseModel, IResponseModel
|
|||||||
Id = collection.Id;
|
Id = collection.Id;
|
||||||
ExternalId = collection.ExternalId;
|
ExternalId = collection.ExternalId;
|
||||||
Groups = groups?.Select(c => new AssociationWithPermissionsResponseModel(c));
|
Groups = groups?.Select(c => new AssociationWithPermissionsResponseModel(c));
|
||||||
|
Type = collection.Type;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -38,4 +40,8 @@ public class CollectionResponseModel : CollectionBaseModel, IResponseModel
|
|||||||
/// The associated groups that this collection is assigned to.
|
/// The associated groups that this collection is assigned to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<AssociationWithPermissionsResponseModel> Groups { get; set; }
|
public IEnumerable<AssociationWithPermissionsResponseModel> Groups { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The type of this collection
|
||||||
|
/// </summary>
|
||||||
|
public CollectionType Type { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
@ -18,12 +19,14 @@ public class CollectionResponseModel : ResponseModel
|
|||||||
OrganizationId = collection.OrganizationId;
|
OrganizationId = collection.OrganizationId;
|
||||||
Name = collection.Name;
|
Name = collection.Name;
|
||||||
ExternalId = collection.ExternalId;
|
ExternalId = collection.ExternalId;
|
||||||
|
Type = collection.Type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid OrganizationId { get; set; }
|
public Guid OrganizationId { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string ExternalId { get; set; }
|
public string ExternalId { get; set; }
|
||||||
|
public CollectionType Type { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Models.Response.Providers;
|
|||||||
using Bit.Api.KeyManagement.Models.Response;
|
using Bit.Api.KeyManagement.Models.Response;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
@ -11,7 +12,7 @@ namespace Bit.Api.Models.Response;
|
|||||||
public class ProfileResponseModel : ResponseModel
|
public class ProfileResponseModel : ResponseModel
|
||||||
{
|
{
|
||||||
public ProfileResponseModel(User user,
|
public ProfileResponseModel(User user,
|
||||||
PrivateKeysResponseModel privateKeysResponseModel,
|
UserAccountKeysData userAccountKeysData,
|
||||||
IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails,
|
IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails,
|
||||||
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
|
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
|
||||||
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
|
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
|
||||||
@ -34,7 +35,7 @@ public class ProfileResponseModel : ResponseModel
|
|||||||
TwoFactorEnabled = twoFactorEnabled;
|
TwoFactorEnabled = twoFactorEnabled;
|
||||||
Key = user.Key;
|
Key = user.Key;
|
||||||
PrivateKey = user.PrivateKey;
|
PrivateKey = user.PrivateKey;
|
||||||
AccountKeys = privateKeysResponseModel;
|
AccountKeys = new PrivateKeysResponseModel(userAccountKeysData);
|
||||||
SecurityStamp = user.SecurityStamp;
|
SecurityStamp = user.SecurityStamp;
|
||||||
ForcePasswordReset = user.ForcePasswordReset;
|
ForcePasswordReset = user.ForcePasswordReset;
|
||||||
UsesKeyConnector = user.UsesKeyConnector;
|
UsesKeyConnector = user.UsesKeyConnector;
|
||||||
|
@ -31,8 +31,8 @@ using Bit.Api.Billing;
|
|||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Auth.Identity.TokenProviders;
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
using Bit.Core.Tools.ImportFeatures;
|
using Bit.Core.Tools.ImportFeatures;
|
||||||
using Bit.Core.Tools.ReportFeatures;
|
|
||||||
using Bit.Core.Auth.Models.Api.Request;
|
using Bit.Core.Auth.Models.Api.Request;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||||
using Bit.Core.Tools.SendFeatures;
|
using Bit.Core.Tools.SendFeatures;
|
||||||
using Bit.Api.KeyManagement;
|
using Bit.Api.KeyManagement;
|
||||||
|
|
||||||
|
@ -42,7 +42,6 @@ public class CiphersController : Controller
|
|||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly ILogger<CiphersController> _logger;
|
private readonly ILogger<CiphersController> _logger;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IFeatureService _featureService;
|
|
||||||
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
|
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly ICollectionRepository _collectionRepository;
|
private readonly ICollectionRepository _collectionRepository;
|
||||||
@ -57,7 +56,6 @@ public class CiphersController : Controller
|
|||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ILogger<CiphersController> logger,
|
ILogger<CiphersController> logger,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IFeatureService featureService,
|
|
||||||
IOrganizationCiphersQuery organizationCiphersQuery,
|
IOrganizationCiphersQuery organizationCiphersQuery,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
ICollectionRepository collectionRepository)
|
ICollectionRepository collectionRepository)
|
||||||
@ -71,7 +69,6 @@ public class CiphersController : Controller
|
|||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_featureService = featureService;
|
|
||||||
_organizationCiphersQuery = organizationCiphersQuery;
|
_organizationCiphersQuery = organizationCiphersQuery;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_collectionRepository = collectionRepository;
|
_collectionRepository = collectionRepository;
|
||||||
@ -375,11 +372,6 @@ public class CiphersController : Controller
|
|||||||
|
|
||||||
private async Task<bool> CanDeleteOrRestoreCipherAsAdminAsync(Guid organizationId, IEnumerable<Guid> cipherIds)
|
private async Task<bool> CanDeleteOrRestoreCipherAsAdminAsync(Guid organizationId, IEnumerable<Guid> cipherIds)
|
||||||
{
|
{
|
||||||
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
|
|
||||||
{
|
|
||||||
return await CanEditCipherAsAdminAsync(organizationId, cipherIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
var org = _currentContext.GetOrganization(organizationId);
|
var org = _currentContext.GetOrganization(organizationId);
|
||||||
|
|
||||||
// If we're not an "admin" or if we're a provider user we don't need to check the ciphers
|
// If we're not an "admin" or if we're a provider user we don't need to check the ciphers
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Api.KeyManagement.Models.Response;
|
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Api.KeyManagement.Queries;
|
|
||||||
using Bit.Api.Vault.Models.Response;
|
using Bit.Api.Vault.Models.Response;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
@ -117,9 +116,9 @@ public class SyncController : Controller
|
|||||||
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
||||||
|
|
||||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||||
var accountKeys = new PrivateKeysResponseModel(await _userAccountKeysQuery.Run(user));
|
var userAccountKeys = await _userAccountKeysQuery.Run(user);
|
||||||
|
|
||||||
var response = new SyncResponseModel(_globalSettings, user, accountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
|
var response = new SyncResponseModel(_globalSettings, user, userAccountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
|
||||||
organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
|
organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
|
||||||
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
||||||
return response;
|
return response;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Api.KeyManagement.Models.Response;
|
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.Tools.Models.Response;
|
using Bit.Api.Tools.Models.Response;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
@ -21,7 +21,7 @@ public class SyncResponseModel : ResponseModel
|
|||||||
public SyncResponseModel(
|
public SyncResponseModel(
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
User user,
|
User user,
|
||||||
PrivateKeysResponseModel privateKeysResponseModel,
|
UserAccountKeysData userAccountKeysData,
|
||||||
bool userTwoFactorEnabled,
|
bool userTwoFactorEnabled,
|
||||||
bool userHasPremiumFromOrganization,
|
bool userHasPremiumFromOrganization,
|
||||||
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
||||||
@ -38,7 +38,7 @@ public class SyncResponseModel : ResponseModel
|
|||||||
IEnumerable<Send> sends)
|
IEnumerable<Send> sends)
|
||||||
: base("sync")
|
: base("sync")
|
||||||
{
|
{
|
||||||
Profile = new ProfileResponseModel(user, privateKeysResponseModel, organizationUserDetails, providerUserDetails,
|
Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,
|
||||||
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
|
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
|
||||||
Folders = folders.Select(f => new FolderResponseModel(f));
|
Folders = folders.Select(f => new FolderResponseModel(f));
|
||||||
Ciphers = ciphers.Select(cipher =>
|
Ciphers = ciphers.Select(cipher =>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<ProjectReference Include="..\Core\Core.csproj" />
|
<ProjectReference Include="..\Core\Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Interfaces;
|
using Bit.Core.AdminConsole.Interfaces;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
@ -9,23 +10,75 @@ using Bit.Core.Utilities;
|
|||||||
|
|
||||||
namespace Bit.Core.Entities;
|
namespace Bit.Core.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An association table between one <see cref="User"/> and one <see cref="Organization"/>, representing that user's
|
||||||
|
/// membership in the organization. "Member" refers to the OrganizationUser object.
|
||||||
|
/// </summary>
|
||||||
public class OrganizationUser : ITableObject<Guid>, IExternal, IOrganizationUser
|
public class OrganizationUser : ITableObject<Guid>, IExternal, IOrganizationUser
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A unique random identifier.
|
||||||
|
/// </summary>
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the Organization that the user is a member of.
|
||||||
|
/// </summary>
|
||||||
public Guid OrganizationId { get; set; }
|
public Guid OrganizationId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the User that is the member. This is NULL if the Status is Invited (or Invited and then Revoked), because
|
||||||
|
/// it is not linked to a specific User yet.
|
||||||
|
/// </summary>
|
||||||
public Guid? UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The email address of the user invited to the organization. This is NULL if the Status is not Invited (or
|
||||||
|
/// Invited and then Revoked), because in that case the OrganizationUser is linked to a User
|
||||||
|
/// and the email is stored on the User object.
|
||||||
|
/// </summary>
|
||||||
[MaxLength(256)]
|
[MaxLength(256)]
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The Organization symmetric key encrypted with the User's public key. NULL if the user is not in a Confirmed
|
||||||
|
/// (or Confirmed and then Revoked) status.
|
||||||
|
/// </summary>
|
||||||
public string? Key { get; set; }
|
public string? Key { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The User's symmetric key encrypted with the Organization's public key. NULL if the OrganizationUser
|
||||||
|
/// is not enrolled in account recovery.
|
||||||
|
/// </summary>
|
||||||
public string? ResetPasswordKey { get; set; }
|
public string? ResetPasswordKey { get; set; }
|
||||||
|
/// <inheritdoc cref="OrganizationUserStatusType"/>
|
||||||
public OrganizationUserStatusType Status { get; set; }
|
public OrganizationUserStatusType Status { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The User's role in the Organization.
|
||||||
|
/// </summary>
|
||||||
public OrganizationUserType Type { get; set; }
|
public OrganizationUserType Type { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// An ID used to identify the OrganizationUser with an external directory service. Used by Directory Connector
|
||||||
|
/// and SCIM.
|
||||||
|
/// </summary>
|
||||||
[MaxLength(300)]
|
[MaxLength(300)]
|
||||||
public string? ExternalId { get; set; }
|
public string? ExternalId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The date the OrganizationUser was created, i.e. when the User was first invited to the Organization.
|
||||||
|
/// </summary>
|
||||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||||
|
/// <summary>
|
||||||
|
/// The last date the OrganizationUser entry was updated.
|
||||||
|
/// </summary>
|
||||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||||
|
/// <summary>
|
||||||
|
/// A json blob representing the <see cref="Bit.Core.Models.Data.Permissions"/> of the OrganizationUser if they
|
||||||
|
/// are a Custom user role (i.e. the <see cref="OrganizationUserType"/> is Custom). MAY be NULL if they are not
|
||||||
|
/// a custom user, but this is not guaranteed; do not use this to determine their role.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Avoid using this property directly - instead use the <see cref="GetPermissions"/> and <see cref="SetPermissions"/>
|
||||||
|
/// helper methods.
|
||||||
|
/// </remarks>
|
||||||
public string? Permissions { get; set; }
|
public string? Permissions { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// True if the User has access to Secrets Manager for this Organization, false otherwise.
|
||||||
|
/// </summary>
|
||||||
public bool AccessSecretsManager { get; set; }
|
public bool AccessSecretsManager { get; set; }
|
||||||
|
|
||||||
public void SetNewId()
|
public void SetNewId()
|
||||||
|
@ -1,9 +1,34 @@
|
|||||||
namespace Bit.Core.Enums;
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the different stages of a member's lifecycle in an organization.
|
||||||
|
/// The <see cref="OrganizationUser"/> object is populated differently depending on their Status.
|
||||||
|
/// </summary>
|
||||||
public enum OrganizationUserStatusType : short
|
public enum OrganizationUserStatusType : short
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The OrganizationUser entry only represents an invitation to join the organization. It is not linked to a
|
||||||
|
/// specific User yet.
|
||||||
|
/// </summary>
|
||||||
Invited = 0,
|
Invited = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// The User has accepted the invitation and linked their User account to the OrganizationUser entry.
|
||||||
|
/// </summary>
|
||||||
Accepted = 1,
|
Accepted = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// An administrator has granted the User access to the organization. This is the final step in the User becoming
|
||||||
|
/// a "full" member of the organization, including a key exchange so that they can decrypt organization data.
|
||||||
|
/// </summary>
|
||||||
Confirmed = 2,
|
Confirmed = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// The OrganizationUser has been revoked from the organization and cannot access organization data while in this state.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// An OrganizationUser may move into this status from any other status, and will move back to their original status
|
||||||
|
/// if restored. This allows an administrator to easily suspend and restore access without going through the
|
||||||
|
/// Invite flow again.
|
||||||
|
/// </remarks>
|
||||||
Revoked = -1,
|
Revoked = -1,
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
|
||||||
public interface IUpdateOrganizationUserCommand
|
public interface IUpdateOrganizationUserCommand
|
||||||
{
|
{
|
||||||
Task UpdateUserAsync(OrganizationUser organizationUser, Guid? savingUserId,
|
Task UpdateUserAsync(OrganizationUser organizationUser, OrganizationUserType existingUserType, Guid? savingUserId,
|
||||||
List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess);
|
List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess);
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
|||||||
InviteOrganization = request.InviteOrganization,
|
InviteOrganization = request.InviteOrganization,
|
||||||
PerformedBy = request.PerformedBy,
|
PerformedBy = request.PerformedBy,
|
||||||
PerformedAt = request.PerformedAt,
|
PerformedAt = request.PerformedAt,
|
||||||
OccupiedPmSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId),
|
OccupiedPmSeats = (await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)).Total,
|
||||||
OccupiedSmSeats = await organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)
|
OccupiedSmSeats = await organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
@ -83,14 +84,9 @@ public class InviteUsersPasswordManagerValidator(
|
|||||||
return invalidEnvironment.Map(request);
|
return invalidEnvironment.Map(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
var organizationValidationResult = await inviteUsersOrganizationValidator.ValidateAsync(request.InviteOrganization);
|
// Organizations managed by a provider need to be scaled by the provider. This needs to be checked in the event seats are increasing.
|
||||||
|
|
||||||
if (organizationValidationResult is Invalid<InviteOrganization> organizationValidation)
|
|
||||||
{
|
|
||||||
return organizationValidation.Map(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
var provider = await providerRepository.GetByOrganizationIdAsync(request.InviteOrganization.OrganizationId);
|
var provider = await providerRepository.GetByOrganizationIdAsync(request.InviteOrganization.OrganizationId);
|
||||||
|
|
||||||
if (provider is not null)
|
if (provider is not null)
|
||||||
{
|
{
|
||||||
var providerValidationResult = InvitingUserOrganizationProviderValidator.Validate(new InviteOrganizationProvider(provider));
|
var providerValidationResult = InvitingUserOrganizationProviderValidator.Validate(new InviteOrganizationProvider(provider));
|
||||||
@ -101,6 +97,13 @@ public class InviteUsersPasswordManagerValidator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var organizationValidationResult = await inviteUsersOrganizationValidator.ValidateAsync(request.InviteOrganization);
|
||||||
|
|
||||||
|
if (organizationValidationResult is Invalid<InviteOrganization> organizationValidation)
|
||||||
|
{
|
||||||
|
return organizationValidation.Map(request);
|
||||||
|
}
|
||||||
|
|
||||||
var paymentSubscription = await paymentService.GetSubscriptionAsync(
|
var paymentSubscription = await paymentService.GetSubscriptionAsync(
|
||||||
await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId));
|
await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId));
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
|
|
||||||
using Bit.Core.AdminConsole.Utilities.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
|
||||||
|
|
||||||
public static class InviteUserPaymentValidation
|
public static class InviteUserPaymentValidation
|
||||||
{
|
{
|
||||||
|
@ -70,8 +70,8 @@ public class RestoreOrganizationUserCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var organization = await organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
var organization = await organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
||||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
var availableSeats = organization.Seats.GetValueOrDefault(0) - seatCounts.Total;
|
||||||
|
|
||||||
if (availableSeats < 1)
|
if (availableSeats < 1)
|
||||||
{
|
{
|
||||||
@ -163,8 +163,8 @@ public class RestoreOrganizationUserCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
var availableSeats = organization.Seats.GetValueOrDefault(0) - seatCounts.Total;
|
||||||
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
||||||
await organizationService.AutoAddSeatsAsync(organization, newSeatsRequired);
|
await organizationService.AutoAddSeatsAsync(organization, newSeatsRequired);
|
||||||
|
|
||||||
|
@ -55,11 +55,13 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
|
|||||||
/// Update an organization user.
|
/// Update an organization user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="organizationUser">The modified organization user to save.</param>
|
/// <param name="organizationUser">The modified organization user to save.</param>
|
||||||
|
/// <param name="existingUserType">The current type (member role) of the user.</param>
|
||||||
/// <param name="savingUserId">The userId of the currently logged in user who is making the change.</param>
|
/// <param name="savingUserId">The userId of the currently logged in user who is making the change.</param>
|
||||||
/// <param name="collectionAccess">The user's updated collection access. If set to null, this removes all collection access.</param>
|
/// <param name="collectionAccess">The user's updated collection access. If set to null, this removes all collection access.</param>
|
||||||
/// <param name="groupAccess">The user's updated group access. If set to null, groups are not updated.</param>
|
/// <param name="groupAccess">The user's updated group access. If set to null, groups are not updated.</param>
|
||||||
/// <exception cref="BadRequestException"></exception>
|
/// <exception cref="BadRequestException"></exception>
|
||||||
public async Task UpdateUserAsync(OrganizationUser organizationUser, Guid? savingUserId,
|
public async Task UpdateUserAsync(OrganizationUser organizationUser, OrganizationUserType existingUserType,
|
||||||
|
Guid? savingUserId,
|
||||||
List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess)
|
List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess)
|
||||||
{
|
{
|
||||||
// Avoid multiple enumeration
|
// Avoid multiple enumeration
|
||||||
@ -83,15 +85,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (organizationUser.UserId.HasValue && organization.PlanType == PlanType.Free && organizationUser.Type is OrganizationUserType.Admin or OrganizationUserType.Owner)
|
await EnsureUserCannotBeAdminOrOwnerForMultipleFreeOrganizationAsync(organizationUser, existingUserType, organization);
|
||||||
{
|
|
||||||
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
|
|
||||||
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(organizationUser.UserId.Value);
|
|
||||||
if (adminCount > 0)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("User can only be an admin of one free organization.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collectionAccessList.Count != 0)
|
if (collectionAccessList.Count != 0)
|
||||||
{
|
{
|
||||||
@ -151,6 +145,40 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
|
|||||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Updated);
|
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task EnsureUserCannotBeAdminOrOwnerForMultipleFreeOrganizationAsync(OrganizationUser updatedOrgUser, OrganizationUserType existingUserType, Entities.Organization organization)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (organization.PlanType != PlanType.Free)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!updatedOrgUser.UserId.HasValue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (updatedOrgUser.Type is not (OrganizationUserType.Admin or OrganizationUserType.Owner))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
|
||||||
|
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(updatedOrgUser.UserId!.Value);
|
||||||
|
|
||||||
|
var isCurrentAdminOrOwner = existingUserType is OrganizationUserType.Admin or OrganizationUserType.Owner;
|
||||||
|
|
||||||
|
if (isCurrentAdminOrOwner && adminCount <= 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCurrentAdminOrOwner && adminCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestException("User can only be an admin of one free organization.");
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ValidateCollectionAccessAsync(OrganizationUser originalUser,
|
private async Task ValidateCollectionAccessAsync(OrganizationUser originalUser,
|
||||||
ICollection<CollectionAccessSelection> collectionAccess)
|
ICollection<CollectionAccessSelection> collectionAccess)
|
||||||
{
|
{
|
||||||
|
@ -104,8 +104,8 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
|||||||
throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages));
|
throw new BadRequestException(string.Join(", ", commandResult.ErrorMessages));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(currentActiveRevocableOrganizationUsers.Select(x =>
|
await Task.WhenAll(nonCompliantUsers.Select(nonCompliantUser =>
|
||||||
_mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email)));
|
_mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), nonCompliantUser.user.Email)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool MembersWithNoMasterPasswordWillLoseAccess(
|
private static bool MembersWithNoMasterPasswordWillLoseAccess(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
@ -25,4 +26,14 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
|
|||||||
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
||||||
Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType);
|
Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType);
|
||||||
Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids);
|
Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the number of occupied seats for an organization.
|
||||||
|
/// OrganizationUsers occupy a seat, unless they are revoked.
|
||||||
|
/// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
|
||||||
|
/// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The ID of the organization to get the occupied seat count for.</param>
|
||||||
|
/// <returns>The number of occupied seats for the organization.</returns>
|
||||||
|
Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
|
||||||
}
|
}
|
||||||
|
@ -18,16 +18,6 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
|||||||
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
|
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
|
||||||
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
|
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
|
||||||
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
|
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the number of occupied seats for an organization.
|
|
||||||
/// Occupied seats are OrganizationUsers that have at least been invited.
|
|
||||||
/// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
|
|
||||||
/// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="organizationId">The ID of the organization to get the occupied seat count for.</param>
|
|
||||||
/// <returns>The number of occupied seats for the organization.</returns>
|
|
||||||
Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
|
|
||||||
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
|
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
|
||||||
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
||||||
Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
|
Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
|
||||||
|
@ -294,11 +294,20 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
|
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
|
||||||
{
|
{
|
||||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
if (occupiedSeats > newSeatTotal)
|
|
||||||
|
if (seatCounts.Total > newSeatTotal)
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
if (organization.UseAdminSponsoredFamilies || seatCounts.Sponsored > 0)
|
||||||
$"Your new plan only has ({newSeatTotal}) seats. Remove some users.");
|
{
|
||||||
|
throw new BadRequestException($"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " +
|
||||||
|
$"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Your organization currently has {seatCounts.Total} seats filled. " +
|
||||||
|
$"Your new plan only has ({newSeatTotal}) seats. Remove some users.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -726,8 +735,8 @@ public class OrganizationService : IOrganizationService
|
|||||||
var newSeatsRequired = 0;
|
var newSeatsRequired = 0;
|
||||||
if (organization.Seats.HasValue)
|
if (organization.Seats.HasValue)
|
||||||
{
|
{
|
||||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
var availableSeats = organization.Seats.Value - occupiedSeats;
|
var availableSeats = organization.Seats.Value - seatCounts.Total;
|
||||||
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
|
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1177,8 +1186,8 @@ public class OrganizationService : IOrganizationService
|
|||||||
var enoughSeatsAvailable = true;
|
var enoughSeatsAvailable = true;
|
||||||
if (organization.Seats.HasValue)
|
if (organization.Seats.HasValue)
|
||||||
{
|
{
|
||||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
seatsAvailable = organization.Seats.Value - occupiedSeats;
|
seatsAvailable = organization.Seats.Value - seatCounts.Total;
|
||||||
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,12 @@ public static class StripeConstants
|
|||||||
public const string Reverse = "reverse";
|
public const string Reverse = "reverse";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class TaxIdType
|
||||||
|
{
|
||||||
|
public const string EUVAT = "eu_vat";
|
||||||
|
public const string SpanishNIF = "es_cif";
|
||||||
|
}
|
||||||
|
|
||||||
public static class ValidateTaxLocationTiming
|
public static class ValidateTaxLocationTiming
|
||||||
{
|
{
|
||||||
public const string Deferred = "deferred";
|
public const string Deferred = "deferred";
|
||||||
|
@ -31,6 +31,7 @@ public record PlanAdapter : Plan
|
|||||||
HasScim = HasFeature("scim");
|
HasScim = HasFeature("scim");
|
||||||
HasResetPassword = HasFeature("resetPassword");
|
HasResetPassword = HasFeature("resetPassword");
|
||||||
UsersGetPremium = HasFeature("usersGetPremium");
|
UsersGetPremium = HasFeature("usersGetPremium");
|
||||||
|
HasCustomPermissions = HasFeature("customPermissions");
|
||||||
UpgradeSortOrder = plan.AdditionalData.TryGetValue("upgradeSortOrder", out var upgradeSortOrder)
|
UpgradeSortOrder = plan.AdditionalData.TryGetValue("upgradeSortOrder", out var upgradeSortOrder)
|
||||||
? int.Parse(upgradeSortOrder)
|
? int.Parse(upgradeSortOrder)
|
||||||
: 0;
|
: 0;
|
||||||
@ -141,6 +142,7 @@ public record PlanAdapter : Plan
|
|||||||
var stripeSeatPlanId = GetStripeSeatPlanId(seats);
|
var stripeSeatPlanId = GetStripeSeatPlanId(seats);
|
||||||
var hasAdditionalSeatsOption = seats.IsScalable;
|
var hasAdditionalSeatsOption = seats.IsScalable;
|
||||||
var seatPrice = GetSeatPrice(seats);
|
var seatPrice = GetSeatPrice(seats);
|
||||||
|
var baseSeats = GetBaseSeats(seats);
|
||||||
var maxSeats = GetMaxSeats(seats);
|
var maxSeats = GetMaxSeats(seats);
|
||||||
var allowSeatAutoscale = seats.IsScalable;
|
var allowSeatAutoscale = seats.IsScalable;
|
||||||
var maxProjects = plan.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0;
|
var maxProjects = plan.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0;
|
||||||
@ -156,6 +158,7 @@ public record PlanAdapter : Plan
|
|||||||
StripeSeatPlanId = stripeSeatPlanId,
|
StripeSeatPlanId = stripeSeatPlanId,
|
||||||
HasAdditionalSeatsOption = hasAdditionalSeatsOption,
|
HasAdditionalSeatsOption = hasAdditionalSeatsOption,
|
||||||
SeatPrice = seatPrice,
|
SeatPrice = seatPrice,
|
||||||
|
BaseSeats = baseSeats,
|
||||||
MaxSeats = maxSeats,
|
MaxSeats = maxSeats,
|
||||||
AllowSeatAutoscale = allowSeatAutoscale,
|
AllowSeatAutoscale = allowSeatAutoscale,
|
||||||
MaxProjects = maxProjects
|
MaxProjects = maxProjects
|
||||||
@ -168,8 +171,16 @@ public record PlanAdapter : Plan
|
|||||||
private static decimal GetBasePrice(PurchasableDTO purchasable)
|
private static decimal GetBasePrice(PurchasableDTO purchasable)
|
||||||
=> purchasable.FromPackaged(x => x.Price);
|
=> purchasable.FromPackaged(x => x.Price);
|
||||||
|
|
||||||
|
private static int GetBaseSeats(FreeOrScalableDTO freeOrScalable)
|
||||||
|
=> freeOrScalable.Match(
|
||||||
|
free => free.Quantity,
|
||||||
|
scalable => scalable.Provided);
|
||||||
|
|
||||||
private static int GetBaseSeats(PurchasableDTO purchasable)
|
private static int GetBaseSeats(PurchasableDTO purchasable)
|
||||||
=> purchasable.FromPackaged(x => x.Quantity);
|
=> purchasable.Match(
|
||||||
|
free => free.Quantity,
|
||||||
|
packaged => packaged.Quantity,
|
||||||
|
scalable => scalable.Provided);
|
||||||
|
|
||||||
private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable)
|
private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable)
|
||||||
=> freeOrScalable.Match(
|
=> freeOrScalable.Match(
|
||||||
|
@ -31,7 +31,6 @@ public class OrganizationBillingService(
|
|||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
ILogger<OrganizationBillingService> logger,
|
ILogger<OrganizationBillingService> logger,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient,
|
||||||
ISetupIntentCache setupIntentCache,
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
@ -78,14 +77,14 @@ public class OrganizationBillingService(
|
|||||||
var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization);
|
var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization);
|
||||||
|
|
||||||
var isManaged = organization.Status == OrganizationStatusType.Managed;
|
var isManaged = organization.Status == OrganizationStatusType.Managed;
|
||||||
var orgOccupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||||
{
|
{
|
||||||
return OrganizationMetadata.Default with
|
return OrganizationMetadata.Default with
|
||||||
{
|
{
|
||||||
IsEligibleForSelfHost = isEligibleForSelfHost,
|
IsEligibleForSelfHost = isEligibleForSelfHost,
|
||||||
IsManaged = isManaged,
|
IsManaged = isManaged,
|
||||||
OrganizationOccupiedSeats = orgOccupiedSeats
|
OrganizationOccupiedSeats = orgOccupiedSeats.Total
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +119,7 @@ public class OrganizationBillingService(
|
|||||||
invoice?.DueDate,
|
invoice?.DueDate,
|
||||||
invoice?.Created,
|
invoice?.Created,
|
||||||
subscription.CurrentPeriodEnd,
|
subscription.CurrentPeriodEnd,
|
||||||
orgOccupiedSeats);
|
orgOccupiedSeats.Total);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task
|
public async Task
|
||||||
@ -247,12 +246,23 @@ public class OrganizationBillingService(
|
|||||||
organization.Id,
|
organization.Id,
|
||||||
customerSetup.TaxInformation.Country,
|
customerSetup.TaxInformation.Country,
|
||||||
customerSetup.TaxInformation.TaxId);
|
customerSetup.TaxInformation.TaxId);
|
||||||
|
|
||||||
|
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
||||||
}
|
}
|
||||||
|
|
||||||
customerCreateOptions.TaxIdData =
|
customerCreateOptions.TaxIdData =
|
||||||
[
|
[
|
||||||
new() { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId }
|
new() { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
|
||||||
|
{
|
||||||
|
customerCreateOptions.TaxIdData.Add(new CustomerTaxIdDataOptions
|
||||||
|
{
|
||||||
|
Type = StripeConstants.TaxIdType.EUVAT,
|
||||||
|
Value = $"ES{customerSetup.TaxInformation.TaxId}"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource;
|
var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource;
|
||||||
|
@ -648,6 +648,12 @@ public class SubscriberService(
|
|||||||
{
|
{
|
||||||
await stripeAdapter.TaxIdCreateAsync(customer.Id,
|
await stripeAdapter.TaxIdCreateAsync(customer.Id,
|
||||||
new TaxIdCreateOptions { Type = taxIdType, Value = taxInformation.TaxId });
|
new TaxIdCreateOptions { Type = taxIdType, Value = taxInformation.TaxId });
|
||||||
|
|
||||||
|
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
|
||||||
|
{
|
||||||
|
await stripeAdapter.TaxIdCreateAsync(customer.Id,
|
||||||
|
new TaxIdCreateOptions { Type = StripeConstants.TaxIdType.EUVAT, Value = $"ES{taxInformation.TaxId}" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (StripeException e)
|
catch (StripeException e)
|
||||||
{
|
{
|
||||||
|
@ -80,6 +80,15 @@ public class PreviewTaxAmountCommand(
|
|||||||
Value = taxInformation.TaxId
|
Value = taxInformation.TaxId
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
|
||||||
|
{
|
||||||
|
options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions
|
||||||
|
{
|
||||||
|
Type = StripeConstants.TaxIdType.EUVAT,
|
||||||
|
Value = $"ES{parameters.TaxInformation.TaxId}"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (planType.GetProductTier() == ProductTierType.Families)
|
if (planType.GetProductTier() == ProductTierType.Families)
|
||||||
|
@ -181,6 +181,8 @@ public static class FeatureFlagKeys
|
|||||||
public const string EnablePMFlightRecorder = "enable-pm-flight-recorder";
|
public const string EnablePMFlightRecorder = "enable-pm-flight-recorder";
|
||||||
public const string MobileErrorReporting = "mobile-error-reporting";
|
public const string MobileErrorReporting = "mobile-error-reporting";
|
||||||
public const string AndroidChromeAutofill = "android-chrome-autofill";
|
public const string AndroidChromeAutofill = "android-chrome-autofill";
|
||||||
|
public const string EnablePMPreloginSettings = "enable-pm-prelogin-settings";
|
||||||
|
public const string AppIntents = "app-intents";
|
||||||
|
|
||||||
/* Platform Team */
|
/* Platform Team */
|
||||||
public const string PersistPopupView = "persist-popup-view";
|
public const string PersistPopupView = "persist-popup-view";
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
using Bit.Core.Entities;
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
#nullable enable
|
namespace Bit.Core.Dirt.Reports.Entities;
|
||||||
|
|
||||||
namespace Bit.Core.Tools.Entities;
|
|
||||||
|
|
||||||
public class PasswordHealthReportApplication : ITableObject<Guid>, IRevisable
|
public class PasswordHealthReportApplication : ITableObject<Guid>, IRevisable
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Core.Tools.Models.Data;
|
namespace Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
|
||||||
public class MemberAccessDetails
|
public class MemberAccessDetails
|
||||||
{
|
{
|
||||||
@ -30,13 +30,13 @@ public class MemberAccessCipherDetails
|
|||||||
public bool UsesKeyConnector { get; set; }
|
public bool UsesKeyConnector { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The details for the member's collection access depending
|
/// The details for the member's collection access depending
|
||||||
/// on the collections and groups they are assigned to
|
/// on the collections and groups they are assigned to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<MemberAccessDetails> AccessDetails { get; set; }
|
public IEnumerable<MemberAccessDetails> AccessDetails { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A distinct list of the cipher ids associated with
|
/// A distinct list of the cipher ids associated with
|
||||||
/// the organization member
|
/// the organization member
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<string> CipherIds { get; set; }
|
public IEnumerable<string> CipherIds { get; set; }
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Dirt.Reports.Entities;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Tools.Entities;
|
|
||||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
|
||||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
|
||||||
using Bit.Core.Tools.Repositories;
|
|
||||||
|
|
||||||
namespace Bit.Core.Tools.ReportFeatures;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures;
|
||||||
|
|
||||||
public class AddPasswordHealthReportApplicationCommand : IAddPasswordHealthReportApplicationCommand
|
public class AddPasswordHealthReportApplicationCommand : IAddPasswordHealthReportApplicationCommand
|
||||||
{
|
{
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
|
||||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
using Bit.Core.Tools.Repositories;
|
using Bit.Core.Exceptions;
|
||||||
|
|
||||||
namespace Bit.Core.Tools.ReportFeatures;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures;
|
||||||
|
|
||||||
public class DropPasswordHealthReportApplicationCommand : IDropPasswordHealthReportApplicationCommand
|
public class DropPasswordHealthReportApplicationCommand : IDropPasswordHealthReportApplicationCommand
|
||||||
{
|
{
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Dirt.Reports.Entities;
|
||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
|
||||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
using Bit.Core.Tools.Repositories;
|
using Bit.Core.Exceptions;
|
||||||
|
|
||||||
namespace Bit.Core.Tools.ReportFeatures;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures;
|
||||||
|
|
||||||
public class GetPasswordHealthReportApplicationQuery : IGetPasswordHealthReportApplicationQuery
|
public class GetPasswordHealthReportApplicationQuery : IGetPasswordHealthReportApplicationQuery
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Dirt.Reports.Entities;
|
||||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
|
|
||||||
namespace Bit.Core.Tools.ReportFeatures.Interfaces;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
|
||||||
|
|
||||||
public interface IAddPasswordHealthReportApplicationCommand
|
public interface IAddPasswordHealthReportApplicationCommand
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
|
|
||||||
namespace Bit.Core.Tools.ReportFeatures.Interfaces;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
|
||||||
|
|
||||||
public interface IDropPasswordHealthReportApplicationCommand
|
public interface IDropPasswordHealthReportApplicationCommand
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Dirt.Reports.Entities;
|
||||||
|
|
||||||
namespace Bit.Core.Tools.ReportFeatures.Interfaces;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
|
||||||
|
|
||||||
public interface IGetPasswordHealthReportApplicationQuery
|
public interface IGetPasswordHealthReportApplicationQuery
|
||||||
{
|
{
|
||||||
|
@ -2,21 +2,21 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Models.Data;
|
|
||||||
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
|
||||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
using Bit.Core.Vault.Queries;
|
using Bit.Core.Vault.Queries;
|
||||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||||
|
|
||||||
namespace Bit.Core.Tools.ReportFeatures;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures;
|
||||||
|
|
||||||
public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
|
public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.Tools.Models.Data;
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
using Bit.Core.Tools.ReportFeatures.Requests;
|
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
|
|
||||||
namespace Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||||
|
|
||||||
public interface IMemberAccessCipherDetailsQuery
|
public interface IMemberAccessCipherDetailsQuery
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using Bit.Core.Tools.ReportFeatures.Interfaces;
|
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
|
||||||
using Bit.Core.Tools.ReportFeatures.OrganizationReportMembers.Interfaces;
|
using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Core.Tools.ReportFeatures;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures;
|
||||||
|
|
||||||
public static class ReportingServiceCollectionExtensions
|
public static class ReportingServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Core.Tools.ReportFeatures.Requests;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
|
|
||||||
public class AddPasswordHealthReportApplicationRequest
|
public class AddPasswordHealthReportApplicationRequest
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Core.Tools.ReportFeatures.Requests;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
|
|
||||||
public class DropPasswordHealthReportApplicationRequest
|
public class DropPasswordHealthReportApplicationRequest
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Core.Tools.ReportFeatures.Requests;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
|
|
||||||
public class MemberAccessCipherDetailsRequest
|
public class MemberAccessCipherDetailsRequest
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Dirt.Reports.Entities;
|
||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
namespace Bit.Core.Tools.Repositories;
|
namespace Bit.Core.Dirt.Reports.Repositories;
|
||||||
|
|
||||||
public interface IPasswordHealthReportApplicationRepository : IRepository<PasswordHealthReportApplication, Guid>
|
public interface IPasswordHealthReportApplicationRepository : IRepository<PasswordHealthReportApplication, Guid>
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
@ -14,6 +15,8 @@ public class Collection : ITableObject<Guid>
|
|||||||
public string? ExternalId { get; set; }
|
public string? ExternalId { get; set; }
|
||||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public CollectionType Type { get; set; } = CollectionType.SharedCollection;
|
||||||
|
public string? DefaultUserCollectionEmail { get; set; }
|
||||||
|
|
||||||
public void SetNewId()
|
public void SetNewId()
|
||||||
{
|
{
|
||||||
|
7
src/Core/Enums/CollectionType.cs
Normal file
7
src/Core/Enums/CollectionType.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Core.Enums;
|
||||||
|
|
||||||
|
public enum CollectionType
|
||||||
|
{
|
||||||
|
SharedCollection = 0,
|
||||||
|
DefaultUserCollection = 1,
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Api.KeyManagement.Queries.Interfaces;
|
||||||
|
|
||||||
|
public interface IUserAccountKeysQuery
|
||||||
|
{
|
||||||
|
Task<UserAccountKeysData> Run(User user);
|
||||||
|
}
|
@ -1,15 +1,12 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.KeyManagement.Models.Data.Models;
|
using Bit.Core.KeyManagement.Models.Data.Models;
|
||||||
using Bit.Core.KeyManagement.Repositories;
|
using Bit.Core.KeyManagement.Repositories;
|
||||||
|
|
||||||
namespace Bit.Api.KeyManagement.Queries;
|
namespace Bit.Core.KeyManagement.Queries;
|
||||||
|
|
||||||
public interface IUserAccountKeysQuery
|
|
||||||
{
|
|
||||||
Task<UserAccountKeysData> Run(User user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UserAccountKeysQuery(IUserSignatureKeyPairRepository signatureKeyPairRepository) : IUserAccountKeysQuery
|
public class UserAccountKeysQuery(IUserSignatureKeyPairRepository signatureKeyPairRepository) : IUserAccountKeysQuery
|
||||||
{
|
{
|
@ -0,0 +1,8 @@
|
|||||||
|
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
|
public class OrganizationSeatCounts
|
||||||
|
{
|
||||||
|
public int Users { get; set; }
|
||||||
|
public int Sponsored { get; set; }
|
||||||
|
public int Total => Users + Sponsored;
|
||||||
|
}
|
@ -16,7 +16,7 @@ public class CreateSponsorshipCommand(
|
|||||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
IOrganizationUserRepository organizationUserRepository) : ICreateSponsorshipCommand
|
IOrganizationRepository organizationRepository) : ICreateSponsorshipCommand
|
||||||
{
|
{
|
||||||
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(
|
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(
|
||||||
Organization sponsoringOrganization,
|
Organization sponsoringOrganization,
|
||||||
@ -89,8 +89,8 @@ public class CreateSponsorshipCommand(
|
|||||||
|
|
||||||
if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)
|
if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)
|
||||||
{
|
{
|
||||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id);
|
var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id);
|
||||||
var availableSeats = sponsoringOrganization.Seats.Value - occupiedSeats;
|
var availableSeats = sponsoringOrganization.Seats.Value - seatCounts.Total;
|
||||||
|
|
||||||
if (availableSeats <= 0)
|
if (availableSeats <= 0)
|
||||||
{
|
{
|
||||||
|
@ -107,12 +107,20 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
|||||||
(newPlan.PasswordManager.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
|
(newPlan.PasswordManager.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
|
||||||
if (!organization.Seats.HasValue || organization.Seats.Value > updatedPasswordManagerSeats)
|
if (!organization.Seats.HasValue || organization.Seats.Value > updatedPasswordManagerSeats)
|
||||||
{
|
{
|
||||||
var occupiedSeats =
|
var seatCounts =
|
||||||
await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
if (occupiedSeats > updatedPasswordManagerSeats)
|
if (seatCounts.Total > updatedPasswordManagerSeats)
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
if (organization.UseAdminSponsoredFamilies || seatCounts.Sponsored > 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " +
|
||||||
|
$"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Your organization currently has {seatCounts.Total} seats filled. " +
|
||||||
$"Your new plan only has ({updatedPasswordManagerSeats}) seats. Remove some users.");
|
$"Your new plan only has ({updatedPasswordManagerSeats}) seats. Remove some users.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -842,7 +842,13 @@ public class StripePaymentService : IPaymentService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _stripeAdapter.TaxIdCreateAsync(customer.Id,
|
await _stripeAdapter.TaxIdCreateAsync(customer.Id,
|
||||||
new TaxIdCreateOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber, });
|
new TaxIdCreateOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber });
|
||||||
|
|
||||||
|
if (taxInfo.TaxIdType == StripeConstants.TaxIdType.SpanishNIF)
|
||||||
|
{
|
||||||
|
await _stripeAdapter.TaxIdCreateAsync(customer.Id,
|
||||||
|
new TaxIdCreateOptions { Type = StripeConstants.TaxIdType.EUVAT, Value = $"ES{taxInfo.TaxIdNumber}" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (StripeException e)
|
catch (StripeException e)
|
||||||
{
|
{
|
||||||
@ -1000,6 +1006,15 @@ public class StripePaymentService : IPaymentService
|
|||||||
Value = parameters.TaxInformation.TaxId
|
Value = parameters.TaxInformation.TaxId
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
|
||||||
|
{
|
||||||
|
options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions
|
||||||
|
{
|
||||||
|
Type = StripeConstants.TaxIdType.EUVAT,
|
||||||
|
Value = $"ES{parameters.TaxInformation.TaxId}"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(gatewayCustomerId))
|
if (!string.IsNullOrWhiteSpace(gatewayCustomerId))
|
||||||
@ -1154,6 +1169,15 @@ public class StripePaymentService : IPaymentService
|
|||||||
Value = parameters.TaxInformation.TaxId
|
Value = parameters.TaxInformation.TaxId
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
|
||||||
|
{
|
||||||
|
options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions
|
||||||
|
{
|
||||||
|
Type = StripeConstants.TaxIdType.EUVAT,
|
||||||
|
Value = $"ES{parameters.TaxInformation.TaxId}"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Customer gatewayCustomer = null;
|
Customer gatewayCustomer = null;
|
||||||
|
@ -12,7 +12,6 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models;
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
@ -29,12 +28,9 @@ using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tokens;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Repositories;
|
|
||||||
using Fido2NetLib;
|
using Fido2NetLib;
|
||||||
using Fido2NetLib.Objects;
|
using Fido2NetLib.Objects;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -44,12 +40,11 @@ using JsonSerializer = System.Text.Json.JsonSerializer;
|
|||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
public class UserService : UserManager<User>, IUserService, IDisposable
|
public class UserService : UserManager<User>, IUserService
|
||||||
{
|
{
|
||||||
private const string PremiumPlanId = "premium-annually";
|
private const string PremiumPlanId = "premium-annually";
|
||||||
|
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly ICipherRepository _cipherRepository;
|
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||||
@ -65,17 +60,14 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
private readonly IPaymentService _paymentService;
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly IDataProtector _organizationServiceDataProtector;
|
|
||||||
private readonly IFido2 _fido2;
|
private readonly IFido2 _fido2;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
|
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
|
||||||
private readonly IProviderUserRepository _providerUserRepository;
|
private readonly IProviderUserRepository _providerUserRepository;
|
||||||
private readonly IStripeSyncService _stripeSyncService;
|
private readonly IStripeSyncService _stripeSyncService;
|
||||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly IPremiumUserBillingService _premiumUserBillingService;
|
private readonly IPremiumUserBillingService _premiumUserBillingService;
|
||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
|
||||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IDistributedCache _distributedCache;
|
private readonly IDistributedCache _distributedCache;
|
||||||
@ -83,7 +75,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
|
|
||||||
public UserService(
|
public UserService(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
ICipherRepository cipherRepository,
|
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationDomainRepository organizationDomainRepository,
|
IOrganizationDomainRepository organizationDomainRepository,
|
||||||
@ -101,7 +92,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
ILicensingService licenseService,
|
ILicensingService licenseService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IDataProtectionProvider dataProtectionProvider,
|
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
@ -111,10 +101,8 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
IAcceptOrgUserCommand acceptOrgUserCommand,
|
IAcceptOrgUserCommand acceptOrgUserCommand,
|
||||||
IProviderUserRepository providerUserRepository,
|
IProviderUserRepository providerUserRepository,
|
||||||
IStripeSyncService stripeSyncService,
|
IStripeSyncService stripeSyncService,
|
||||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IPremiumUserBillingService premiumUserBillingService,
|
IPremiumUserBillingService premiumUserBillingService,
|
||||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
|
||||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
|
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IDistributedCache distributedCache,
|
IDistributedCache distributedCache,
|
||||||
@ -131,7 +119,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
logger)
|
logger)
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_cipherRepository = cipherRepository;
|
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationDomainRepository = organizationDomainRepository;
|
_organizationDomainRepository = organizationDomainRepository;
|
||||||
@ -147,18 +134,14 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
_paymentService = paymentService;
|
_paymentService = paymentService;
|
||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
|
|
||||||
"OrganizationServiceDataProtector");
|
|
||||||
_fido2 = fido2;
|
_fido2 = fido2;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_acceptOrgUserCommand = acceptOrgUserCommand;
|
_acceptOrgUserCommand = acceptOrgUserCommand;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
_stripeSyncService = stripeSyncService;
|
_stripeSyncService = stripeSyncService;
|
||||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_premiumUserBillingService = premiumUserBillingService;
|
_premiumUserBillingService = premiumUserBillingService;
|
||||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
|
||||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_distributedCache = distributedCache;
|
_distributedCache = distributedCache;
|
||||||
|
@ -821,11 +821,6 @@ public class CipherService : ICipherService
|
|||||||
|
|
||||||
private async Task<bool> UserCanDeleteAsync(CipherDetails cipher, Guid userId)
|
private async Task<bool> UserCanDeleteAsync(CipherDetails cipher, Guid userId)
|
||||||
{
|
{
|
||||||
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
|
|
||||||
{
|
|
||||||
return await UserCanEditAsync(cipher, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = await _userService.GetUserByIdAsync(userId);
|
var user = await _userService.GetUserByIdAsync(userId);
|
||||||
var organizationAbility = cipher.OrganizationId.HasValue ?
|
var organizationAbility = cipher.OrganizationId.HasValue ?
|
||||||
await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null;
|
await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null;
|
||||||
@ -835,11 +830,6 @@ public class CipherService : ICipherService
|
|||||||
|
|
||||||
private async Task<bool> UserCanRestoreAsync(CipherDetails cipher, Guid userId)
|
private async Task<bool> UserCanRestoreAsync(CipherDetails cipher, Guid userId)
|
||||||
{
|
{
|
||||||
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
|
|
||||||
{
|
|
||||||
return await UserCanEditAsync(cipher, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = await _userService.GetUserByIdAsync(userId);
|
var user = await _userService.GetUserByIdAsync(userId);
|
||||||
var organizationAbility = cipher.OrganizationId.HasValue ?
|
var organizationAbility = cipher.OrganizationId.HasValue ?
|
||||||
await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null;
|
await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null;
|
||||||
@ -1059,17 +1049,11 @@ public class CipherService : ICipherService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This method is used to filter ciphers based on the user's permissions to delete them.
|
// This method is used to filter ciphers based on the user's permissions to delete them.
|
||||||
// It supports both the old and new logic depending on the feature flag.
|
|
||||||
private async Task<List<T>> FilterCiphersByDeletePermission<T>(
|
private async Task<List<T>> FilterCiphersByDeletePermission<T>(
|
||||||
IEnumerable<T> ciphers,
|
IEnumerable<T> ciphers,
|
||||||
HashSet<Guid> cipherIdsSet,
|
HashSet<Guid> cipherIdsSet,
|
||||||
Guid userId) where T : CipherDetails
|
Guid userId) where T : CipherDetails
|
||||||
{
|
{
|
||||||
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitItemDeletion))
|
|
||||||
{
|
|
||||||
return ciphers.Where(c => cipherIdsSet.Contains(c.Id) && c.Edit).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = await _userService.GetUserByIdAsync(userId);
|
var user = await _userService.GetUserByIdAsync(userId);
|
||||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||||
|
|
||||||
|
@ -8,6 +8,16 @@ namespace Bit.Icons.Controllers;
|
|||||||
[Route("")]
|
[Route("")]
|
||||||
public class IconsController : Controller
|
public class IconsController : Controller
|
||||||
{
|
{
|
||||||
|
// Basic bwi-globe icon
|
||||||
|
private static readonly byte[] _notFoundImage = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUg" +
|
||||||
|
"AAABMAAAATCAQAAADYWf5HAAABu0lEQVR42nXSvWuTURTH8R+t0heI9Y04aJycdBLNJNrBFBU7OFgUER3q21I0bXK+JwZ" +
|
||||||
|
"pXISm/QdcRB3EgqBBsNihsUbbgODQQSKCuKSDOApJuuhj8tCYQj/jvYfD795z1MZ+nBKrNKhSwrMxbZTrtRnqlEjZkB/x" +
|
||||||
|
"C/xmhZrlc71qS0Up8yVzTCGucFNKD1JhORVd70SZNU4okNx5d4+U2UXRIpJFWLClsR79YzN88wQvLWNzzPKEeS/wkQGpW" +
|
||||||
|
"VhhqhW8TtDJD3Mm1x/23zLSrZCdpBY8BueTNjHSbc+8wC9HlHgU5Aj5AW5zPdcVdpq0UcknWBSr/pjixO4gfp899Kd23p" +
|
||||||
|
"M2qQCH7LkCnqAqGh73OK/8NPOcaibr90LrW/yWAnaUhqjaOSl9nFR2r5rsqo22ypn1B5IN8VOUMHVgOnNQIX+d62plcz6" +
|
||||||
|
"rg1/jskK8CMb4we4pG6OWHtR/LBJkC2E4a7ZPkuX5ntumAOM2xxveclEhLvGH6XCmLPs735Eetrw63NnOgr9P9q1viC3x" +
|
||||||
|
"lRUGOjImqFDuOBvrYYoaZU9z1uPpYae5NfdvbNVG2ZjDIlXq/oMi46lo++4vjjPBl2Dlg00AAAAASUVORK5CYII=");
|
||||||
|
|
||||||
private readonly IMemoryCache _memoryCache;
|
private readonly IMemoryCache _memoryCache;
|
||||||
private readonly IDomainMappingService _domainMappingService;
|
private readonly IDomainMappingService _domainMappingService;
|
||||||
private readonly IIconFetchingService _iconFetchingService;
|
private readonly IIconFetchingService _iconFetchingService;
|
||||||
@ -89,7 +99,7 @@ public class IconsController : Controller
|
|||||||
|
|
||||||
if (icon == null)
|
if (icon == null)
|
||||||
{
|
{
|
||||||
return new NotFoundResult();
|
return new FileContentResult(_notFoundImage, "image/png");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FileContentResult(icon.Image, icon.Format);
|
return new FileContentResult(icon.Image, icon.Format);
|
||||||
|
@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
|||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
@ -200,11 +201,23 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
|
|||||||
public async Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids)
|
public async Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids)
|
||||||
{
|
{
|
||||||
await using var connection = new SqlConnection(ConnectionString);
|
await using var connection = new SqlConnection(ConnectionString);
|
||||||
|
|
||||||
return (await connection.QueryAsync<Organization>(
|
return (await connection.QueryAsync<Organization>(
|
||||||
$"[{Schema}].[{Table}_ReadManyByIds]",
|
$"[{Schema}].[{Table}_ReadManyByIds]",
|
||||||
new { OrganizationIds = ids.ToGuidIdArrayTVP() },
|
new { OrganizationIds = ids.ToGuidIdArrayTVP() },
|
||||||
commandType: CommandType.StoredProcedure))
|
commandType: CommandType.StoredProcedure))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var result = await connection.QueryAsync<OrganizationSeatCounts>(
|
||||||
|
"[dbo].[Organization_ReadOccupiedSeatCountByOrganizationId]",
|
||||||
|
new { OrganizationId = organizationId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return result.SingleOrDefault() ?? new OrganizationSeatCounts();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,19 +88,6 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
|
|
||||||
{
|
|
||||||
using (var connection = new SqlConnection(ConnectionString))
|
|
||||||
{
|
|
||||||
var result = await connection.ExecuteScalarAsync<int>(
|
|
||||||
"[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]",
|
|
||||||
new { OrganizationId = organizationId },
|
|
||||||
commandType: CommandType.StoredProcedure);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId)
|
public async Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId)
|
||||||
{
|
{
|
||||||
using (var connection = new SqlConnection(ConnectionString))
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Billing.Providers.Repositories;
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
using Bit.Core.KeyManagement.Repositories;
|
using Bit.Core.KeyManagement.Repositories;
|
||||||
using Bit.Core.NotificationCenter.Repositories;
|
using Bit.Core.NotificationCenter.Repositories;
|
||||||
using Bit.Core.Platform.Installations;
|
using Bit.Core.Platform.Installations;
|
||||||
@ -12,6 +13,7 @@ using Bit.Core.Vault.Repositories;
|
|||||||
using Bit.Infrastructure.Dapper.AdminConsole.Repositories;
|
using Bit.Infrastructure.Dapper.AdminConsole.Repositories;
|
||||||
using Bit.Infrastructure.Dapper.Auth.Repositories;
|
using Bit.Infrastructure.Dapper.Auth.Repositories;
|
||||||
using Bit.Infrastructure.Dapper.Billing.Repositories;
|
using Bit.Infrastructure.Dapper.Billing.Repositories;
|
||||||
|
using Bit.Infrastructure.Dapper.Dirt;
|
||||||
using Bit.Infrastructure.Dapper.KeyManagement.Repositories;
|
using Bit.Infrastructure.Dapper.KeyManagement.Repositories;
|
||||||
using Bit.Infrastructure.Dapper.NotificationCenter.Repositories;
|
using Bit.Infrastructure.Dapper.NotificationCenter.Repositories;
|
||||||
using Bit.Infrastructure.Dapper.Platform;
|
using Bit.Infrastructure.Dapper.Platform;
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
|
using Bit.Core.Dirt.Reports.Entities;
|
||||||
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tools.Repositories;
|
|
||||||
using Bit.Infrastructure.Dapper.Repositories;
|
using Bit.Infrastructure.Dapper.Repositories;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using ToolsEntities = Bit.Core.Tools.Entities;
|
|
||||||
|
|
||||||
namespace Bit.Infrastructure.Dapper.Tools.Repositories;
|
namespace Bit.Infrastructure.Dapper.Dirt;
|
||||||
|
|
||||||
public class PasswordHealthReportApplicationRepository : Repository<ToolsEntities.PasswordHealthReportApplication, Guid>, IPasswordHealthReportApplicationRepository
|
public class PasswordHealthReportApplicationRepository : Repository<PasswordHealthReportApplication, Guid>, IPasswordHealthReportApplicationRepository
|
||||||
{
|
{
|
||||||
public PasswordHealthReportApplicationRepository(GlobalSettings globalSettings)
|
public PasswordHealthReportApplicationRepository(GlobalSettings globalSettings)
|
||||||
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||||
@ -18,11 +18,11 @@ public class PasswordHealthReportApplicationRepository : Repository<ToolsEntitie
|
|||||||
: base(connectionString, readOnlyConnectionString)
|
: base(connectionString, readOnlyConnectionString)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public async Task<ICollection<ToolsEntities.PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId)
|
public async Task<ICollection<PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId)
|
||||||
{
|
{
|
||||||
using (var connection = new SqlConnection(ReadOnlyConnectionString))
|
using (var connection = new SqlConnection(ReadOnlyConnectionString))
|
||||||
{
|
{
|
||||||
var results = await connection.QueryAsync<ToolsEntities.PasswordHealthReportApplication>(
|
var results = await connection.QueryAsync<PasswordHealthReportApplication>(
|
||||||
$"[{Schema}].[PasswordHealthReportApplication_ReadByOrganizationId]",
|
$"[{Schema}].[PasswordHealthReportApplication_ReadByOrganizationId]",
|
||||||
new { OrganizationId = organizationId },
|
new { OrganizationId = organizationId },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
@ -5,6 +5,7 @@ using Bit.Core.Billing.Constants;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using LinqToDB.Tools;
|
using LinqToDB.Tools;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -375,4 +376,28 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
{
|
{
|
||||||
throw new NotImplementedException("Collection enhancements migration is not yet supported for Entity Framework.");
|
throw new NotImplementedException("Collection enhancements migration is not yet supported for Entity Framework.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var users = await dbContext.OrganizationUsers
|
||||||
|
.Where(ou => ou.OrganizationId == organizationId && ou.Status >= 0)
|
||||||
|
.CountAsync();
|
||||||
|
|
||||||
|
var sponsored = await dbContext.OrganizationSponsorships
|
||||||
|
.Where(os => os.SponsoringOrganizationId == organizationId &&
|
||||||
|
os.IsAdminInitiated &&
|
||||||
|
(os.ToDelete == false || (os.ToDelete == true && os.ValidUntil != null && os.ValidUntil > DateTime.UtcNow)) &&
|
||||||
|
(os.SponsoredOrganizationId == null || (os.SponsoredOrganizationId != null && (os.ValidUntil == null || os.ValidUntil > DateTime.UtcNow))))
|
||||||
|
.CountAsync();
|
||||||
|
|
||||||
|
return new OrganizationSeatCounts
|
||||||
|
{
|
||||||
|
Users = users,
|
||||||
|
Sponsored = sponsored
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,12 +228,6 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
|||||||
return await GetCountFromQuery(query);
|
return await GetCountFromQuery(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
|
|
||||||
{
|
|
||||||
var query = new OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery(organizationId);
|
|
||||||
return await GetCountFromQuery(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> GetCountByOrganizationIdAsync(Guid organizationId)
|
public async Task<int> GetCountByOrganizationIdAsync(Guid organizationId)
|
||||||
{
|
{
|
||||||
var query = new OrganizationUserReadCountByOrganizationIdQuery(organizationId);
|
var query = new OrganizationUserReadCountByOrganizationIdQuery(organizationId);
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Infrastructure.EntityFramework.Models;
|
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
|
||||||
|
|
||||||
public class OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery : IQuery<OrganizationUser>
|
|
||||||
{
|
|
||||||
private readonly Guid _organizationId;
|
|
||||||
|
|
||||||
public OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery(Guid organizationId)
|
|
||||||
{
|
|
||||||
_organizationId = organizationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)
|
|
||||||
{
|
|
||||||
var orgUsersQuery = from ou in dbContext.OrganizationUsers
|
|
||||||
where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited
|
|
||||||
select new OrganizationUser { Id = ou.Id, OrganizationId = ou.OrganizationId, Status = ou.Status };
|
|
||||||
|
|
||||||
// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
|
|
||||||
// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
|
|
||||||
var sponsorshipsQuery = from os in dbContext.OrganizationSponsorships
|
|
||||||
where os.SponsoringOrganizationId == _organizationId &&
|
|
||||||
os.IsAdminInitiated &&
|
|
||||||
(
|
|
||||||
// Not marked for deletion - always count
|
|
||||||
(!os.ToDelete) ||
|
|
||||||
// Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)
|
|
||||||
(os.ToDelete && os.ValidUntil.HasValue && os.ValidUntil.Value > DateTime.UtcNow)
|
|
||||||
) &&
|
|
||||||
(
|
|
||||||
// SENT status: When SponsoredOrganizationId is null
|
|
||||||
os.SponsoredOrganizationId == null ||
|
|
||||||
// ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future
|
|
||||||
(os.SponsoredOrganizationId != null &&
|
|
||||||
(!os.ValidUntil.HasValue || os.ValidUntil.Value > DateTime.UtcNow))
|
|
||||||
)
|
|
||||||
select new OrganizationUser
|
|
||||||
{
|
|
||||||
Id = os.Id,
|
|
||||||
OrganizationId = _organizationId,
|
|
||||||
Status = OrganizationUserStatusType.Invited
|
|
||||||
};
|
|
||||||
|
|
||||||
return orgUsersQuery.Concat(sponsorshipsQuery);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,8 @@
|
|||||||
using Bit.Infrastructure.EntityFramework.Tools.Models;
|
using Bit.Infrastructure.EntityFramework.Dirt.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Tools.Configurations;
|
namespace Bit.Infrastructure.EntityFramework.Dirt.Configurations;
|
||||||
|
|
||||||
public class PasswordHealthReportApplicationEntityTypeConfiguration : IEntityTypeConfiguration<PasswordHealthReportApplication>
|
public class PasswordHealthReportApplicationEntityTypeConfiguration : IEntityTypeConfiguration<PasswordHealthReportApplication>
|
||||||
{
|
{
|
@ -1,9 +1,9 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Tools.Models;
|
namespace Bit.Infrastructure.EntityFramework.Dirt.Models;
|
||||||
|
|
||||||
public class PasswordHealthReportApplication : Core.Tools.Entities.PasswordHealthReportApplication
|
public class PasswordHealthReportApplication : Core.Dirt.Reports.Entities.PasswordHealthReportApplication
|
||||||
{
|
{
|
||||||
public virtual Organization Organization { get; set; }
|
public virtual Organization Organization { get; set; }
|
||||||
}
|
}
|
||||||
@ -12,7 +12,7 @@ public class PasswordHealthReportApplicationProfile : Profile
|
|||||||
{
|
{
|
||||||
public PasswordHealthReportApplicationProfile()
|
public PasswordHealthReportApplicationProfile()
|
||||||
{
|
{
|
||||||
CreateMap<Core.Tools.Entities.PasswordHealthReportApplication, PasswordHealthReportApplication>()
|
CreateMap<Core.Dirt.Reports.Entities.PasswordHealthReportApplication, PasswordHealthReportApplication>()
|
||||||
.ReverseMap();
|
.ReverseMap();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,22 +1,21 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Bit.Core.Tools.Repositories;
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Dirt.Models;
|
||||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Tools.Models;
|
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using AdminConsoleEntities = Bit.Core.Tools.Entities;
|
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Tools.Repositories;
|
namespace Bit.Infrastructure.EntityFramework.Dirt.Repositories;
|
||||||
|
|
||||||
public class PasswordHealthReportApplicationRepository :
|
public class PasswordHealthReportApplicationRepository :
|
||||||
Repository<AdminConsoleEntities.PasswordHealthReportApplication, PasswordHealthReportApplication, Guid>,
|
Repository<Core.Dirt.Reports.Entities.PasswordHealthReportApplication, PasswordHealthReportApplication, Guid>,
|
||||||
IPasswordHealthReportApplicationRepository
|
IPasswordHealthReportApplicationRepository
|
||||||
{
|
{
|
||||||
public PasswordHealthReportApplicationRepository(IServiceScopeFactory serviceScopeFactory,
|
public PasswordHealthReportApplicationRepository(IServiceScopeFactory serviceScopeFactory,
|
||||||
IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.PasswordHealthReportApplications)
|
IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.PasswordHealthReportApplications)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public async Task<ICollection<AdminConsoleEntities.PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId)
|
public async Task<ICollection<Core.Dirt.Reports.Entities.PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId)
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
{
|
{
|
||||||
@ -24,7 +23,7 @@ public class PasswordHealthReportApplicationRepository :
|
|||||||
var results = await dbContext.PasswordHealthReportApplications
|
var results = await dbContext.PasswordHealthReportApplications
|
||||||
.Where(p => p.OrganizationId == organizationId)
|
.Where(p => p.OrganizationId == organizationId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
return Mapper.Map<ICollection<AdminConsoleEntities.PasswordHealthReportApplication>>(results);
|
return Mapper.Map<ICollection<Core.Dirt.Reports.Entities.PasswordHealthReportApplication>>(results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,6 +2,7 @@
|
|||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Billing.Providers.Repositories;
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Billing.Repositories;
|
using Bit.Core.Billing.Repositories;
|
||||||
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.KeyManagement.Repositories;
|
using Bit.Core.KeyManagement.Repositories;
|
||||||
using Bit.Core.NotificationCenter.Repositories;
|
using Bit.Core.NotificationCenter.Repositories;
|
||||||
@ -13,6 +14,7 @@ using Bit.Core.Vault.Repositories;
|
|||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Auth.Repositories;
|
using Bit.Infrastructure.EntityFramework.Auth.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Billing.Repositories;
|
using Bit.Infrastructure.EntityFramework.Billing.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Dirt.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories;
|
using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;
|
using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Platform;
|
using Bit.Infrastructure.EntityFramework.Platform;
|
||||||
|
@ -4,11 +4,11 @@ using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
|||||||
using Bit.Infrastructure.EntityFramework.Auth.Models;
|
using Bit.Infrastructure.EntityFramework.Auth.Models;
|
||||||
using Bit.Infrastructure.EntityFramework.Billing.Models;
|
using Bit.Infrastructure.EntityFramework.Billing.Models;
|
||||||
using Bit.Infrastructure.EntityFramework.Converters;
|
using Bit.Infrastructure.EntityFramework.Converters;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Dirt.Models;
|
||||||
using Bit.Infrastructure.EntityFramework.Models;
|
using Bit.Infrastructure.EntityFramework.Models;
|
||||||
using Bit.Infrastructure.EntityFramework.NotificationCenter.Models;
|
using Bit.Infrastructure.EntityFramework.NotificationCenter.Models;
|
||||||
using Bit.Infrastructure.EntityFramework.Platform;
|
using Bit.Infrastructure.EntityFramework.Platform;
|
||||||
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
|
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
|
||||||
using Bit.Infrastructure.EntityFramework.Tools.Models;
|
|
||||||
using Bit.Infrastructure.EntityFramework.Vault.Models;
|
using Bit.Infrastructure.EntityFramework.Vault.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.2.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.3.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -23,6 +23,7 @@ using Bit.Core.Auth.UserFeatures;
|
|||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Services.Implementations;
|
using Bit.Core.Billing.Services.Implementations;
|
||||||
using Bit.Core.Billing.TrialInitiation;
|
using Bit.Core.Billing.TrialInitiation;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.HostedServices;
|
using Bit.Core.HostedServices;
|
||||||
@ -43,7 +44,6 @@ using Bit.Core.Services;
|
|||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Core.Tools.ImportFeatures;
|
using Bit.Core.Tools.ImportFeatures;
|
||||||
using Bit.Core.Tools.ReportFeatures;
|
|
||||||
using Bit.Core.Tools.SendFeatures;
|
using Bit.Core.Tools.SendFeatures;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
@Name VARCHAR(MAX),
|
@Name VARCHAR(MAX),
|
||||||
@ExternalId NVARCHAR(300),
|
@ExternalId NVARCHAR(300),
|
||||||
@CreationDate DATETIME2(7),
|
@CreationDate DATETIME2(7),
|
||||||
@RevisionDate DATETIME2(7)
|
@RevisionDate DATETIME2(7),
|
||||||
|
@DefaultUserCollectionEmail NVARCHAR(256) = NULL,
|
||||||
|
@Type TINYINT = 0
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -16,7 +18,9 @@ BEGIN
|
|||||||
[Name],
|
[Name],
|
||||||
[ExternalId],
|
[ExternalId],
|
||||||
[CreationDate],
|
[CreationDate],
|
||||||
[RevisionDate]
|
[RevisionDate],
|
||||||
|
[DefaultUserCollectionEmail],
|
||||||
|
[Type]
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@ -25,7 +29,9 @@ BEGIN
|
|||||||
@Name,
|
@Name,
|
||||||
@ExternalId,
|
@ExternalId,
|
||||||
@CreationDate,
|
@CreationDate,
|
||||||
@RevisionDate
|
@RevisionDate,
|
||||||
|
@DefaultUserCollectionEmail,
|
||||||
|
@Type
|
||||||
)
|
)
|
||||||
|
|
||||||
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
|
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
|
||||||
|
@ -6,12 +6,14 @@ CREATE PROCEDURE [dbo].[Collection_CreateWithGroupsAndUsers]
|
|||||||
@CreationDate DATETIME2(7),
|
@CreationDate DATETIME2(7),
|
||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
|
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
|
||||||
@Users AS [dbo].[CollectionAccessSelectionType] READONLY
|
@Users AS [dbo].[CollectionAccessSelectionType] READONLY,
|
||||||
|
@DefaultUserCollectionEmail NVARCHAR(256) = NULL,
|
||||||
|
@Type TINYINT = 0
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
|
|
||||||
EXEC [dbo].[Collection_Create] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate
|
EXEC [dbo].[Collection_Create] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type
|
||||||
|
|
||||||
-- Groups
|
-- Groups
|
||||||
;WITH [AvailableGroupsCTE] AS(
|
;WITH [AvailableGroupsCTE] AS(
|
||||||
|
@ -13,7 +13,9 @@ BEGIN
|
|||||||
ExternalId,
|
ExternalId,
|
||||||
MIN([ReadOnly]) AS [ReadOnly],
|
MIN([ReadOnly]) AS [ReadOnly],
|
||||||
MIN([HidePasswords]) AS [HidePasswords],
|
MIN([HidePasswords]) AS [HidePasswords],
|
||||||
MAX([Manage]) AS [Manage]
|
MAX([Manage]) AS [Manage],
|
||||||
|
[DefaultUserCollectionEmail],
|
||||||
|
[Type]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[UserCollectionDetails](@UserId)
|
[dbo].[UserCollectionDetails](@UserId)
|
||||||
GROUP BY
|
GROUP BY
|
||||||
@ -22,5 +24,7 @@ BEGIN
|
|||||||
[Name],
|
[Name],
|
||||||
CreationDate,
|
CreationDate,
|
||||||
RevisionDate,
|
RevisionDate,
|
||||||
ExternalId
|
ExternalId,
|
||||||
|
[DefaultUserCollectionEmail],
|
||||||
|
[Type]
|
||||||
END
|
END
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
@Name VARCHAR(MAX),
|
@Name VARCHAR(MAX),
|
||||||
@ExternalId NVARCHAR(300),
|
@ExternalId NVARCHAR(300),
|
||||||
@CreationDate DATETIME2(7),
|
@CreationDate DATETIME2(7),
|
||||||
@RevisionDate DATETIME2(7)
|
@RevisionDate DATETIME2(7),
|
||||||
|
@DefaultUserCollectionEmail NVARCHAR(256) = NULL,
|
||||||
|
@Type TINYINT = 0
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -16,9 +18,11 @@ BEGIN
|
|||||||
[Name] = @Name,
|
[Name] = @Name,
|
||||||
[ExternalId] = @ExternalId,
|
[ExternalId] = @ExternalId,
|
||||||
[CreationDate] = @CreationDate,
|
[CreationDate] = @CreationDate,
|
||||||
[RevisionDate] = @RevisionDate
|
[RevisionDate] = @RevisionDate,
|
||||||
|
[DefaultUserCollectionEmail] = @DefaultUserCollectionEmail,
|
||||||
|
[Type] = @Type
|
||||||
WHERE
|
WHERE
|
||||||
[Id] = @Id
|
[Id] = @Id
|
||||||
|
|
||||||
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
|
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
|
||||||
END
|
END
|
||||||
|
@ -6,12 +6,14 @@
|
|||||||
@CreationDate DATETIME2(7),
|
@CreationDate DATETIME2(7),
|
||||||
@RevisionDate DATETIME2(7),
|
@RevisionDate DATETIME2(7),
|
||||||
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
|
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
|
||||||
@Users AS [dbo].[CollectionAccessSelectionType] READONLY
|
@Users AS [dbo].[CollectionAccessSelectionType] READONLY,
|
||||||
|
@DefaultUserCollectionEmail NVARCHAR(256) = NULL,
|
||||||
|
@Type TINYINT = 0
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
|
|
||||||
EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate
|
EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type
|
||||||
|
|
||||||
-- Groups
|
-- Groups
|
||||||
-- Delete groups that are no longer in source
|
-- Delete groups that are no longer in source
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Organization_ReadOccupiedSeatCountByOrganizationId]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
(
|
||||||
|
-- Count organization users
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM [dbo].[OrganizationUserView]
|
||||||
|
WHERE OrganizationId = @OrganizationId
|
||||||
|
AND Status >= 0 --Invited
|
||||||
|
) as Users,
|
||||||
|
(
|
||||||
|
-- Count admin-initiated sponsorships towards the seat count
|
||||||
|
-- Introduced in https://bitwarden.atlassian.net/browse/PM-17772
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM [dbo].[OrganizationSponsorship]
|
||||||
|
WHERE SponsoringOrganizationId = @OrganizationId
|
||||||
|
AND IsAdminInitiated = 1
|
||||||
|
AND (
|
||||||
|
-- Not marked for deletion - always count
|
||||||
|
(ToDelete = 0)
|
||||||
|
OR
|
||||||
|
-- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)
|
||||||
|
(ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE())
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
-- SENT status: When SponsoredOrganizationId is null
|
||||||
|
SponsoredOrganizationId IS NULL
|
||||||
|
OR
|
||||||
|
-- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future
|
||||||
|
(SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE()))
|
||||||
|
)
|
||||||
|
) as Sponsored
|
||||||
|
END
|
||||||
|
GO
|
@ -1,10 +1,12 @@
|
|||||||
CREATE TABLE [dbo].[Collection] (
|
CREATE TABLE [dbo].[Collection] (
|
||||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
|
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
|
||||||
[Name] VARCHAR (MAX) NOT NULL,
|
[Name] VARCHAR (MAX) NOT NULL,
|
||||||
[ExternalId] NVARCHAR (300) NULL,
|
[ExternalId] NVARCHAR (300) NULL,
|
||||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||||
|
[DefaultUserCollectionEmail] NVARCHAR(256) NULL,
|
||||||
|
[Type] TINYINT NOT NULL DEFAULT(0),
|
||||||
CONSTRAINT [PK_Collection] PRIMARY KEY CLUSTERED ([Id] ASC),
|
CONSTRAINT [PK_Collection] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||||
CONSTRAINT [FK_Collection_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE
|
CONSTRAINT [FK_Collection_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
@ -73,7 +73,9 @@ BEGIN
|
|||||||
C.[Name],
|
C.[Name],
|
||||||
C.[CreationDate],
|
C.[CreationDate],
|
||||||
C.[RevisionDate],
|
C.[RevisionDate],
|
||||||
C.[ExternalId]
|
C.[ExternalId],
|
||||||
|
C.[DefaultUserCollectionEmail],
|
||||||
|
C.[Type]
|
||||||
|
|
||||||
IF (@IncludeAccessRelationships = 1)
|
IF (@IncludeAccessRelationships = 1)
|
||||||
BEGIN
|
BEGIN
|
||||||
|
@ -73,7 +73,9 @@ BEGIN
|
|||||||
C.[Name],
|
C.[Name],
|
||||||
C.[CreationDate],
|
C.[CreationDate],
|
||||||
C.[RevisionDate],
|
C.[RevisionDate],
|
||||||
C.[ExternalId]
|
C.[ExternalId],
|
||||||
|
C.[DefaultUserCollectionEmail],
|
||||||
|
C.[Type]
|
||||||
|
|
||||||
IF (@IncludeAccessRelationships = 1)
|
IF (@IncludeAccessRelationships = 1)
|
||||||
BEGIN
|
BEGIN
|
||||||
|
@ -30,6 +30,7 @@ public class OrganizationUserControllerPutTests
|
|||||||
OrganizationUser organizationUser, OrganizationAbility organizationAbility,
|
OrganizationUser organizationUser, OrganizationAbility organizationAbility,
|
||||||
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
|
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []);
|
Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []);
|
||||||
|
|
||||||
// Authorize all changes for basic happy path test
|
// Authorize all changes for basic happy path test
|
||||||
@ -41,15 +42,18 @@ public class OrganizationUserControllerPutTests
|
|||||||
// Save these for later - organizationUser object will be mutated
|
// Save these for later - organizationUser object will be mutated
|
||||||
var orgUserId = organizationUser.Id;
|
var orgUserId = organizationUser.Id;
|
||||||
var orgUserEmail = organizationUser.Email;
|
var orgUserEmail = organizationUser.Email;
|
||||||
|
var existingUserType = organizationUser.Type;
|
||||||
|
|
||||||
|
// Act
|
||||||
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
|
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
|
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
|
||||||
ou.Type == model.Type &&
|
ou.Type == model.Type &&
|
||||||
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
|
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
|
||||||
ou.AccessSecretsManager == model.AccessSecretsManager &&
|
ou.AccessSecretsManager == model.AccessSecretsManager &&
|
||||||
ou.Id == orgUserId &&
|
ou.Id == orgUserId &&
|
||||||
ou.Email == orgUserEmail),
|
ou.Email == orgUserEmail), existingUserType,
|
||||||
savingUserId,
|
savingUserId,
|
||||||
Arg.Is<List<CollectionAccessSelection>>(cas =>
|
Arg.Is<List<CollectionAccessSelection>>(cas =>
|
||||||
cas.All(c => model.Collections.Any(m => m.Id == c.Id))),
|
cas.All(c => model.Collections.Any(m => m.Id == c.Id))),
|
||||||
@ -77,6 +81,7 @@ public class OrganizationUserControllerPutTests
|
|||||||
OrganizationUser organizationUser, OrganizationAbility organizationAbility,
|
OrganizationUser organizationUser, OrganizationAbility organizationAbility,
|
||||||
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
|
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
// Updating self
|
// Updating self
|
||||||
organizationUser.UserId = savingUserId;
|
organizationUser.UserId = savingUserId;
|
||||||
organizationAbility.AllowAdminAccessToAllCollectionItems = false;
|
organizationAbility.AllowAdminAccessToAllCollectionItems = false;
|
||||||
@ -88,15 +93,18 @@ public class OrganizationUserControllerPutTests
|
|||||||
|
|
||||||
var orgUserId = organizationUser.Id;
|
var orgUserId = organizationUser.Id;
|
||||||
var orgUserEmail = organizationUser.Email;
|
var orgUserEmail = organizationUser.Email;
|
||||||
|
var existingUserType = organizationUser.Type;
|
||||||
|
|
||||||
|
// Act
|
||||||
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
|
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
|
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
|
||||||
ou.Type == model.Type &&
|
ou.Type == model.Type &&
|
||||||
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
|
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
|
||||||
ou.AccessSecretsManager == model.AccessSecretsManager &&
|
ou.AccessSecretsManager == model.AccessSecretsManager &&
|
||||||
ou.Id == orgUserId &&
|
ou.Id == orgUserId &&
|
||||||
ou.Email == orgUserEmail),
|
ou.Email == orgUserEmail), existingUserType,
|
||||||
savingUserId,
|
savingUserId,
|
||||||
Arg.Is<List<CollectionAccessSelection>>(cas =>
|
Arg.Is<List<CollectionAccessSelection>>(cas =>
|
||||||
cas.All(c => model.Collections.Any(m => m.Id == c.Id))),
|
cas.All(c => model.Collections.Any(m => m.Id == c.Id))),
|
||||||
@ -110,6 +118,7 @@ public class OrganizationUserControllerPutTests
|
|||||||
OrganizationUser organizationUser, OrganizationAbility organizationAbility,
|
OrganizationUser organizationUser, OrganizationAbility organizationAbility,
|
||||||
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
|
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
// Updating self
|
// Updating self
|
||||||
organizationUser.UserId = savingUserId;
|
organizationUser.UserId = savingUserId;
|
||||||
organizationAbility.AllowAdminAccessToAllCollectionItems = true;
|
organizationAbility.AllowAdminAccessToAllCollectionItems = true;
|
||||||
@ -121,15 +130,18 @@ public class OrganizationUserControllerPutTests
|
|||||||
|
|
||||||
var orgUserId = organizationUser.Id;
|
var orgUserId = organizationUser.Id;
|
||||||
var orgUserEmail = organizationUser.Email;
|
var orgUserEmail = organizationUser.Email;
|
||||||
|
var existingUserType = organizationUser.Type;
|
||||||
|
|
||||||
|
// Act
|
||||||
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
|
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
|
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
|
||||||
ou.Type == model.Type &&
|
ou.Type == model.Type &&
|
||||||
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
|
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
|
||||||
ou.AccessSecretsManager == model.AccessSecretsManager &&
|
ou.AccessSecretsManager == model.AccessSecretsManager &&
|
||||||
ou.Id == orgUserId &&
|
ou.Id == orgUserId &&
|
||||||
ou.Email == orgUserEmail),
|
ou.Email == orgUserEmail), existingUserType,
|
||||||
savingUserId,
|
savingUserId,
|
||||||
Arg.Is<List<CollectionAccessSelection>>(cas =>
|
Arg.Is<List<CollectionAccessSelection>>(cas =>
|
||||||
cas.All(c => model.Collections.Any(m => m.Id == c.Id))),
|
cas.All(c => model.Collections.Any(m => m.Id == c.Id))),
|
||||||
@ -142,6 +154,7 @@ public class OrganizationUserControllerPutTests
|
|||||||
OrganizationUser organizationUser, OrganizationAbility organizationAbility,
|
OrganizationUser organizationUser, OrganizationAbility organizationAbility,
|
||||||
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
|
SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)
|
||||||
{
|
{
|
||||||
|
// Arrange
|
||||||
var editedCollectionId = CoreHelpers.GenerateComb();
|
var editedCollectionId = CoreHelpers.GenerateComb();
|
||||||
var readonlyCollectionId1 = CoreHelpers.GenerateComb();
|
var readonlyCollectionId1 = CoreHelpers.GenerateComb();
|
||||||
var readonlyCollectionId2 = CoreHelpers.GenerateComb();
|
var readonlyCollectionId2 = CoreHelpers.GenerateComb();
|
||||||
@ -194,16 +207,19 @@ public class OrganizationUserControllerPutTests
|
|||||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => c.Id == readonlyCollectionId1 || c.Id == readonlyCollectionId2),
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => c.Id == readonlyCollectionId1 || c.Id == readonlyCollectionId2),
|
||||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess)))
|
Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess)))
|
||||||
.Returns(AuthorizationResult.Failed());
|
.Returns(AuthorizationResult.Failed());
|
||||||
|
var existingUserType = organizationUser.Type;
|
||||||
|
|
||||||
|
// Act
|
||||||
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
|
await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
// Expect all collection access (modified and unmodified) to be saved
|
// Expect all collection access (modified and unmodified) to be saved
|
||||||
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
|
await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>
|
||||||
ou.Type == model.Type &&
|
ou.Type == model.Type &&
|
||||||
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
|
ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&
|
||||||
ou.AccessSecretsManager == model.AccessSecretsManager &&
|
ou.AccessSecretsManager == model.AccessSecretsManager &&
|
||||||
ou.Id == orgUserId &&
|
ou.Id == orgUserId &&
|
||||||
ou.Email == orgUserEmail),
|
ou.Email == orgUserEmail), existingUserType,
|
||||||
savingUserId,
|
savingUserId,
|
||||||
Arg.Is<List<CollectionAccessSelection>>(cas =>
|
Arg.Is<List<CollectionAccessSelection>>(cas =>
|
||||||
cas.Select(c => c.Id).SequenceEqual(currentCollectionAccess.Select(c => c.Id)) &&
|
cas.Select(c => c.Id).SequenceEqual(currentCollectionAccess.Select(c => c.Id)) &&
|
||||||
|
@ -4,7 +4,7 @@ using Bit.Api.Auth.Controllers;
|
|||||||
using Bit.Api.Auth.Models.Request;
|
using Bit.Api.Auth.Models.Request;
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||||
using Bit.Api.KeyManagement.Queries;
|
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Api.KeyManagement.Validators;
|
using Bit.Api.KeyManagement.Validators;
|
||||||
using Bit.Api.Tools.Models.Request;
|
using Bit.Api.Tools.Models.Request;
|
||||||
using Bit.Api.Vault.Models.Request;
|
using Bit.Api.Vault.Models.Request;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user