diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7562dd354a..22e5fad21e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/protect-files.yml b/.github/workflows/protect-files.yml index 25a019c76d..22f8bc57c0 100644 --- a/.github/workflows/protect-files.yml +++ b/.github/workflows/protect-files.yml @@ -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 }} diff --git a/Directory.Build.props b/Directory.Build.props index 62c0d4b76a..3a60d958ca 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ net6.0 - 2023.8.1 + 2023.8.2 Bit.$(MSBuildProjectName) true enable diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandler.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandler.cs index 262d431fb3..92461e61a9 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandler.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandler.cs @@ -38,6 +38,9 @@ public class SecretAuthorizationHandler : AuthorizationHandler 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); + } +} diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs index be9534cfc8..47547eb0b1 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs @@ -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(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs index 9fa346fbc8..7d8a429a6e 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs @@ -329,7 +329,8 @@ public class SecretRepository : Repository(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), }; diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs index f9bb77837d..40d4d1f42a 100644 --- a/bitwarden_license/src/Sso/Controllers/AccountController.cs +++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs @@ -47,6 +47,7 @@ public class AccountController : Controller private readonly IGlobalSettings _globalSettings; private readonly Core.Services.IEventService _eventService; private readonly IDataProtectorTokenFactory _dataProtector; + private readonly IOrganizationDomainRepository _organizationDomainRepository; public AccountController( IAuthenticationSchemeProvider schemeProvider, @@ -65,7 +66,8 @@ public class AccountController : Controller UserManager userManager, IGlobalSettings globalSettings, Core.Services.IEventService eventService, - IDataProtectorTokenFactory dataProtector) + IDataProtectorTokenFactory 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); diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandlerTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandlerTests.cs index d7e49fb46d..f1737e0ad4 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandlerTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandlerTests.cs @@ -232,6 +232,69 @@ public class SecretAuthorizationHandlerTests Assert.True(authzContext.HasSucceeded); } + [Theory] + [BitAutoData] + public async Task CanReadSecret_AccessToSecretsManagerFalse_DoesNotSucceed( + SutProvider sutProvider, Secret secret, + ClaimsPrincipal claimsPrincipal) + { + var requirement = SecretOperations.Read; + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId) + .Returns(false); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, secret); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.False(authzContext.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task CanReadSecret_NullResource_DoesNotSucceed( + SutProvider sutProvider, Secret secret, + ClaimsPrincipal claimsPrincipal, + Guid userId) + { + var requirement = SecretOperations.Read; + SetupPermission(sutProvider, PermissionType.RunAsAdmin, secret.OrganizationId, userId); + var authzContext = new AuthorizationHandlerContext(new List { 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 sutProvider, Secret secret, + ClaimsPrincipal claimsPrincipal, + Guid userId) + { + var requirement = SecretOperations.Read; + SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId); + sutProvider.GetDependency() + .AccessToSecretAsync(secret.Id, userId, Arg.Any()) + .Returns((read, write)); + var authzContext = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, secret); + + await sutProvider.Sut.HandleAsync(authzContext); + + Assert.Equal(expected, authzContext.HasSucceeded); + } + [Theory] [BitAutoData] public async Task CanUpdateSecret_AccessToSecretsManagerFalse_DoesNotSucceed( diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs new file mode 100644 index 0000000000..6706e01657 --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs @@ -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 sutProvider, + Guid organizationId) + { + sutProvider.GetDependency().GetByIdAsync(default).ReturnsNull(); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId)); + + await sutProvider.GetDependency().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 sutProvider, Organization organization) + { + organization.PlanType = planType; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + + await Assert.ThrowsAsync( + async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id)); + + await sutProvider.GetDependency().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 sutProvider, Organization organization) + { + organization.PlanType = planType; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + + var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id); + + Assert.Null(limit); + Assert.Null(overLimit); + + await sutProvider.GetDependency().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 sutProvider, Organization organization) + { + organization.PlanType = planType; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + sutProvider.GetDependency().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().Received(1) + .GetProjectCountByOrganizationIdAsync(organization.Id); + } +} diff --git a/src/Api/Controllers/InfoController.cs b/src/Api/Controllers/InfoController.cs index 9c6b7b8866..fcd41540d0 100644 --- a/src/Api/Controllers/InfoController.cs +++ b/src/Api/Controllers/InfoController.cs @@ -21,7 +21,7 @@ public class InfoController : Controller [HttpGet("~/ip")] public JsonResult Ip() { - var headerSet = new HashSet { "x-forwarded-for", "cf-connecting-ip", "client-ip" }; + var headerSet = new HashSet { "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); diff --git a/src/Api/Controllers/OrganizationUsersController.cs b/src/Api/Controllers/OrganizationUsersController.cs index 6d1424f01e..3dbaea3ff9 100644 --- a/src/Api/Controllers/OrganizationUsersController.cs +++ b/src/Api/Controllers/OrganizationUsersController.cs @@ -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); } diff --git a/src/Api/Models/Request/Organizations/SecretsManagerSubscriptionUpdateRequestModel.cs b/src/Api/Models/Request/Organizations/SecretsManagerSubscriptionUpdateRequestModel.cs index a8adbe92e5..96e9603fc3 100644 --- a/src/Api/Models/Request/Organizations/SecretsManagerSubscriptionUpdateRequestModel.cs +++ b/src/Api/Models/Request/Organizations/SecretsManagerSubscriptionUpdateRequestModel.cs @@ -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); } } diff --git a/src/Api/SecretsManager/Controllers/ProjectsController.cs b/src/Api/SecretsManager/Controllers/ProjectsController.cs index 7ed428ad54..4f3815ca78 100644 --- a/src/Api/SecretsManager/Controllers/ProjectsController.cs +++ b/src/Api/SecretsManager/Controllers/ProjectsController.cs @@ -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); diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index 99f641a172..a88a784bed 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -207,4 +207,45 @@ public class SecretsController : Controller var responses = results.Select(r => new BulkDeleteResponseModel(r.Secret.Id, r.Error)); return new ListResponseModel(responses); } + + [HttpPost("secrets/get-by-ids")] + public async Task> 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(responses); + } } diff --git a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs index 0e2d59da27..bb28276b03 100644 --- a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs +++ b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs @@ -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; diff --git a/src/Api/SecretsManager/Models/Request/GetSecretsRequestModel.cs b/src/Api/SecretsManager/Models/Request/GetSecretsRequestModel.cs new file mode 100644 index 0000000000..42dbce5232 --- /dev/null +++ b/src/Api/SecretsManager/Models/Request/GetSecretsRequestModel.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.SecretsManager.Models.Request; + +public class GetSecretsRequestModel +{ + [Required] + public IEnumerable Ids { get; set; } +} diff --git a/src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs b/src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs index 9d6f8c9aa1..fd895594a4 100644 --- a/src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs @@ -13,7 +13,7 @@ public class SecretCreateRequestModel : IValidatableObject [Required] [EncryptedString] - [EncryptedStringLength(5000)] + [EncryptedStringLength(35000)] public string Value { get; set; } [Required] diff --git a/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs b/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs index d5cd320fff..a08ed90c3c 100644 --- a/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs @@ -13,7 +13,7 @@ public class SecretUpdateRequestModel : IValidatableObject [Required] [EncryptedString] - [EncryptedStringLength(5000)] + [EncryptedStringLength(35000)] public string Value { get; set; } [Required] diff --git a/src/Api/SecretsManager/Models/Response/BaseSecretResponseModel.cs b/src/Api/SecretsManager/Models/Response/BaseSecretResponseModel.cs new file mode 100644 index 0000000000..0579baec07 --- /dev/null +++ b/src/Api/SecretsManager/Models/Response/BaseSecretResponseModel.cs @@ -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 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; } + } +} diff --git a/src/Api/SecretsManager/Models/Response/SecretResponseModel.cs b/src/Api/SecretsManager/Models/Response/SecretResponseModel.cs index 9f940a8782..c4633f78f0 100644 --- a/src/Api/SecretsManager/Models/Response/SecretResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/SecretResponseModel.cs @@ -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 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; } - } } diff --git a/src/Api/appsettings.json b/src/Api/appsettings.json index fde1db479e..e49491857f 100644 --- a/src/Api/appsettings.json +++ b/src/Api/appsettings.json @@ -79,7 +79,7 @@ "IpRateLimitOptions": { "EnableEndpointRateLimiting": true, "StackBlockedRequests": false, - "RealIpHeader": "CF-Connecting-IP", + "RealIpHeader": "X-Connecting-IP", "ClientIdHeader": "X-ClientId", "HttpStatusCode": 429, "IpWhitelist": [], diff --git a/src/Billing/Constants/HandledStripeWebhook.cs b/src/Billing/Constants/HandledStripeWebhook.cs index f7baa4675a..7b894a295e 100644 --- a/src/Billing/Constants/HandledStripeWebhook.cs +++ b/src/Billing/Constants/HandledStripeWebhook.cs @@ -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"; } diff --git a/src/Billing/Constants/StripeInvoiceStatus.cs b/src/Billing/Constants/StripeInvoiceStatus.cs new file mode 100644 index 0000000000..82d286d8a2 --- /dev/null +++ b/src/Billing/Constants/StripeInvoiceStatus.cs @@ -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"; +} diff --git a/src/Billing/Constants/StripeSubscriptionStatus.cs b/src/Billing/Constants/StripeSubscriptionStatus.cs new file mode 100644 index 0000000000..4589b50051 --- /dev/null +++ b/src/Billing/Constants/StripeSubscriptionStatus.cs @@ -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"; +} diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index c45407b53d..1b6ddea429 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -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 += $"
  • Org, {orgNote}
  • "; if (!customFields.Any(kvp => kvp.Key == _billingSettings.FreshDesk.OrgFieldName)) diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index 88f3f9e8a3..00a8fa5ac6 100644 --- a/src/Billing/Controllers/StripeController.cs +++ b/src/Billing/Controllers/StripeController.cs @@ -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 /// private async Task ValidateCloudRegionAsync(Event parsedEvent) { - string customerRegion; - var serverRegion = _globalSettings.BaseServiceUri.CloudRegion; var eventType = parsedEvent.Type; + var expandOptions = new List { "customer" }; - switch (eventType) + try { - case HandledStripeWebhook.SubscriptionDeleted: - case HandledStripeWebhook.SubscriptionUpdated: - { - var subscription = await GetSubscriptionAsync(parsedEvent, true, new List { "customer" }); - customerRegion = GetCustomerRegionFromMetadata(subscription.Customer.Metadata); + Dictionary 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 { "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 { "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; + } } /// - /// Gets the region from the customer metadata. If no region is present, defaults to "US" + /// Gets the customer's region from the metadata. /// - /// - /// - private static string GetCustomerRegionFromMetadata(Dictionary customerMetadata) + /// The metadata of the customer. + /// 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. + private static string GetCustomerRegionFromMetadata(IDictionary 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 { "data.latest_invoice" } + }; + + StripeList 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 GetIdsFromMetaData(IDictionary metaData) @@ -598,7 +723,7 @@ public class StripeController : Controller } } - private async Task AttemptToPayInvoiceAsync(Invoice invoice) + private async Task 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 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 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 + { + [btObjIdField] = btObjId.ToString(), + ["region"] = _globalSettings.BaseServiceUri.CloudRegion } - }, - CustomFields = new Dictionary - { - [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 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 GetPaymentMethodAsync(Event parsedEvent, bool fresh = false, + List 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 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); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5cd0349291..8db721d17e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -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 GetAllKeys() { diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index 627e4f5845..4877a9a8f2 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -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 Organizations { get; set; } - public virtual List Providers { get; set; } + public virtual List Organizations { get; set; } + public virtual List 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 GetOrganizations(Dictionary> claimsDict, bool orgApi) + private List GetOrganizations(Dictionary> claimsDict, bool orgApi) { var accessSecretsManager = claimsDict.ContainsKey(Claims.SecretsManagerAccess) ? claimsDict[Claims.SecretsManagerAccess].ToDictionary(s => s.Value, _ => true) : new Dictionary(); - var organizations = new List(); + var organizations = new List(); 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 GetProviders(Dictionary> claimsDict) + private List GetProviders(Dictionary> claimsDict) { - var providers = new List(); + var providers = new List(); 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> OrganizationMembershipAsync( + public async Task> 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> ProviderMembershipAsync( + public async Task> 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; } diff --git a/src/Core/Context/CurrentContentOrganization.cs b/src/Core/Context/CurrentContextOrganization.cs similarity index 81% rename from src/Core/Context/CurrentContentOrganization.cs rename to src/Core/Context/CurrentContextOrganization.cs index b21598a035..cf2c8b40c7 100644 --- a/src/Core/Context/CurrentContentOrganization.cs +++ b/src/Core/Context/CurrentContextOrganization.cs @@ -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; diff --git a/src/Core/Context/CurrentContentProvider.cs b/src/Core/Context/CurrentContextProvider.cs similarity index 77% rename from src/Core/Context/CurrentContentProvider.cs rename to src/Core/Context/CurrentContextProvider.cs index f089be7b8a..57792840cc 100644 --- a/src/Core/Context/CurrentContentProvider.cs +++ b/src/Core/Context/CurrentContextProvider.cs @@ -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; diff --git a/src/Core/Context/ICurrentContext.cs b/src/Core/Context/ICurrentContext.cs index 76a71e01a7..c2e362d435 100644 --- a/src/Core/Context/ICurrentContext.cs +++ b/src/Core/Context/ICurrentContext.cs @@ -16,7 +16,7 @@ public interface ICurrentContext string DeviceIdentifier { get; set; } DeviceType? DeviceType { get; set; } string IpAddress { get; set; } - List Organizations { get; set; } + List 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> OrganizationMembershipAsync( + Task> OrganizationMembershipAsync( IOrganizationUserRepository organizationUserRepository, Guid userId); - Task> ProviderMembershipAsync( + Task> ProviderMembershipAsync( IProviderUserRepository providerUserRepository, Guid userId); Task ProviderIdForOrg(Guid orgId); diff --git a/src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs b/src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs index a2e687a92e..1cb7a41df8 100644 --- a/src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs +++ b/src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs @@ -6,7 +6,7 @@ namespace Bit.Core.Models.Business; public class SecretsManagerSubscriptionUpdate { - public Organization Organization { get; set; } + public Organization Organization { get; } /// /// 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; } /// - /// 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 /// public int? MaxAutoscaleSmSeats { get; set; } @@ -26,8 +25,7 @@ public class SecretsManagerSubscriptionUpdate public int? SmServiceAccounts { get; set; } /// - /// 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 /// public int? MaxAutoscaleSmServiceAccounts { get; set; } @@ -39,7 +37,7 @@ public class SecretsManagerSubscriptionUpdate /// /// Whether the subscription update is a result of autoscaling /// - public bool Autoscaling { get; init; } + public bool Autoscaling { get; } /// /// 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; } } diff --git a/src/Core/Models/Data/Organizations/OrganizationAbility.cs b/src/Core/Models/Data/Organizations/OrganizationAbility.cs index 809f4d5d40..22bf4008eb 100644 --- a/src/Core/Models/Data/Organizations/OrganizationAbility.cs +++ b/src/Core/Models/Data/Organizations/OrganizationAbility.cs @@ -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; } } diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/AddSecretsManagerSubscriptionCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/AddSecretsManagerSubscriptionCommand.cs index 6b514089e1..3741148af4 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/AddSecretsManagerSubscriptionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/AddSecretsManagerSubscriptionCommand.cs @@ -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) { diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpdateSecretsManagerSubscriptionCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpdateSecretsManagerSubscriptionCommand.cs index 38126d17c6..036e4a1795 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpdateSecretsManagerSubscriptionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpdateSecretsManagerSubscriptionCommand.cs @@ -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); } diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs index ec4b482e4b..ab96e55f70 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs @@ -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.")); } } diff --git a/src/Core/SecretsManager/AuthorizationRequirements/SecretOperationRequirement.cs b/src/Core/SecretsManager/AuthorizationRequirements/SecretOperationRequirement.cs index c6956ed306..e737960015 100644 --- a/src/Core/SecretsManager/AuthorizationRequirements/SecretOperationRequirement.cs +++ b/src/Core/SecretsManager/AuthorizationRequirements/SecretOperationRequirement.cs @@ -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) }; } diff --git a/src/Core/SecretsManager/Models/Data/ApiKeyDetails.cs b/src/Core/SecretsManager/Models/Data/ApiKeyDetails.cs index 2c8c3d4692..f8945e9610 100644 --- a/src/Core/SecretsManager/Models/Data/ApiKeyDetails.cs +++ b/src/Core/SecretsManager/Models/Data/ApiKeyDetails.cs @@ -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) diff --git a/src/Core/SecretsManager/Queries/Projects/Interfaces/IMaxProjectsQuery.cs b/src/Core/SecretsManager/Queries/Projects/Interfaces/IMaxProjectsQuery.cs new file mode 100644 index 0000000000..e00f5ed674 --- /dev/null +++ b/src/Core/SecretsManager/Queries/Projects/Interfaces/IMaxProjectsQuery.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.SecretsManager.Queries.Projects.Interfaces; + +public interface IMaxProjectsQuery +{ + Task<(short? max, bool? atMax)> GetByOrgIdAsync(Guid organizationId); +} diff --git a/src/Core/Services/IEventService.cs b/src/Core/Services/IEventService.cs index 2288d1f926..10d8b6dbd0 100644 --- a/src/Core/Services/IEventService.cs +++ b/src/Core/Services/IEventService.cs @@ -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 secrets, EventType type, DateTime? date = null); } diff --git a/src/Core/Services/Implementations/EventService.cs b/src/Core/Services/Implementations/EventService.cs index 96bdfe4500..d5dec0a106 100644 --- a/src/Core/Services/Implementations/EventService.cs +++ b/src/Core/Services/Implementations/EventService.cs @@ -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 secrets, EventType type, DateTime? date = null) { var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); - if (!CanUseEvents(orgAbilities, secret.OrganizationId)) + var eventMessages = new List(); + + 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 GetProviderIdAsync(Guid? orgId) diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index b27e395b0d..b7424a53e1 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -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); } } diff --git a/src/Core/Services/Implementations/PolicyService.cs b/src/Core/Services/Implementations/PolicyService.cs index 64da29e4dc..5f4e680df7 100644 --- a/src/Core/Services/Implementations/PolicyService.cs +++ b/src/Core/Services/Implementations/PolicyService.cs @@ -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 && diff --git a/src/Core/Services/NoopImplementations/NoopEventService.cs b/src/Core/Services/NoopImplementations/NoopEventService.cs index 9eaefdab3a..068e03e35c 100644 --- a/src/Core/Services/NoopImplementations/NoopEventService.cs +++ b/src/Core/Services/NoopImplementations/NoopEventService.cs @@ -119,4 +119,10 @@ public class NoopEventService : IEventService { return Task.FromResult(0); } + + public Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable secrets, EventType type, + DateTime? date = null) + { + return Task.FromResult(0); + } } diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index f0c3d1833a..c128fa8e46 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -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"; /// /// 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> BuildIdentityClaims(User user, ICollection orgs, - ICollection providers, bool isPremium) + public static List> BuildIdentityClaims(User user, ICollection orgs, + ICollection providers, bool isPremium) { var claims = new List>() { @@ -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; + } } diff --git a/src/Identity/IdentityServer/ClientStore.cs b/src/Identity/IdentityServer/ClientStore.cs index f7987b9baf..e2fd33c9db 100644 --- a/src/Identity/IdentityServer/ClientStore.cs +++ b/src/Identity/IdentityServer/ClientStore.cs @@ -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, diff --git a/src/Identity/appsettings.json b/src/Identity/appsettings.json index 609a5004aa..e3626b4e16 100644 --- a/src/Identity/appsettings.json +++ b/src/Identity/appsettings.json @@ -69,7 +69,7 @@ "IpRateLimitOptions": { "EnableEndpointRateLimiting": true, "StackBlockedRequests": false, - "RealIpHeader": "CF-Connecting-IP", + "RealIpHeader": "X-Connecting-IP", "ClientIdHeader": "X-ClientId", "HttpStatusCode": 429, "IpWhitelist": [], diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationRepository.cs index 045ac881ee..702f9bea53 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationRepository.cs @@ -87,7 +87,8 @@ public class OrganizationRepository : Repository, 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)] diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTests.cs index 3f847d5f2c..0d937d3433 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTests.cs @@ -709,6 +709,69 @@ public class SecretsControllerTests : IClassFixture, 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 + { + 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>(); + Assert.NotNull(result); + Assert.NotEmpty(result!.Data); + Assert.Equal(secretIds.Count, result!.Data.Count()); + } + private async Task<(Project Project, List secretIds)> CreateSecretsAsync(Guid orgId, int numberToCreate = 3) { var project = await _projectRepository.CreateAsync(new Project diff --git a/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs index 30287eb953..32239159a6 100644 --- a/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs @@ -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(), Arg.Any(), sutProvider.GetDependency().ClientType); } + [Theory] + [BitAutoData] + public async void Create_AtMaxProjects_Throws(SutProvider sutProvider, + Guid orgId, ProjectCreateRequestModel data) + { + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), data.ToProject(orgId), + Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().GetByOrgIdAsync(orgId).Returns(((short)3, true)); + + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(orgId, data)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(Arg.Any(), Arg.Any(), sutProvider.GetDependency().ClientType); + } + [Theory] [BitAutoData] public async void Create_Success(SutProvider sutProvider, diff --git a/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs index 8afa2000a4..373bf8b183 100644 --- a/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs @@ -346,4 +346,105 @@ public class SecretsControllerTests Assert.Null(result.Error); } } + + [Theory] + [BitAutoData] + public async void GetSecretsByIds_NoSecretsFound_ThrowsNotFound(SutProvider sutProvider, + List data) + { + var (ids, request) = BuildGetSecretsRequestModel(data); + sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(new List()); + await Assert.ThrowsAsync(() => sutProvider.Sut.GetSecretsByIdsAsync(request)); + } + + [Theory] + [BitAutoData] + public async void GetSecretsByIds_SecretsFoundMisMatch_ThrowsNotFound(SutProvider sutProvider, + List data, Secret mockSecret) + { + var (ids, request) = BuildGetSecretsRequestModel(data); + ids.Add(mockSecret.Id); + sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)) + .ReturnsForAnyArgs(new List { mockSecret }); + await Assert.ThrowsAsync(() => sutProvider.Sut.GetSecretsByIdsAsync(request)); + } + + [Theory] + [BitAutoData] + public async void GetSecretsByIds_OrganizationMisMatch_ThrowsNotFound(SutProvider sutProvider, + List data) + { + var (ids, request) = BuildGetSecretsRequestModel(data); + sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); + await Assert.ThrowsAsync(() => sutProvider.Sut.GetSecretsByIdsAsync(request)); + } + + [Theory] + [BitAutoData] + public async void GetSecretsByIds_NoAccessToSecretsManager_ThrowsNotFound( + SutProvider sutProvider, List data) + { + var (ids, request) = BuildGetSecretsRequestModel(data); + var organizationId = SetOrganizations(ref data); + + sutProvider.GetDependency().AccessSecretsManager(Arg.Is(organizationId)) + .ReturnsForAnyArgs(false); + sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); + await Assert.ThrowsAsync(() => sutProvider.Sut.GetSecretsByIdsAsync(request)); + } + + [Theory] + [BitAutoData] + public async void GetSecretsByIds_AccessDenied_ThrowsNotFound(SutProvider sutProvider, + List data) + { + var (ids, request) = BuildGetSecretsRequestModel(data); + var organizationId = SetOrganizations(ref data); + + sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); + sutProvider.GetDependency().AccessSecretsManager(Arg.Is(organizationId)) + .ReturnsForAnyArgs(true); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), data.First(), + Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetSecretsByIdsAsync(request)); + } + + [Theory] + [BitAutoData] + public async void GetSecretsByIds_Success(SutProvider sutProvider, List data) + { + var (ids, request) = BuildGetSecretsRequestModel(data); + var organizationId = SetOrganizations(ref data); + + sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); + sutProvider.GetDependency().AccessSecretsManager(Arg.Is(organizationId)) + .ReturnsForAnyArgs(true); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), data.First(), + Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); + + var results = await sutProvider.Sut.GetSecretsByIdsAsync(request); + Assert.Equal(data.Count, results.Data.Count()); + } + + private static (List Ids, GetSecretsRequestModel request) BuildGetSecretsRequestModel( + IEnumerable data) + { + var ids = data.Select(s => s.Id).ToList(); + var request = new GetSecretsRequestModel { Ids = ids }; + return (ids, request); + } + + private static Guid SetOrganizations(ref List data) + { + var organizationId = data.First().OrganizationId; + foreach (var s in data) + { + s.OrganizationId = organizationId; + } + + return organizationId; + } } diff --git a/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs index 6a311a4826..29dcd117c1 100644 --- a/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs @@ -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(sa => sa.Name == data.Name), Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .AdjustServiceAccountsAsync(Arg.Any(), Arg.Any()); + .UpdateSubscriptionAsync(Arg.Any()); } [Theory] @@ -124,7 +125,12 @@ public class ServiceAccountsControllerTests .CreateAsync(Arg.Is(sa => sa.Name == data.Name), Arg.Any()); await sutProvider.GetDependency().Received(1) - .AdjustServiceAccountsAsync(Arg.Is(organization), Arg.Is(newSlotsRequired)); + .UpdateSubscriptionAsync(Arg.Is(update => + update.Autoscaling == true && + update.SmServiceAccounts == organization.SmServiceAccounts + newSlotsRequired && + !update.SmSeatsChanged && + !update.MaxAutoscaleSmSeatsChanged && + !update.MaxAutoscaleSmServiceAccountsChanged)); } [Theory] diff --git a/test/Core.Test/AutoFixture/CurrentContextFixtures.cs b/test/Core.Test/AutoFixture/CurrentContextFixtures.cs index 0f6c3ee950..a7b2f6d55b 100644 --- a/test/Core.Test/AutoFixture/CurrentContextFixtures.cs +++ b/test/Core.Test/AutoFixture/CurrentContextFixtures.cs @@ -32,7 +32,7 @@ internal class CurrentContextBuilder : ISpecimenBuilder } var obj = new Fixture().WithAutoNSubstitutions().Create(); - obj.Organizations = context.Create>(); + obj.Organizations = context.Create>(); return obj; } } diff --git a/test/Core.Test/AutoFixture/OrganizationFixtures.cs b/test/Core.Test/AutoFixture/OrganizationFixtures.cs index 8e6cfd0607..0116296d33 100644 --- a/test/Core.Test/AutoFixture/OrganizationFixtures.cs +++ b/test/Core.Test/AutoFixture/OrganizationFixtures.cs @@ -137,6 +137,7 @@ public class SecretsManagerOrganizationCustomization : ICustomization fixture.Customize(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) diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs index 9d635a5afc..a09500cf67 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs @@ -95,6 +95,38 @@ public class AddSecretsManagerSubscriptionCommandTests await VerifyDependencyNotCalledAsync(sutProvider); } + [Theory] + [BitAutoData] + public async Task SignUpAsync_ThrowsException_WhenOrganizationEnrolledInSmBeta( + SutProvider sutProvider, + Organization organization) + { + organization.UseSecretsManager = true; + organization.SecretsManagerBeta = true; + + var exception = await Assert.ThrowsAsync( + () => 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 sutProvider, + Organization organization) + { + organization.UseSecretsManager = true; + organization.SecretsManagerBeta = false; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpAsync(organization, 10, 10)); + + Assert.Contains("Organization already uses Secrets Manager", exception.Message); + await VerifyDependencyNotCalledAsync(sutProvider); + } + private static async Task VerifyDependencyNotCalledAsync(SutProvider sutProvider) { await sutProvider.GetDependency().DidNotReceive() diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs index f400115611..2834db335f 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs @@ -125,8 +125,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var update = new SecretsManagerSubscriptionUpdate(organization, autoscaling); - update.AdjustSeats(2); + var update = new SecretsManagerSubscriptionUpdate(organization, autoscaling).AdjustSeats(2); sutProvider.GetDependency().SelfHosted.Returns(true); @@ -151,6 +150,23 @@ public class UpdateSecretsManagerSubscriptionCommandTests await VerifyDependencyNotCalledAsync(sutProvider); } + [Theory] + [BitAutoData] + public async Task UpdateSubscriptionAsync_OrganizationEnrolledInSmBeta_ThrowsException( + SutProvider sutProvider, + Organization organization) + { + organization.UseSecretsManager = true; + organization.SecretsManagerBeta = true; + var update = new SecretsManagerSubscriptionUpdate(organization, false); + + var exception = await Assert.ThrowsAsync( + () => 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(() => 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(() => 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 sutProvider) { - organization.SmSeats = 9; var update = new SecretsManagerSubscriptionUpdate(organization, false) { SmSeats = 10, @@ -267,8 +279,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider 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( () => sutProvider.Sut.UpdateSubscriptionAsync(update)); @@ -283,8 +294,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests Organization organization, SutProvider sutProvider) { - var update = new SecretsManagerSubscriptionUpdate(organization, true); - update.AdjustSeats(-2); + var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustSeats(-2); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("Cannot use autoscaling to subtract seats.", exception.Message); @@ -299,8 +309,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider 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(() => 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(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("Secrets Manager seat limit has been reached.", exception.Message); @@ -375,7 +383,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests sutProvider.GetDependency().GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(8); var exception = await Assert.ThrowsAsync(() => 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 sutProvider) { - organization.SmServiceAccounts = 250; var update = new SecretsManagerSubscriptionUpdate(organization, false) { SmServiceAccounts = 300, @@ -405,11 +412,10 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider 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(() => 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 sutProvider) { - var update = new SecretsManagerSubscriptionUpdate(organization, true); - update.AdjustServiceAccounts(-2); + var update = new SecretsManagerSubscriptionUpdate(organization, true).AdjustServiceAccounts(-2); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscriptionAsync(update)); Assert.Contains("Cannot use autoscaling to subtract service accounts.", exception.Message); @@ -435,8 +440,7 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider 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(() => 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(() => 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( () => 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(() => 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(() => 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); } diff --git a/test/Core.Test/Services/OrganizationServiceTests.cs b/test/Core.Test/Services/OrganizationServiceTests.cs index 07548a16d8..01073f4d2e 100644 --- a/test/Core.Test/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/Services/OrganizationServiceTests.cs @@ -648,6 +648,7 @@ public class OrganizationServiceTests [OrganizationUser(type: OrganizationUserType.Owner, status: OrganizationUserStatusType.Confirmed)] OrganizationUser savingUser, SutProvider sutProvider) { + organization.PlanType = PlanType.EnterpriseAnnually; InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider); // Set up some invites to grant access to SM diff --git a/test/Core.Test/Utilities/CoreHelpersTests.cs b/test/Core.Test/Utilities/CoreHelpersTests.cs index f66f9ca01a..4def694042 100644 --- a/test/Core.Test/Utilities/CoreHelpersTests.cs +++ b/test/Core.Test/Utilities/CoreHelpersTests.cs @@ -273,8 +273,8 @@ public class CoreHelpersTests { "sstamp", user.SecurityStamp }, }.ToList(); - var actual = CoreHelpers.BuildIdentityClaims(user, Array.Empty(), - Array.Empty(), isPremium); + var actual = CoreHelpers.BuildIdentityClaims(user, Array.Empty(), + Array.Empty(), isPremium); foreach (var claim in expected) { @@ -289,23 +289,23 @@ public class CoreHelpersTests var fixture = new Fixture().WithAutoNSubstitutions(); foreach (var organizationUserType in Enum.GetValues().Except(new[] { OrganizationUserType.Custom })) { - var org = fixture.Create(); + var org = fixture.Create(); org.Type = organizationUserType; var expected = new KeyValuePair($"org{organizationUserType.ToString().ToLower()}", org.Id.ToString()); - var actual = CoreHelpers.BuildIdentityClaims(user, new[] { org }, Array.Empty(), false); + var actual = CoreHelpers.BuildIdentityClaims(user, new[] { org }, Array.Empty(), 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(), false); + var actual = CoreHelpers.BuildIdentityClaims(user, new[] { org }, Array.Empty(), false); foreach (var (permitted, claimName) in org.Permissions.ClaimsMap) { var claim = new KeyValuePair(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(); + var providers = new List(); foreach (var providerUserType in Enum.GetValues()) { - var provider = fixture.Create(); + var provider = fixture.Create(); provider.Type = providerUserType; providers.Add(provider); } @@ -357,7 +357,7 @@ public class CoreHelpersTests } } - var actual = CoreHelpers.BuildIdentityClaims(user, Array.Empty(), providers, false); + var actual = CoreHelpers.BuildIdentityClaims(user, Array.Empty(), 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)); + } } diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs index e67217d085..8e9e82c6b7 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs @@ -530,6 +530,7 @@ public class IdentityServerSsoTests var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", + UsePolicies = true }); var organizationUserRepository = factory.Services.GetRequiredService(); diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs index b1e74bd17d..cae6ed172c 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs @@ -556,7 +556,7 @@ public class IdentityServerTests : IClassFixture var organizationUserRepository = _factory.Services.GetService(); var policyRepository = _factory.Services.GetService(); - 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); diff --git a/util/Migrator/DbScripts/2023-08-09_00_OrgAbilitiesUsePolicies.sql b/util/Migrator/DbScripts/2023-08-09_00_OrgAbilitiesUsePolicies.sql new file mode 100644 index 0000000000..f19c189fda --- /dev/null +++ b/util/Migrator/DbScripts/2023-08-09_00_OrgAbilitiesUsePolicies.sql @@ -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 diff --git a/util/Migrator/DbScripts_data_migration/2023-05-16_00_ClientSecretHashDataMigration.sql b/util/Migrator/DbScripts/2023-08-10_00_ClientSecretHashDataMigration.sql similarity index 88% rename from util/Migrator/DbScripts_data_migration/2023-05-16_00_ClientSecretHashDataMigration.sql rename to util/Migrator/DbScripts/2023-08-10_00_ClientSecretHashDataMigration.sql index 5d8261f930..66869d48b7 100644 --- a/util/Migrator/DbScripts_data_migration/2023-05-16_00_ClientSecretHashDataMigration.sql +++ b/util/Migrator/DbScripts/2023-08-10_00_ClientSecretHashDataMigration.sql @@ -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 diff --git a/util/Migrator/DbScripts_future/2023-06-FutureMigration.sql b/util/Migrator/DbScripts/2023-08-10_01_RemoveClientSecret.sql similarity index 96% rename from util/Migrator/DbScripts_future/2023-06-FutureMigration.sql rename to util/Migrator/DbScripts/2023-08-10_01_RemoveClientSecret.sql index 273c4625f1..f9d4b82185 100644 --- a/util/Migrator/DbScripts_future/2023-06-FutureMigration.sql +++ b/util/Migrator/DbScripts/2023-08-10_01_RemoveClientSecret.sql @@ -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,