mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 16:12:49 -05:00
Merge branch 'master' into feature/flexible-collections
This commit is contained in:
38
.github/workflows/build.yml
vendored
38
.github/workflows/build.yml
vendored
@ -514,13 +514,39 @@ jobs:
|
||||
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
self-host-build:
|
||||
name: Self-host build
|
||||
needs: build-docker
|
||||
uses: bitwarden/self-host/.github/workflows/build-unified.yml@master
|
||||
with:
|
||||
server_branch: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
name: Trigger self-host build
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build-docker
|
||||
steps:
|
||||
- name: Login to Azure - CI Subscription
|
||||
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf # v1.4.3
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve github PAT secrets
|
||||
id: retrieve-secret-pat
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@f096207b7a2f31723165aee6ad03e91716686e78
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Trigger self-host build
|
||||
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
|
||||
with:
|
||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
script: |
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'bitwarden',
|
||||
repo: 'self-host',
|
||||
workflow_id: 'build-unified.yml',
|
||||
ref: 'master',
|
||||
inputs: {
|
||||
server_branch: '${{ github.ref }}'
|
||||
}
|
||||
})
|
||||
|
||||
check-failures:
|
||||
name: Check for failures
|
||||
|
2
.github/workflows/protect-files.yml
vendored
2
.github/workflows/protect-files.yml
vendored
@ -49,7 +49,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Add label to pull request
|
||||
if: contains(steps.check-changes.outputs.changes_detected, true)
|
||||
if: contains(steps.check-changes.outputs.changes_detected, 'true')
|
||||
uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90 # 1.0.4
|
||||
with:
|
||||
add-labels: ${{ matrix.label }}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Version>2023.8.1</Version>
|
||||
<Version>2023.8.2</Version>
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -38,6 +38,9 @@ public class SecretAuthorizationHandler : AuthorizationHandler<SecretOperationRe
|
||||
case not null when requirement == SecretOperations.Create:
|
||||
await CanCreateSecretAsync(context, requirement, resource);
|
||||
break;
|
||||
case not null when requirement == SecretOperations.Read:
|
||||
await CanReadSecretAsync(context, requirement, resource);
|
||||
break;
|
||||
case not null when requirement == SecretOperations.Update:
|
||||
await CanUpdateSecretAsync(context, requirement, resource);
|
||||
break;
|
||||
@ -85,6 +88,18 @@ public class SecretAuthorizationHandler : AuthorizationHandler<SecretOperationRe
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanReadSecretAsync(AuthorizationHandlerContext context,
|
||||
SecretOperationRequirement requirement, Secret resource)
|
||||
{
|
||||
var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
|
||||
|
||||
var access = await _secretRepository.AccessToSecretAsync(resource.Id, userId, accessClient);
|
||||
|
||||
if (access.Read)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanUpdateSecretAsync(AuthorizationHandlerContext context,
|
||||
SecretOperationRequirement requirement, Secret resource)
|
||||
|
@ -0,0 +1,45 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||
|
||||
public class MaxProjectsQuery : IMaxProjectsQuery
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
|
||||
public MaxProjectsQuery(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProjectRepository projectRepository)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_projectRepository = projectRepository;
|
||||
}
|
||||
|
||||
public async Task<(short? max, bool? atMax)> GetByOrgIdAsync(Guid organizationId)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var plan = StaticStore.GetSecretsManagerPlan(org.PlanType);
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BadRequestException("Existing plan not found.");
|
||||
}
|
||||
|
||||
if (plan.Type == PlanType.Free)
|
||||
{
|
||||
var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId);
|
||||
return projects >= plan.MaxProjects ? (plan.MaxProjects, true) : (plan.MaxProjects, false);
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
|
||||
using Bit.Commercial.Core.SecretsManager.Commands.Trash;
|
||||
using Bit.Commercial.Core.SecretsManager.Queries;
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
|
||||
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
|
||||
@ -19,6 +20,7 @@ using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
||||
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
|
||||
using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -34,6 +36,7 @@ public static class SecretsManagerCollectionExtensions
|
||||
services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, AccessPolicyAuthorizationHandler>();
|
||||
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
|
||||
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
|
||||
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
|
||||
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
|
||||
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
|
||||
|
@ -329,7 +329,8 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
{
|
||||
Secret = Mapper.Map<Bit.Core.SecretsManager.Entities.Secret>(s),
|
||||
Read = true,
|
||||
Write = false,
|
||||
Write = s.Projects.Any(p =>
|
||||
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write)),
|
||||
}),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
|
||||
};
|
||||
|
@ -47,6 +47,7 @@ public class AccountController : Controller
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly Core.Services.IEventService _eventService;
|
||||
private readonly IDataProtectorTokenFactory<SsoTokenable> _dataProtector;
|
||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||
|
||||
public AccountController(
|
||||
IAuthenticationSchemeProvider schemeProvider,
|
||||
@ -65,7 +66,8 @@ public class AccountController : Controller
|
||||
UserManager<User> userManager,
|
||||
IGlobalSettings globalSettings,
|
||||
Core.Services.IEventService eventService,
|
||||
IDataProtectorTokenFactory<SsoTokenable> dataProtector)
|
||||
IDataProtectorTokenFactory<SsoTokenable> dataProtector,
|
||||
IOrganizationDomainRepository organizationDomainRepository)
|
||||
{
|
||||
_schemeProvider = schemeProvider;
|
||||
_clientStore = clientStore;
|
||||
@ -84,6 +86,7 @@ public class AccountController : Controller
|
||||
_eventService = eventService;
|
||||
_globalSettings = globalSettings;
|
||||
_dataProtector = dataProtector;
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -513,11 +516,21 @@ public class AccountController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
// If the email domain is verified, we can mark the email as verified
|
||||
var emailVerified = false;
|
||||
var emailDomain = CoreHelpers.GetEmailDomain(email);
|
||||
if (!string.IsNullOrWhiteSpace(emailDomain))
|
||||
{
|
||||
var organizationDomain = await _organizationDomainRepository.GetDomainByOrgIdAndDomainNameAsync(orgId, emailDomain);
|
||||
emailVerified = organizationDomain?.VerifiedDate.HasValue ?? false;
|
||||
}
|
||||
|
||||
// Create user record - all existing user flows are handled above
|
||||
var user = new User
|
||||
{
|
||||
Name = name,
|
||||
Email = email,
|
||||
EmailVerified = emailVerified,
|
||||
ApiKey = CoreHelpers.SecureRandomString(30)
|
||||
};
|
||||
await _userService.RegisterUserAsync(user);
|
||||
|
@ -232,6 +232,69 @@ public class SecretAuthorizationHandlerTests
|
||||
Assert.True(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CanReadSecret_AccessToSecretsManagerFalse_DoesNotSucceed(
|
||||
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
|
||||
ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
var requirement = SecretOperations.Read;
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)
|
||||
.Returns(false);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, secret);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CanReadSecret_NullResource_DoesNotSucceed(
|
||||
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
Guid userId)
|
||||
{
|
||||
var requirement = SecretOperations.Read;
|
||||
SetupPermission(sutProvider, PermissionType.RunAsAdmin, secret.OrganizationId, userId);
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, null);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.False(authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PermissionType.RunAsAdmin, true, true, true)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true)]
|
||||
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]
|
||||
[BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, false, false)]
|
||||
[BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, true, false)]
|
||||
[BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, false, true)]
|
||||
[BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, true, true)]
|
||||
public async Task CanReadSecret_AccessCheck(PermissionType permissionType, bool read, bool write,
|
||||
bool expected,
|
||||
SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
Guid userId)
|
||||
{
|
||||
var requirement = SecretOperations.Read;
|
||||
SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);
|
||||
sutProvider.GetDependency<ISecretRepository>()
|
||||
.AccessToSecretAsync(secret.Id, userId, Arg.Any<AccessClientType>())
|
||||
.Returns((read, write));
|
||||
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
|
||||
claimsPrincipal, secret);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(authzContext);
|
||||
|
||||
Assert.Equal(expected, authzContext.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task CanUpdateSecret_AccessToSecretsManagerFalse_DoesNotSucceed(
|
||||
|
@ -0,0 +1,97 @@
|
||||
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class MaxProjectsQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId));
|
||||
|
||||
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()
|
||||
.GetProjectCountByOrganizationIdAsync(organizationId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
||||
[BitAutoData(PlanType.TeamsMonthly2019)]
|
||||
[BitAutoData(PlanType.TeamsAnnually2019)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly2019)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually2019)]
|
||||
[BitAutoData(PlanType.Custom)]
|
||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||
public async Task GetByOrgIdAsync_SmPlanIsNull_ThrowsBadRequest(PlanType planType,
|
||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(
|
||||
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id));
|
||||
|
||||
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()
|
||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.TeamsMonthly)]
|
||||
[BitAutoData(PlanType.TeamsAnnually)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||
public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
|
||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id);
|
||||
|
||||
Assert.Null(limit);
|
||||
Assert.Null(overLimit);
|
||||
|
||||
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()
|
||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.Free, 0, false)]
|
||||
[BitAutoData(PlanType.Free, 1, false)]
|
||||
[BitAutoData(PlanType.Free, 2, false)]
|
||||
[BitAutoData(PlanType.Free, 3, true)]
|
||||
[BitAutoData(PlanType.Free, 4, true)]
|
||||
[BitAutoData(PlanType.Free, 40, true)]
|
||||
public async Task GetByOrgIdAsync_SmFreePlan_Success(PlanType planType, int projects, bool shouldBeAtMax,
|
||||
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(projects);
|
||||
|
||||
var (max, atMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id);
|
||||
|
||||
Assert.NotNull(max);
|
||||
Assert.NotNull(atMax);
|
||||
Assert.Equal(3, max.Value);
|
||||
Assert.Equal(shouldBeAtMax, atMax);
|
||||
|
||||
await sutProvider.GetDependency<IProjectRepository>().Received(1)
|
||||
.GetProjectCountByOrganizationIdAsync(organization.Id);
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ public class InfoController : Controller
|
||||
[HttpGet("~/ip")]
|
||||
public JsonResult Ip()
|
||||
{
|
||||
var headerSet = new HashSet<string> { "x-forwarded-for", "cf-connecting-ip", "client-ip" };
|
||||
var headerSet = new HashSet<string> { "x-forwarded-for", "x-connecting-ip", "cf-connecting-ip", "client-ip", "true-client-ip" };
|
||||
var headers = HttpContext.Request?.Headers
|
||||
.Where(h => headerSet.Contains(h.Key.ToLower()))
|
||||
.ToDictionary(h => h.Key);
|
||||
|
@ -447,8 +447,8 @@ public class OrganizationUsersController : Controller
|
||||
if (additionalSmSeatsRequired > 0)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true);
|
||||
update.AdjustSeats(additionalSmSeatsRequired);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true)
|
||||
.AdjustSeats(additionalSmSeatsRequired);
|
||||
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
|
||||
}
|
||||
|
||||
|
@ -14,11 +14,12 @@ public class SecretsManagerSubscriptionUpdateRequestModel
|
||||
|
||||
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization)
|
||||
{
|
||||
var orgUpdate = new SecretsManagerSubscriptionUpdate(
|
||||
organization,
|
||||
seatAdjustment: SeatAdjustment, maxAutoscaleSeats: MaxAutoscaleSeats,
|
||||
serviceAccountAdjustment: ServiceAccountAdjustment, maxAutoscaleServiceAccounts: MaxAutoscaleServiceAccounts);
|
||||
|
||||
return orgUpdate;
|
||||
return new SecretsManagerSubscriptionUpdate(organization, false)
|
||||
{
|
||||
MaxAutoscaleSmSeats = MaxAutoscaleSeats,
|
||||
MaxAutoscaleSmServiceAccounts = MaxAutoscaleServiceAccounts
|
||||
}
|
||||
.AdjustSeats(SeatAdjustment)
|
||||
.AdjustServiceAccounts(ServiceAccountAdjustment);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
@ -22,6 +23,7 @@ public class ProjectsController : Controller
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly IMaxProjectsQuery _maxProjectsQuery;
|
||||
private readonly ICreateProjectCommand _createProjectCommand;
|
||||
private readonly IUpdateProjectCommand _updateProjectCommand;
|
||||
private readonly IDeleteProjectCommand _deleteProjectCommand;
|
||||
@ -31,6 +33,7 @@ public class ProjectsController : Controller
|
||||
ICurrentContext currentContext,
|
||||
IUserService userService,
|
||||
IProjectRepository projectRepository,
|
||||
IMaxProjectsQuery maxProjectsQuery,
|
||||
ICreateProjectCommand createProjectCommand,
|
||||
IUpdateProjectCommand updateProjectCommand,
|
||||
IDeleteProjectCommand deleteProjectCommand,
|
||||
@ -39,6 +42,7 @@ public class ProjectsController : Controller
|
||||
_currentContext = currentContext;
|
||||
_userService = userService;
|
||||
_projectRepository = projectRepository;
|
||||
_maxProjectsQuery = maxProjectsQuery;
|
||||
_createProjectCommand = createProjectCommand;
|
||||
_updateProjectCommand = updateProjectCommand;
|
||||
_deleteProjectCommand = deleteProjectCommand;
|
||||
@ -74,6 +78,13 @@ public class ProjectsController : Controller
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var (max, atMax) = await _maxProjectsQuery.GetByOrgIdAsync(organizationId);
|
||||
if (atMax != null && atMax.Value)
|
||||
{
|
||||
throw new BadRequestException($"You have reached the maximum number of projects ({max}) for this plan.");
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var result = await _createProjectCommand.CreateAsync(project, userId, _currentContext.ClientType);
|
||||
|
||||
|
@ -207,4 +207,45 @@ public class SecretsController : Controller
|
||||
var responses = results.Select(r => new BulkDeleteResponseModel(r.Secret.Id, r.Error));
|
||||
return new ListResponseModel<BulkDeleteResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpPost("secrets/get-by-ids")]
|
||||
public async Task<ListResponseModel<BaseSecretResponseModel>> GetSecretsByIdsAsync(
|
||||
[FromBody] GetSecretsRequestModel request)
|
||||
{
|
||||
var secrets = (await _secretRepository.GetManyByIds(request.Ids)).ToList();
|
||||
if (!secrets.Any() || secrets.Count != request.Ids.Count())
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Ensure all secrets belong to the same organization.
|
||||
var organizationId = secrets.First().OrganizationId;
|
||||
if (secrets.Any(secret => secret.OrganizationId != organizationId) ||
|
||||
!_currentContext.AccessSecretsManager(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
|
||||
foreach (var secret in secrets)
|
||||
{
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.Read);
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentContext.ClientType == ClientType.ServiceAccount)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, EventType.Secret_Retrieved);
|
||||
await _referenceEventService.RaiseEventAsync(
|
||||
new ReferenceEvent(ReferenceEventType.SmServiceAccountAccessedSecret, org, _currentContext));
|
||||
}
|
||||
|
||||
var responses = secrets.Select(s => new BaseSecretResponseModel(s));
|
||||
return new ListResponseModel<BaseSecretResponseModel>(responses);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
@ -125,8 +126,9 @@ public class ServiceAccountsController : Controller
|
||||
if (newServiceAccountSlotsRequired > 0)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
await _updateSecretsManagerSubscriptionCommand.AdjustServiceAccountsAsync(org,
|
||||
newServiceAccountSlotsRequired);
|
||||
var update = new SecretsManagerSubscriptionUpdate(org, true)
|
||||
.AdjustServiceAccounts(newServiceAccountSlotsRequired);
|
||||
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
|
@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Request;
|
||||
|
||||
public class GetSecretsRequestModel
|
||||
{
|
||||
[Required]
|
||||
public IEnumerable<Guid> Ids { get; set; }
|
||||
}
|
@ -13,7 +13,7 @@ public class SecretCreateRequestModel : IValidatableObject
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(5000)]
|
||||
[EncryptedStringLength(35000)]
|
||||
public string Value { get; set; }
|
||||
|
||||
[Required]
|
||||
|
@ -13,7 +13,7 @@ public class SecretUpdateRequestModel : IValidatableObject
|
||||
|
||||
[Required]
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(5000)]
|
||||
[EncryptedStringLength(35000)]
|
||||
public string Value { get; set; }
|
||||
|
||||
[Required]
|
||||
|
@ -0,0 +1,66 @@
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Response;
|
||||
|
||||
public class BaseSecretResponseModel : ResponseModel
|
||||
{
|
||||
private const string _objectName = "baseSecret";
|
||||
|
||||
public BaseSecretResponseModel(Secret secret, string objectName = _objectName) : base(objectName)
|
||||
{
|
||||
if (secret == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(secret));
|
||||
}
|
||||
|
||||
Id = secret.Id;
|
||||
OrganizationId = secret.OrganizationId;
|
||||
Key = secret.Key;
|
||||
Value = secret.Value;
|
||||
Note = secret.Note;
|
||||
CreationDate = secret.CreationDate;
|
||||
RevisionDate = secret.RevisionDate;
|
||||
Projects = secret.Projects?.Select(p => new SecretResponseInnerProject(p));
|
||||
}
|
||||
|
||||
public BaseSecretResponseModel(string objectName = _objectName) : base(objectName)
|
||||
{
|
||||
}
|
||||
|
||||
public BaseSecretResponseModel() : base(_objectName)
|
||||
{
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid OrganizationId { get; set; }
|
||||
|
||||
public string Key { get; set; }
|
||||
|
||||
public string Value { get; set; }
|
||||
|
||||
public string Note { get; set; }
|
||||
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
public DateTime RevisionDate { get; set; }
|
||||
|
||||
public IEnumerable<SecretResponseInnerProject> Projects { get; set; }
|
||||
|
||||
public class SecretResponseInnerProject
|
||||
{
|
||||
public SecretResponseInnerProject(Project project)
|
||||
{
|
||||
Id = project.Id;
|
||||
Name = project.Name;
|
||||
}
|
||||
|
||||
public SecretResponseInnerProject()
|
||||
{
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
@ -1,28 +1,13 @@
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Response;
|
||||
|
||||
public class SecretResponseModel : ResponseModel
|
||||
public class SecretResponseModel : BaseSecretResponseModel
|
||||
{
|
||||
private const string _objectName = "secret";
|
||||
|
||||
public SecretResponseModel(Secret secret, bool read, bool write) : base(_objectName)
|
||||
public SecretResponseModel(Secret secret, bool read, bool write) : base(secret, _objectName)
|
||||
{
|
||||
if (secret == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(secret));
|
||||
}
|
||||
|
||||
Id = secret.Id;
|
||||
OrganizationId = secret.OrganizationId;
|
||||
Key = secret.Key;
|
||||
Value = secret.Value;
|
||||
Note = secret.Note;
|
||||
CreationDate = secret.CreationDate;
|
||||
RevisionDate = secret.RevisionDate;
|
||||
Projects = secret.Projects?.Select(p => new SecretResponseInnerProject(p));
|
||||
|
||||
Read = read;
|
||||
Write = write;
|
||||
}
|
||||
@ -31,39 +16,7 @@ public class SecretResponseModel : ResponseModel
|
||||
{
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid OrganizationId { get; set; }
|
||||
|
||||
public string Key { get; set; }
|
||||
|
||||
public string Value { get; set; }
|
||||
|
||||
public string Note { get; set; }
|
||||
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
public DateTime RevisionDate { get; set; }
|
||||
|
||||
public IEnumerable<SecretResponseInnerProject> Projects { get; set; }
|
||||
|
||||
public bool Read { get; set; }
|
||||
|
||||
public bool Write { get; set; }
|
||||
|
||||
public class SecretResponseInnerProject
|
||||
{
|
||||
public SecretResponseInnerProject(Project project)
|
||||
{
|
||||
Id = project.Id;
|
||||
Name = project.Name;
|
||||
}
|
||||
|
||||
public SecretResponseInnerProject()
|
||||
{
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@
|
||||
"IpRateLimitOptions": {
|
||||
"EnableEndpointRateLimiting": true,
|
||||
"StackBlockedRequests": false,
|
||||
"RealIpHeader": "CF-Connecting-IP",
|
||||
"RealIpHeader": "X-Connecting-IP",
|
||||
"ClientIdHeader": "X-ClientId",
|
||||
"HttpStatusCode": 429,
|
||||
"IpWhitelist": [],
|
||||
|
@ -10,4 +10,5 @@ public static class HandledStripeWebhook
|
||||
public const string PaymentSucceeded = "invoice.payment_succeeded";
|
||||
public const string PaymentFailed = "invoice.payment_failed";
|
||||
public const string InvoiceCreated = "invoice.created";
|
||||
public const string PaymentMethodAttached = "payment_method.attached";
|
||||
}
|
||||
|
10
src/Billing/Constants/StripeInvoiceStatus.cs
Normal file
10
src/Billing/Constants/StripeInvoiceStatus.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Bit.Billing.Constants;
|
||||
|
||||
public static class StripeInvoiceStatus
|
||||
{
|
||||
public const string Draft = "draft";
|
||||
public const string Open = "open";
|
||||
public const string Paid = "paid";
|
||||
public const string Void = "void";
|
||||
public const string Uncollectible = "uncollectible";
|
||||
}
|
13
src/Billing/Constants/StripeSubscriptionStatus.cs
Normal file
13
src/Billing/Constants/StripeSubscriptionStatus.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Bit.Billing.Constants;
|
||||
|
||||
public static class StripeSubscriptionStatus
|
||||
{
|
||||
public const string Trialing = "trialing";
|
||||
public const string Active = "active";
|
||||
public const string Incomplete = "incomplete";
|
||||
public const string IncompleteExpired = "incomplete_expired";
|
||||
public const string PastDue = "past_due";
|
||||
public const string Canceled = "canceled";
|
||||
public const string Unpaid = "unpaid";
|
||||
public const string Paused = "paused";
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using Bit.Billing.Models;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
@ -77,7 +78,9 @@ public class FreshdeskController : Controller
|
||||
|
||||
foreach (var org in orgs)
|
||||
{
|
||||
var orgNote = $"{org.Name} ({org.Seats.GetValueOrDefault()}): " +
|
||||
// Prevent org names from injecting any additional HTML
|
||||
var orgName = HttpUtility.HtmlEncode(org.Name);
|
||||
var orgNote = $"{orgName} ({org.Seats.GetValueOrDefault()}): " +
|
||||
$"{_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}";
|
||||
note += $"<li>Org, {orgNote}</li>";
|
||||
if (!customFields.Any(kvp => kvp.Key == _billingSettings.FreshDesk.OrgFieldName))
|
||||
|
@ -10,12 +10,19 @@ using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Braintree;
|
||||
using Braintree.Exceptions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Stripe;
|
||||
using Customer = Stripe.Customer;
|
||||
using Event = Stripe.Event;
|
||||
using PaymentMethod = Stripe.PaymentMethod;
|
||||
using Subscription = Stripe.Subscription;
|
||||
using TaxRate = Bit.Core.Entities.TaxRate;
|
||||
using Transaction = Bit.Core.Entities.Transaction;
|
||||
using TransactionType = Bit.Core.Enums.TransactionType;
|
||||
|
||||
namespace Bit.Billing.Controllers;
|
||||
|
||||
@ -132,10 +139,10 @@ public class StripeController : Controller
|
||||
var ids = GetIdsFromMetaData(subscription.Metadata);
|
||||
var organizationId = ids.Item1 ?? Guid.Empty;
|
||||
var userId = ids.Item2 ?? Guid.Empty;
|
||||
var subCanceled = subDeleted && subscription.Status == "canceled";
|
||||
var subUnpaid = subUpdated && subscription.Status == "unpaid";
|
||||
var subActive = subUpdated && subscription.Status == "active";
|
||||
var subIncompleteExpired = subUpdated && subscription.Status == "incomplete_expired";
|
||||
var subCanceled = subDeleted && subscription.Status == StripeSubscriptionStatus.Canceled;
|
||||
var subUnpaid = subUpdated && subscription.Status == StripeSubscriptionStatus.Unpaid;
|
||||
var subActive = subUpdated && subscription.Status == StripeSubscriptionStatus.Active;
|
||||
var subIncompleteExpired = subUpdated && subscription.Status == StripeSubscriptionStatus.IncompleteExpired;
|
||||
|
||||
if (subCanceled || subUnpaid || subIncompleteExpired)
|
||||
{
|
||||
@ -147,7 +154,17 @@ public class StripeController : Controller
|
||||
// user
|
||||
else if (userId != Guid.Empty)
|
||||
{
|
||||
await _userService.DisablePremiumAsync(userId, subscription.CurrentPeriodEnd);
|
||||
if (subUnpaid && subscription.Items.Any(i => i.Price.Id is PremiumPlanId or PremiumPlanIdAppStore))
|
||||
{
|
||||
await CancelSubscription(subscription.Id);
|
||||
await VoidOpenInvoices(subscription.Id);
|
||||
}
|
||||
|
||||
var user = await _userService.GetUserByIdAsync(userId);
|
||||
if (user.Premium)
|
||||
{
|
||||
await _userService.DisablePremiumAsync(userId, subscription.CurrentPeriodEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -265,7 +282,7 @@ public class StripeController : Controller
|
||||
});
|
||||
foreach (var sub in subscriptions)
|
||||
{
|
||||
if (sub.Status != "canceled" && sub.Status != "incomplete_expired")
|
||||
if (sub.Status != StripeSubscriptionStatus.Canceled && sub.Status != StripeSubscriptionStatus.IncompleteExpired)
|
||||
{
|
||||
ids = GetIdsFromMetaData(sub.Metadata);
|
||||
if (ids.Item1.HasValue || ids.Item2.HasValue)
|
||||
@ -415,7 +432,7 @@ public class StripeController : Controller
|
||||
{
|
||||
var subscriptionService = new SubscriptionService();
|
||||
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
|
||||
if (subscription?.Status == "active")
|
||||
if (subscription?.Status == StripeSubscriptionStatus.Active)
|
||||
{
|
||||
if (DateTime.UtcNow - invoice.Created < TimeSpan.FromMinutes(1))
|
||||
{
|
||||
@ -472,6 +489,11 @@ public class StripeController : Controller
|
||||
await AttemptToPayInvoiceAsync(invoice);
|
||||
}
|
||||
}
|
||||
else if (parsedEvent.Type.Equals(HandledStripeWebhook.PaymentMethodAttached))
|
||||
{
|
||||
var paymentMethod = await GetPaymentMethodAsync(parsedEvent);
|
||||
await HandlePaymentMethodAttachedAsync(paymentMethod);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Unsupported event received. " + parsedEvent.Type);
|
||||
@ -490,60 +512,163 @@ public class StripeController : Controller
|
||||
/// <exception cref="Exception"></exception>
|
||||
private async Task<bool> ValidateCloudRegionAsync(Event parsedEvent)
|
||||
{
|
||||
string customerRegion;
|
||||
|
||||
var serverRegion = _globalSettings.BaseServiceUri.CloudRegion;
|
||||
var eventType = parsedEvent.Type;
|
||||
var expandOptions = new List<string> { "customer" };
|
||||
|
||||
switch (eventType)
|
||||
try
|
||||
{
|
||||
case HandledStripeWebhook.SubscriptionDeleted:
|
||||
case HandledStripeWebhook.SubscriptionUpdated:
|
||||
{
|
||||
var subscription = await GetSubscriptionAsync(parsedEvent, true, new List<string> { "customer" });
|
||||
customerRegion = GetCustomerRegionFromMetadata(subscription.Customer.Metadata);
|
||||
Dictionary<string, string> customerMetadata;
|
||||
switch (eventType)
|
||||
{
|
||||
case HandledStripeWebhook.SubscriptionDeleted:
|
||||
case HandledStripeWebhook.SubscriptionUpdated:
|
||||
customerMetadata = (await GetSubscriptionAsync(parsedEvent, true, expandOptions))?.Customer
|
||||
?.Metadata;
|
||||
break;
|
||||
}
|
||||
case HandledStripeWebhook.ChargeSucceeded:
|
||||
case HandledStripeWebhook.ChargeRefunded:
|
||||
{
|
||||
var charge = await GetChargeAsync(parsedEvent, true, new List<string> { "customer" });
|
||||
customerRegion = GetCustomerRegionFromMetadata(charge.Customer.Metadata);
|
||||
case HandledStripeWebhook.ChargeSucceeded:
|
||||
case HandledStripeWebhook.ChargeRefunded:
|
||||
customerMetadata = (await GetChargeAsync(parsedEvent, true, expandOptions))?.Customer?.Metadata;
|
||||
break;
|
||||
}
|
||||
case HandledStripeWebhook.UpcomingInvoice:
|
||||
var eventInvoice = await GetInvoiceAsync(parsedEvent);
|
||||
var customer = await GetCustomerAsync(eventInvoice.CustomerId);
|
||||
customerRegion = GetCustomerRegionFromMetadata(customer.Metadata);
|
||||
break;
|
||||
case HandledStripeWebhook.PaymentSucceeded:
|
||||
case HandledStripeWebhook.PaymentFailed:
|
||||
case HandledStripeWebhook.InvoiceCreated:
|
||||
{
|
||||
var invoice = await GetInvoiceAsync(parsedEvent, true, new List<string> { "customer" });
|
||||
customerRegion = GetCustomerRegionFromMetadata(invoice.Customer.Metadata);
|
||||
case HandledStripeWebhook.UpcomingInvoice:
|
||||
customerMetadata = (await GetInvoiceAsync(parsedEvent))?.Customer?.Metadata;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// For all Stripe events that we're not listening to, just return 200
|
||||
return false;
|
||||
}
|
||||
}
|
||||
case HandledStripeWebhook.PaymentSucceeded:
|
||||
case HandledStripeWebhook.PaymentFailed:
|
||||
case HandledStripeWebhook.InvoiceCreated:
|
||||
customerMetadata = (await GetInvoiceAsync(parsedEvent, true, expandOptions))?.Customer?.Metadata;
|
||||
break;
|
||||
case HandledStripeWebhook.PaymentMethodAttached:
|
||||
customerMetadata = (await GetPaymentMethodAsync(parsedEvent, true, expandOptions))
|
||||
?.Customer
|
||||
?.Metadata;
|
||||
break;
|
||||
default:
|
||||
customerMetadata = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return customerRegion == serverRegion;
|
||||
if (customerMetadata is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var customerRegion = GetCustomerRegionFromMetadata(customerMetadata);
|
||||
|
||||
return customerRegion == serverRegion;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Encountered unexpected error while validating cloud region");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the region from the customer metadata. If no region is present, defaults to "US"
|
||||
/// Gets the customer's region from the metadata.
|
||||
/// </summary>
|
||||
/// <param name="customerMetadata"></param>
|
||||
/// <returns></returns>
|
||||
private static string GetCustomerRegionFromMetadata(Dictionary<string, string> customerMetadata)
|
||||
/// <param name="customerMetadata">The metadata of the customer.</param>
|
||||
/// <returns>The region of the customer. If the region is not specified, it returns "US", if metadata is null,
|
||||
/// it returns null. It is case insensitive.</returns>
|
||||
private static string GetCustomerRegionFromMetadata(IDictionary<string, string> customerMetadata)
|
||||
{
|
||||
return customerMetadata.TryGetValue("region", out var value)
|
||||
? value
|
||||
: "US";
|
||||
const string defaultRegion = "US";
|
||||
|
||||
if (customerMetadata is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (customerMetadata.TryGetValue("region", out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
var miscasedRegionKey = customerMetadata.Keys
|
||||
.FirstOrDefault(key =>
|
||||
key.Equals("region", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (miscasedRegionKey is null)
|
||||
{
|
||||
return defaultRegion;
|
||||
}
|
||||
|
||||
_ = customerMetadata.TryGetValue(miscasedRegionKey, out var regionValue);
|
||||
|
||||
return !string.IsNullOrWhiteSpace(regionValue)
|
||||
? regionValue
|
||||
: defaultRegion;
|
||||
}
|
||||
|
||||
private async Task HandlePaymentMethodAttachedAsync(PaymentMethod paymentMethod)
|
||||
{
|
||||
if (paymentMethod is null)
|
||||
{
|
||||
_logger.LogWarning("Attempted to handle the event payment_method.attached but paymentMethod was null");
|
||||
return;
|
||||
}
|
||||
|
||||
var subscriptionService = new SubscriptionService();
|
||||
var subscriptionListOptions = new SubscriptionListOptions
|
||||
{
|
||||
Customer = paymentMethod.CustomerId,
|
||||
Status = StripeSubscriptionStatus.Unpaid,
|
||||
Expand = new List<string> { "data.latest_invoice" }
|
||||
};
|
||||
|
||||
StripeList<Subscription> unpaidSubscriptions;
|
||||
try
|
||||
{
|
||||
unpaidSubscriptions = await subscriptionService.ListAsync(subscriptionListOptions);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e,
|
||||
"Attempted to get unpaid invoices for customer {CustomerId} but encountered an error while calling Stripe",
|
||||
paymentMethod.CustomerId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var unpaidSubscription in unpaidSubscriptions)
|
||||
{
|
||||
await AttemptToPayOpenSubscriptionAsync(unpaidSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AttemptToPayOpenSubscriptionAsync(Subscription unpaidSubscription)
|
||||
{
|
||||
var latestInvoice = unpaidSubscription.LatestInvoice;
|
||||
|
||||
if (unpaidSubscription.LatestInvoice is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Attempted to pay unpaid subscription {SubscriptionId} but latest invoice didn't exist",
|
||||
unpaidSubscription.Id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (latestInvoice.Status != StripeInvoiceStatus.Open)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Attempted to pay unpaid subscription {SubscriptionId} but latest invoice wasn't \"open\"",
|
||||
unpaidSubscription.Id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await AttemptToPayInvoiceAsync(latestInvoice, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e,
|
||||
"Attempted to pay open invoice {InvoiceId} on unpaid subscription {SubscriptionId} but encountered an error",
|
||||
latestInvoice.Id, unpaidSubscription.Id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private Tuple<Guid?, Guid?> GetIdsFromMetaData(IDictionary<string, string> metaData)
|
||||
@ -598,7 +723,7 @@ public class StripeController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> AttemptToPayInvoiceAsync(Invoice invoice)
|
||||
private async Task<bool> AttemptToPayInvoiceAsync(Invoice invoice, bool attemptToPayWithStripe = false)
|
||||
{
|
||||
var customerService = new CustomerService();
|
||||
var customer = await customerService.GetAsync(invoice.CustomerId);
|
||||
@ -606,10 +731,17 @@ public class StripeController : Controller
|
||||
{
|
||||
return await AttemptToPayInvoiceWithAppleReceiptAsync(invoice, customer);
|
||||
}
|
||||
else if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false)
|
||||
|
||||
if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false)
|
||||
{
|
||||
return await AttemptToPayInvoiceWithBraintreeAsync(invoice, customer);
|
||||
}
|
||||
|
||||
if (attemptToPayWithStripe)
|
||||
{
|
||||
return await AttemptToPayInvoiceWithStripeAsync(invoice);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -708,8 +840,11 @@ public class StripeController : Controller
|
||||
|
||||
private async Task<bool> AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice, Customer customer)
|
||||
{
|
||||
_logger.LogDebug("Attempting to pay invoice with Braintree");
|
||||
if (!customer?.Metadata?.ContainsKey("btCustomerId") ?? true)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Attempted to pay invoice with Braintree but btCustomerId wasn't on Stripe customer metadata");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -718,6 +853,8 @@ public class StripeController : Controller
|
||||
var ids = GetIdsFromMetaData(subscription?.Metadata);
|
||||
if (!ids.Item1.HasValue && !ids.Item2.HasValue)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Attempted to pay invoice with Braintree but Stripe subscription metadata didn't contain either a organizationId or userId");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -740,25 +877,36 @@ public class StripeController : Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
var transactionResult = await _btGateway.Transaction.SaleAsync(
|
||||
new Braintree.TransactionRequest
|
||||
{
|
||||
Amount = btInvoiceAmount,
|
||||
CustomerId = customer.Metadata["btCustomerId"],
|
||||
Options = new Braintree.TransactionOptionsRequest
|
||||
Result<Braintree.Transaction> transactionResult;
|
||||
try
|
||||
{
|
||||
transactionResult = await _btGateway.Transaction.SaleAsync(
|
||||
new Braintree.TransactionRequest
|
||||
{
|
||||
SubmitForSettlement = true,
|
||||
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
||||
Amount = btInvoiceAmount,
|
||||
CustomerId = customer.Metadata["btCustomerId"],
|
||||
Options = new Braintree.TransactionOptionsRequest
|
||||
{
|
||||
CustomField = $"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}"
|
||||
SubmitForSettlement = true,
|
||||
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
||||
{
|
||||
CustomField =
|
||||
$"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}"
|
||||
}
|
||||
},
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[btObjIdField] = btObjId.ToString(),
|
||||
["region"] = _globalSettings.BaseServiceUri.CloudRegion
|
||||
}
|
||||
},
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[btObjIdField] = btObjId.ToString(),
|
||||
["region"] = _globalSettings.BaseServiceUri.CloudRegion
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
catch (NotFoundException e)
|
||||
{
|
||||
_logger.LogError(e,
|
||||
"Attempted to make a payment with Braintree, but customer did not exist for the given btCustomerId present on the Stripe metadata");
|
||||
throw;
|
||||
}
|
||||
|
||||
if (!transactionResult.IsSuccess())
|
||||
{
|
||||
@ -802,6 +950,25 @@ public class StripeController : Controller
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> AttemptToPayInvoiceWithStripeAsync(Invoice invoice)
|
||||
{
|
||||
try
|
||||
{
|
||||
var invoiceService = new InvoiceService();
|
||||
await invoiceService.PayAsync(invoice.Id);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
e,
|
||||
"Exception occurred while trying to pay Stripe invoice with Id: {InvoiceId}",
|
||||
invoice.Id);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private bool UnpaidAutoChargeInvoiceForSubscriptionCycle(Invoice invoice)
|
||||
{
|
||||
return invoice.AmountDue > 0 && !invoice.Paid && invoice.CollectionMethod == "charge_automatically" &&
|
||||
@ -886,6 +1053,31 @@ public class StripeController : Controller
|
||||
return customer;
|
||||
}
|
||||
|
||||
private async Task<PaymentMethod> GetPaymentMethodAsync(Event parsedEvent, bool fresh = false,
|
||||
List<string> expandOptions = null)
|
||||
{
|
||||
if (parsedEvent.Data.Object is not PaymentMethod eventPaymentMethod)
|
||||
{
|
||||
throw new Exception("Invoice is null (from parsed event). " + parsedEvent.Id);
|
||||
}
|
||||
|
||||
if (!fresh)
|
||||
{
|
||||
return eventPaymentMethod;
|
||||
}
|
||||
|
||||
var paymentMethodService = new PaymentMethodService();
|
||||
var paymentMethodGetOptions = new PaymentMethodGetOptions { Expand = expandOptions };
|
||||
var paymentMethod = await paymentMethodService.GetAsync(eventPaymentMethod.Id, paymentMethodGetOptions);
|
||||
|
||||
if (paymentMethod == null)
|
||||
{
|
||||
throw new Exception($"Payment method is null. {eventPaymentMethod.Id}");
|
||||
}
|
||||
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
private async Task<Subscription> VerifyCorrectTaxRateForCharge(Invoice invoice, Subscription subscription)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(invoice?.CustomerAddress?.Country) && !string.IsNullOrWhiteSpace(invoice?.CustomerAddress?.PostalCode))
|
||||
@ -922,12 +1114,8 @@ public class StripeController : Controller
|
||||
var subscriptionService = new SubscriptionService();
|
||||
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
|
||||
// attempt count 4 = 11 days after initial failure
|
||||
if (invoice.AttemptCount > 3 && subscription.Items.Any(i => i.Price.Id == PremiumPlanId || i.Price.Id == PremiumPlanIdAppStore))
|
||||
{
|
||||
await CancelSubscription(invoice.SubscriptionId);
|
||||
await VoidOpenInvoices(invoice.SubscriptionId);
|
||||
}
|
||||
else
|
||||
if (invoice.AttemptCount <= 3 ||
|
||||
!subscription.Items.Any(i => i.Price.Id is PremiumPlanId or PremiumPlanIdAppStore))
|
||||
{
|
||||
await AttemptToPayInvoiceAsync(invoice);
|
||||
}
|
||||
@ -944,7 +1132,7 @@ public class StripeController : Controller
|
||||
var invoiceService = new InvoiceService();
|
||||
var options = new InvoiceListOptions
|
||||
{
|
||||
Status = "open",
|
||||
Status = StripeInvoiceStatus.Open,
|
||||
Subscription = subscriptionId
|
||||
};
|
||||
var invoices = invoiceService.List(options);
|
||||
|
@ -37,6 +37,7 @@ public static class FeatureFlagKeys
|
||||
public const string DisplayLowKdfIterationWarning = "display-kdf-iteration-warning";
|
||||
public const string TrustedDeviceEncryption = "trusted-device-encryption";
|
||||
public const string SecretsManagerBilling = "sm-ga-billing";
|
||||
public const string AutofillV2 = "autofill-v2";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -26,8 +26,8 @@ public class CurrentContext : ICurrentContext
|
||||
public virtual string DeviceIdentifier { get; set; }
|
||||
public virtual DeviceType? DeviceType { get; set; }
|
||||
public virtual string IpAddress { get; set; }
|
||||
public virtual List<CurrentContentOrganization> Organizations { get; set; }
|
||||
public virtual List<CurrentContentProvider> Providers { get; set; }
|
||||
public virtual List<CurrentContextOrganization> Organizations { get; set; }
|
||||
public virtual List<CurrentContextProvider> Providers { get; set; }
|
||||
public virtual Guid? InstallationId { get; set; }
|
||||
public virtual Guid? OrganizationId { get; set; }
|
||||
public virtual bool CloudflareWorkerProxied { get; set; }
|
||||
@ -166,17 +166,17 @@ public class CurrentContext : ICurrentContext
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
private List<CurrentContentOrganization> GetOrganizations(Dictionary<string, IEnumerable<Claim>> claimsDict, bool orgApi)
|
||||
private List<CurrentContextOrganization> GetOrganizations(Dictionary<string, IEnumerable<Claim>> claimsDict, bool orgApi)
|
||||
{
|
||||
var accessSecretsManager = claimsDict.ContainsKey(Claims.SecretsManagerAccess)
|
||||
? claimsDict[Claims.SecretsManagerAccess].ToDictionary(s => s.Value, _ => true)
|
||||
: new Dictionary<string, bool>();
|
||||
|
||||
var organizations = new List<CurrentContentOrganization>();
|
||||
var organizations = new List<CurrentContextOrganization>();
|
||||
if (claimsDict.ContainsKey(Claims.OrganizationOwner))
|
||||
{
|
||||
organizations.AddRange(claimsDict[Claims.OrganizationOwner].Select(c =>
|
||||
new CurrentContentOrganization
|
||||
new CurrentContextOrganization
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
Type = OrganizationUserType.Owner,
|
||||
@ -185,7 +185,7 @@ public class CurrentContext : ICurrentContext
|
||||
}
|
||||
else if (orgApi && OrganizationId.HasValue)
|
||||
{
|
||||
organizations.Add(new CurrentContentOrganization
|
||||
organizations.Add(new CurrentContextOrganization
|
||||
{
|
||||
Id = OrganizationId.Value,
|
||||
Type = OrganizationUserType.Owner,
|
||||
@ -195,7 +195,7 @@ public class CurrentContext : ICurrentContext
|
||||
if (claimsDict.ContainsKey(Claims.OrganizationAdmin))
|
||||
{
|
||||
organizations.AddRange(claimsDict[Claims.OrganizationAdmin].Select(c =>
|
||||
new CurrentContentOrganization
|
||||
new CurrentContextOrganization
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
Type = OrganizationUserType.Admin,
|
||||
@ -206,7 +206,7 @@ public class CurrentContext : ICurrentContext
|
||||
if (claimsDict.ContainsKey(Claims.OrganizationUser))
|
||||
{
|
||||
organizations.AddRange(claimsDict[Claims.OrganizationUser].Select(c =>
|
||||
new CurrentContentOrganization
|
||||
new CurrentContextOrganization
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
Type = OrganizationUserType.User,
|
||||
@ -217,7 +217,7 @@ public class CurrentContext : ICurrentContext
|
||||
if (claimsDict.ContainsKey(Claims.OrganizationManager))
|
||||
{
|
||||
organizations.AddRange(claimsDict[Claims.OrganizationManager].Select(c =>
|
||||
new CurrentContentOrganization
|
||||
new CurrentContextOrganization
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
Type = OrganizationUserType.Manager,
|
||||
@ -228,7 +228,7 @@ public class CurrentContext : ICurrentContext
|
||||
if (claimsDict.ContainsKey(Claims.OrganizationCustom))
|
||||
{
|
||||
organizations.AddRange(claimsDict[Claims.OrganizationCustom].Select(c =>
|
||||
new CurrentContentOrganization
|
||||
new CurrentContextOrganization
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
Type = OrganizationUserType.Custom,
|
||||
@ -240,13 +240,13 @@ public class CurrentContext : ICurrentContext
|
||||
return organizations;
|
||||
}
|
||||
|
||||
private List<CurrentContentProvider> GetProviders(Dictionary<string, IEnumerable<Claim>> claimsDict)
|
||||
private List<CurrentContextProvider> GetProviders(Dictionary<string, IEnumerable<Claim>> claimsDict)
|
||||
{
|
||||
var providers = new List<CurrentContentProvider>();
|
||||
var providers = new List<CurrentContextProvider>();
|
||||
if (claimsDict.ContainsKey(Claims.ProviderAdmin))
|
||||
{
|
||||
providers.AddRange(claimsDict[Claims.ProviderAdmin].Select(c =>
|
||||
new CurrentContentProvider
|
||||
new CurrentContextProvider
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
Type = ProviderUserType.ProviderAdmin
|
||||
@ -256,7 +256,7 @@ public class CurrentContext : ICurrentContext
|
||||
if (claimsDict.ContainsKey(Claims.ProviderServiceUser))
|
||||
{
|
||||
providers.AddRange(claimsDict[Claims.ProviderServiceUser].Select(c =>
|
||||
new CurrentContentProvider
|
||||
new CurrentContextProvider
|
||||
{
|
||||
Id = new Guid(c.Value),
|
||||
Type = ProviderUserType.ServiceUser
|
||||
@ -483,26 +483,26 @@ public class CurrentContext : ICurrentContext
|
||||
return Organizations?.Any(o => o.Id == orgId && o.AccessSecretsManager) ?? false;
|
||||
}
|
||||
|
||||
public async Task<ICollection<CurrentContentOrganization>> OrganizationMembershipAsync(
|
||||
public async Task<ICollection<CurrentContextOrganization>> OrganizationMembershipAsync(
|
||||
IOrganizationUserRepository organizationUserRepository, Guid userId)
|
||||
{
|
||||
if (Organizations == null)
|
||||
{
|
||||
var userOrgs = await organizationUserRepository.GetManyDetailsByUserAsync(userId);
|
||||
Organizations = userOrgs.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed)
|
||||
.Select(ou => new CurrentContentOrganization(ou)).ToList();
|
||||
.Select(ou => new CurrentContextOrganization(ou)).ToList();
|
||||
}
|
||||
return Organizations;
|
||||
}
|
||||
|
||||
public async Task<ICollection<CurrentContentProvider>> ProviderMembershipAsync(
|
||||
public async Task<ICollection<CurrentContextProvider>> ProviderMembershipAsync(
|
||||
IProviderUserRepository providerUserRepository, Guid userId)
|
||||
{
|
||||
if (Providers == null)
|
||||
{
|
||||
var userProviders = await providerUserRepository.GetManyByUserAsync(userId);
|
||||
Providers = userProviders.Where(ou => ou.Status == ProviderUserStatusType.Confirmed)
|
||||
.Select(ou => new CurrentContentProvider(ou)).ToList();
|
||||
.Select(ou => new CurrentContextProvider(ou)).ToList();
|
||||
}
|
||||
return Providers;
|
||||
}
|
||||
|
@ -5,11 +5,11 @@ using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Context;
|
||||
|
||||
public class CurrentContentOrganization
|
||||
public class CurrentContextOrganization
|
||||
{
|
||||
public CurrentContentOrganization() { }
|
||||
public CurrentContextOrganization() { }
|
||||
|
||||
public CurrentContentOrganization(OrganizationUserOrganizationDetails orgUser)
|
||||
public CurrentContextOrganization(OrganizationUserOrganizationDetails orgUser)
|
||||
{
|
||||
Id = orgUser.OrganizationId;
|
||||
Type = orgUser.Type;
|
@ -5,11 +5,11 @@ using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Context;
|
||||
|
||||
public class CurrentContentProvider
|
||||
public class CurrentContextProvider
|
||||
{
|
||||
public CurrentContentProvider() { }
|
||||
public CurrentContextProvider() { }
|
||||
|
||||
public CurrentContentProvider(ProviderUser providerUser)
|
||||
public CurrentContextProvider(ProviderUser providerUser)
|
||||
{
|
||||
Id = providerUser.ProviderId;
|
||||
Type = providerUser.Type;
|
@ -16,7 +16,7 @@ public interface ICurrentContext
|
||||
string DeviceIdentifier { get; set; }
|
||||
DeviceType? DeviceType { get; set; }
|
||||
string IpAddress { get; set; }
|
||||
List<CurrentContentOrganization> Organizations { get; set; }
|
||||
List<CurrentContextOrganization> Organizations { get; set; }
|
||||
Guid? InstallationId { get; set; }
|
||||
Guid? OrganizationId { get; set; }
|
||||
ClientType ClientType { get; set; }
|
||||
@ -64,10 +64,10 @@ public interface ICurrentContext
|
||||
bool AccessProviderOrganizations(Guid providerId);
|
||||
bool ManageProviderOrganizations(Guid providerId);
|
||||
|
||||
Task<ICollection<CurrentContentOrganization>> OrganizationMembershipAsync(
|
||||
Task<ICollection<CurrentContextOrganization>> OrganizationMembershipAsync(
|
||||
IOrganizationUserRepository organizationUserRepository, Guid userId);
|
||||
|
||||
Task<ICollection<CurrentContentProvider>> ProviderMembershipAsync(
|
||||
Task<ICollection<CurrentContextProvider>> ProviderMembershipAsync(
|
||||
IProviderUserRepository providerUserRepository, Guid userId);
|
||||
|
||||
Task<Guid?> ProviderIdForOrg(Guid orgId);
|
||||
|
@ -6,7 +6,7 @@ namespace Bit.Core.Models.Business;
|
||||
|
||||
public class SecretsManagerSubscriptionUpdate
|
||||
{
|
||||
public Organization Organization { get; set; }
|
||||
public Organization Organization { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The total seats the organization will have after the update, including any base seats included in the plan
|
||||
@ -14,8 +14,7 @@ public class SecretsManagerSubscriptionUpdate
|
||||
public int? SmSeats { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The new autoscale limit for seats, expressed as a total (not an adjustment).
|
||||
/// This may or may not be the same as the current autoscale limit.
|
||||
/// The new autoscale limit for seats after the update
|
||||
/// </summary>
|
||||
public int? MaxAutoscaleSmSeats { get; set; }
|
||||
|
||||
@ -26,8 +25,7 @@ public class SecretsManagerSubscriptionUpdate
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The new autoscale limit for service accounts, expressed as a total (not an adjustment).
|
||||
/// This may or may not be the same as the current autoscale limit.
|
||||
/// The new autoscale limit for service accounts after the update
|
||||
/// </summary>
|
||||
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
||||
|
||||
@ -39,7 +37,7 @@ public class SecretsManagerSubscriptionUpdate
|
||||
/// <summary>
|
||||
/// Whether the subscription update is a result of autoscaling
|
||||
/// </summary>
|
||||
public bool Autoscaling { get; init; }
|
||||
public bool Autoscaling { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The seats the organization will have after the update, excluding the base seats included in the plan
|
||||
@ -57,18 +55,11 @@ public class SecretsManagerSubscriptionUpdate
|
||||
public bool MaxAutoscaleSmServiceAccountsChanged =>
|
||||
MaxAutoscaleSmServiceAccounts != Organization.MaxAutoscaleSmServiceAccounts;
|
||||
public Plan Plan => Utilities.StaticStore.GetSecretsManagerPlan(Organization.PlanType);
|
||||
public bool SmSeatAutoscaleLimitReached => SmSeats.HasValue && MaxAutoscaleSmSeats.HasValue && SmSeats == MaxAutoscaleSmSeats;
|
||||
|
||||
public SecretsManagerSubscriptionUpdate(
|
||||
Organization organization,
|
||||
int seatAdjustment, int? maxAutoscaleSeats,
|
||||
int serviceAccountAdjustment, int? maxAutoscaleServiceAccounts) : this(organization, false)
|
||||
{
|
||||
AdjustSeats(seatAdjustment);
|
||||
AdjustServiceAccounts(serviceAccountAdjustment);
|
||||
|
||||
MaxAutoscaleSmSeats = maxAutoscaleSeats;
|
||||
MaxAutoscaleSmServiceAccounts = maxAutoscaleServiceAccounts;
|
||||
}
|
||||
public bool SmServiceAccountAutoscaleLimitReached => SmServiceAccounts.HasValue &&
|
||||
MaxAutoscaleSmServiceAccounts.HasValue &&
|
||||
SmServiceAccounts == MaxAutoscaleSmServiceAccounts;
|
||||
|
||||
public SecretsManagerSubscriptionUpdate(Organization organization, bool autoscaling)
|
||||
{
|
||||
@ -91,13 +82,15 @@ public class SecretsManagerSubscriptionUpdate
|
||||
Autoscaling = autoscaling;
|
||||
}
|
||||
|
||||
public void AdjustSeats(int adjustment)
|
||||
public SecretsManagerSubscriptionUpdate AdjustSeats(int adjustment)
|
||||
{
|
||||
SmSeats = SmSeats.GetValueOrDefault() + adjustment;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void AdjustServiceAccounts(int adjustment)
|
||||
public SecretsManagerSubscriptionUpdate AdjustServiceAccounts(int adjustment)
|
||||
{
|
||||
SmServiceAccounts = SmServiceAccounts.GetValueOrDefault() + adjustment;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ public class OrganizationAbility
|
||||
UseScim = organization.UseScim;
|
||||
UseResetPassword = organization.UseResetPassword;
|
||||
UseCustomPermissions = organization.UseCustomPermissions;
|
||||
UsePolicies = organization.UsePolicies;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@ -33,4 +34,5 @@ public class OrganizationAbility
|
||||
public bool UseScim { get; set; }
|
||||
public bool UseResetPassword { get; set; }
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
public bool UsePolicies { get; set; }
|
||||
}
|
||||
|
@ -62,6 +62,17 @@ public class AddSecretsManagerSubscriptionCommand : IAddSecretsManagerSubscripti
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (organization.SecretsManagerBeta)
|
||||
{
|
||||
throw new BadRequestException("Organization is enrolled in Secrets Manager Beta. " +
|
||||
"Please contact Customer Success to add Secrets Manager to your subscription.");
|
||||
}
|
||||
|
||||
if (organization.UseSecretsManager)
|
||||
{
|
||||
throw new BadRequestException("Organization already uses Secrets Manager.");
|
||||
}
|
||||
|
||||
var plan = StaticStore.GetSecretsManagerPlan(organization.PlanType);
|
||||
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId) && plan.Product != ProductType.Free)
|
||||
{
|
||||
|
@ -1,11 +1,9 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
|
||||
public interface IUpdateSecretsManagerSubscriptionCommand
|
||||
{
|
||||
Task UpdateSubscriptionAsync(SecretsManagerSubscriptionUpdate update);
|
||||
Task AdjustServiceAccountsAsync(Organization organization, int smServiceAccountsAdjustment);
|
||||
Task ValidateUpdate(SecretsManagerSubscriptionUpdate update);
|
||||
}
|
||||
|
@ -2,13 +2,11 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
||||
@ -51,82 +49,46 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
{
|
||||
await ValidateUpdate(update);
|
||||
|
||||
await FinalizeSubscriptionAdjustmentAsync(update.Organization, update.Plan, update);
|
||||
await FinalizeSubscriptionAdjustmentAsync(update);
|
||||
|
||||
await SendEmailIfAutoscaleLimitReached(update.Organization);
|
||||
}
|
||||
|
||||
public async Task AdjustServiceAccountsAsync(Organization organization, int smServiceAccountsAdjustment)
|
||||
{
|
||||
var update = new SecretsManagerSubscriptionUpdate(
|
||||
organization, seatAdjustment: 0, maxAutoscaleSeats: organization?.MaxAutoscaleSmSeats,
|
||||
serviceAccountAdjustment: smServiceAccountsAdjustment, maxAutoscaleServiceAccounts: organization?.MaxAutoscaleSmServiceAccounts)
|
||||
if (update.SmSeatAutoscaleLimitReached)
|
||||
{
|
||||
Autoscaling = true
|
||||
};
|
||||
await SendSeatLimitEmailAsync(update.Organization);
|
||||
}
|
||||
|
||||
await UpdateSubscriptionAsync(update);
|
||||
if (update.SmServiceAccountAutoscaleLimitReached)
|
||||
{
|
||||
await SendServiceAccountLimitEmailAsync(update.Organization);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FinalizeSubscriptionAdjustmentAsync(Organization organization,
|
||||
Plan plan, SecretsManagerSubscriptionUpdate update)
|
||||
private async Task FinalizeSubscriptionAdjustmentAsync(SecretsManagerSubscriptionUpdate update)
|
||||
{
|
||||
if (update.SmSeatsChanged)
|
||||
{
|
||||
await ProcessChargesAndRaiseEventsForAdjustSeatsAsync(organization, plan, update);
|
||||
organization.SmSeats = update.SmSeats;
|
||||
await _paymentService.AdjustSeatsAsync(update.Organization, update.Plan, update.SmSeatsExcludingBase, update.ProrationDate);
|
||||
|
||||
// TODO: call ReferenceEventService - see AC-1481
|
||||
}
|
||||
|
||||
if (update.SmServiceAccountsChanged)
|
||||
{
|
||||
await ProcessChargesAndRaiseEventsForAdjustServiceAccountsAsync(organization, plan, update);
|
||||
organization.SmServiceAccounts = update.SmServiceAccounts;
|
||||
await _paymentService.AdjustServiceAccountsAsync(update.Organization, update.Plan,
|
||||
update.SmServiceAccountsExcludingBase, update.ProrationDate);
|
||||
|
||||
// TODO: call ReferenceEventService - see AC-1481
|
||||
}
|
||||
|
||||
if (update.MaxAutoscaleSmSeatsChanged)
|
||||
{
|
||||
organization.MaxAutoscaleSmSeats = update.MaxAutoscaleSmSeats;
|
||||
}
|
||||
|
||||
if (update.MaxAutoscaleSmServiceAccountsChanged)
|
||||
{
|
||||
organization.MaxAutoscaleSmServiceAccounts = update.MaxAutoscaleSmServiceAccounts;
|
||||
}
|
||||
var organization = update.Organization;
|
||||
organization.SmSeats = update.SmSeats;
|
||||
organization.SmServiceAccounts = update.SmServiceAccounts;
|
||||
organization.MaxAutoscaleSmSeats = update.MaxAutoscaleSmSeats;
|
||||
organization.MaxAutoscaleSmServiceAccounts = update.MaxAutoscaleSmServiceAccounts;
|
||||
|
||||
await ReplaceAndUpdateCacheAsync(organization);
|
||||
}
|
||||
|
||||
private async Task ProcessChargesAndRaiseEventsForAdjustSeatsAsync(Organization organization, Plan plan,
|
||||
SecretsManagerSubscriptionUpdate update)
|
||||
{
|
||||
await _paymentService.AdjustSeatsAsync(organization, plan, update.SmSeatsExcludingBase, update.ProrationDate);
|
||||
|
||||
// TODO: call ReferenceEventService - see AC-1481
|
||||
}
|
||||
|
||||
private async Task ProcessChargesAndRaiseEventsForAdjustServiceAccountsAsync(Organization organization, Plan plan,
|
||||
SecretsManagerSubscriptionUpdate update)
|
||||
{
|
||||
await _paymentService.AdjustServiceAccountsAsync(organization, plan,
|
||||
update.SmServiceAccountsExcludingBase, update.ProrationDate);
|
||||
|
||||
// TODO: call ReferenceEventService - see AC-1481
|
||||
}
|
||||
|
||||
private async Task SendEmailIfAutoscaleLimitReached(Organization organization)
|
||||
{
|
||||
if (organization.SmSeats.HasValue && organization.MaxAutoscaleSmSeats.HasValue && organization.SmSeats == organization.MaxAutoscaleSmSeats)
|
||||
{
|
||||
await SendSeatLimitEmailAsync(organization, organization.MaxAutoscaleSmSeats.Value);
|
||||
}
|
||||
|
||||
if (organization.SmServiceAccounts.HasValue && organization.MaxAutoscaleSmServiceAccounts.HasValue && organization.SmServiceAccounts == organization.MaxAutoscaleSmServiceAccounts)
|
||||
{
|
||||
await SendServiceAccountLimitEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendSeatLimitEmailAsync(Organization organization, int MaxAutoscaleValue)
|
||||
private async Task SendSeatLimitEmailAsync(Organization organization)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -134,16 +96,16 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
OrganizationUserType.Owner))
|
||||
.Select(u => u.Email).Distinct();
|
||||
|
||||
await _mailService.SendSecretsManagerMaxSeatLimitReachedEmailAsync(organization, MaxAutoscaleValue, ownerEmails);
|
||||
await _mailService.SendSecretsManagerMaxSeatLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmSeats.Value, ownerEmails);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, $"Error encountered notifying organization owners of Seats limit reached.");
|
||||
_logger.LogError(e, $"Error encountered notifying organization owners of seats limit reached.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendServiceAccountLimitEmailAsync(Organization organization, int MaxAutoscaleValue)
|
||||
private async Task SendServiceAccountLimitEmailAsync(Organization organization)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -151,12 +113,12 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
OrganizationUserType.Owner))
|
||||
.Select(u => u.Email).Distinct();
|
||||
|
||||
await _mailService.SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, MaxAutoscaleValue, ownerEmails);
|
||||
await _mailService.SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts.Value, ownerEmails);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, $"Error encountered notifying organization owners of Service Accounts limit reached.");
|
||||
_logger.LogError(e, $"Error encountered notifying organization owners of service accounts limit reached.");
|
||||
}
|
||||
|
||||
}
|
||||
@ -171,46 +133,45 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
|
||||
var organization = update.Organization;
|
||||
ValidateOrganization(organization);
|
||||
|
||||
var plan = GetPlanForOrganization(organization);
|
||||
ValidateOrganization(update);
|
||||
|
||||
if (update.SmSeatsChanged)
|
||||
{
|
||||
await ValidateSmSeatsUpdateAsync(organization, update, plan);
|
||||
await ValidateSmSeatsUpdateAsync(update);
|
||||
}
|
||||
|
||||
if (update.SmServiceAccountsChanged)
|
||||
{
|
||||
await ValidateSmServiceAccountsUpdateAsync(organization, update, plan);
|
||||
await ValidateSmServiceAccountsUpdateAsync(update);
|
||||
}
|
||||
|
||||
if (update.MaxAutoscaleSmSeatsChanged)
|
||||
{
|
||||
ValidateMaxAutoscaleSmSeatsUpdateAsync(organization, update.MaxAutoscaleSmSeats, plan);
|
||||
ValidateMaxAutoscaleSmSeatsUpdateAsync(update);
|
||||
}
|
||||
|
||||
if (update.MaxAutoscaleSmServiceAccountsChanged)
|
||||
{
|
||||
ValidateMaxAutoscaleSmServiceAccountUpdate(organization, update.MaxAutoscaleSmServiceAccounts, plan);
|
||||
ValidateMaxAutoscaleSmServiceAccountUpdate(update);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateOrganization(Organization organization)
|
||||
private void ValidateOrganization(SecretsManagerSubscriptionUpdate update)
|
||||
{
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException("Organization is not found.");
|
||||
}
|
||||
var organization = update.Organization;
|
||||
|
||||
if (!organization.UseSecretsManager)
|
||||
{
|
||||
throw new BadRequestException("Organization has no access to Secrets Manager.");
|
||||
}
|
||||
|
||||
var plan = GetPlanForOrganization(organization);
|
||||
if (plan.Product == ProductType.Free)
|
||||
if (organization.SecretsManagerBeta)
|
||||
{
|
||||
throw new BadRequestException("Organization is enrolled in Secrets Manager Beta. " +
|
||||
"Please contact Customer Success to add Secrets Manager to your subscription.");
|
||||
}
|
||||
|
||||
if (update.Plan.Product == ProductType.Free)
|
||||
{
|
||||
// No need to check the organization is set up with Stripe
|
||||
return;
|
||||
@ -227,18 +188,11 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
}
|
||||
}
|
||||
|
||||
private Plan GetPlanForOrganization(Organization organization)
|
||||
private async Task ValidateSmSeatsUpdateAsync(SecretsManagerSubscriptionUpdate update)
|
||||
{
|
||||
var plan = StaticStore.SecretManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BadRequestException("Existing plan not found.");
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
var organization = update.Organization;
|
||||
var plan = update.Plan;
|
||||
|
||||
private async Task ValidateSmSeatsUpdateAsync(Organization organization, SecretsManagerSubscriptionUpdate update, Plan plan)
|
||||
{
|
||||
// Check if the organization has unlimited seats
|
||||
if (organization.SmSeats == null)
|
||||
{
|
||||
@ -282,21 +236,24 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
// Check minimum seats currently in use by the organization
|
||||
if (organization.SmSeats.Value > update.SmSeats.Value)
|
||||
{
|
||||
var currentSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
|
||||
if (currentSeats > update.SmSeats.Value)
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);
|
||||
if (occupiedSeats > update.SmSeats.Value)
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {currentSeats} Secrets Manager seats. " +
|
||||
$"Your plan only allows {update.SmSeats} Secrets Manager seats. Remove some Secrets Manager users.");
|
||||
throw new BadRequestException($"{occupiedSeats} users are currently occupying Secrets Manager seats. " +
|
||||
"You cannot decrease your subscription below your current occupied seat count.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateSmServiceAccountsUpdateAsync(Organization organization, SecretsManagerSubscriptionUpdate update, Plan plan)
|
||||
private async Task ValidateSmServiceAccountsUpdateAsync(SecretsManagerSubscriptionUpdate update)
|
||||
{
|
||||
var organization = update.Organization;
|
||||
var plan = update.Plan;
|
||||
|
||||
// Check if the organization has unlimited service accounts
|
||||
if (organization.SmServiceAccounts == null)
|
||||
{
|
||||
throw new BadRequestException("Organization has no Service Accounts limit, no need to adjust Service Accounts");
|
||||
throw new BadRequestException("Organization has no service accounts limit, no need to adjust service accounts");
|
||||
}
|
||||
|
||||
if (update.Autoscaling && update.SmServiceAccounts.Value < organization.SmServiceAccounts.Value)
|
||||
@ -326,13 +283,13 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
// Check minimum service accounts included with plan
|
||||
if (plan.BaseServiceAccount.HasValue && plan.BaseServiceAccount.Value > update.SmServiceAccounts.Value)
|
||||
{
|
||||
throw new BadRequestException($"Plan has a minimum of {plan.BaseServiceAccount} Service Accounts.");
|
||||
throw new BadRequestException($"Plan has a minimum of {plan.BaseServiceAccount} service accounts.");
|
||||
}
|
||||
|
||||
// Check minimum service accounts required by business logic
|
||||
if (update.SmServiceAccounts.Value <= 0)
|
||||
{
|
||||
throw new BadRequestException("You must have at least 1 Service Account.");
|
||||
throw new BadRequestException("You must have at least 1 service account.");
|
||||
}
|
||||
|
||||
// Check minimum service accounts currently in use by the organization
|
||||
@ -341,30 +298,32 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
var currentServiceAccounts = await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);
|
||||
if (currentServiceAccounts > update.SmServiceAccounts)
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} Service Accounts. " +
|
||||
$"Your plan only allows {update.SmServiceAccounts} Service Accounts. Remove some Service Accounts.");
|
||||
throw new BadRequestException($"Your organization currently has {currentServiceAccounts} service accounts. " +
|
||||
$"You cannot decrease your subscription below your current service account usage.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateMaxAutoscaleSmSeatsUpdateAsync(Organization organization, int? maxAutoscaleSeats, Plan plan)
|
||||
private void ValidateMaxAutoscaleSmSeatsUpdateAsync(SecretsManagerSubscriptionUpdate update)
|
||||
{
|
||||
if (!maxAutoscaleSeats.HasValue)
|
||||
var plan = update.Plan;
|
||||
|
||||
if (!update.MaxAutoscaleSmSeats.HasValue)
|
||||
{
|
||||
// autoscale limit has been turned off, no validation required
|
||||
return;
|
||||
}
|
||||
|
||||
if (organization.SmSeats.HasValue && maxAutoscaleSeats.Value < organization.SmSeats.Value)
|
||||
if (update.SmSeats.HasValue && update.MaxAutoscaleSmSeats.Value < update.SmSeats.Value)
|
||||
{
|
||||
throw new BadRequestException($"Cannot set max Secrets Manager seat autoscaling below current Secrets Manager seat count.");
|
||||
}
|
||||
|
||||
if (plan.MaxUsers.HasValue && maxAutoscaleSeats.Value > plan.MaxUsers)
|
||||
if (plan.MaxUsers.HasValue && update.MaxAutoscaleSmSeats.Value > plan.MaxUsers)
|
||||
{
|
||||
throw new BadRequestException(string.Concat(
|
||||
$"Your plan has a Secrets Manager seat limit of {plan.MaxUsers}, ",
|
||||
$"but you have specified a max autoscale count of {maxAutoscaleSeats}.",
|
||||
$"but you have specified a max autoscale count of {update.MaxAutoscaleSmSeats}.",
|
||||
"Reduce your max autoscale count."));
|
||||
}
|
||||
|
||||
@ -374,30 +333,32 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateMaxAutoscaleSmServiceAccountUpdate(Organization organization, int? maxAutoscaleServiceAccounts, Plan plan)
|
||||
private void ValidateMaxAutoscaleSmServiceAccountUpdate(SecretsManagerSubscriptionUpdate update)
|
||||
{
|
||||
if (!maxAutoscaleServiceAccounts.HasValue)
|
||||
var plan = update.Plan;
|
||||
|
||||
if (!update.MaxAutoscaleSmServiceAccounts.HasValue)
|
||||
{
|
||||
// autoscale limit has been turned off, no validation required
|
||||
return;
|
||||
}
|
||||
|
||||
if (organization.SmServiceAccounts.HasValue && maxAutoscaleServiceAccounts.Value < organization.SmServiceAccounts.Value)
|
||||
if (update.SmServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value < update.SmServiceAccounts.Value)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
$"Cannot set max Service Accounts autoscaling below current Service Accounts count.");
|
||||
$"Cannot set max service accounts autoscaling below current service accounts count.");
|
||||
}
|
||||
|
||||
if (!plan.AllowServiceAccountsAutoscale)
|
||||
{
|
||||
throw new BadRequestException("Your plan does not allow Service Accounts autoscaling.");
|
||||
throw new BadRequestException("Your plan does not allow service accounts autoscaling.");
|
||||
}
|
||||
|
||||
if (plan.MaxServiceAccounts.HasValue && maxAutoscaleServiceAccounts.Value > plan.MaxServiceAccounts)
|
||||
if (plan.MaxServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value > plan.MaxServiceAccounts)
|
||||
{
|
||||
throw new BadRequestException(string.Concat(
|
||||
$"Your plan has a Service Accounts limit of {plan.MaxServiceAccounts}, ",
|
||||
$"but you have specified a max autoscale count of {maxAutoscaleServiceAccounts}.",
|
||||
$"Your plan has a service account limit of {plan.MaxServiceAccounts}, ",
|
||||
$"but you have specified a max autoscale count of {update.MaxAutoscaleSmServiceAccounts}.",
|
||||
"Reduce your max autoscale count."));
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ public class SecretOperationRequirement : OperationAuthorizationRequirement
|
||||
public static class SecretOperations
|
||||
{
|
||||
public static readonly SecretOperationRequirement Create = new() { Name = nameof(Create) };
|
||||
public static readonly SecretOperationRequirement Read = new() { Name = nameof(Read) };
|
||||
public static readonly SecretOperationRequirement Update = new() { Name = nameof(Update) };
|
||||
public static readonly SecretOperationRequirement Delete = new() { Name = nameof(Delete) };
|
||||
}
|
||||
|
@ -4,8 +4,6 @@ namespace Bit.Core.SecretsManager.Models.Data;
|
||||
|
||||
public class ApiKeyDetails : ApiKey
|
||||
{
|
||||
public string ClientSecret { get; set; } // Deprecated as of 2023-05-17
|
||||
|
||||
protected ApiKeyDetails() { }
|
||||
|
||||
protected ApiKeyDetails(ApiKey apiKey)
|
||||
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||
|
||||
public interface IMaxProjectsQuery
|
||||
{
|
||||
Task<(short? max, bool? atMax)> GetByOrgIdAsync(Guid organizationId);
|
||||
}
|
@ -29,4 +29,5 @@ public interface IEventService
|
||||
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, DateTime? date = null);
|
||||
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, EventSystemUser systemUser, DateTime? date = null);
|
||||
Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null);
|
||||
Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null);
|
||||
}
|
||||
|
@ -406,22 +406,34 @@ public class EventService : IEventService
|
||||
}
|
||||
|
||||
public async Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null)
|
||||
{
|
||||
await LogServiceAccountSecretsEventAsync(serviceAccountId, new[] { secret }, type, date);
|
||||
}
|
||||
|
||||
public async Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null)
|
||||
{
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
if (!CanUseEvents(orgAbilities, secret.OrganizationId))
|
||||
var eventMessages = new List<IEvent>();
|
||||
|
||||
foreach (var secret in secrets)
|
||||
{
|
||||
return;
|
||||
if (!CanUseEvents(orgAbilities, secret.OrganizationId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var e = new EventMessage(_currentContext)
|
||||
{
|
||||
OrganizationId = secret.OrganizationId,
|
||||
Type = type,
|
||||
SecretId = secret.Id,
|
||||
ServiceAccountId = serviceAccountId,
|
||||
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
||||
};
|
||||
eventMessages.Add(e);
|
||||
}
|
||||
|
||||
var e = new EventMessage(_currentContext)
|
||||
{
|
||||
OrganizationId = secret.OrganizationId,
|
||||
Type = type,
|
||||
SecretId = secret.Id,
|
||||
ServiceAccountId = serviceAccountId,
|
||||
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
||||
};
|
||||
await _eventWriteService.CreateAsync(e);
|
||||
await _eventWriteService.CreateManyAsync(eventMessages);
|
||||
}
|
||||
|
||||
private async Task<Guid?> GetProviderIdAsync(Guid? orgId)
|
||||
|
@ -861,8 +861,8 @@ public class OrganizationService : IOrganizationService
|
||||
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount);
|
||||
if (additionalSmSeatsRequired > 0)
|
||||
{
|
||||
smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, true);
|
||||
smSubscriptionUpdate.AdjustSeats(additionalSmSeatsRequired);
|
||||
smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, true)
|
||||
.AdjustSeats(additionalSmSeatsRequired);
|
||||
await _updateSecretsManagerSubscriptionCommand.ValidateUpdate(smSubscriptionUpdate);
|
||||
}
|
||||
|
||||
@ -1418,8 +1418,8 @@ public class OrganizationService : IOrganizationService
|
||||
if (additionalSmSeatsRequired > 0)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(user.OrganizationId);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true);
|
||||
update.AdjustSeats(additionalSmSeatsRequired);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true)
|
||||
.AdjustSeats(additionalSmSeatsRequired);
|
||||
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ namespace Bit.Core.Services;
|
||||
|
||||
public class PolicyService : IPolicyService
|
||||
{
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
@ -21,6 +22,7 @@ public class PolicyService : IPolicyService
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public PolicyService(
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IEventService eventService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -29,6 +31,7 @@ public class PolicyService : IPolicyService
|
||||
IMailService mailService,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_eventService = eventService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -199,7 +202,9 @@ public class PolicyService : IPolicyService
|
||||
{
|
||||
var organizationUserPolicyDetails = await _organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(userId, policyType);
|
||||
var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType);
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
return organizationUserPolicyDetails.Where(o =>
|
||||
(!orgAbilities.ContainsKey(o.OrganizationId) || orgAbilities[o.OrganizationId].UsePolicies) &&
|
||||
o.PolicyEnabled &&
|
||||
!excludedUserTypes.Contains(o.OrganizationUserType) &&
|
||||
o.OrganizationUserStatus >= minStatus &&
|
||||
|
@ -119,4 +119,10 @@ public class NoopEventService : IEventService
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type,
|
||||
DateTime? date = null)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public static class CoreHelpers
|
||||
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private static readonly DateTime _max = new DateTime(9999, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private static readonly Random _random = new Random();
|
||||
private static readonly string CloudFlareConnectingIp = "CF-Connecting-IP";
|
||||
private static readonly string RealConnectingIp = "X-Connecting-IP";
|
||||
|
||||
/// <summary>
|
||||
/// Generate sequential Guid for Sql Server.
|
||||
@ -50,20 +50,20 @@ public static class CoreHelpers
|
||||
{
|
||||
var guidArray = startingGuid.ToByteArray();
|
||||
|
||||
// Get the days and milliseconds which will be used to build the byte string
|
||||
// Get the days and milliseconds which will be used to build the byte string
|
||||
var days = new TimeSpan(time.Ticks - _baseDateTicks);
|
||||
var msecs = time.TimeOfDay;
|
||||
|
||||
// Convert to a byte array
|
||||
// Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333
|
||||
// Convert to a byte array
|
||||
// Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333
|
||||
var daysArray = BitConverter.GetBytes(days.Days);
|
||||
var msecsArray = BitConverter.GetBytes((long)(msecs.TotalMilliseconds / 3.333333));
|
||||
|
||||
// Reverse the bytes to match SQL Servers ordering
|
||||
// Reverse the bytes to match SQL Servers ordering
|
||||
Array.Reverse(daysArray);
|
||||
Array.Reverse(msecsArray);
|
||||
|
||||
// Copy the bytes into the guid
|
||||
// Copy the bytes into the guid
|
||||
Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2);
|
||||
Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4);
|
||||
|
||||
@ -557,9 +557,9 @@ public static class CoreHelpers
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!globalSettings.SelfHosted && httpContext.Request.Headers.ContainsKey(CloudFlareConnectingIp))
|
||||
if (!globalSettings.SelfHosted && httpContext.Request.Headers.ContainsKey(RealConnectingIp))
|
||||
{
|
||||
return httpContext.Request.Headers[CloudFlareConnectingIp].ToString();
|
||||
return httpContext.Request.Headers[RealConnectingIp].ToString();
|
||||
}
|
||||
|
||||
return httpContext.Connection?.RemoteIpAddress?.ToString();
|
||||
@ -624,8 +624,8 @@ public static class CoreHelpers
|
||||
return configDict;
|
||||
}
|
||||
|
||||
public static List<KeyValuePair<string, string>> BuildIdentityClaims(User user, ICollection<CurrentContentOrganization> orgs,
|
||||
ICollection<CurrentContentProvider> providers, bool isPremium)
|
||||
public static List<KeyValuePair<string, string>> BuildIdentityClaims(User user, ICollection<CurrentContextOrganization> orgs,
|
||||
ICollection<CurrentContextProvider> providers, bool isPremium)
|
||||
{
|
||||
var claims = new List<KeyValuePair<string, string>>()
|
||||
{
|
||||
@ -817,4 +817,19 @@ public static class CoreHelpers
|
||||
.ToString();
|
||||
|
||||
}
|
||||
|
||||
public static string GetEmailDomain(string email)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
var emailParts = email.Split('@', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (emailParts.Length == 2)
|
||||
{
|
||||
return emailParts[1].Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -107,11 +107,6 @@ public class ClientStore : IClientStore
|
||||
break;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(apiKey.ClientSecretHash))
|
||||
{
|
||||
apiKey.ClientSecretHash = apiKey.ClientSecret.Sha256();
|
||||
}
|
||||
|
||||
var client = new Client
|
||||
{
|
||||
ClientId = clientId,
|
||||
|
@ -69,7 +69,7 @@
|
||||
"IpRateLimitOptions": {
|
||||
"EnableEndpointRateLimiting": true,
|
||||
"StackBlockedRequests": false,
|
||||
"RealIpHeader": "CF-Connecting-IP",
|
||||
"RealIpHeader": "X-Connecting-IP",
|
||||
"ClientIdHeader": "X-ClientId",
|
||||
"HttpStatusCode": 429,
|
||||
"IpWhitelist": [],
|
||||
|
@ -87,7 +87,8 @@ public class OrganizationRepository : Repository<Core.Entities.Organization, Org
|
||||
UseKeyConnector = e.UseKeyConnector,
|
||||
UseResetPassword = e.UseResetPassword,
|
||||
UseScim = e.UseScim,
|
||||
UseCustomPermissions = e.UseCustomPermissions
|
||||
UseCustomPermissions = e.UseCustomPermissions,
|
||||
UsePolicies = e.UsePolicies
|
||||
}).ToListAsync();
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ CREATE PROCEDURE [dbo].[ApiKey_Create]
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@ServiceAccountId UNIQUEIDENTIFIER,
|
||||
@Name VARCHAR(200),
|
||||
@ClientSecret VARCHAR(30) = 'migrated', -- Deprecated as of 2023-05-17
|
||||
@ClientSecretHash VARCHAR(128) = NULL,
|
||||
@ClientSecretHash VARCHAR(128),
|
||||
@Scope NVARCHAR(4000),
|
||||
@EncryptedPayload NVARCHAR(4000),
|
||||
@Key VARCHAR(MAX),
|
||||
@ -14,18 +13,11 @@ AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
IF (@ClientSecretHash IS NULL)
|
||||
BEGIN
|
||||
DECLARE @hb VARBINARY(128) = HASHBYTES('SHA2_256', @ClientSecret);
|
||||
SET @ClientSecretHash = CAST(N'' as xml).value('xs:base64Binary(sql:variable("@hb"))', 'VARCHAR(128)');
|
||||
END
|
||||
|
||||
INSERT INTO [dbo].[ApiKey]
|
||||
INSERT INTO [dbo].[ApiKey]
|
||||
(
|
||||
[Id],
|
||||
[ServiceAccountId],
|
||||
[Name],
|
||||
[ClientSecret],
|
||||
[ClientSecretHash],
|
||||
[Scope],
|
||||
[EncryptedPayload],
|
||||
@ -34,12 +26,11 @@ BEGIN
|
||||
[CreationDate],
|
||||
[RevisionDate]
|
||||
)
|
||||
VALUES
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@ServiceAccountId,
|
||||
@Name,
|
||||
@ClientSecret,
|
||||
@ClientSecretHash,
|
||||
@Scope,
|
||||
@EncryptedPayload,
|
||||
|
@ -2,7 +2,6 @@
|
||||
[Id] UNIQUEIDENTIFIER,
|
||||
[ServiceAccountId] UNIQUEIDENTIFIER NULL,
|
||||
[Name] VARCHAR(200) NOT NULL,
|
||||
[ClientSecret] VARCHAR(30) NOT NULL,
|
||||
[ClientSecretHash] VARCHAR(128) NULL,
|
||||
[Scope] NVARCHAR (4000) NOT NULL,
|
||||
[EncryptedPayload] NVARCHAR (4000) NOT NULL,
|
||||
|
@ -19,6 +19,7 @@ BEGIN
|
||||
[UseKeyConnector],
|
||||
[UseScim],
|
||||
[UseResetPassword],
|
||||
[UsePolicies],
|
||||
[Enabled]
|
||||
FROM
|
||||
[dbo].[Organization]
|
||||
|
@ -1,42 +0,0 @@
|
||||
CREATE PROCEDURE [dbo].[ApiKey_Create]
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@ServiceAccountId UNIQUEIDENTIFIER,
|
||||
@Name VARCHAR(200),
|
||||
@ClientSecretHash VARCHAR(128),
|
||||
@Scope NVARCHAR(4000),
|
||||
@EncryptedPayload NVARCHAR(4000),
|
||||
@Key VARCHAR(MAX),
|
||||
@ExpireAt DATETIME2(7),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [dbo].[ApiKey]
|
||||
(
|
||||
[Id],
|
||||
[ServiceAccountId],
|
||||
[Name],
|
||||
[ClientSecretHash],
|
||||
[Scope],
|
||||
[EncryptedPayload],
|
||||
[Key],
|
||||
[ExpireAt],
|
||||
[CreationDate],
|
||||
[RevisionDate]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@ServiceAccountId,
|
||||
@Name,
|
||||
@ClientSecretHash,
|
||||
@Scope,
|
||||
@EncryptedPayload,
|
||||
@Key,
|
||||
@ExpireAt,
|
||||
@CreationDate,
|
||||
@RevisionDate
|
||||
)
|
||||
END
|
@ -1,18 +0,0 @@
|
||||
CREATE TABLE [dbo].[ApiKey] (
|
||||
[Id] UNIQUEIDENTIFIER,
|
||||
[ServiceAccountId] UNIQUEIDENTIFIER NULL,
|
||||
[Name] VARCHAR(200) NOT NULL,
|
||||
[ClientSecretHash] VARCHAR(128) NULL,
|
||||
[Scope] NVARCHAR (4000) NOT NULL,
|
||||
[EncryptedPayload] NVARCHAR (4000) NOT NULL,
|
||||
[Key] VARCHAR (MAX) NOT NULL,
|
||||
[ExpireAt] DATETIME2(7) NULL,
|
||||
[CreationDate] DATETIME2(7) NOT NULL,
|
||||
[RevisionDate] DATETIME2(7) NOT NULL,
|
||||
CONSTRAINT [PK_ApiKey] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_ApiKey_ServiceAccountId] FOREIGN KEY ([ServiceAccountId]) REFERENCES [dbo].[ServiceAccount] ([Id])
|
||||
);
|
||||
|
||||
GO
|
||||
CREATE NONCLUSTERED INDEX [IX_ApiKey_ServiceAccountId]
|
||||
ON [dbo].[ApiKey]([ServiceAccountId] ASC);
|
@ -116,6 +116,19 @@ public class ProjectsControllerTests : IClassFixture<ApiApplicationFactory>, IAs
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PermissionType.RunAsAdmin)]
|
||||
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||
public async Task Create_AtMaxProjects_BadRequest(PermissionType permissionType)
|
||||
{
|
||||
var (_, organization) = await SetupProjectsWithAccessAsync(permissionType, 3);
|
||||
var request = new ProjectCreateRequestModel { Name = _mockEncryptedString };
|
||||
|
||||
var response = await _client.PostAsJsonAsync($"/organizations/{organization.Id}/projects", request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PermissionType.RunAsAdmin)]
|
||||
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||
|
@ -709,6 +709,69 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
||||
Assert.Empty(secrets);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(false, true)]
|
||||
public async Task GetSecretsByIds_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets)
|
||||
{
|
||||
var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets);
|
||||
await LoginAsync(_email);
|
||||
|
||||
var secret = await _secretRepository.CreateAsync(new Secret
|
||||
{
|
||||
OrganizationId = org.Id,
|
||||
Key = _mockEncryptedString,
|
||||
Value = _mockEncryptedString,
|
||||
Note = _mockEncryptedString,
|
||||
});
|
||||
|
||||
var request = new GetSecretsRequestModel { Ids = new[] { secret.Id } };
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/secrets/get-by-ids", request);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PermissionType.RunAsAdmin)]
|
||||
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||
public async Task GetSecretsByIds_Success(PermissionType permissionType)
|
||||
{
|
||||
var (org, _) = await _organizationHelper.Initialize(true, true);
|
||||
await LoginAsync(_email);
|
||||
|
||||
var (project, secretIds) = await CreateSecretsAsync(org.Id);
|
||||
|
||||
if (permissionType == PermissionType.RunAsUserWithPermission)
|
||||
{
|
||||
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
|
||||
await LoginAsync(email);
|
||||
|
||||
var accessPolicies = new List<BaseAccessPolicy>
|
||||
{
|
||||
new UserProjectAccessPolicy
|
||||
{
|
||||
GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,
|
||||
},
|
||||
};
|
||||
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
||||
}
|
||||
else
|
||||
{
|
||||
var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.Admin, true);
|
||||
await LoginAsync(email);
|
||||
}
|
||||
|
||||
var request = new GetSecretsRequestModel { Ids = secretIds };
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/secrets/get-by-ids", request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<BaseSecretResponseModel>>();
|
||||
Assert.NotNull(result);
|
||||
Assert.NotEmpty(result!.Data);
|
||||
Assert.Equal(secretIds.Count, result!.Data.Count());
|
||||
}
|
||||
|
||||
private async Task<(Project Project, List<Guid> secretIds)> CreateSecretsAsync(Guid orgId, int numberToCreate = 3)
|
||||
{
|
||||
var project = await _projectRepository.CreateAsync(new Project
|
||||
|
@ -8,6 +8,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||
@ -122,6 +123,24 @@ public class ProjectsControllerTests
|
||||
.CreateAsync(Arg.Any<Project>(), Arg.Any<Guid>(), sutProvider.GetDependency<ICurrentContext>().ClientType);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void Create_AtMaxProjects_Throws(SutProvider<ProjectsController> sutProvider,
|
||||
Guid orgId, ProjectCreateRequestModel data)
|
||||
{
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(orgId),
|
||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
|
||||
sutProvider.GetDependency<IMaxProjectsQuery>().GetByOrgIdAsync(orgId).Returns(((short)3, true));
|
||||
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(orgId, data));
|
||||
|
||||
await sutProvider.GetDependency<ICreateProjectCommand>().DidNotReceiveWithAnyArgs()
|
||||
.CreateAsync(Arg.Any<Project>(), Arg.Any<Guid>(), sutProvider.GetDependency<ICurrentContext>().ClientType);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void Create_Success(SutProvider<ProjectsController> sutProvider,
|
||||
|
@ -346,4 +346,105 @@ public class SecretsControllerTests
|
||||
Assert.Null(result.Error);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void GetSecretsByIds_NoSecretsFound_ThrowsNotFound(SutProvider<SecretsController> sutProvider,
|
||||
List<Secret> data)
|
||||
{
|
||||
var (ids, request) = BuildGetSecretsRequestModel(data);
|
||||
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(new List<Secret>());
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void GetSecretsByIds_SecretsFoundMisMatch_ThrowsNotFound(SutProvider<SecretsController> sutProvider,
|
||||
List<Secret> data, Secret mockSecret)
|
||||
{
|
||||
var (ids, request) = BuildGetSecretsRequestModel(data);
|
||||
ids.Add(mockSecret.Id);
|
||||
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids))
|
||||
.ReturnsForAnyArgs(new List<Secret> { mockSecret });
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void GetSecretsByIds_OrganizationMisMatch_ThrowsNotFound(SutProvider<SecretsController> sutProvider,
|
||||
List<Secret> data)
|
||||
{
|
||||
var (ids, request) = BuildGetSecretsRequestModel(data);
|
||||
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void GetSecretsByIds_NoAccessToSecretsManager_ThrowsNotFound(
|
||||
SutProvider<SecretsController> sutProvider, List<Secret> data)
|
||||
{
|
||||
var (ids, request) = BuildGetSecretsRequestModel(data);
|
||||
var organizationId = SetOrganizations(ref data);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
|
||||
.ReturnsForAnyArgs(false);
|
||||
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void GetSecretsByIds_AccessDenied_ThrowsNotFound(SutProvider<SecretsController> sutProvider,
|
||||
List<Secret> data)
|
||||
{
|
||||
var (ids, request) = BuildGetSecretsRequestModel(data);
|
||||
var organizationId = SetOrganizations(ref data);
|
||||
|
||||
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
|
||||
.ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.First(),
|
||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void GetSecretsByIds_Success(SutProvider<SecretsController> sutProvider, List<Secret> data)
|
||||
{
|
||||
var (ids, request) = BuildGetSecretsRequestModel(data);
|
||||
var organizationId = SetOrganizations(ref data);
|
||||
|
||||
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))
|
||||
.ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IAuthorizationService>()
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.First(),
|
||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
|
||||
|
||||
var results = await sutProvider.Sut.GetSecretsByIdsAsync(request);
|
||||
Assert.Equal(data.Count, results.Data.Count());
|
||||
}
|
||||
|
||||
private static (List<Guid> Ids, GetSecretsRequestModel request) BuildGetSecretsRequestModel(
|
||||
IEnumerable<Secret> data)
|
||||
{
|
||||
var ids = data.Select(s => s.Id).ToList();
|
||||
var request = new GetSecretsRequestModel { Ids = ids };
|
||||
return (ids, request);
|
||||
}
|
||||
|
||||
private static Guid SetOrganizations(ref List<Secret> data)
|
||||
{
|
||||
var organizationId = data.First().OrganizationId;
|
||||
foreach (var s in data)
|
||||
{
|
||||
s.OrganizationId = organizationId;
|
||||
}
|
||||
|
||||
return organizationId;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
|
||||
@ -106,7 +107,7 @@ public class ServiceAccountsControllerTests
|
||||
.CreateAsync(Arg.Is<ServiceAccount>(sa => sa.Name == data.Name), Arg.Any<Guid>());
|
||||
|
||||
await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().DidNotReceiveWithAnyArgs()
|
||||
.AdjustServiceAccountsAsync(Arg.Any<Organization>(), Arg.Any<int>());
|
||||
.UpdateSubscriptionAsync(Arg.Any<SecretsManagerSubscriptionUpdate>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@ -124,7 +125,12 @@ public class ServiceAccountsControllerTests
|
||||
.CreateAsync(Arg.Is<ServiceAccount>(sa => sa.Name == data.Name), Arg.Any<Guid>());
|
||||
|
||||
await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().Received(1)
|
||||
.AdjustServiceAccountsAsync(Arg.Is(organization), Arg.Is(newSlotsRequired));
|
||||
.UpdateSubscriptionAsync(Arg.Is<SecretsManagerSubscriptionUpdate>(update =>
|
||||
update.Autoscaling == true &&
|
||||
update.SmServiceAccounts == organization.SmServiceAccounts + newSlotsRequired &&
|
||||
!update.SmSeatsChanged &&
|
||||
!update.MaxAutoscaleSmSeatsChanged &&
|
||||
!update.MaxAutoscaleSmServiceAccountsChanged));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
@ -32,7 +32,7 @@ internal class CurrentContextBuilder : ISpecimenBuilder
|
||||
}
|
||||
|
||||
var obj = new Fixture().WithAutoNSubstitutions().Create<ICurrentContext>();
|
||||
obj.Organizations = context.Create<List<CurrentContentOrganization>>();
|
||||
obj.Organizations = context.Create<List<CurrentContextOrganization>>();
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
@ -137,6 +137,7 @@ public class SecretsManagerOrganizationCustomization : ICustomization
|
||||
fixture.Customize<Organization>(composer => composer
|
||||
.With(o => o.Id, organizationId)
|
||||
.With(o => o.UseSecretsManager, true)
|
||||
.With(o => o.SecretsManagerBeta, false)
|
||||
.With(o => o.PlanType, planType)
|
||||
.With(o => o.Plan, StaticStore.GetPasswordManagerPlan(planType).Name)
|
||||
.With(o => o.MaxAutoscaleSmSeats, (int?)null)
|
||||
|
@ -95,6 +95,38 @@ public class AddSecretsManagerSubscriptionCommandTests
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SignUpAsync_ThrowsException_WhenOrganizationEnrolledInSmBeta(
|
||||
SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
organization.UseSecretsManager = true;
|
||||
organization.SecretsManagerBeta = true;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SignUpAsync(organization, 10, 10));
|
||||
|
||||
Assert.Contains("Organization is enrolled in Secrets Manager Beta", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SignUpAsync_ThrowsException_WhenOrganizationAlreadyHasSecretsManager(
|
||||
SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
organization.UseSecretsManager = true;
|
||||
organization.SecretsManagerBeta = false;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SignUpAsync(organization, 10, 10));
|
||||
|
||||
Assert.Contains("Organization already uses Secrets Manager", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
private static async Task VerifyDependencyNotCalledAsync(SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
await sutProvider.GetDependency<IPaymentService>().DidNotReceive()
|
||||
|
@ -125,8 +125,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
Organization organization,
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, autoscaling);
|
||||
update.AdjustSeats(2);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, autoscaling).AdjustSeats(2);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
||||
|
||||
@ -151,6 +150,23 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task UpdateSubscriptionAsync_OrganizationEnrolledInSmBeta_ThrowsException(
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
organization.UseSecretsManager = true;
|
||||
organization.SecretsManagerBeta = true;
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
|
||||
Assert.Contains("Organization is enrolled in Secrets Manager Beta", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||
@ -163,8 +179,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
organization.GatewayCustomerId = null;
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false);
|
||||
update.AdjustSeats(1);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustSeats(1);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("No payment method found.", exception.Message);
|
||||
@ -183,8 +198,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
organization.GatewaySubscriptionId = null;
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false);
|
||||
update.AdjustSeats(1);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustSeats(1);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("No subscription found.", exception.Message);
|
||||
@ -223,8 +237,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
var expectedSmServiceAccounts = organizationServiceAccounts + smServiceAccountsAdjustment;
|
||||
var expectedSmServiceAccountsExcludingBase = expectedSmServiceAccounts - plan.BaseServiceAccount.GetValueOrDefault();
|
||||
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false);
|
||||
update.AdjustServiceAccounts(10);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(10);
|
||||
|
||||
await sutProvider.Sut.UpdateSubscriptionAsync(update);
|
||||
|
||||
@ -247,7 +260,6 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
Organization organization,
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
organization.SmSeats = 9;
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false)
|
||||
{
|
||||
SmSeats = 10,
|
||||
@ -267,8 +279,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
organization.SmSeats = null;
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false);
|
||||
update.AdjustSeats(1);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustSeats(1);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
@ -283,8 +294,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
Organization organization,
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true);
|
||||
update.AdjustSeats(-2);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustSeats(-2);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Cannot use autoscaling to subtract seats.", exception.Message);
|
||||
@ -299,8 +309,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false);
|
||||
update.AdjustSeats(1);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustSeats(1);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("You have reached the maximum number of Secrets Manager seats (2) for this plan",
|
||||
@ -317,8 +326,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
organization.SmSeats = 9;
|
||||
organization.MaxAutoscaleSmSeats = 10;
|
||||
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true);
|
||||
update.AdjustSeats(2);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustSeats(2);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Secrets Manager seat limit has been reached.", exception.Message);
|
||||
@ -375,7 +383,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(8);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Your organization currently has 8 Secrets Manager seats. Your plan only allows 7 Secrets Manager seats. Remove some Secrets Manager users", exception.Message);
|
||||
Assert.Contains("8 users are currently occupying Secrets Manager seats. You cannot decrease your subscription below your current occupied seat count", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
@ -385,7 +393,6 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
Organization organization,
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
organization.SmServiceAccounts = 250;
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false)
|
||||
{
|
||||
SmServiceAccounts = 300,
|
||||
@ -405,11 +412,10 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
organization.SmServiceAccounts = null;
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false);
|
||||
update.AdjustServiceAccounts(1);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Organization has no Service Accounts limit, no need to adjust Service Accounts", exception.Message);
|
||||
Assert.Contains("Organization has no service accounts limit, no need to adjust service accounts", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
@ -419,8 +425,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
Organization organization,
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true);
|
||||
update.AdjustServiceAccounts(-2);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(-2);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Cannot use autoscaling to subtract service accounts.", exception.Message);
|
||||
@ -435,8 +440,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)
|
||||
{
|
||||
organization.PlanType = planType;
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false);
|
||||
update.AdjustServiceAccounts(1);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false).AdjustServiceAccounts(1);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("You have reached the maximum number of service accounts (3) for this plan",
|
||||
@ -453,8 +457,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
organization.SmServiceAccounts = 9;
|
||||
organization.MaxAutoscaleSmServiceAccounts = 10;
|
||||
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true);
|
||||
update.AdjustServiceAccounts(2);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(2);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Secrets Manager service account limit has been reached.", exception.Message);
|
||||
@ -492,7 +495,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Plan has a minimum of 200 Service Accounts", exception.Message);
|
||||
Assert.Contains("Plan has a minimum of 200 service accounts", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
@ -516,7 +519,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
.Returns(currentServiceAccounts);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Your organization currently has 301 Service Accounts. Your plan only allows 201 Service Accounts. Remove some Service Accounts", exception.Message);
|
||||
Assert.Contains("Your organization currently has 301 service accounts. You cannot decrease your subscription below your current service account usage", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
@ -588,7 +591,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false) { MaxAutoscaleSmServiceAccounts = 3 };
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));
|
||||
Assert.Contains("Your plan does not allow Service Accounts autoscaling.", exception.Message);
|
||||
Assert.Contains("Your plan does not allow service accounts autoscaling.", exception.Message);
|
||||
await VerifyDependencyNotCalledAsync(sutProvider);
|
||||
}
|
||||
|
||||
|
@ -648,6 +648,7 @@ public class OrganizationServiceTests
|
||||
[OrganizationUser(type: OrganizationUserType.Owner, status: OrganizationUserStatusType.Confirmed)] OrganizationUser savingUser,
|
||||
SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider);
|
||||
|
||||
// Set up some invites to grant access to SM
|
||||
|
@ -273,8 +273,8 @@ public class CoreHelpersTests
|
||||
{ "sstamp", user.SecurityStamp },
|
||||
}.ToList();
|
||||
|
||||
var actual = CoreHelpers.BuildIdentityClaims(user, Array.Empty<CurrentContentOrganization>(),
|
||||
Array.Empty<CurrentContentProvider>(), isPremium);
|
||||
var actual = CoreHelpers.BuildIdentityClaims(user, Array.Empty<CurrentContextOrganization>(),
|
||||
Array.Empty<CurrentContextProvider>(), isPremium);
|
||||
|
||||
foreach (var claim in expected)
|
||||
{
|
||||
@ -289,23 +289,23 @@ public class CoreHelpersTests
|
||||
var fixture = new Fixture().WithAutoNSubstitutions();
|
||||
foreach (var organizationUserType in Enum.GetValues<OrganizationUserType>().Except(new[] { OrganizationUserType.Custom }))
|
||||
{
|
||||
var org = fixture.Create<CurrentContentOrganization>();
|
||||
var org = fixture.Create<CurrentContextOrganization>();
|
||||
org.Type = organizationUserType;
|
||||
|
||||
var expected = new KeyValuePair<string, string>($"org{organizationUserType.ToString().ToLower()}", org.Id.ToString());
|
||||
var actual = CoreHelpers.BuildIdentityClaims(user, new[] { org }, Array.Empty<CurrentContentProvider>(), false);
|
||||
var actual = CoreHelpers.BuildIdentityClaims(user, new[] { org }, Array.Empty<CurrentContextProvider>(), false);
|
||||
|
||||
Assert.Contains(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, UserCustomize]
|
||||
public void BuildIdentityClaims_CustomOrganizationUserClaims_Success(User user, CurrentContentOrganization org)
|
||||
public void BuildIdentityClaims_CustomOrganizationUserClaims_Success(User user, CurrentContextOrganization org)
|
||||
{
|
||||
var fixture = new Fixture().WithAutoNSubstitutions();
|
||||
org.Type = OrganizationUserType.Custom;
|
||||
|
||||
var actual = CoreHelpers.BuildIdentityClaims(user, new[] { org }, Array.Empty<CurrentContentProvider>(), false);
|
||||
var actual = CoreHelpers.BuildIdentityClaims(user, new[] { org }, Array.Empty<CurrentContextProvider>(), false);
|
||||
foreach (var (permitted, claimName) in org.Permissions.ClaimsMap)
|
||||
{
|
||||
var claim = new KeyValuePair<string, string>(claimName, org.Id.ToString());
|
||||
@ -325,10 +325,10 @@ public class CoreHelpersTests
|
||||
public void BuildIdentityClaims_ProviderClaims_Success(User user)
|
||||
{
|
||||
var fixture = new Fixture().WithAutoNSubstitutions();
|
||||
var providers = new List<CurrentContentProvider>();
|
||||
var providers = new List<CurrentContextProvider>();
|
||||
foreach (var providerUserType in Enum.GetValues<ProviderUserType>())
|
||||
{
|
||||
var provider = fixture.Create<CurrentContentProvider>();
|
||||
var provider = fixture.Create<CurrentContextProvider>();
|
||||
provider.Type = providerUserType;
|
||||
providers.Add(provider);
|
||||
}
|
||||
@ -357,7 +357,7 @@ public class CoreHelpersTests
|
||||
}
|
||||
}
|
||||
|
||||
var actual = CoreHelpers.BuildIdentityClaims(user, Array.Empty<CurrentContentOrganization>(), providers, false);
|
||||
var actual = CoreHelpers.BuildIdentityClaims(user, Array.Empty<CurrentContextOrganization>(), providers, false);
|
||||
foreach (var claim in claims)
|
||||
{
|
||||
Assert.Contains(claim, actual);
|
||||
@ -416,4 +416,25 @@ public class CoreHelpersTests
|
||||
{
|
||||
Assert.Equal(expected, CoreHelpers.ObfuscateEmail(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("user@example.com")]
|
||||
[InlineData("user@example.com ")]
|
||||
[InlineData("user.name@example.com")]
|
||||
public void GetEmailDomain_Success(string email)
|
||||
{
|
||||
Assert.Equal("example.com", CoreHelpers.GetEmailDomain(email));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(null)]
|
||||
[InlineData("userexample.com")]
|
||||
[InlineData("user@")]
|
||||
[InlineData("@example.com")]
|
||||
[InlineData("user@ex@ample.com")]
|
||||
public void GetEmailDomain_ReturnsNull(string wrongEmail)
|
||||
{
|
||||
Assert.Null(CoreHelpers.GetEmailDomain(wrongEmail));
|
||||
}
|
||||
}
|
||||
|
@ -530,6 +530,7 @@ public class IdentityServerSsoTests
|
||||
var organization = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
UsePolicies = true
|
||||
});
|
||||
|
||||
var organizationUserRepository = factory.Services.GetRequiredService<IOrganizationUserRepository>();
|
||||
|
@ -556,7 +556,7 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
||||
var organizationUserRepository = _factory.Services.GetService<IOrganizationUserRepository>();
|
||||
var policyRepository = _factory.Services.GetService<IPolicyRepository>();
|
||||
|
||||
var organization = new Bit.Core.Entities.Organization { Id = organizationId, Enabled = true, UseSso = ssoPolicyEnabled };
|
||||
var organization = new Bit.Core.Entities.Organization { Id = organizationId, Enabled = true, UseSso = ssoPolicyEnabled, UsePolicies = true };
|
||||
await organizationRepository.CreateAsync(organization);
|
||||
|
||||
var user = await userRepository.GetByEmailAsync(username);
|
||||
|
@ -0,0 +1,27 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadAbilities]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
[Id],
|
||||
[UseEvents],
|
||||
[Use2fa],
|
||||
CASE
|
||||
WHEN [Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}' THEN
|
||||
1
|
||||
ELSE
|
||||
0
|
||||
END AS [Using2fa],
|
||||
[UsersGetPremium],
|
||||
[UseCustomPermissions],
|
||||
[UseSso],
|
||||
[UseKeyConnector],
|
||||
[UseScim],
|
||||
[UseResetPassword],
|
||||
[UsePolicies],
|
||||
[Enabled]
|
||||
FROM
|
||||
[dbo].[Organization]
|
||||
END
|
||||
GO
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
This is the data migration script for the client secret hash updates.
|
||||
The initial migration util/Migrator/DbScripts/2023-05-16_00_ClientSecretHash.sql should be run prior.
|
||||
The final migration is in util/Migrator/DbScripts_future/2023-06-FutureMigration.sql.
|
||||
The final migration is in util/Migrator/DbScripts/2023-08-10_01_RemoveClientSecret
|
||||
*/
|
||||
IF COL_LENGTH('[dbo].[ApiKey]', 'ClientSecretHash') IS NOT NULL AND COL_LENGTH('[dbo].[ApiKey]', 'ClientSecret') IS NOT NULL
|
||||
BEGIN
|
||||
@ -9,7 +9,7 @@ BEGIN
|
||||
-- Add index
|
||||
IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_ApiKey_ClientSecretHash')
|
||||
BEGIN
|
||||
CREATE NONCLUSTERED INDEX [IX_ApiKey_ClientSecretHash]
|
||||
CREATE NONCLUSTERED INDEX [IX_ApiKey_ClientSecretHash]
|
||||
ON [dbo].[ApiKey]([ClientSecretHash] ASC)
|
||||
WITH (ONLINE = ON)
|
||||
END
|
||||
@ -30,7 +30,7 @@ BEGIN
|
||||
WHERE [ClientSecretHash] IS NULL
|
||||
|
||||
SET @BatchSize = @@ROWCOUNT
|
||||
|
||||
|
||||
COMMIT TRANSACTION Migrate_ClientSecretHash
|
||||
END
|
||||
|
@ -36,7 +36,7 @@ AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [dbo].[ApiKey]
|
||||
INSERT INTO [dbo].[ApiKey]
|
||||
(
|
||||
[Id],
|
||||
[ServiceAccountId],
|
||||
@ -49,7 +49,7 @@ BEGIN
|
||||
[CreationDate],
|
||||
[RevisionDate]
|
||||
)
|
||||
VALUES
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@ServiceAccountId,
|
Reference in New Issue
Block a user