diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 694e9048a7..a7717be4e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: persist-credentials: false - name: Set up .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 - name: Verify format run: dotnet format --verify-no-changes @@ -39,8 +39,7 @@ jobs: build-artifacts: name: Build Docker images runs-on: ubuntu-22.04 - needs: - - lint + needs: lint outputs: has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} permissions: @@ -120,7 +119,7 @@ jobs: fi - name: Set up .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 - name: Set up Node uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 @@ -271,7 +270,7 @@ jobs: output-format: sarif - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 + uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} @@ -295,7 +294,7 @@ jobs: persist-credentials: false - name: Set up .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main @@ -401,8 +400,7 @@ jobs: build-mssqlmigratorutility: name: Build MSSQL migrator utility runs-on: ubuntu-22.04 - needs: - - lint + needs: lint defaults: run: shell: bash @@ -422,7 +420,7 @@ jobs: persist-credentials: false - name: Set up .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 - name: Print environment run: | @@ -452,14 +450,13 @@ jobs: path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility if-no-files-found: error - self-host-build: - name: Trigger self-host build + bitwarden-lite-build: + name: Trigger Bitwarden lite build if: | github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') runs-on: ubuntu-22.04 - needs: - - build-artifacts + needs: build-artifacts permissions: id-token: write steps: @@ -505,11 +502,10 @@ jobs: }); trigger-k8s-deploy: - name: Trigger k8s deploy + name: Trigger K8s deploy if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 - needs: - - build-artifacts + needs: build-artifacts permissions: id-token: write steps: @@ -539,7 +535,7 @@ jobs: owner: ${{ github.repository_owner }} repositories: devops - - name: Trigger k8s deploy + - name: Trigger K8s deploy uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.app-token.outputs.token }} @@ -557,8 +553,7 @@ jobs: setup-ephemeral-environment: name: Setup Ephemeral Environment - needs: - - build-artifacts + needs: build-artifacts if: | needs.build-artifacts.outputs.has_secrets == 'true' && github.event_name == 'pull_request' @@ -581,7 +576,7 @@ jobs: - build-artifacts - upload - build-mssqlmigratorutility - - self-host-build + - bitwarden-lite-build - trigger-k8s-deploy permissions: id-token: write diff --git a/.github/workflows/cleanup-after-pr.yml b/.github/workflows/cleanup-after-pr.yml deleted file mode 100644 index 4e59f1fa96..0000000000 --- a/.github/workflows/cleanup-after-pr.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Container registry cleanup - -on: - pull_request: - types: [closed] - -env: - _AZ_REGISTRY: "bitwardenprod.azurecr.io" - -jobs: - build-docker: - name: Remove branch-specific Docker images - runs-on: ubuntu-22.04 - permissions: - id-token: write - steps: - - name: Log in to Azure - uses: bitwarden/gh-actions/azure-login@main - with: - subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - tenant_id: ${{ secrets.AZURE_TENANT_ID }} - client_id: ${{ secrets.AZURE_CLIENT_ID }} - - - name: Log in to Azure ACR - run: az acr login -n "$_AZ_REGISTRY" --only-show-errors - - ########## Remove Docker images ########## - - name: Remove the Docker image from ACR - env: - REF: ${{ github.event.pull_request.head.ref }} - SERVICES: | - services: - - Admin - - Api - - Attachments - - Events - - EventsProcessor - - Icons - - Identity - - K8S-Proxy - - MsSql - - Nginx - - Notifications - - Server - - Setup - - Sso - run: | - for SERVICE in $(echo "${SERVICES}" | yq e ".services[]" - ) - do - SERVICE_NAME=$(echo "$SERVICE" | awk '{print tolower($0)}') - IMAGE_TAG=$(echo "${REF}" | sed "s#/#-#g") # slash safe branch name - - echo "[*] Checking if remote exists: $_AZ_REGISTRY/$SERVICE_NAME:$IMAGE_TAG" - TAG_EXISTS=$( - az acr repository show-tags --name "$_AZ_REGISTRY" --repository "$SERVICE_NAME" \ - | jq --arg TAG "$IMAGE_TAG" -e '. | any(. == $TAG)' - ) - - if [[ "$TAG_EXISTS" == "true" ]]; then - echo "[*] Tag exists. Removing tag" - az acr repository delete --name "$_AZ_REGISTRY" --image "$SERVICE_NAME:$IMAGE_TAG" --yes - else - echo "[*] Tag does not exist. No action needed" - fi - done - - - name: Log out of Docker - run: docker logout - - - name: Log out from Azure - uses: bitwarden/gh-actions/azure-logout@main diff --git a/.github/workflows/review-code.yml b/.github/workflows/review-code.yml index 0e0597fccf..000f402096 100644 --- a/.github/workflows/review-code.yml +++ b/.github/workflows/review-code.yml @@ -2,7 +2,7 @@ name: Code Review on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, synchronize, reopened] permissions: {} diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 0fbdb5d069..4630c18e40 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -49,7 +49,7 @@ jobs: persist-credentials: false - name: Set up .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 - name: Restore tools run: dotnet tool restore @@ -183,7 +183,7 @@ jobs: persist-credentials: false - name: Set up .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 - name: Print environment run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 550d943dbc..a6d07bb650 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,10 +32,10 @@ jobs: persist-credentials: false - name: Set up .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 - name: Install rust - uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable + uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable with: toolchain: stable diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs index 7141f8429d..afbef321a9 100644 --- a/bitwarden_license/src/Sso/Controllers/AccountController.cs +++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs @@ -680,22 +680,10 @@ public class AccountController : Controller ApiKey = CoreHelpers.SecureRandomString(30) }; - /* - The feature flag is checked here so that we can send the new MJML welcome email templates. - The other organization invites flows have an OrganizationUser allowing the RegisterUserCommand the ability - to fetch the Organization. The old method RegisterUser(User) here does not have that context, so we need - to use a new method RegisterSSOAutoProvisionedUserAsync(User, Organization) to send the correct email. - [PM-28057]: Prefer RegisterSSOAutoProvisionedUserAsync for SSO auto-provisioned users. - TODO: Remove Feature flag: PM-28221 - */ - if (_featureService.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)) - { - await _registerUserCommand.RegisterSSOAutoProvisionedUserAsync(newUser, organization); - } - else - { - await _registerUserCommand.RegisterUser(newUser); - } + // Always use RegisterSSOAutoProvisionedUserAsync to ensure organization context is available + // for domain validation (BlockClaimedDomainAccountCreation policy) and welcome emails. + // The feature flag logic for welcome email templates is handled internally by RegisterUserCommand. + await _registerUserCommand.RegisterSSOAutoProvisionedUserAsync(newUser, organization); // If the organization has 2fa policy enabled, make sure to default jit user 2fa to email var twoFactorPolicy = diff --git a/bitwarden_license/test/SSO.Test/Controllers/AccountControllerTest.cs b/bitwarden_license/test/SSO.Test/Controllers/AccountControllerTest.cs index b276174814..66cb018923 100644 --- a/bitwarden_license/test/SSO.Test/Controllers/AccountControllerTest.cs +++ b/bitwarden_license/test/SSO.Test/Controllers/AccountControllerTest.cs @@ -6,7 +6,6 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; -using Bit.Core.Auth.UserFeatures.Registration; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; @@ -21,7 +20,6 @@ using Duende.IdentityServer.Models; using Duende.IdentityServer.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using NSubstitute; @@ -1013,133 +1011,6 @@ public class AccountControllerTest } } - [Theory, BitAutoData] - public async Task AutoProvisionUserAsync_WithFeatureFlagEnabled_CallsRegisterSSOAutoProvisionedUser( - SutProvider sutProvider) - { - // Arrange - var orgId = Guid.NewGuid(); - var providerUserId = "ext-new-user"; - var email = "newuser@example.com"; - var organization = new Organization { Id = orgId, Name = "Test Org", Seats = null }; - - // No existing user (JIT provisioning scenario) - sutProvider.GetDependency().GetByEmailAsync(email).Returns((User?)null); - sutProvider.GetDependency().GetByIdAsync(orgId).Returns(organization); - sutProvider.GetDependency().GetByOrganizationEmailAsync(orgId, email) - .Returns((OrganizationUser?)null); - - // Feature flag enabled - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates) - .Returns(true); - - // Mock the RegisterSSOAutoProvisionedUserAsync to return success - sutProvider.GetDependency() - .RegisterSSOAutoProvisionedUserAsync(Arg.Any(), Arg.Any()) - .Returns(IdentityResult.Success); - - var claims = new[] - { - new Claim(JwtClaimTypes.Email, email), - new Claim(JwtClaimTypes.Name, "New User") - } as IEnumerable; - var config = new SsoConfigurationData(); - - var method = typeof(AccountController).GetMethod( - "CreateUserAndOrgUserConditionallyAsync", - BindingFlags.Instance | BindingFlags.NonPublic); - Assert.NotNull(method); - - // Act - var task = (Task<(User user, Organization organization, OrganizationUser orgUser)>)method!.Invoke( - sutProvider.Sut, - new object[] - { - orgId.ToString(), - providerUserId, - claims, - null!, - config - })!; - - var result = await task; - - // Assert - await sutProvider.GetDependency().Received(1) - .RegisterSSOAutoProvisionedUserAsync( - Arg.Is(u => u.Email == email && u.Name == "New User"), - Arg.Is(o => o.Id == orgId && o.Name == "Test Org")); - - Assert.NotNull(result.user); - Assert.Equal(email, result.user.Email); - Assert.Equal(organization.Id, result.organization.Id); - } - - [Theory, BitAutoData] - public async Task AutoProvisionUserAsync_WithFeatureFlagDisabled_CallsRegisterUserInstead( - SutProvider sutProvider) - { - // Arrange - var orgId = Guid.NewGuid(); - var providerUserId = "ext-legacy-user"; - var email = "legacyuser@example.com"; - var organization = new Organization { Id = orgId, Name = "Test Org", Seats = null }; - - // No existing user (JIT provisioning scenario) - sutProvider.GetDependency().GetByEmailAsync(email).Returns((User?)null); - sutProvider.GetDependency().GetByIdAsync(orgId).Returns(organization); - sutProvider.GetDependency().GetByOrganizationEmailAsync(orgId, email) - .Returns((OrganizationUser?)null); - - // Feature flag disabled - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates) - .Returns(false); - - // Mock the RegisterUser to return success - sutProvider.GetDependency() - .RegisterUser(Arg.Any()) - .Returns(IdentityResult.Success); - - var claims = new[] - { - new Claim(JwtClaimTypes.Email, email), - new Claim(JwtClaimTypes.Name, "Legacy User") - } as IEnumerable; - var config = new SsoConfigurationData(); - - var method = typeof(AccountController).GetMethod( - "CreateUserAndOrgUserConditionallyAsync", - BindingFlags.Instance | BindingFlags.NonPublic); - Assert.NotNull(method); - - // Act - var task = (Task<(User user, Organization organization, OrganizationUser orgUser)>)method!.Invoke( - sutProvider.Sut, - new object[] - { - orgId.ToString(), - providerUserId, - claims, - null!, - config - })!; - - var result = await task; - - // Assert - await sutProvider.GetDependency().Received(1) - .RegisterUser(Arg.Is(u => u.Email == email && u.Name == "Legacy User")); - - // Verify the new method was NOT called - await sutProvider.GetDependency().DidNotReceive() - .RegisterSSOAutoProvisionedUserAsync(Arg.Any(), Arg.Any()); - - Assert.NotNull(result.user); - Assert.Equal(email, result.user.Email); - } - [Theory, BitAutoData] public void ExternalChallenge_WithMatchingOrgId_Succeeds( SutProvider sutProvider, diff --git a/dev/verify_migrations.ps1 b/dev/verify_migrations.ps1 index d63c34f2bd..ad0d34cef1 100644 --- a/dev/verify_migrations.ps1 +++ b/dev/verify_migrations.ps1 @@ -41,7 +41,7 @@ $migrationPath = "util/Migrator/DbScripts" # Get list of migrations from base reference try { - $baseMigrations = git ls-tree -r --name-only $BaseRef -- "$migrationPath/*.sql" 2>$null | Sort-Object + $baseMigrations = git ls-tree -r --name-only $BaseRef -- "$migrationPath/" 2>$null | Where-Object { $_ -like "*.sql" } | Sort-Object if ($LASTEXITCODE -ne 0) { Write-Host "Warning: Could not retrieve migrations from base reference '$BaseRef'" $baseMigrations = @() @@ -53,7 +53,7 @@ catch { } # Get list of migrations from current reference -$currentMigrations = git ls-tree -r --name-only $CurrentRef -- "$migrationPath/*.sql" | Sort-Object +$currentMigrations = git ls-tree -r --name-only $CurrentRef -- "$migrationPath/" | Where-Object { $_ -like "*.sql" } | Sort-Object # Find added migrations $addedMigrations = $currentMigrations | Where-Object { $_ -notin $baseMigrations } diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index cd370e3898..1dbab08ca6 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -496,6 +496,7 @@ public class OrganizationsController : Controller organization.UseOrganizationDomains = model.UseOrganizationDomains; organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies; organization.UseAutomaticUserConfirmation = model.UseAutomaticUserConfirmation; + organization.UseDisableSmAdsForUsers = model.UseDisableSmAdsForUsers; organization.UsePhishingBlocker = model.UsePhishingBlocker; //secrets diff --git a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs index 4fff85e1e8..77a8496e5b 100644 --- a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs @@ -107,6 +107,7 @@ public class OrganizationEditModel : OrganizationViewModel MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts; UseOrganizationDomains = org.UseOrganizationDomains; UseAutomaticUserConfirmation = org.UseAutomaticUserConfirmation; + UseDisableSmAdsForUsers = org.UseDisableSmAdsForUsers; UsePhishingBlocker = org.UsePhishingBlocker; _plans = plans; @@ -196,6 +197,8 @@ public class OrganizationEditModel : OrganizationViewModel public int? MaxAutoscaleSmServiceAccounts { get; set; } [Display(Name = "Use Organization Domains")] public bool UseOrganizationDomains { get; set; } + [Display(Name = "Disable SM Ads For Users")] + public new bool UseDisableSmAdsForUsers { get; set; } [Display(Name = "Automatic User Confirmation")] public bool UseAutomaticUserConfirmation { get; set; } @@ -330,6 +333,7 @@ public class OrganizationEditModel : OrganizationViewModel existingOrganization.SmServiceAccounts = SmServiceAccounts; existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts; existingOrganization.UseOrganizationDomains = UseOrganizationDomains; + existingOrganization.UseDisableSmAdsForUsers = UseDisableSmAdsForUsers; existingOrganization.UsePhishingBlocker = UsePhishingBlocker; return existingOrganization; } diff --git a/src/Admin/AdminConsole/Models/OrganizationViewModel.cs b/src/Admin/AdminConsole/Models/OrganizationViewModel.cs index 457686be53..652535b592 100644 --- a/src/Admin/AdminConsole/Models/OrganizationViewModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationViewModel.cs @@ -76,6 +76,7 @@ public class OrganizationViewModel public bool UseSecretsManager => Organization.UseSecretsManager; public bool UseRiskInsights => Organization.UseRiskInsights; public bool UsePhishingBlocker => Organization.UsePhishingBlocker; + public bool UseDisableSmAdsForUsers => Organization.UseDisableSmAdsForUsers; public IEnumerable OwnersDetails { get; set; } public IEnumerable AdminsDetails { get; set; } } diff --git a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml index b22859ed60..0b60d99e3c 100644 --- a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml +++ b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml @@ -185,6 +185,13 @@ + @if (FeatureService.IsEnabled(FeatureFlagKeys.SM1719_RemoveSecretsManagerAds)) + { +
+ + +
+ }

Access Intelligence

diff --git a/src/Api/AdminConsole/Authorization/Requirements/MemberRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/MemberRequirement.cs new file mode 100644 index 0000000000..ed205524d1 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Requirements/MemberRequirement.cs @@ -0,0 +1,14 @@ +using Bit.Core.Context; + +namespace Bit.Api.AdminConsole.Authorization.Requirements; + +/// +/// Requires that the user is a member of the organization. +/// +public class MemberRequirement : IOrganizationRequirement +{ + public Task AuthorizeAsync( + CurrentContextOrganization? organizationClaims, + Func> isProviderUserForOrg) + => Task.FromResult(organizationClaims is not null); +} diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index a380d2f0d9..5cdd857f3f 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -19,6 +19,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimed using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; @@ -81,6 +82,7 @@ public class OrganizationUsersController : BaseAdminConsoleController private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand; private readonly V1_RevokeOrganizationUserCommand _revokeOrganizationUserCommand; private readonly IAdminRecoverAccountCommand _adminRecoverAccountCommand; + private readonly ISelfRevokeOrganizationUserCommand _selfRevokeOrganizationUserCommand; public OrganizationUsersController(IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, @@ -112,7 +114,8 @@ public class OrganizationUsersController : BaseAdminConsoleController IBulkResendOrganizationInvitesCommand bulkResendOrganizationInvitesCommand, IAdminRecoverAccountCommand adminRecoverAccountCommand, IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand, - V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand revokeOrganizationUserCommandVNext) + V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand revokeOrganizationUserCommandVNext, + ISelfRevokeOrganizationUserCommand selfRevokeOrganizationUserCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -145,6 +148,7 @@ public class OrganizationUsersController : BaseAdminConsoleController _initPendingOrganizationCommand = initPendingOrganizationCommand; _revokeOrganizationUserCommand = revokeOrganizationUserCommand; _adminRecoverAccountCommand = adminRecoverAccountCommand; + _selfRevokeOrganizationUserCommand = selfRevokeOrganizationUserCommand; } [HttpGet("{id}")] @@ -635,6 +639,20 @@ public class OrganizationUsersController : BaseAdminConsoleController await RestoreOrRevokeUserAsync(orgId, id, _revokeOrganizationUserCommand.RevokeUserAsync); } + [HttpPut("revoke-self")] + [Authorize] + public async Task RevokeSelfAsync(Guid orgId) + { + var userId = _userService.GetProperUserId(User); + if (!userId.HasValue) + { + throw new UnauthorizedAccessException(); + } + + var result = await _selfRevokeOrganizationUserCommand.SelfRevokeUserAsync(orgId, userId.Value); + return Handle(result); + } + [HttpPatch("{id}/revoke")] [Obsolete("This endpoint is deprecated. Use PUT method instead")] [Authorize] diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index ae1d12e887..bce0332d67 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -7,7 +7,6 @@ using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Response.Helpers; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; @@ -212,7 +211,6 @@ public class PoliciesController : Controller } [HttpPut("{type}/vnext")] - [RequireFeatureAttribute(FeatureFlagKeys.CreateDefaultLocation)] [Authorize] public async Task PutVNext(Guid orgId, PolicyType type, [FromBody] SavePolicyRequest model) { diff --git a/src/Api/AdminConsole/Models/Response/BaseProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/BaseProfileOrganizationResponseModel.cs index f5ef468b4e..c3378cd11d 100644 --- a/src/Api/AdminConsole/Models/Response/BaseProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/BaseProfileOrganizationResponseModel.cs @@ -48,6 +48,7 @@ public abstract class BaseProfileOrganizationResponseModel : ResponseModel UseAutomaticUserConfirmation = organizationDetails.UseAutomaticUserConfirmation; UseSecretsManager = organizationDetails.UseSecretsManager; UsePhishingBlocker = organizationDetails.UsePhishingBlocker; + UseDisableSMAdsForUsers = organizationDetails.UseDisableSMAdsForUsers; UsePasswordManager = organizationDetails.UsePasswordManager; SelfHost = organizationDetails.SelfHost; Seats = organizationDetails.Seats; @@ -100,6 +101,7 @@ public abstract class BaseProfileOrganizationResponseModel : ResponseModel public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } public bool UseAutomaticUserConfirmation { get; set; } + public bool UseDisableSMAdsForUsers { get; set; } public bool UsePhishingBlocker { get; set; } public bool SelfHost { get; set; } public int? Seats { get; set; } diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs index 9a3543f4bb..46c5a50b6c 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs @@ -74,6 +74,7 @@ public class OrganizationResponseModel : ResponseModel UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation; + UseDisableSmAdsForUsers = organization.UseDisableSmAdsForUsers; UsePhishingBlocker = organization.UsePhishingBlocker; } @@ -124,6 +125,7 @@ public class OrganizationResponseModel : ResponseModel public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } public bool UseAutomaticUserConfirmation { get; set; } + public bool UseDisableSmAdsForUsers { get; set; } public bool UsePhishingBlocker { get; set; } } diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 839d00f7a1..1223e2f05f 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -38,7 +38,9 @@ public class AccountsController : Controller private readonly IProviderUserRepository _providerUserRepository; private readonly IUserService _userService; private readonly IPolicyService _policyService; + private readonly ISetInitialMasterPasswordCommandV1 _setInitialMasterPasswordCommandV1; private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; + private readonly ITdeSetPasswordCommand _tdeSetPasswordCommand; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IFeatureService _featureService; @@ -54,6 +56,8 @@ public class AccountsController : Controller IUserService userService, IPolicyService policyService, ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand, + ISetInitialMasterPasswordCommandV1 setInitialMasterPasswordCommandV1, + ITdeSetPasswordCommand tdeSetPasswordCommand, ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IFeatureService featureService, @@ -69,6 +73,8 @@ public class AccountsController : Controller _userService = userService; _policyService = policyService; _setInitialMasterPasswordCommand = setInitialMasterPasswordCommand; + _setInitialMasterPasswordCommandV1 = setInitialMasterPasswordCommandV1; + _tdeSetPasswordCommand = tdeSetPasswordCommand; _tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _featureService = featureService; @@ -208,7 +214,7 @@ public class AccountsController : Controller } [HttpPost("set-password")] - public async Task PostSetPasswordAsync([FromBody] SetPasswordRequestModel model) + public async Task PostSetPasswordAsync([FromBody] SetInitialPasswordRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -216,33 +222,48 @@ public class AccountsController : Controller throw new UnauthorizedAccessException(); } - try + if (model.IsV2Request()) { - user = model.ToUser(user); + if (model.IsTdeSetPasswordRequest()) + { + await _tdeSetPasswordCommand.SetMasterPasswordAsync(user, model.ToData()); + } + else + { + await _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(user, model.ToData()); + } } - catch (Exception e) + else { - ModelState.AddModelError(string.Empty, e.Message); + // TODO removed with https://bitwarden.atlassian.net/browse/PM-27327 + try + { + user = model.ToUser(user); + } + catch (Exception e) + { + ModelState.AddModelError(string.Empty, e.Message); + throw new BadRequestException(ModelState); + } + + var result = await _setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync( + user, + model.MasterPasswordHash, + model.Key, + model.OrgIdentifier); + + if (result.Succeeded) + { + return; + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + throw new BadRequestException(ModelState); } - - var result = await _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync( - user, - model.MasterPasswordHash, - model.Key, - model.OrgIdentifier); - - if (result.Succeeded) - { - return; - } - - foreach (var error in result.Errors) - { - ModelState.AddModelError(string.Empty, error.Description); - } - - throw new BadRequestException(ModelState); } [HttpPost("verify-password")] diff --git a/src/Api/Auth/Controllers/WebAuthnController.cs b/src/Api/Auth/Controllers/WebAuthnController.cs index 60b8621c5e..833087e99c 100644 --- a/src/Api/Auth/Controllers/WebAuthnController.cs +++ b/src/Api/Auth/Controllers/WebAuthnController.cs @@ -21,7 +21,6 @@ using Microsoft.AspNetCore.Mvc; namespace Bit.Api.Auth.Controllers; [Route("webauthn")] -[Authorize(Policies.Web)] public class WebAuthnController : Controller { private readonly IUserService _userService; @@ -62,6 +61,7 @@ public class WebAuthnController : Controller _featureService = featureService; } + [Authorize(Policies.Web)] [HttpGet("")] public async Task> Get() { @@ -71,6 +71,7 @@ public class WebAuthnController : Controller return new ListResponseModel(credentials.Select(c => new WebAuthnCredentialResponseModel(c))); } + [Authorize(Policies.Application)] [HttpPost("attestation-options")] public async Task AttestationOptions([FromBody] SecretVerificationRequestModel model) { @@ -88,6 +89,7 @@ public class WebAuthnController : Controller }; } + [Authorize(Policies.Web)] [HttpPost("assertion-options")] public async Task AssertionOptions([FromBody] SecretVerificationRequestModel model) { @@ -104,6 +106,7 @@ public class WebAuthnController : Controller }; } + [Authorize(Policies.Application)] [HttpPost("")] public async Task Post([FromBody] WebAuthnLoginCredentialCreateRequestModel model) { @@ -149,6 +152,7 @@ public class WebAuthnController : Controller } } + [Authorize(Policies.Application)] [HttpPut()] public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateRequestModel model) { @@ -172,6 +176,7 @@ public class WebAuthnController : Controller await _credentialRepository.UpdateAsync(credential); } + [Authorize(Policies.Web)] [HttpPost("{id}/delete")] public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model) { diff --git a/src/Api/Auth/Models/Request/Accounts/SetInitialPasswordRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/SetInitialPasswordRequestModel.cs new file mode 100644 index 0000000000..55ffdca94b --- /dev/null +++ b/src/Api/Auth/Models/Request/Accounts/SetInitialPasswordRequestModel.cs @@ -0,0 +1,160 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Api.KeyManagement.Models.Requests; +using Bit.Core.Auth.Models.Api.Request.Accounts; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.KeyManagement.Models.Api.Request; +using Bit.Core.Utilities; + +namespace Bit.Api.Auth.Models.Request.Accounts; + +public class SetInitialPasswordRequestModel : IValidatableObject +{ + // TODO will be removed with https://bitwarden.atlassian.net/browse/PM-27327 + [Obsolete("Use MasterPasswordAuthentication instead")] + [StringLength(300)] + public string? MasterPasswordHash { get; set; } + + [Obsolete("Use MasterPasswordUnlock instead")] + public string? Key { get; set; } + + [Obsolete("Use AccountKeys instead")] + public KeysRequestModel? Keys { get; set; } + + [Obsolete("Use MasterPasswordAuthentication instead")] + public KdfType? Kdf { get; set; } + + [Obsolete("Use MasterPasswordAuthentication instead")] + public int? KdfIterations { get; set; } + + [Obsolete("Use MasterPasswordAuthentication instead")] + public int? KdfMemory { get; set; } + + [Obsolete("Use MasterPasswordAuthentication instead")] + public int? KdfParallelism { get; set; } + + public MasterPasswordAuthenticationDataRequestModel? MasterPasswordAuthentication { get; set; } + public MasterPasswordUnlockDataRequestModel? MasterPasswordUnlock { get; set; } + public AccountKeysRequestModel? AccountKeys { get; set; } + + [StringLength(50)] + public string? MasterPasswordHint { get; set; } + + [Required] + public required string OrgIdentifier { get; set; } + + // TODO removed with https://bitwarden.atlassian.net/browse/PM-27327 + public User ToUser(User existingUser) + { + existingUser.MasterPasswordHint = MasterPasswordHint; + existingUser.Kdf = Kdf!.Value; + existingUser.KdfIterations = KdfIterations!.Value; + existingUser.KdfMemory = KdfMemory; + existingUser.KdfParallelism = KdfParallelism; + existingUser.Key = Key; + Keys?.ToUser(existingUser); + return existingUser; + } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (IsV2Request()) + { + // V2 registration + + // Validate Kdf + var authenticationKdf = MasterPasswordAuthentication!.Kdf.ToData(); + var unlockKdf = MasterPasswordUnlock!.Kdf.ToData(); + + // Currently, KDF settings are not saved separately for authentication and unlock and must therefore be equal + if (!authenticationKdf.Equals(unlockKdf)) + { + yield return new ValidationResult("KDF settings must be equal for authentication and unlock.", + [$"{nameof(MasterPasswordAuthentication)}.{nameof(MasterPasswordAuthenticationDataRequestModel.Kdf)}", + $"{nameof(MasterPasswordUnlock)}.{nameof(MasterPasswordUnlockDataRequestModel.Kdf)}"]); + } + + var authenticationValidationErrors = KdfSettingsValidator.Validate(authenticationKdf).ToList(); + if (authenticationValidationErrors.Count != 0) + { + yield return authenticationValidationErrors.First(); + } + + var unlockValidationErrors = KdfSettingsValidator.Validate(unlockKdf).ToList(); + if (unlockValidationErrors.Count != 0) + { + yield return unlockValidationErrors.First(); + } + + yield break; + } + + // V1 registration + // TODO removed with https://bitwarden.atlassian.net/browse/PM-27327 + if (string.IsNullOrEmpty(MasterPasswordHash)) + { + yield return new ValidationResult("MasterPasswordHash must be supplied."); + } + + if (string.IsNullOrEmpty(Key)) + { + yield return new ValidationResult("Key must be supplied."); + } + + if (Kdf == null) + { + yield return new ValidationResult("Kdf must be supplied."); + yield break; + } + + if (KdfIterations == null) + { + yield return new ValidationResult("KdfIterations must be supplied."); + yield break; + } + + if (Kdf == KdfType.Argon2id) + { + if (KdfMemory == null) + { + yield return new ValidationResult("KdfMemory must be supplied when Kdf is Argon2id."); + } + + if (KdfParallelism == null) + { + yield return new ValidationResult("KdfParallelism must be supplied when Kdf is Argon2id."); + } + } + + var validationErrors = KdfSettingsValidator + .Validate(Kdf!.Value, KdfIterations!.Value, KdfMemory, KdfParallelism).ToList(); + if (validationErrors.Count != 0) + { + yield return validationErrors.First(); + } + } + + public bool IsV2Request() + { + // AccountKeys can be null for TDE users, so we don't check that here + return MasterPasswordAuthentication != null && MasterPasswordUnlock != null; + } + + public bool IsTdeSetPasswordRequest() + { + return AccountKeys == null; + } + + public SetInitialMasterPasswordDataModel ToData() + { + return new SetInitialMasterPasswordDataModel + { + MasterPasswordAuthentication = MasterPasswordAuthentication!.ToData(), + MasterPasswordUnlock = MasterPasswordUnlock!.ToData(), + OrgSsoIdentifier = OrgIdentifier, + AccountKeys = AccountKeys?.ToAccountKeysData(), + MasterPasswordHint = MasterPasswordHint + }; + } +} diff --git a/src/Api/Auth/Models/Request/Accounts/SetPasswordRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/SetPasswordRequestModel.cs deleted file mode 100644 index 0d809c6c11..0000000000 --- a/src/Api/Auth/Models/Request/Accounts/SetPasswordRequestModel.cs +++ /dev/null @@ -1,40 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.ComponentModel.DataAnnotations; -using Bit.Core.Auth.Models.Api.Request.Accounts; -using Bit.Core.Entities; -using Bit.Core.Enums; - -namespace Bit.Api.Auth.Models.Request.Accounts; - -public class SetPasswordRequestModel -{ - [Required] - [StringLength(300)] - public string MasterPasswordHash { get; set; } - [Required] - public string Key { get; set; } - [StringLength(50)] - public string MasterPasswordHint { get; set; } - public KeysRequestModel Keys { get; set; } - [Required] - public KdfType Kdf { get; set; } - [Required] - public int KdfIterations { get; set; } - public int? KdfMemory { get; set; } - public int? KdfParallelism { get; set; } - public string OrgIdentifier { get; set; } - - public User ToUser(User existingUser) - { - existingUser.MasterPasswordHint = MasterPasswordHint; - existingUser.Kdf = Kdf; - existingUser.KdfIterations = KdfIterations; - existingUser.KdfMemory = KdfMemory; - existingUser.KdfParallelism = KdfParallelism; - existingUser.Key = Key; - Keys?.ToUser(existingUser); - return existingUser; - } -} diff --git a/src/Api/Billing/Controllers/AccountsController.cs b/src/Api/Billing/Controllers/AccountsController.cs index 5d3e095fdd..e3410de503 100644 --- a/src/Api/Billing/Controllers/AccountsController.cs +++ b/src/Api/Billing/Controllers/AccountsController.cs @@ -3,13 +3,10 @@ using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Response; using Bit.Api.Utilities; using Bit.Core; -using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Services; using Bit.Core.Exceptions; -using Bit.Core.KeyManagement.Queries.Interfaces; -using Bit.Core.Models.Business; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; @@ -22,59 +19,9 @@ namespace Bit.Api.Billing.Controllers; [Authorize("Application")] public class AccountsController( IUserService userService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IUserAccountKeysQuery userAccountKeysQuery, IFeatureService featureService, ILicensingService licensingService) : Controller { - // TODO: Remove when pm-24996-implement-upgrade-from-free-dialog is removed - [HttpPost("premium")] - public async Task PostPremiumAsync( - PremiumRequestModel model, - [FromServices] GlobalSettings globalSettings) - { - var user = await userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var valid = model.Validate(globalSettings); - UserLicense? license = null; - if (valid && globalSettings.SelfHosted) - { - license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); - } - - if (!valid && !globalSettings.SelfHosted && string.IsNullOrWhiteSpace(model.Country)) - { - throw new BadRequestException("Country is required."); - } - - if (!valid || (globalSettings.SelfHosted && license == null)) - { - throw new BadRequestException("Invalid license."); - } - - var result = await userService.SignUpPremiumAsync(user, model.PaymentToken, - model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license, - new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode }); - - var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); - var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user); - var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); - var accountKeys = await userAccountKeysQuery.Run(user); - - var profile = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled, - userHasPremiumFromOrganization, organizationIdsClaimingActiveUser); - return new PaymentResponseModel - { - UserProfile = profile, - PaymentIntentClientSecret = result.Item2, - Success = result.Item1 - }; - } - // TODO: Migrate to Query / AccountBillingVNextController as part of Premium -> Organization upgrade work. [HttpGet("subscription")] public async Task GetSubscriptionAsync( diff --git a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs index b01b629e4f..d1e9b9206a 100644 --- a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs @@ -1,7 +1,9 @@ using Bit.Api.Billing.Attributes; using Bit.Api.Billing.Models.Requests.Payment; using Bit.Api.Billing.Models.Requests.Premium; +using Bit.Api.Billing.Models.Requests.Storage; using Bit.Core; +using Bit.Core.Billing.Licenses.Queries; using Bit.Core.Billing.Payment.Commands; using Bit.Core.Billing.Payment.Queries; using Bit.Core.Billing.Premium.Commands; @@ -21,7 +23,10 @@ public class AccountBillingVNextController( ICreatePremiumCloudHostedSubscriptionCommand createPremiumCloudHostedSubscriptionCommand, IGetCreditQuery getCreditQuery, IGetPaymentMethodQuery getPaymentMethodQuery, - IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController + IGetUserLicenseQuery getUserLicenseQuery, + IUpdatePaymentMethodCommand updatePaymentMethodCommand, + IUpdatePremiumStorageCommand updatePremiumStorageCommand, + IUpgradePremiumToOrganizationCommand upgradePremiumToOrganizationCommand) : BaseBillingController { [HttpGet("credit")] [InjectUser] @@ -66,7 +71,6 @@ public class AccountBillingVNextController( } [HttpPost("subscription")] - [RequireFeature(FeatureFlagKeys.PM24996ImplementUpgradeFromFreeDialog)] [InjectUser] public async Task CreateSubscriptionAsync( [BindNever] User user, @@ -77,4 +81,35 @@ public class AccountBillingVNextController( user, paymentMethod, billingAddress, additionalStorageGb); return Handle(result); } + + [HttpGet("license")] + [InjectUser] + public async Task GetLicenseAsync( + [BindNever] User user) + { + var response = await getUserLicenseQuery.Run(user); + return TypedResults.Ok(response); + } + + [HttpPut("storage")] + [RequireFeature(FeatureFlagKeys.PM29594_UpdateIndividualSubscriptionPage)] + [InjectUser] + public async Task UpdateStorageAsync( + [BindNever] User user, + [FromBody] StorageUpdateRequest request) + { + var result = await updatePremiumStorageCommand.Run(user, request.AdditionalStorageGb); + return Handle(result); + } + + [HttpPost("upgrade")] + [InjectUser] + public async Task UpgradePremiumToOrganizationAsync( + [BindNever] User user, + [FromBody] UpgradePremiumToOrganizationRequest request) + { + var (organizationName, key, planType) = request.ToDomain(); + var result = await upgradePremiumToOrganizationCommand.Run(user, organizationName, key, planType); + return Handle(result); + } } diff --git a/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingVNextController.cs index b86f29bdbc..ccc1dd9734 100644 --- a/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingVNextController.cs @@ -1,7 +1,6 @@ using Bit.Api.Billing.Attributes; using Bit.Api.Billing.Models.Requests.Premium; using Bit.Api.Utilities; -using Bit.Core; using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Premium.Commands; using Bit.Core.Entities; @@ -20,7 +19,6 @@ public class SelfHostedAccountBillingVNextController( ICreatePremiumSelfHostedSubscriptionCommand createPremiumSelfHostedSubscriptionCommand) : BaseBillingController { [HttpPost("license")] - [RequireFeature(FeatureFlagKeys.PM24996ImplementUpgradeFromFreeDialog)] [InjectUser] public async Task UploadLicenseAsync( [BindNever] User user, diff --git a/src/Api/Billing/Models/Requests/Premium/UpgradePremiumToOrganizationRequest.cs b/src/Api/Billing/Models/Requests/Premium/UpgradePremiumToOrganizationRequest.cs new file mode 100644 index 0000000000..14375efc78 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Premium/UpgradePremiumToOrganizationRequest.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using Bit.Core.Billing.Enums; + +namespace Bit.Api.Billing.Models.Requests.Premium; + +public class UpgradePremiumToOrganizationRequest +{ + [Required] + public string OrganizationName { get; set; } = null!; + + [Required] + public string Key { get; set; } = null!; + + [Required] + [JsonConverter(typeof(JsonStringEnumConverter))] + public ProductTierType Tier { get; set; } + + [Required] + [JsonConverter(typeof(JsonStringEnumConverter))] + public PlanCadenceType Cadence { get; set; } + + private PlanType PlanType => + Tier switch + { + ProductTierType.Families => PlanType.FamiliesAnnually, + ProductTierType.Teams => Cadence == PlanCadenceType.Monthly + ? PlanType.TeamsMonthly + : PlanType.TeamsAnnually, + ProductTierType.Enterprise => Cadence == PlanCadenceType.Monthly + ? PlanType.EnterpriseMonthly + : PlanType.EnterpriseAnnually, + _ => throw new InvalidOperationException("Cannot upgrade to an Organization subscription that isn't Families, Teams or Enterprise.") + }; + + public (string OrganizationName, string Key, PlanType PlanType) ToDomain() => (OrganizationName, Key, PlanType); +} diff --git a/src/Api/Billing/Models/Requests/Storage/StorageUpdateRequest.cs b/src/Api/Billing/Models/Requests/Storage/StorageUpdateRequest.cs new file mode 100644 index 0000000000..0b18fc1e6f --- /dev/null +++ b/src/Api/Billing/Models/Requests/Storage/StorageUpdateRequest.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.Billing.Models.Requests.Storage; + +/// +/// Request model for updating storage allocation on a user's premium subscription. +/// Allows for both increasing and decreasing storage in an idempotent manner. +/// +public class StorageUpdateRequest : IValidatableObject +{ + /// + /// The additional storage in GB beyond the base storage. + /// Must be between 0 and the maximum allowed (minus base storage). + /// + [Required] + [Range(0, 99)] + public short AdditionalStorageGb { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (AdditionalStorageGb < 0) + { + yield return new ValidationResult( + "Additional storage cannot be negative.", + new[] { nameof(AdditionalStorageGb) }); + } + + if (AdditionalStorageGb > 99) + { + yield return new ValidationResult( + "Maximum additional storage is 99 GB.", + new[] { nameof(AdditionalStorageGb) }); + } + } +} diff --git a/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs b/src/Api/Dirt/Controllers/OrganizationIntegrationConfigurationController.cs similarity index 92% rename from src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs rename to src/Api/Dirt/Controllers/OrganizationIntegrationConfigurationController.cs index f172a23529..4296aa3edd 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs +++ b/src/Api/Dirt/Controllers/OrganizationIntegrationConfigurationController.cs @@ -1,12 +1,12 @@ -using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Api.Dirt.Models.Request; +using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; using Bit.Core.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bit.Api.AdminConsole.Controllers; +namespace Bit.Api.Dirt.Controllers; [Route("organizations/{organizationId:guid}/integrations/{integrationId:guid}/configurations")] [Authorize("Application")] diff --git a/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs b/src/Api/Dirt/Controllers/OrganizationIntegrationController.cs similarity index 91% rename from src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs rename to src/Api/Dirt/Controllers/OrganizationIntegrationController.cs index b82fe3dfa8..960db648c2 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs +++ b/src/Api/Dirt/Controllers/OrganizationIntegrationController.cs @@ -1,12 +1,12 @@ -using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Api.Dirt.Models.Request; +using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; using Bit.Core.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bit.Api.AdminConsole.Controllers; +namespace Bit.Api.Dirt.Controllers; [Route("organizations/{organizationId:guid}/integrations")] [Authorize("Application")] diff --git a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs b/src/Api/Dirt/Controllers/SlackIntegrationController.cs similarity index 94% rename from src/Api/AdminConsole/Controllers/SlackIntegrationController.cs rename to src/Api/Dirt/Controllers/SlackIntegrationController.cs index 7b53f73f81..e98ed0d3fa 100644 --- a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs +++ b/src/Api/Dirt/Controllers/SlackIntegrationController.cs @@ -1,16 +1,16 @@ using System.Text.Json; -using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bit.Api.AdminConsole.Controllers; +namespace Bit.Api.Dirt.Controllers; [Route("organizations")] [Authorize("Application")] diff --git a/src/Api/AdminConsole/Controllers/TeamsIntegrationController.cs b/src/Api/Dirt/Controllers/TeamsIntegrationController.cs similarity index 94% rename from src/Api/AdminConsole/Controllers/TeamsIntegrationController.cs rename to src/Api/Dirt/Controllers/TeamsIntegrationController.cs index 36d107bbcc..b2bd55017c 100644 --- a/src/Api/AdminConsole/Controllers/TeamsIntegrationController.cs +++ b/src/Api/Dirt/Controllers/TeamsIntegrationController.cs @@ -1,18 +1,18 @@ using System.Text.Json; -using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; -namespace Bit.Api.AdminConsole.Controllers; +namespace Bit.Api.Dirt.Controllers; [Route("organizations")] [Authorize("Application")] diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs b/src/Api/Dirt/Models/Request/OrganizationIntegrationConfigurationRequestModel.cs similarity index 86% rename from src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs rename to src/Api/Dirt/Models/Request/OrganizationIntegrationConfigurationRequestModel.cs index 9341392d68..e918bea2d6 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs +++ b/src/Api/Dirt/Models/Request/OrganizationIntegrationConfigurationRequestModel.cs @@ -1,8 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; using Bit.Core.Enums; - -namespace Bit.Api.AdminConsole.Models.Request.Organizations; +namespace Bit.Api.Dirt.Models.Request; public class OrganizationIntegrationConfigurationRequestModel { diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs b/src/Api/Dirt/Models/Request/OrganizationIntegrationRequestModel.cs similarity index 94% rename from src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs rename to src/Api/Dirt/Models/Request/OrganizationIntegrationRequestModel.cs index 668afe70bf..259671bd66 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs +++ b/src/Api/Dirt/Models/Request/OrganizationIntegrationRequestModel.cs @@ -1,10 +1,10 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Api.AdminConsole.Models.Request.Organizations; +namespace Bit.Api.Dirt.Models.Request; public class OrganizationIntegrationRequestModel : IValidatableObject { diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs b/src/Api/Dirt/Models/Response/OrganizationIntegrationConfigurationResponseModel.cs similarity index 90% rename from src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs rename to src/Api/Dirt/Models/Response/OrganizationIntegrationConfigurationResponseModel.cs index d070375d88..62a3aea405 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs +++ b/src/Api/Dirt/Models/Response/OrganizationIntegrationConfigurationResponseModel.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; using Bit.Core.Enums; using Bit.Core.Models.Api; -namespace Bit.Api.AdminConsole.Models.Response.Organizations; +namespace Bit.Api.Dirt.Models.Response; public class OrganizationIntegrationConfigurationResponseModel : ResponseModel { diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs b/src/Api/Dirt/Models/Response/OrganizationIntegrationResponseModel.cs similarity index 93% rename from src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs rename to src/Api/Dirt/Models/Response/OrganizationIntegrationResponseModel.cs index 0c31e07bef..60e885fe82 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs +++ b/src/Api/Dirt/Models/Response/OrganizationIntegrationResponseModel.cs @@ -1,10 +1,10 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Models.Api; -namespace Bit.Api.AdminConsole.Models.Response.Organizations; +namespace Bit.Api.Dirt.Models.Response; public class OrganizationIntegrationResponseModel : ResponseModel { diff --git a/src/Api/KeyManagement/Models/Requests/MasterPasswordAuthenticationDataRequestModel.cs b/src/Api/KeyManagement/Models/Requests/MasterPasswordAuthenticationDataRequestModel.cs index d65dc8fcb7..4f70a1135f 100644 --- a/src/Api/KeyManagement/Models/Requests/MasterPasswordAuthenticationDataRequestModel.cs +++ b/src/Api/KeyManagement/Models/Requests/MasterPasswordAuthenticationDataRequestModel.cs @@ -6,8 +6,11 @@ namespace Bit.Api.KeyManagement.Models.Requests; public class MasterPasswordAuthenticationDataRequestModel { public required KdfRequestModel Kdf { get; init; } + [Required] public required string MasterPasswordAuthenticationHash { get; init; } - [StringLength(256)] public required string Salt { get; init; } + [Required] + [StringLength(256)] + public required string Salt { get; init; } public MasterPasswordAuthenticationData ToData() { diff --git a/src/Api/KeyManagement/Models/Requests/MasterPasswordUnlockDataRequestModel.cs b/src/Api/KeyManagement/Models/Requests/MasterPasswordUnlockDataRequestModel.cs index ce7a2b343f..e1d7863cae 100644 --- a/src/Api/KeyManagement/Models/Requests/MasterPasswordUnlockDataRequestModel.cs +++ b/src/Api/KeyManagement/Models/Requests/MasterPasswordUnlockDataRequestModel.cs @@ -7,8 +7,12 @@ namespace Bit.Api.KeyManagement.Models.Requests; public class MasterPasswordUnlockDataRequestModel { public required KdfRequestModel Kdf { get; init; } - [EncryptedString] public required string MasterKeyWrappedUserKey { get; init; } - [StringLength(256)] public required string Salt { get; init; } + [Required] + [EncryptedString] + public required string MasterKeyWrappedUserKey { get; init; } + [Required] + [StringLength(256)] + public required string Salt { get; init; } public MasterPasswordUnlockData ToData() { diff --git a/src/Api/KeyManagement/Validators/SendRotationValidator.cs b/src/Api/KeyManagement/Validators/SendRotationValidator.cs index 10a5d996b7..a781ab99ee 100644 --- a/src/Api/KeyManagement/Validators/SendRotationValidator.cs +++ b/src/Api/KeyManagement/Validators/SendRotationValidator.cs @@ -44,7 +44,7 @@ public class SendRotationValidator : IRotationValidator _logger; private readonly GlobalSettings _globalSettings; @@ -42,6 +43,7 @@ public class SendsController : Controller ISendAuthorizationService sendAuthorizationService, IAnonymousSendCommand anonymousSendCommand, INonAnonymousSendCommand nonAnonymousSendCommand, + ISendOwnerQuery sendOwnerQuery, ISendFileStorageService sendFileStorageService, ILogger logger, GlobalSettings globalSettings) @@ -51,6 +53,7 @@ public class SendsController : Controller _sendAuthorizationService = sendAuthorizationService; _anonymousSendCommand = anonymousSendCommand; _nonAnonymousSendCommand = nonAnonymousSendCommand; + _sendOwnerQuery = sendOwnerQuery; _sendFileStorageService = sendFileStorageService; _logger = logger; _globalSettings = globalSettings; @@ -70,7 +73,11 @@ public class SendsController : Controller var guid = new Guid(CoreHelpers.Base64UrlDecode(id)); var send = await _sendRepository.GetByIdAsync(guid); - SendAccessResult sendAuthResult = + if (send == null) + { + throw new BadRequestException("Could not locate send"); + } + var sendAuthResult = await _sendAuthorizationService.AccessAsync(send, model.Password); if (sendAuthResult.Equals(SendAccessResult.PasswordRequired)) { @@ -86,7 +93,7 @@ public class SendsController : Controller throw new NotFoundException(); } - var sendResponse = new SendAccessResponseModel(send, _globalSettings); + var sendResponse = new SendAccessResponseModel(send); if (send.UserId.HasValue && !send.HideEmail.GetValueOrDefault()) { var creator = await _userService.GetUserByIdAsync(send.UserId.Value); @@ -181,33 +188,29 @@ public class SendsController : Controller [HttpGet("{id}")] public async Task Get(string id) { - var userId = _userService.GetProperUserId(User).Value; - var send = await _sendRepository.GetByIdAsync(new Guid(id)); - if (send == null || send.UserId != userId) - { - throw new NotFoundException(); - } - - return new SendResponseModel(send, _globalSettings); + var sendId = new Guid(id); + var send = await _sendOwnerQuery.Get(sendId, User); + return new SendResponseModel(send); } [HttpGet("")] public async Task> GetAll() { - var userId = _userService.GetProperUserId(User).Value; - var sends = await _sendRepository.GetManyByUserIdAsync(userId); - var responses = sends.Select(s => new SendResponseModel(s, _globalSettings)); - return new ListResponseModel(responses); + var sends = await _sendOwnerQuery.GetOwned(User); + var responses = sends.Select(s => new SendResponseModel(s)); + var result = new ListResponseModel(responses); + + return result; } [HttpPost("")] public async Task Post([FromBody] SendRequestModel model) { model.ValidateCreation(); - var userId = _userService.GetProperUserId(User).Value; + var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found"); var send = model.ToSend(userId, _sendAuthorizationService); await _nonAnonymousSendCommand.SaveSendAsync(send); - return new SendResponseModel(send, _globalSettings); + return new SendResponseModel(send); } [HttpPost("file/v2")] @@ -229,27 +232,27 @@ public class SendsController : Controller } model.ValidateCreation(); - var userId = _userService.GetProperUserId(User).Value; + var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found"); var (send, data) = model.ToSend(userId, model.File.FileName, _sendAuthorizationService); var uploadUrl = await _nonAnonymousSendCommand.SaveFileSendAsync(send, data, model.FileLength.Value); return new SendFileUploadDataResponseModel { Url = uploadUrl, FileUploadType = _sendFileStorageService.FileUploadType, - SendResponse = new SendResponseModel(send, _globalSettings) + SendResponse = new SendResponseModel(send) }; } [HttpGet("{id}/file/{fileId}")] public async Task RenewFileUpload(string id, string fileId) { - var userId = _userService.GetProperUserId(User).Value; + var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found"); var sendId = new Guid(id); var send = await _sendRepository.GetByIdAsync(sendId); - var fileData = JsonSerializer.Deserialize(send?.Data); + var fileData = JsonSerializer.Deserialize(send?.Data ?? string.Empty); if (send == null || send.Type != SendType.File || (send.UserId.HasValue && send.UserId.Value != userId) || - !send.UserId.HasValue || fileData.Id != fileId || fileData.Validated) + !send.UserId.HasValue || fileData?.Id != fileId || fileData.Validated) { // Not found if Send isn't found, user doesn't have access, request is faulty, // or we've already validated the file. This last is to emulate create-only blob permissions for Azure @@ -260,7 +263,7 @@ public class SendsController : Controller { Url = await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId), FileUploadType = _sendFileStorageService.FileUploadType, - SendResponse = new SendResponseModel(send, _globalSettings), + SendResponse = new SendResponseModel(send), }; } @@ -270,12 +273,16 @@ public class SendsController : Controller [DisableFormValueModelBinding] public async Task PostFileForExistingSend(string id, string fileId) { - if (!Request?.ContentType.Contains("multipart/") ?? true) + if (!Request?.ContentType?.Contains("multipart/") ?? true) { throw new BadRequestException("Invalid content."); } var send = await _sendRepository.GetByIdAsync(new Guid(id)); + if (send == null) + { + throw new BadRequestException("Could not locate send"); + } await Request.GetFileAsync(async (stream) => { await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); @@ -286,36 +293,39 @@ public class SendsController : Controller public async Task Put(string id, [FromBody] SendRequestModel model) { model.ValidateEdit(); - var userId = _userService.GetProperUserId(User).Value; + var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found"); var send = await _sendRepository.GetByIdAsync(new Guid(id)); if (send == null || send.UserId != userId) { throw new NotFoundException(); } - await _nonAnonymousSendCommand.SaveSendAsync(model.ToSend(send, _sendAuthorizationService)); - return new SendResponseModel(send, _globalSettings); + await _nonAnonymousSendCommand.SaveSendAsync(model.UpdateSend(send, _sendAuthorizationService)); + return new SendResponseModel(send); } [HttpPut("{id}/remove-password")] public async Task PutRemovePassword(string id) { - var userId = _userService.GetProperUserId(User).Value; + var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found"); var send = await _sendRepository.GetByIdAsync(new Guid(id)); if (send == null || send.UserId != userId) { throw new NotFoundException(); } + // This endpoint exists because PUT preserves existing Password/Emails when not provided. + // This allows clients to update other fields without re-submitting sensitive auth data. send.Password = null; + send.AuthType = AuthType.None; await _nonAnonymousSendCommand.SaveSendAsync(send); - return new SendResponseModel(send, _globalSettings); + return new SendResponseModel(send); } [HttpDelete("{id}")] public async Task Delete(string id) { - var userId = _userService.GetProperUserId(User).Value; + var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found"); var send = await _sendRepository.GetByIdAsync(new Guid(id)); if (send == null || send.UserId != userId) { diff --git a/src/Api/Tools/Models/Request/SendRequestModel.cs b/src/Api/Tools/Models/Request/SendRequestModel.cs index a38257db60..f3308dbd5a 100644 --- a/src/Api/Tools/Models/Request/SendRequestModel.cs +++ b/src/Api/Tools/Models/Request/SendRequestModel.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; +using Bit.Api.Tools.Utilities; using Bit.Core.Exceptions; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; @@ -10,35 +11,119 @@ using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Services; using Bit.Core.Utilities; +using static System.StringSplitOptions; + namespace Bit.Api.Tools.Models.Request; +/// +/// A send request issued by a Bitwarden client +/// public class SendRequestModel { + /// + /// Indicates whether the send contains text or file data. + /// public SendType Type { get; set; } + + /// + /// Specifies the authentication method required to access this Send. + /// + public AuthType? AuthType { get; set; } + + /// + /// Estimated length of the file accompanying the send. when + /// is . + /// public long? FileLength { get; set; } = null; + + /// + /// Label for the send. + /// [EncryptedString] [EncryptedStringLength(1000)] public string Name { get; set; } + + /// + /// Notes for the send. This is only visible to the owner of the send. + /// [EncryptedString] [EncryptedStringLength(1000)] public string Notes { get; set; } + + /// + /// A base64-encoded byte array containing the Send's encryption key. This key is + /// also provided to send recipients in the Send's URL. + /// [Required] [EncryptedString] [EncryptedStringLength(1000)] public string Key { get; set; } + + /// + /// The maximum number of times a send can be accessed before it expires. + /// When this value is , there is no limit. + /// [Range(1, int.MaxValue)] public int? MaxAccessCount { get; set; } + + /// + /// The date after which a send cannot be accessed. When this value is + /// , there is no expiration date. + /// public DateTime? ExpirationDate { get; set; } + + /// + /// The date after which a send may be automatically deleted from the server. + /// When this is , the send may be deleted after it has + /// exceeded the global send timeout limit. + /// [Required] public DateTime? DeletionDate { get; set; } + + /// + /// Contains file metadata uploaded with the send. + /// The file content is uploaded separately. + /// public SendFileModel File { get; set; } + + /// + /// Contains text data uploaded with the send. + /// public SendTextModel Text { get; set; } + + /// + /// Base64-encoded byte array of a password hash that grants access to the send. + /// Mutually exclusive with . + /// [StringLength(1000)] public string Password { get; set; } + + /// + /// Comma-separated list of emails that may access the send using OTP + /// authentication. Mutually exclusive with . + /// + [StringLength(4000)] + public string Emails { get; set; } + + /// + /// When , send access is disabled. + /// Defaults to . + /// [Required] public bool? Disabled { get; set; } + + /// + /// When send access hides the user's email address + /// and displays a confirmation message instead. Defaults to . + /// public bool? HideEmail { get; set; } + /// + /// Transforms the request into a send object. + /// + /// The user that owns the send. + /// Hashes the send password. + /// The send object public Send ToSend(Guid userId, ISendAuthorizationService sendAuthorizationService) { var send = new Send @@ -46,12 +131,21 @@ public class SendRequestModel Type = Type, UserId = (Guid?)userId }; - ToSend(send, sendAuthorizationService); + send = UpdateSend(send, sendAuthorizationService); return send; } + /// + /// Transforms the request into a send object and file data. + /// + /// The user that owns the send. + /// Name of the file uploaded with the send. + /// Hashes the send password. + /// The send object and file data. public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendAuthorizationService sendAuthorizationService) { + // FIXME: This method does two things: creates a send and a send file data. + // It should only do one thing. var send = ToSendBase(new Send { Type = Type, @@ -61,7 +155,13 @@ public class SendRequestModel return (send, data); } - public Send ToSend(Send existingSend, ISendAuthorizationService sendAuthorizationService) + /// + /// Update a send object with request content + /// + /// The send to update + /// Hashes the send password. + /// The send object + public Send UpdateSend(Send existingSend, ISendAuthorizationService sendAuthorizationService) { existingSend = ToSendBase(existingSend, sendAuthorizationService); switch (existingSend.Type) @@ -81,6 +181,12 @@ public class SendRequestModel return existingSend; } + /// + /// Validates that the request is internally consistent for send creation. + /// + /// + /// Thrown when the send's expiration date has already expired. + /// public void ValidateCreation() { var now = DateTime.UtcNow; @@ -94,6 +200,13 @@ public class SendRequestModel ValidateEdit(); } + /// + /// Validates that the request is internally consistent for send administration. + /// + /// + /// Thrown when the send's deletion date has already expired or when its + /// expiration occurs after its deletion. + /// public void ValidateEdit() { var now = DateTime.UtcNow; @@ -134,12 +247,30 @@ public class SendRequestModel existingSend.ExpirationDate = ExpirationDate; existingSend.DeletionDate = DeletionDate.Value; existingSend.MaxAccessCount = MaxAccessCount; - if (!string.IsNullOrWhiteSpace(Password)) + + if (!string.IsNullOrWhiteSpace(Emails)) + { + // normalize encoding + var emails = Emails.Split(',', RemoveEmptyEntries | TrimEntries); + existingSend.Emails = string.Join(",", emails); + existingSend.Password = null; + existingSend.AuthType = Core.Tools.Enums.AuthType.Email; + } + else if (!string.IsNullOrWhiteSpace(Password)) { existingSend.Password = authorizationService.HashPassword(Password); + existingSend.Emails = null; + existingSend.AuthType = Core.Tools.Enums.AuthType.Password; } + else + { + // Neither Password nor Emails provided - preserve existing values and infer AuthType + existingSend.AuthType = SendUtilities.InferAuthType(existingSend); + } + existingSend.Disabled = Disabled.GetValueOrDefault(); existingSend.HideEmail = HideEmail.GetValueOrDefault(); + return existingSend; } @@ -149,8 +280,15 @@ public class SendRequestModel } } +/// +/// A send request issued by a Bitwarden client +/// public class SendWithIdRequestModel : SendRequestModel { + /// + /// Identifies the send. When this is , the client is requesting + /// a new send. + /// [Required] public Guid? Id { get; set; } } diff --git a/src/Api/Tools/Models/Response/SendAccessResponseModel.cs b/src/Api/Tools/Models/Response/SendAccessResponseModel.cs index b544862fcd..b722dd5fff 100644 --- a/src/Api/Tools/Models/Response/SendAccessResponseModel.cs +++ b/src/Api/Tools/Models/Response/SendAccessResponseModel.cs @@ -3,7 +3,6 @@ using System.Text.Json; using Bit.Core.Models.Api; -using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; @@ -11,9 +10,22 @@ using Bit.Core.Utilities; namespace Bit.Api.Tools.Models.Response; +/// +/// A response issued to a Bitwarden client in response to access operations. +/// public class SendAccessResponseModel : ResponseModel { - public SendAccessResponseModel(Send send, GlobalSettings globalSettings) + /// + /// Instantiates a send access response model + /// + /// Content to transmit to the client. + /// + /// Thrown when is + /// + /// + /// Thrown when has an invalid . + /// + public SendAccessResponseModel(Send send) : base("send-access") { if (send == null) @@ -23,6 +35,7 @@ public class SendAccessResponseModel : ResponseModel Id = CoreHelpers.Base64UrlEncode(send.Id.ToByteArray()); Type = send.Type; + AuthType = send.AuthType; SendData sendData; switch (send.Type) @@ -45,11 +58,52 @@ public class SendAccessResponseModel : ResponseModel ExpirationDate = send.ExpirationDate; } + /// + /// Identifies the send in a send URL + /// public string Id { get; set; } + + /// + /// Indicates whether the send contains text or file data. + /// public SendType Type { get; set; } + + /// + /// Specifies the authentication method required to access this Send. + /// + public AuthType? AuthType { get; set; } + + /// + /// Label for the send. This is only visible to the owner of the send. + /// + /// + /// This field contains a base64-encoded byte array. The array contains + /// the E2E-encrypted encrypted content. + /// public string Name { get; set; } + + /// + /// Describes the file attached to the send. + /// + /// + /// File content is downloaded separately using + /// + /// public SendFileModel File { get; set; } + + /// + /// Contains text data uploaded with the send. + /// public SendTextModel Text { get; set; } + + /// + /// The date after which a send cannot be accessed. When this value is + /// , there is no expiration date. + /// public DateTime? ExpirationDate { get; set; } + + /// + /// Indicates the person that created the send to the accessor. + /// public string CreatorIdentifier { get; set; } } diff --git a/src/Api/Tools/Models/Response/SendResponseModel.cs b/src/Api/Tools/Models/Response/SendResponseModel.cs index 17a70cd2db..f7f6b683d6 100644 --- a/src/Api/Tools/Models/Response/SendResponseModel.cs +++ b/src/Api/Tools/Models/Response/SendResponseModel.cs @@ -2,8 +2,8 @@ #nullable disable using System.Text.Json; +using Bit.Api.Tools.Utilities; using Bit.Core.Models.Api; -using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; @@ -11,9 +11,23 @@ using Bit.Core.Utilities; namespace Bit.Api.Tools.Models.Response; +/// +/// A response issued to a Bitwarden client in response to ownership operations. +/// +/// public class SendResponseModel : ResponseModel { - public SendResponseModel(Send send, GlobalSettings globalSettings) + /// + /// Instantiates a send response model + /// + /// Content to transmit to the client. + /// + /// Thrown when is + /// + /// + /// Thrown when has an invalid . + /// + public SendResponseModel(Send send) : base("send") { if (send == null) @@ -24,6 +38,7 @@ public class SendResponseModel : ResponseModel Id = send.Id; AccessId = CoreHelpers.Base64UrlEncode(send.Id.ToByteArray()); Type = send.Type; + AuthType = send.AuthType ?? SendUtilities.InferAuthType(send); Key = send.Key; MaxAccessCount = send.MaxAccessCount; AccessCount = send.AccessCount; @@ -31,6 +46,7 @@ public class SendResponseModel : ResponseModel ExpirationDate = send.ExpirationDate; DeletionDate = send.DeletionDate; Password = send.Password; + Emails = send.Emails; Disabled = send.Disabled; HideEmail = send.HideEmail.GetValueOrDefault(); @@ -55,20 +71,113 @@ public class SendResponseModel : ResponseModel Notes = sendData.Notes; } + /// + /// Identifies the send to its owner + /// public Guid Id { get; set; } + + /// + /// Identifies the send in a send URL + /// public string AccessId { get; set; } + + /// + /// Indicates whether the send contains text or file data. + /// public SendType Type { get; set; } + + /// + /// Specifies the authentication method required to access this Send. + /// + public AuthType? AuthType { get; set; } + + /// + /// Label for the send. + /// + /// + /// This field contains a base64-encoded byte array. The array contains + /// the E2E-encrypted encrypted content. + /// public string Name { get; set; } + + /// + /// Notes for the send. This is only visible to the owner of the send. + /// This field is encrypted. + /// + /// + /// This field contains a base64-encoded byte array. The array contains + /// the E2E-encrypted encrypted content. + /// public string Notes { get; set; } + + /// + /// Contains file metadata uploaded with the send. + /// The file content is uploaded separately. + /// public SendFileModel File { get; set; } + + /// + /// Contains text data uploaded with the send. + /// public SendTextModel Text { get; set; } + + /// + /// A base64-encoded byte array containing the Send's encryption key. + /// It's also provided to send recipients in the Send's URL. + /// + /// + /// This field contains a base64-encoded byte array. The array contains + /// the E2E-encrypted content. + /// public string Key { get; set; } + + /// + /// The maximum number of times a send can be accessed before it expires. + /// When this value is , there is no limit. + /// public int? MaxAccessCount { get; set; } + + /// + /// The number of times a send has been accessed since it was created. + /// public int AccessCount { get; set; } + + /// + /// Base64-encoded byte array of a password hash that grants access to the send. + /// Mutually exclusive with . + /// public string Password { get; set; } + + /// + /// Comma-separated list of emails that may access the send using OTP + /// authentication. Mutually exclusive with . + /// + public string Emails { get; set; } + + /// + /// When , send access is disabled. + /// public bool Disabled { get; set; } + + /// + /// The last time this send's data changed. + /// public DateTime RevisionDate { get; set; } + + /// + /// The date after which a send cannot be accessed. When this value is + /// , there is no expiration date. + /// public DateTime? ExpirationDate { get; set; } + + /// + /// The date after which a send may be automatically deleted from the server. + /// public DateTime DeletionDate { get; set; } + + /// + /// When send access hides the user's email address + /// and displays a confirmation message instead. + /// public bool HideEmail { get; set; } } diff --git a/src/Api/Tools/Utilities/InferAuthType.cs b/src/Api/Tools/Utilities/InferAuthType.cs new file mode 100644 index 0000000000..785fde1ec9 --- /dev/null +++ b/src/Api/Tools/Utilities/InferAuthType.cs @@ -0,0 +1,23 @@ +namespace Bit.Api.Tools.Utilities; + +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; + +public class SendUtilities +{ + public static AuthType InferAuthType(Send send) + { + if (!string.IsNullOrWhiteSpace(send.Password)) + { + return AuthType.Password; + } + + if (!string.IsNullOrWhiteSpace(send.Emails)) + { + return AuthType.Email; + } + + return AuthType.None; + } +} + diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 6a506cc01f..9e107b491d 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -10,7 +10,6 @@ using Bit.Api.Utilities; using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; using Bit.Core; -using Bit.Core.AdminConsole.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -43,7 +42,6 @@ public class CiphersController : Controller private readonly ICipherService _cipherService; private readonly IUserService _userService; private readonly IAttachmentStorageService _attachmentStorageService; - private readonly IProviderService _providerService; private readonly ICurrentContext _currentContext; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; @@ -52,7 +50,6 @@ public class CiphersController : Controller private readonly ICollectionRepository _collectionRepository; private readonly IArchiveCiphersCommand _archiveCiphersCommand; private readonly IUnarchiveCiphersCommand _unarchiveCiphersCommand; - private readonly IFeatureService _featureService; public CiphersController( ICipherRepository cipherRepository, @@ -60,7 +57,6 @@ public class CiphersController : Controller ICipherService cipherService, IUserService userService, IAttachmentStorageService attachmentStorageService, - IProviderService providerService, ICurrentContext currentContext, ILogger logger, GlobalSettings globalSettings, @@ -68,15 +64,13 @@ public class CiphersController : Controller IApplicationCacheService applicationCacheService, ICollectionRepository collectionRepository, IArchiveCiphersCommand archiveCiphersCommand, - IUnarchiveCiphersCommand unarchiveCiphersCommand, - IFeatureService featureService) + IUnarchiveCiphersCommand unarchiveCiphersCommand) { _cipherRepository = cipherRepository; _collectionCipherRepository = collectionCipherRepository; _cipherService = cipherService; _userService = userService; _attachmentStorageService = attachmentStorageService; - _providerService = providerService; _currentContext = currentContext; _logger = logger; _globalSettings = globalSettings; @@ -85,7 +79,6 @@ public class CiphersController : Controller _collectionRepository = collectionRepository; _archiveCiphersCommand = archiveCiphersCommand; _unarchiveCiphersCommand = unarchiveCiphersCommand; - _featureService = featureService; } [HttpGet("{id}")] @@ -344,8 +337,7 @@ public class CiphersController : Controller throw new NotFoundException(); } - bool excludeDefaultUserCollections = _featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation) && !includeMemberItems; - var allOrganizationCiphers = excludeDefaultUserCollections + var allOrganizationCiphers = !includeMemberItems ? await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections(organizationId) : @@ -911,7 +903,7 @@ public class CiphersController : Controller [HttpPut("{id}/archive")] [RequireFeature(FeatureFlagKeys.ArchiveVaultItems)] - public async Task PutArchive(Guid id) + public async Task PutArchive(Guid id) { var userId = _userService.GetProperUserId(User).Value; @@ -922,12 +914,16 @@ public class CiphersController : Controller throw new BadRequestException("Cipher was not archived. Ensure the provided ID is correct and you have permission to archive it."); } - return new CipherMiniResponseModel(archivedCipherOrganizationDetails.First(), _globalSettings, archivedCipherOrganizationDetails.First().OrganizationUseTotp); + return new CipherResponseModel(archivedCipherOrganizationDetails.First(), + await _userService.GetUserByPrincipalAsync(User), + await _applicationCacheService.GetOrganizationAbilitiesAsync(), + _globalSettings + ); } [HttpPut("archive")] [RequireFeature(FeatureFlagKeys.ArchiveVaultItems)] - public async Task> PutArchiveMany([FromBody] CipherBulkArchiveRequestModel model) + public async Task> PutArchiveMany([FromBody] CipherBulkArchiveRequestModel model) { if (!_globalSettings.SelfHosted && model.Ids.Count() > 500) { @@ -935,6 +931,7 @@ public class CiphersController : Controller } var userId = _userService.GetProperUserId(User).Value; + var user = await _userService.GetUserByPrincipalAsync(User); var cipherIdsToArchive = new HashSet(model.Ids); @@ -945,9 +942,14 @@ public class CiphersController : Controller throw new BadRequestException("No ciphers were archived. Ensure the provided IDs are correct and you have permission to archive them."); } - var responses = archivedCiphers.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp)); + var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var responses = archivedCiphers.Select(c => new CipherResponseModel(c, + user, + organizationAbilities, + _globalSettings + )); - return new ListResponseModel(responses); + return new ListResponseModel(responses); } [HttpDelete("{id}")] @@ -1109,7 +1111,7 @@ public class CiphersController : Controller [HttpPut("{id}/unarchive")] [RequireFeature(FeatureFlagKeys.ArchiveVaultItems)] - public async Task PutUnarchive(Guid id) + public async Task PutUnarchive(Guid id) { var userId = _userService.GetProperUserId(User).Value; @@ -1120,12 +1122,16 @@ public class CiphersController : Controller throw new BadRequestException("Cipher was not unarchived. Ensure the provided ID is correct and you have permission to archive it."); } - return new CipherMiniResponseModel(unarchivedCipherDetails.First(), _globalSettings, unarchivedCipherDetails.First().OrganizationUseTotp); + return new CipherResponseModel(unarchivedCipherDetails.First(), + await _userService.GetUserByPrincipalAsync(User), + await _applicationCacheService.GetOrganizationAbilitiesAsync(), + _globalSettings + ); } [HttpPut("unarchive")] [RequireFeature(FeatureFlagKeys.ArchiveVaultItems)] - public async Task> PutUnarchiveMany([FromBody] CipherBulkUnarchiveRequestModel model) + public async Task> PutUnarchiveMany([FromBody] CipherBulkUnarchiveRequestModel model) { if (!_globalSettings.SelfHosted && model.Ids.Count() > 500) { @@ -1133,6 +1139,8 @@ public class CiphersController : Controller } var userId = _userService.GetProperUserId(User).Value; + var user = await _userService.GetUserByPrincipalAsync(User); + var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); var cipherIdsToUnarchive = new HashSet(model.Ids); @@ -1143,9 +1151,9 @@ public class CiphersController : Controller throw new BadRequestException("Ciphers were not unarchived. Ensure the provided ID is correct and you have permission to archive it."); } - var responses = unarchivedCipherOrganizationDetails.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp)); + var responses = unarchivedCipherOrganizationDetails.Select(c => new CipherResponseModel(c, user, organizationAbilities, _globalSettings)); - return new ListResponseModel(responses); + return new ListResponseModel(responses); } [HttpPut("{id}/restore")] diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index 18a1aec559..9a2c279a28 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -80,6 +80,7 @@ public class CipherRequestModel { existingCipher.FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : (Guid?)new Guid(FolderId); existingCipher.Favorite = Favorite; + existingCipher.ArchivedDate = ArchivedDate; ToCipher(existingCipher); return existingCipher; } @@ -127,9 +128,9 @@ public class CipherRequestModel var userIdKey = userId.HasValue ? userId.ToString().ToUpperInvariant() : null; existingCipher.Reprompt = Reprompt; existingCipher.Key = Key; - existingCipher.ArchivedDate = ArchivedDate; existingCipher.Folders = UpdateUserSpecificJsonField(existingCipher.Folders, userIdKey, FolderId); existingCipher.Favorites = UpdateUserSpecificJsonField(existingCipher.Favorites, userIdKey, Favorite); + existingCipher.Archives = UpdateUserSpecificJsonField(existingCipher.Archives, userIdKey, ArchivedDate); var hasAttachments2 = (Attachments2?.Count ?? 0) > 0; var hasAttachments = (Attachments?.Count ?? 0) > 0; diff --git a/src/Api/Vault/Models/Response/CipherResponseModel.cs b/src/Api/Vault/Models/Response/CipherResponseModel.cs index dfacc1a551..ac11eb3cd3 100644 --- a/src/Api/Vault/Models/Response/CipherResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherResponseModel.cs @@ -70,7 +70,6 @@ public class CipherMiniResponseModel : ResponseModel DeletedDate = cipher.DeletedDate; Reprompt = cipher.Reprompt.GetValueOrDefault(CipherRepromptType.None); Key = cipher.Key; - ArchivedDate = cipher.ArchivedDate; } public Guid Id { get; set; } @@ -111,7 +110,6 @@ public class CipherMiniResponseModel : ResponseModel public DateTime? DeletedDate { get; set; } public CipherRepromptType Reprompt { get; set; } public string Key { get; set; } - public DateTime? ArchivedDate { get; set; } } public class CipherResponseModel : CipherMiniResponseModel @@ -127,6 +125,7 @@ public class CipherResponseModel : CipherMiniResponseModel FolderId = cipher.FolderId; Favorite = cipher.Favorite; Edit = cipher.Edit; + ArchivedDate = cipher.ArchivedDate; ViewPassword = cipher.ViewPassword; Permissions = new CipherPermissionsResponseModel(user, cipher, organizationAbilities); } @@ -135,6 +134,7 @@ public class CipherResponseModel : CipherMiniResponseModel public bool Favorite { get; set; } public bool Edit { get; set; } public bool ViewPassword { get; set; } + public DateTime? ArchivedDate { get; set; } public CipherPermissionsResponseModel Permissions { get; set; } } diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index 1981ac834e..c965320b94 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -56,7 +56,7 @@ public class SyncResponseModel() : ResponseModel("sync") c => new CollectionDetailsResponseModel(c)) ?? new List(); Domains = excludeDomains ? null : new DomainsResponseModel(user, false); Policies = policies?.Select(p => new PolicyResponseModel(p)) ?? new List(); - Sends = sends.Select(s => new SendResponseModel(s, globalSettings)); + Sends = sends.Select(s => new SendResponseModel(s)); UserDecryption = new UserDecryptionResponseModel { MasterPasswordUnlock = user.HasMasterPassword() diff --git a/src/Billing/Jobs/ReconcileAdditionalStorageJob.cs b/src/Billing/Jobs/ReconcileAdditionalStorageJob.cs index 312ed3122b..7dfc44069c 100644 --- a/src/Billing/Jobs/ReconcileAdditionalStorageJob.cs +++ b/src/Billing/Jobs/ReconcileAdditionalStorageJob.cs @@ -4,6 +4,7 @@ using Bit.Billing.Services; using Bit.Core; using Bit.Core.Billing.Constants; using Bit.Core.Jobs; +using Bit.Core.Repositories; using Bit.Core.Services; using Quartz; using Stripe; @@ -13,12 +14,23 @@ namespace Bit.Billing.Jobs; public class ReconcileAdditionalStorageJob( IStripeFacade stripeFacade, ILogger logger, - IFeatureService featureService) : BaseJob(logger) + IFeatureService featureService, + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IStripeEventUtilityService stripeEventUtilityService) : BaseJob(logger) { private const string _storageGbMonthlyPriceId = "storage-gb-monthly"; private const string _storageGbAnnuallyPriceId = "storage-gb-annually"; private const string _personalStorageGbAnnuallyPriceId = "personal-storage-gb-annually"; private const int _storageGbToRemove = 4; + private const short _includedStorageGb = 5; + + public enum SubscriptionPlanTier + { + Personal, + Organization, + Unknown + } protected override async Task ExecuteJobAsync(IJobExecutionContext context) { @@ -34,6 +46,7 @@ public class ReconcileAdditionalStorageJob( var subscriptionsFound = 0; var subscriptionsUpdated = 0; var subscriptionsWithErrors = 0; + var databaseUpdatesFailed = 0; var failures = new List(); logger.LogInformation("Starting ReconcileAdditionalStorageJob (live mode: {LiveMode})", liveMode); @@ -51,11 +64,13 @@ public class ReconcileAdditionalStorageJob( { logger.LogWarning( "Job cancelled!! Exiting. Progress at time of cancellation: Subscriptions found: {SubscriptionsFound}, " + - "Updated: {SubscriptionsUpdated}, Errors: {SubscriptionsWithErrors}{Failures}", + "Stripe updates: {StripeUpdates}, Database updates: {DatabaseFailed} failed, " + + "Errors: {SubscriptionsWithErrors}{Failures}", subscriptionsFound, liveMode ? subscriptionsUpdated : $"(In live mode, would have updated) {subscriptionsUpdated}", + databaseUpdatesFailed, subscriptionsWithErrors, failures.Count > 0 ? $", Failures: {Environment.NewLine}{string.Join(Environment.NewLine, failures)}" @@ -99,20 +114,68 @@ public class ReconcileAdditionalStorageJob( subscriptionsUpdated++; - if (!liveMode) + // Now, prepare the database update so we can log details out if not in live mode + var (organizationId, userId, _) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata ?? new Dictionary()); + var subscriptionPlanTier = DetermineSubscriptionPlanTier(userId, organizationId); + + if (subscriptionPlanTier == SubscriptionPlanTier.Unknown) { - logger.LogInformation( - "Not live mode (dry-run): Would have updated subscription {SubscriptionId} with item changes: {NewLine}{UpdateOptions}", - subscription.Id, - Environment.NewLine, - JsonSerializer.Serialize(updateOptions)); + logger.LogError( + "Cannot determine subscription plan tier for {SubscriptionId}. Skipping subscription. ", + subscription.Id); + subscriptionsWithErrors++; continue; } + var entityId = + subscriptionPlanTier switch + { + SubscriptionPlanTier.Personal => userId!.Value, + SubscriptionPlanTier.Organization => organizationId!.Value, + _ => throw new ArgumentOutOfRangeException(nameof(subscriptionPlanTier), subscriptionPlanTier, null) + }; + + // Calculate new MaxStorageGb + var currentStorageQuantity = GetCurrentStorageQuantityFromSubscription(subscription, priceId); + var newMaxStorageGb = CalculateNewMaxStorageGb(currentStorageQuantity, updateOptions); + + if (!liveMode) + { + logger.LogInformation( + "Not live mode (dry-run): Would have updated subscription {SubscriptionId} with item changes: {NewLine}{UpdateOptions}" + + "{NewLine2}And would have updated database record tier: {Tier} to new MaxStorageGb: {MaxStorageGb}", + subscription.Id, + Environment.NewLine, + JsonSerializer.Serialize(updateOptions), + Environment.NewLine, + subscriptionPlanTier, + newMaxStorageGb); + continue; + } + + // Live mode enabled - continue with updates to stripe and database try { await stripeFacade.UpdateSubscription(subscription.Id, updateOptions); - logger.LogInformation("Successfully updated subscription: {SubscriptionId}", subscription.Id); + logger.LogInformation("Successfully updated Stripe subscription: {SubscriptionId}", subscription.Id); + + logger.LogInformation( + "Updating MaxStorageGb in database for subscription {SubscriptionId} ({Type}): New MaxStorageGb: {MaxStorage}", + subscription.Id, + subscriptionPlanTier, + newMaxStorageGb); + + var dbUpdateSuccess = await UpdateDatabaseMaxStorageAsync( + subscriptionPlanTier, + entityId, + newMaxStorageGb, + subscription.Id); + + if (!dbUpdateSuccess) + { + databaseUpdatesFailed++; + failures.Add($"Subscription {subscription.Id}: Database update failed"); + } } catch (Exception ex) { @@ -125,12 +188,14 @@ public class ReconcileAdditionalStorageJob( } logger.LogInformation( - "ReconcileAdditionalStorageJob completed. Subscriptions found: {SubscriptionsFound}, " + - "Updated: {SubscriptionsUpdated}, Errors: {SubscriptionsWithErrors}{Failures}", + "ReconcileAdditionalStorageJob FINISHED. Subscriptions found: {SubscriptionsFound}, " + + "Subscriptions updated: {SubscriptionsUpdated}, Database failures: {DatabaseFailed}, " + + "Total Subscriptions With Errors: {SubscriptionsWithErrors}{Failures}", subscriptionsFound, liveMode ? subscriptionsUpdated : $"(In live mode, would have updated) {subscriptionsUpdated}", + databaseUpdatesFailed, subscriptionsWithErrors, failures.Count > 0 ? $", Failures: {Environment.NewLine}{string.Join(Environment.NewLine, failures)}" @@ -182,6 +247,117 @@ public class ReconcileAdditionalStorageJob( return hasUpdates ? updateOptions : null; } + public SubscriptionPlanTier DetermineSubscriptionPlanTier( + Guid? userId, + Guid? organizationId) + { + return userId.HasValue + ? SubscriptionPlanTier.Personal + : organizationId.HasValue + ? SubscriptionPlanTier.Organization + : SubscriptionPlanTier.Unknown; + } + + public long GetCurrentStorageQuantityFromSubscription( + Subscription subscription, + string storagePriceId) + { + return subscription.Items?.Data?.FirstOrDefault(item => item?.Price?.Id == storagePriceId)?.Quantity ?? 0; + } + + public short CalculateNewMaxStorageGb( + long currentQuantity, + SubscriptionUpdateOptions? updateOptions) + { + if (updateOptions?.Items == null) + { + return (short)(_includedStorageGb + currentQuantity); + } + + // If the update marks item as deleted, new quantity is whatever the base storage gb + if (updateOptions.Items.Any(i => i.Deleted == true)) + { + return _includedStorageGb; + } + + // If the update has a new quantity, use it to calculate the new max + var updatedItem = updateOptions.Items.FirstOrDefault(i => i.Quantity.HasValue); + if (updatedItem?.Quantity != null) + { + return (short)(_includedStorageGb + updatedItem.Quantity.Value); + } + + // Otherwise, no change + return (short)(_includedStorageGb + currentQuantity); + } + + public async Task UpdateDatabaseMaxStorageAsync( + SubscriptionPlanTier subscriptionPlanTier, + Guid entityId, + short newMaxStorageGb, + string subscriptionId) + { + try + { + switch (subscriptionPlanTier) + { + case SubscriptionPlanTier.Personal: + { + var user = await userRepository.GetByIdAsync(entityId); + if (user == null) + { + logger.LogError( + "User not found for subscription {SubscriptionId}. Database not updated.", + subscriptionId); + return false; + } + + user.MaxStorageGb = newMaxStorageGb; + await userRepository.ReplaceAsync(user); + + logger.LogInformation( + "Successfully updated User {UserId} MaxStorageGb to {MaxStorageGb} for subscription {SubscriptionId}", + user.Id, + newMaxStorageGb, + subscriptionId); + return true; + } + case SubscriptionPlanTier.Organization: + { + var organization = await organizationRepository.GetByIdAsync(entityId); + if (organization == null) + { + logger.LogError( + "Organization not found for subscription {SubscriptionId}. Database not updated.", + subscriptionId); + return false; + } + + organization.MaxStorageGb = newMaxStorageGb; + await organizationRepository.ReplaceAsync(organization); + + logger.LogInformation( + "Successfully updated Organization {OrganizationId} MaxStorageGb to {MaxStorageGb} for subscription {SubscriptionId}", + organization.Id, + newMaxStorageGb, + subscriptionId); + return true; + } + case SubscriptionPlanTier.Unknown: + default: + return false; + } + } + catch (Exception ex) + { + logger.LogError(ex, + "Failed to update database MaxStorageGb for subscription {SubscriptionId} (Plan Tier: {SubscriptionType})", + subscriptionId, + subscriptionPlanTier); + return false; + } + } + public static ITrigger GetTrigger() { return TriggerBuilder.Create() diff --git a/src/Billing/Models/PayPalIPNTransactionModel.cs b/src/Billing/Models/PayPalIPNTransactionModel.cs index 34db5fdd04..240fa5ab76 100644 --- a/src/Billing/Models/PayPalIPNTransactionModel.cs +++ b/src/Billing/Models/PayPalIPNTransactionModel.cs @@ -43,7 +43,7 @@ public class PayPalIPNTransactionModel var merchantGross = Extract(data, "mc_gross"); if (!string.IsNullOrEmpty(merchantGross)) { - MerchantGross = decimal.Parse(merchantGross); + MerchantGross = decimal.Parse(merchantGross, CultureInfo.InvariantCulture); } MerchantCurrency = Extract(data, "mc_currency"); diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index 338b150de6..266779d574 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -134,6 +134,11 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable /// public bool UseAutomaticUserConfirmation { get; set; } + /// + /// If set to true, disables Secrets Manager ads for users in the organization + /// + public bool UseDisableSmAdsForUsers { get; set; } + /// /// If set to true, the organization has phishing protection enabled. /// @@ -338,6 +343,7 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable UseRiskInsights = license.UseRiskInsights; UseOrganizationDomains = license.UseOrganizationDomains; UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies; + UseDisableSmAdsForUsers = license.UseDisableSmAdsForUsers; UseAutomaticUserConfirmation = license.UseAutomaticUserConfirmation; UsePhishingBlocker = license.UsePhishingBlocker; } diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegration.cs deleted file mode 100644 index 8785a74896..0000000000 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegration.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; - -public record DatadogIntegration(string ApiKey, Uri Uri); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs deleted file mode 100644 index dc2733c889..0000000000 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; - -public record SlackIntegration(string Token); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs deleted file mode 100644 index 5b4fae0c76..0000000000 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; - -public record SlackIntegrationConfiguration(string ChannelId); diff --git a/src/Core/AdminConsole/Models/Data/IProfileOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/IProfileOrganizationDetails.cs index 0368678641..ed1d9e5f13 100644 --- a/src/Core/AdminConsole/Models/Data/IProfileOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/IProfileOrganizationDetails.cs @@ -53,5 +53,7 @@ public interface IProfileOrganizationDetails bool UseAdminSponsoredFamilies { get; set; } bool UseOrganizationDomains { get; set; } bool UseAutomaticUserConfirmation { get; set; } + bool UseDisableSMAdsForUsers { get; set; } + bool UsePhishingBlocker { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs index 00b9280337..2a7c2cb628 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -65,5 +65,6 @@ public class OrganizationUserOrganizationDetails : IProfileOrganizationDetails public bool UseAdminSponsoredFamilies { get; set; } public bool? IsAdminInitiated { get; set; } public bool UseAutomaticUserConfirmation { get; set; } + public bool UseDisableSMAdsForUsers { get; set; } public bool UsePhishingBlocker { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs index 484320c271..d74fb4f138 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs @@ -154,6 +154,7 @@ public class SelfHostedOrganizationDetails : Organization Status = Status, UseRiskInsights = UseRiskInsights, UseAdminSponsoredFamilies = UseAdminSponsoredFamilies, + UseDisableSmAdsForUsers = UseDisableSmAdsForUsers, UsePhishingBlocker = UsePhishingBlocker, }; } diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs index dcec028dcc..69b17a9a80 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs @@ -56,5 +56,6 @@ public class ProviderUserOrganizationDetails : IProfileOrganizationDetails public string? SsoExternalId { get; set; } public string? Permissions { get; set; } public string? ResetPasswordKey { get; set; } + public bool UseDisableSMAdsForUsers { get; set; } public bool UsePhishingBlocker { get; set; } } diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationBaseView.cs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationBaseView.cs new file mode 100644 index 0000000000..f888496fe8 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationBaseView.cs @@ -0,0 +1,12 @@ +using Bit.Core.Platform.Mail.Mailer; + +namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation; + +public abstract class OrganizationConfirmationBaseView : BaseMailView +{ + public required string OrganizationName { get; set; } + public required string TitleFirst { get; set; } + public required string TitleSecondBold { get; set; } + public required string TitleThird { get; set; } + public required string WebVaultUrl { get; set; } +} diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.cs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.cs new file mode 100644 index 0000000000..6a8a0e18fa --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.cs @@ -0,0 +1,12 @@ +using Bit.Core.Platform.Mail.Mailer; + +namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation; + +public class OrganizationConfirmationEnterpriseTeamsView : OrganizationConfirmationBaseView +{ +} + +public class OrganizationConfirmationEnterpriseTeams : BaseMail +{ + public override required string Subject { get; set; } +} diff --git a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.html.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs similarity index 89% rename from src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.html.hbs rename to src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs index 65e37e87dd..8477efff26 100644 --- a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.html.hbs +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs @@ -178,7 +178,7 @@ - + Log in @@ -502,12 +502,12 @@ - + -
+
- +
@@ -595,8 +595,8 @@ - -
- + +
@@ -612,13 +612,13 @@
- +
+ - @@ -635,13 +635,13 @@ -
+ - +
- +
+ - @@ -658,13 +658,13 @@ -
+ - +
- +
+ - @@ -681,13 +681,13 @@ -
+ - +
- +
+ - @@ -704,13 +704,13 @@ -
+ - +
- +
+ - @@ -727,13 +727,13 @@ -
+ - +
- +
+ - @@ -750,13 +750,13 @@ -
+ - +
- +
+ - @@ -777,15 +777,15 @@ diff --git a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.text.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.text.hbs similarity index 100% rename from src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.text.hbs rename to src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.text.hbs diff --git a/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.cs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.cs new file mode 100644 index 0000000000..9228ec2208 --- /dev/null +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.cs @@ -0,0 +1,12 @@ +using Bit.Core.Platform.Mail.Mailer; + +namespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation; + +public class OrganizationConfirmationFamilyFreeView : OrganizationConfirmationBaseView +{ +} + +public class OrganizationConfirmationFamilyFree : BaseMail +{ + public override required string Subject { get; set; } +} diff --git a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.html.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs similarity index 91% rename from src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.html.hbs rename to src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs index c22bc80a51..cbe09d3e93 100644 --- a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.html.hbs +++ b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs @@ -182,7 +182,7 @@ @@ -670,12 +670,12 @@
+ - +
-

+

© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa Barbara, CA, USA

Always confirm you are on a trusted Bitwarden domain before logging in:
- bitwarden.com | - Learn why we include this + bitwarden.com | + Learn why we include this

- + Log in
- + -
+
- +
@@ -763,8 +763,8 @@ - -
- + +
@@ -780,13 +780,13 @@
- +
+ - @@ -803,13 +803,13 @@ -
+ - +
- +
+ - @@ -826,13 +826,13 @@ -
+ - +
- +
+ - @@ -849,13 +849,13 @@ -
+ - +
- +
+ - @@ -872,13 +872,13 @@ -
+ - +
- +
+ - @@ -895,13 +895,13 @@ -
+ - +
- +
+ - @@ -918,13 +918,13 @@ -
+ - +
- +
+ - @@ -945,15 +945,15 @@ diff --git a/src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.text.hbs b/src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.text.hbs similarity index 100% rename from src/Core/MailTemplates/Handlebars/MJML/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.text.hbs rename to src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.text.hbs diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/OrganizationAbility.cs similarity index 95% rename from src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/OrganizationAbility.cs index 7c8389c103..40249fa2be 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/OrganizationAbility.cs @@ -29,6 +29,7 @@ public class OrganizationAbility UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation; + UseDisableSmAdsForUsers = organization.UseDisableSmAdsForUsers; UsePhishingBlocker = organization.UsePhishingBlocker; } @@ -52,5 +53,6 @@ public class OrganizationAbility public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } public bool UseAutomaticUserConfirmation { get; set; } + public bool UseDisableSmAdsForUsers { get; set; } public bool UsePhishingBlocker { get; set; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md new file mode 100644 index 0000000000..7b92ba3fef --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md @@ -0,0 +1,141 @@ +# Organization Ability Flags + +## Overview + +Many Bitwarden features are tied to specific subscription plans. For example, SCIM and SSO are Enterprise features, +while Event Logs are available to Teams and Enterprise plans. When developing features that require plan-based access +control, we use **Organization Ability Flags** (or simply _abilities_) — explicit boolean properties on the Organization +entity that indicate whether an organization can use a specific feature. + +## The Rule + +**Never check plan types to control feature access.** Always use a dedicated ability flag on the Organization entity. + +### ❌ Don't Do This + +```csharp +// Checking plan type directly +if (organization.PlanType == PlanType.Enterprise || + organization.PlanType == PlanType.Teams || + organization.PlanType == PlanType.Family) +{ + // allow feature... +} +``` + +### ❌ Don't Do This + +```csharp +// Piggybacking off another feature's ability +if (organization.PlanType == PlanType.Enterprise && organization.UseEvents) +{ + // assume they can use some other feature... +} +``` + +### ✅ Do This Instead + +```csharp +// Check the explicit ability flag +if (organization.UseEvents) +{ + // allow UseEvents feature... +} +``` + +## Why This Pattern Matters + +Using explicit ability flags instead of plan type checks provides several benefits: + +1. **Simplicity** — A single boolean check is cleaner and less error-prone than maintaining lists of plan types. + +2. **Centralized Control** — Feature access is managed in one place: the ability assignment during organization + creation/upgrade. No need to hunt through the codebase for scattered plan type checks. + +3. **Flexibility** — Abilities can be set independently of plan type, enabling: + + - Early access programs for features not yet tied to a plan + - Trial access to help customers evaluate a feature before upgrading + - Custom arrangements for specific customers + - A/B testing of features across different cohorts + +4. **Safe Refactoring** — When plans change (e.g., adding a new plan tier, renaming plans, or moving features between + tiers), we only update the ability assignment logic—not every place the feature is used. + +5. **Graceful Downgrades** — When an organization downgrades, we update their abilities. All feature checks + automatically respect the new access level. + +## How It Works + +### Ability Assignment at Signup/Upgrade + +When an organization is created or changes plans, the ability flags are set based on the plan's capabilities: + +```csharp +// During organization creation or plan change +organization.UseGroups = plan.HasGroups; +organization.UseSso = plan.HasSso; +organization.UseScim = plan.HasScim; +organization.UsePolicies = plan.HasPolicies; +organization.UseEvents = plan.HasEvents; +// ... etc +``` + +### Modifying Abilities for Existing Organizations + +To change abilities for existing organizations (e.g., rolling out a feature to a new plan tier), create a database +migration that updates the relevant flag: + +```sql +-- Example: Enable UseEvents for all Teams organizations +UPDATE [dbo].[Organization] +SET UseEvents = 1 +WHERE PlanType IN (17, 18) -- TeamsMonthly = 17, TeamsAnnually = 18 +``` + +Then update the plan-to-ability assignment code so new organizations get the correct value. + +## Adding a New Ability + +When developing a new plan-gated feature: + +1. **Add the ability to the Organization and OrganizationAbility entities** — Create a `Use[FeatureName]` boolean + property. + +2. **Add a database migration** — Add the new column to the Organization table. + +3. **Update plan definitions** — Add a corresponding `Has[FeatureName]` property to the Plan model and configure which + plans include it. + +4. **Update organization creation/upgrade logic** — Ensure the ability is set based on the plan. + +5. **Update the organization license claims** (if applicable) - to make the feature available on self-hosted instances. + +6. **Implement checks throughout client and server** — Use the ability consistently everywhere the feature is accessed. + - Clients: get the organization object from `OrganizationService`. + - Server: if you already have the full `Organization` object in scope, you can use it directly. If not, use the + `IApplicationCacheService` to retrieve the `OrganizationAbility`, which is a simplified, cached representation + of the organization ability flags. Note that some older flags may be missing from `OrganizationAbility` but + can be added if needed. + +## Existing Abilities + +For reference, here are some current organization ability flags (not a complete list): + +| Ability | Description | Plans | +|--------------------------|-------------------------------|-------------------| +| `UseGroups` | Group-based collection access | Teams, Enterprise | +| `UseDirectory` | Directory Connector sync | Teams, Enterprise | +| `UseEvents` | Event logging | Teams, Enterprise | +| `UseTotp` | Authenticator (TOTP) | Teams, Enterprise | +| `UseSso` | Single Sign-On | Enterprise | +| `UseScim` | SCIM provisioning | Teams, Enterprise | +| `UsePolicies` | Enterprise policies | Enterprise | +| `UseResetPassword` | Admin password reset | Enterprise | +| `UseOrganizationDomains` | Domain verification/claiming | Enterprise | + +## Questions? + +If you're unsure whether your feature needs a new ability or which existing ability to use, reach out to your team lead +or members of the Admin Console or Architecture teams. When in doubt, adding an explicit ability is almost always the +right choice—it's easy to do and keeps our access control clean and maintainable. diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs index 67b5f0da80..1b488677ae 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs @@ -1,5 +1,7 @@ -using Bit.Core.AdminConsole.Models.Data.OrganizationUsers; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.Entities; @@ -25,6 +27,8 @@ public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserReposi IPushNotificationService pushNotificationService, IPolicyRequirementQuery policyRequirementQuery, ICollectionRepository collectionRepository, + IFeatureService featureService, + ISendOrganizationConfirmationCommand sendOrganizationConfirmationCommand, TimeProvider timeProvider, ILogger logger) : IAutomaticallyConfirmOrganizationUserCommand { @@ -143,9 +147,7 @@ public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserReposi { var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value); - await mailService.SendOrganizationConfirmedEmailAsync(request.Organization!.Name, - user!.Email, - request.OrganizationUser.AccessSecretsManager); + await SendOrganizationConfirmedEmailAsync(request.Organization!, user!.Email, request.OrganizationUser.AccessSecretsManager); } catch (Exception ex) { @@ -183,4 +185,23 @@ public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserReposi Organization = await organizationRepository.GetByIdAsync(request.OrganizationId) }; } + + /// + /// Sends the organization confirmed email using either the new mailer pattern or the legacy mail service, + /// depending on the feature flag. + /// + /// The organization the user was confirmed to. + /// The email address of the confirmed user. + /// Whether the user has access to Secrets Manager. + internal async Task SendOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager) + { + if (featureService.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)) + { + await sendOrganizationConfirmationCommand.SendConfirmationAsync(organization, userEmail, accessSecretsManager); + } + else + { + await mailService.SendOrganizationConfirmedEmailAsync(organization.Name, userEmail, accessSecretsManager); + } + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index b6b49e93e9..0b82ac7ea4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -1,8 +1,10 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; @@ -35,7 +37,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand private readonly IFeatureService _featureService; private readonly ICollectionRepository _collectionRepository; private readonly IAutomaticUserConfirmationPolicyEnforcementValidator _automaticUserConfirmationPolicyEnforcementValidator; - + private readonly ISendOrganizationConfirmationCommand _sendOrganizationConfirmationCommand; public ConfirmOrganizationUserCommand( IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, @@ -50,7 +52,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand IPolicyRequirementQuery policyRequirementQuery, IFeatureService featureService, ICollectionRepository collectionRepository, - IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator) + IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator, ISendOrganizationConfirmationCommand sendOrganizationConfirmationCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -66,8 +68,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand _featureService = featureService; _collectionRepository = collectionRepository; _automaticUserConfirmationPolicyEnforcementValidator = automaticUserConfirmationPolicyEnforcementValidator; + _sendOrganizationConfirmationCommand = sendOrganizationConfirmationCommand; } - public async Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId, string defaultUserCollectionName = null) { @@ -170,7 +172,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand orgUser.Email = null; await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); - await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager); + await SendOrganizationConfirmedEmailAsync(organization, user.Email, orgUser.AccessSecretsManager); succeededUsers.Add(orgUser); result.Add(Tuple.Create(orgUser, "")); } @@ -280,11 +282,6 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand /// The encrypted default user collection name. private async Task CreateDefaultCollectionAsync(OrganizationUser organizationUser, string defaultUserCollectionName) { - if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) - { - return; - } - // Skip if no collection name provided (backwards compatibility) if (string.IsNullOrWhiteSpace(defaultUserCollectionName)) { @@ -323,11 +320,6 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand private async Task CreateManyDefaultCollectionsAsync(Guid organizationId, IEnumerable confirmedOrganizationUsers, string defaultUserCollectionName) { - if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) - { - return; - } - // Skip if no collection name provided (backwards compatibility) if (string.IsNullOrWhiteSpace(defaultUserCollectionName)) { @@ -349,4 +341,23 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand await _collectionRepository.UpsertDefaultCollectionsAsync(organizationId, eligibleOrganizationUserIds, defaultUserCollectionName); } + + /// + /// Sends the organization confirmed email using either the new mailer pattern or the legacy mail service, + /// depending on the feature flag. + /// + /// The organization the user was confirmed to. + /// The email address of the confirmed user. + /// Whether the user has access to Secrets Manager. + internal async Task SendOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager) + { + if (_featureService.IsEnabled(FeatureFlagKeys.OrganizationConfirmationEmail)) + { + await _sendOrganizationConfirmationCommand.SendConfirmationAsync(organization, userEmail, accessSecretsManager); + } + else + { + await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), userEmail, accessSecretsManager); + } + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/ISendOrganizationConfirmationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/ISendOrganizationConfirmationCommand.cs new file mode 100644 index 0000000000..ae4d7acda5 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/ISendOrganizationConfirmationCommand.cs @@ -0,0 +1,22 @@ +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation; + +public interface ISendOrganizationConfirmationCommand +{ + /// + /// Sends an organization confirmation email to the specified user. + /// + /// The organization to send the confirmation email for. + /// The email address of the user to send the confirmation to. + /// Whether the user has access to Secrets Manager. + Task SendConfirmationAsync(Organization organization, string userEmail, bool accessSecretsManager); + + /// + /// Sends organization confirmation emails to multiple users. + /// + /// The organization to send the confirmation emails for. + /// The email addresses of the users to send confirmations to. + /// Whether the users have access to Secrets Manager. + Task SendConfirmationsAsync(Organization organization, IEnumerable userEmails, bool accessSecretsManager); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommand.cs new file mode 100644 index 0000000000..952478ce36 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommand.cs @@ -0,0 +1,110 @@ +using System.Net; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation; +using Bit.Core.Billing.Enums; +using Bit.Core.Platform.Mail.Mailer; +using Bit.Core.Settings; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation; + +public class SendOrganizationConfirmationCommand(IMailer mailer, GlobalSettings globalSettings) : ISendOrganizationConfirmationCommand +{ + private const string _titleFirst = "You're confirmed as a member of "; + private const string _titleThird = "!"; + + private static string GetConfirmationSubject(string organizationName) => + $"You can now access items from {organizationName}"; + private string GetWebVaultUrl(bool accessSecretsManager) => accessSecretsManager + ? globalSettings.BaseServiceUri.VaultWithHashAndSecretManagerProduct + : globalSettings.BaseServiceUri.VaultWithHash; + + public async Task SendConfirmationAsync(Organization organization, string userEmail, bool accessSecretsManager = false) + { + await SendConfirmationsAsync(organization, [userEmail], accessSecretsManager); + } + + public async Task SendConfirmationsAsync(Organization organization, IEnumerable userEmails, bool accessSecretsManager = false) + { + var userEmailsList = userEmails.ToList(); + + if (userEmailsList.Count == 0) + { + return; + } + + var organizationName = WebUtility.HtmlDecode(organization.Name); + + if (IsEnterpriseOrTeamsPlan(organization.PlanType)) + { + await SendEnterpriseTeamsEmailsAsync(userEmailsList, organizationName, accessSecretsManager); + return; + } + + await SendFamilyFreeConfirmEmailsAsync(userEmailsList, organizationName, accessSecretsManager); + } + + private async Task SendEnterpriseTeamsEmailsAsync(List userEmailsList, string organizationName, bool accessSecretsManager) + { + var mail = new OrganizationConfirmationEnterpriseTeams + { + ToEmails = userEmailsList, + Subject = GetConfirmationSubject(organizationName), + View = new OrganizationConfirmationEnterpriseTeamsView + { + OrganizationName = organizationName, + TitleFirst = _titleFirst, + TitleSecondBold = organizationName, + TitleThird = _titleThird, + WebVaultUrl = GetWebVaultUrl(accessSecretsManager) + } + }; + + await mailer.SendEmail(mail); + } + + private async Task SendFamilyFreeConfirmEmailsAsync(List userEmailsList, string organizationName, bool accessSecretsManager) + { + var mail = new OrganizationConfirmationFamilyFree + { + ToEmails = userEmailsList, + Subject = GetConfirmationSubject(organizationName), + View = new OrganizationConfirmationFamilyFreeView + { + OrganizationName = organizationName, + TitleFirst = _titleFirst, + TitleSecondBold = organizationName, + TitleThird = _titleThird, + WebVaultUrl = GetWebVaultUrl(accessSecretsManager) + } + }; + + await mailer.SendEmail(mail); + } + + + private static bool IsEnterpriseOrTeamsPlan(PlanType planType) + { + return planType switch + { + PlanType.TeamsMonthly2019 or + PlanType.TeamsAnnually2019 or + PlanType.TeamsMonthly2020 or + PlanType.TeamsAnnually2020 or + PlanType.TeamsMonthly2023 or + PlanType.TeamsAnnually2023 or + PlanType.TeamsStarter2023 or + PlanType.TeamsMonthly or + PlanType.TeamsAnnually or + PlanType.TeamsStarter or + PlanType.EnterpriseMonthly2019 or + PlanType.EnterpriseAnnually2019 or + PlanType.EnterpriseMonthly2020 or + PlanType.EnterpriseAnnually2020 or + PlanType.EnterpriseMonthly2023 or + PlanType.EnterpriseAnnually2023 or + PlanType.EnterpriseMonthly or + PlanType.EnterpriseAnnually => true, + _ => false + }; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/Errors.cs new file mode 100644 index 0000000000..8c19544aa9 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/Errors.cs @@ -0,0 +1,7 @@ +using Bit.Core.AdminConsole.Utilities.v2; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser; + +public record OrganizationUserNotFound() : NotFoundError("Organization user not found."); +public record NotEligibleForSelfRevoke() : BadRequestError("User is not eligible for self-revocation. The organization data ownership policy must be enabled and the user must be a confirmed member."); +public record LastOwnerCannotSelfRevoke() : BadRequestError("The last owner cannot revoke themselves."); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/ISelfRevokeOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/ISelfRevokeOrganizationUserCommand.cs new file mode 100644 index 0000000000..3153465a38 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/ISelfRevokeOrganizationUserCommand.cs @@ -0,0 +1,22 @@ +using Bit.Core.AdminConsole.Utilities.v2.Results; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser; + +/// +/// Allows users to revoke themselves from an organization when declining to migrate personal items +/// under the OrganizationDataOwnership policy. +/// +public interface ISelfRevokeOrganizationUserCommand +{ + /// + /// Revokes a user from an organization. + /// + /// The organization ID. + /// The user ID to revoke. + /// A indicating success or containing an error. + /// + /// Validates the OrganizationDataOwnership policy is enabled and applies to the user (currently Owners/Admins are exempt), + /// the user is a confirmed member, and prevents the last owner from revoking themselves. + /// + Task SelfRevokeUserAsync(Guid organizationId, Guid userId); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/SelfRevokeOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/SelfRevokeOrganizationUserCommand.cs new file mode 100644 index 0000000000..afc0236af4 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/SelfRevokeOrganizationUserCommand.cs @@ -0,0 +1,56 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.AdminConsole.Utilities.v2.Results; +using Bit.Core.Enums; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Services; +using OneOf.Types; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser; + +public class SelfRevokeOrganizationUserCommand( + IOrganizationUserRepository organizationUserRepository, + IPolicyRequirementQuery policyRequirementQuery, + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, + IEventService eventService, + IPushNotificationService pushNotificationService) + : ISelfRevokeOrganizationUserCommand +{ + public async Task SelfRevokeUserAsync(Guid organizationId, Guid userId) + { + var organizationUser = await organizationUserRepository.GetByOrganizationAsync(organizationId, userId); + if (organizationUser == null) + { + return new OrganizationUserNotFound(); + } + + var policyRequirement = await policyRequirementQuery.GetAsync(userId); + + if (!policyRequirement.EligibleForSelfRevoke(organizationId)) + { + return new NotEligibleForSelfRevoke(); + } + + // Prevent the last owner from revoking themselves, which would brick the organization + if (organizationUser.Type == OrganizationUserType.Owner) + { + var hasOtherOwner = await hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( + organizationId, + [organizationUser.Id], + includeProvider: true); + + if (!hasOtherOwner) + { + return new LastOwnerCannotSelfRevoke(); + } + } + + await organizationUserRepository.RevokeAsync(organizationUser.Id); + await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_SelfRevoked); + await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId!.Value); + + return new None(); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs index c9653053ea..d30ba5c39f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs @@ -83,6 +83,24 @@ public class OrganizationDataOwnershipPolicyRequirement : IPolicyRequirement return _policyDetails.Any(p => p.OrganizationId == organizationId && p.OrganizationUserStatus == OrganizationUserStatusType.Confirmed); } + + /// + /// Determines if a user is eligible for self-revocation under the Organization Data Ownership policy. + /// A user is eligible if they are a confirmed member of the organization and the policy is enabled. + /// This also handles exempt roles (Owner/Admin) and policy disabled state via the factory's Enforce predicate. + /// + /// The organization ID to check. + /// True if the user is eligible for self-revocation (policy applies to them), false otherwise. + /// + /// Self-revoke is used to opt out of migrating the user's personal vault to the organization as required by this policy. + /// + public bool EligibleForSelfRevoke(Guid organizationId) + { + var policyDetail = _policyDetails + .FirstOrDefault(p => p.OrganizationId == organizationId); + + return policyDetail?.HasStatus([OrganizationUserStatusType.Confirmed]) ?? false; + } } public record DefaultCollectionRequest(Guid OrganizationUserId, bool ShouldCreateDefaultCollection) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs index 0bee2a55af..7a47baa65a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs @@ -6,15 +6,13 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Repositories; -using Bit.Core.Services; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; public class OrganizationDataOwnershipPolicyValidator( IPolicyRepository policyRepository, ICollectionRepository collectionRepository, - IEnumerable> factories, - IFeatureService featureService) + IEnumerable> factories) : OrganizationPolicyValidator(policyRepository, factories), IPostSavePolicySideEffect, IOnPolicyPostUpdateEvent { public PolicyType Type => PolicyType.OrganizationDataOwnership; @@ -32,11 +30,6 @@ public class OrganizationDataOwnershipPolicyValidator( Policy postUpdatedPolicy, Policy? previousPolicyState) { - if (!featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) - { - return; - } - if (policyRequest.Metadata is not OrganizationModelOwnershipPolicyModel metadata) { return; diff --git a/src/Core/AdminConsole/Services/IIntegrationConfigurationDetailsCache.cs b/src/Core/AdminConsole/Services/IIntegrationConfigurationDetailsCache.cs deleted file mode 100644 index ad27429112..0000000000 --- a/src/Core/AdminConsole/Services/IIntegrationConfigurationDetailsCache.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable enable - -using Bit.Core.Enums; -using Bit.Core.Models.Data.Organizations; - -namespace Bit.Core.Services; - -public interface IIntegrationConfigurationDetailsCache -{ - List GetConfigurationDetails( - Guid organizationId, - IntegrationType integrationType, - EventType eventType); -} diff --git a/src/Core/AdminConsole/Services/OrganizationFactory.cs b/src/Core/AdminConsole/Services/OrganizationFactory.cs index 0c64a27431..fadafd179c 100644 --- a/src/Core/AdminConsole/Services/OrganizationFactory.cs +++ b/src/Core/AdminConsole/Services/OrganizationFactory.cs @@ -62,6 +62,8 @@ public static class OrganizationFactory UseAdminSponsoredFamilies = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseAdminSponsoredFamilies), UseAutomaticUserConfirmation = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseAutomaticUserConfirmation), + UseDisableSmAdsForUsers = + claimsPrincipal.GetValue(OrganizationLicenseConstants.UseDisableSmAdsForUsers), UsePhishingBlocker = claimsPrincipal.GetValue(OrganizationLicenseConstants.UsePhishingBlocker), }; @@ -113,6 +115,7 @@ public static class OrganizationFactory UseOrganizationDomains = license.UseOrganizationDomains, UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies, UseAutomaticUserConfirmation = license.UseAutomaticUserConfirmation, + UseDisableSmAdsForUsers = license.UseDisableSmAdsForUsers, UsePhishingBlocker = license.UsePhishingBlocker, }; } diff --git a/src/Core/Auth/Identity/Policies.cs b/src/Core/Auth/Identity/Policies.cs index b2d94b0a6e..698a890006 100644 --- a/src/Core/Auth/Identity/Policies.cs +++ b/src/Core/Auth/Identity/Policies.cs @@ -5,12 +5,94 @@ public static class Policies /// /// Policy for managing access to the Send feature. /// - public const string Send = "Send"; // [Authorize(Policy = Policies.Send)] - public const string Application = "Application"; // [Authorize(Policy = Policies.Application)] - public const string Web = "Web"; // [Authorize(Policy = Policies.Web)] - public const string Push = "Push"; // [Authorize(Policy = Policies.Push)] + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Send)] + /// + /// + /// + public const string Send = "Send"; + + /// + /// Policy to manage access to general API endpoints. + /// + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Application)] + /// + /// + /// + public const string Application = "Application"; + + /// + /// Policy to manage access to API endpoints intended for use by the Web Vault and browser extension only. + /// + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Web)] + /// + /// + /// + public const string Web = "Web"; + + /// + /// Policy to restrict access to API endpoints for the Push feature. + /// + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Push)] + /// + /// + /// + public const string Push = "Push"; + + // TODO: This is unused public const string Licensing = "Licensing"; // [Authorize(Policy = Policies.Licensing)] - public const string Organization = "Organization"; // [Authorize(Policy = Policies.Organization)] - public const string Installation = "Installation"; // [Authorize(Policy = Policies.Installation)] - public const string Secrets = "Secrets"; // [Authorize(Policy = Policies.Secrets)] + + /// + /// Policy to restrict access to API endpoints related to the Organization features. + /// + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Licensing)] + /// + /// + /// + public const string Organization = "Organization"; + + /// + /// Policy to restrict access to API endpoints related to the setting up new installations. + /// + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Installation)] + /// + /// + /// + public const string Installation = "Installation"; + + /// + /// Policy to restrict access to API endpoints for Secrets Manager features. + /// + /// + /// + /// Can be used with the Authorize attribute, for example: + /// + /// [Authorize(Policy = Policies.Secrets)] + /// + /// + /// + public const string Secrets = "Secrets"; } diff --git a/src/Core/Auth/Models/Data/SetInitialMasterPasswordDataModel.cs b/src/Core/Auth/Models/Data/SetInitialMasterPasswordDataModel.cs new file mode 100644 index 0000000000..82bcb3da5e --- /dev/null +++ b/src/Core/Auth/Models/Data/SetInitialMasterPasswordDataModel.cs @@ -0,0 +1,23 @@ +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.Auth.Models.Data; + +/// +/// Data model for setting an initial master password for a user. +/// +public class SetInitialMasterPasswordDataModel +{ + public required MasterPasswordAuthenticationData MasterPasswordAuthentication { get; set; } + public required MasterPasswordUnlockData MasterPasswordUnlock { get; set; } + + /// + /// Organization SSO identifier. + /// + public required string OrgSsoIdentifier { get; set; } + + /// + /// User account keys. Required for Master Password decryption user. + /// + public required UserAccountKeysData? AccountKeys { get; set; } + public string? MasterPasswordHint { get; set; } +} diff --git a/src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ISetInitialMasterPasswordCommand.cs b/src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ISetInitialMasterPasswordCommand.cs index 31dd19d5bf..d695f2586c 100644 --- a/src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ISetInitialMasterPasswordCommand.cs +++ b/src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ISetInitialMasterPasswordCommand.cs @@ -1,19 +1,25 @@ -using Bit.Core.Entities; -using Microsoft.AspNetCore.Identity; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Entities; +using Bit.Core.Exceptions; namespace Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; /// /// Manages the setting of the initial master password for a in an organization. -/// This class is primarily invoked in two scenarios: -/// 1) In organizations configured with Single Sign-On (SSO) and master password decryption: +/// In organizations configured with Single Sign-On (SSO) and master password decryption: /// just in time (JIT) provisioned users logging in via SSO are required to set a master password. -/// 2) In organizations configured with SSO and trusted devices decryption: -/// Users who are upgraded to have admin account recovery permissions must set a master password -/// to ensure their ability to reset other users' accounts. /// public interface ISetInitialMasterPasswordCommand { - public Task SetInitialMasterPasswordAsync(User user, string masterPassword, string key, - string orgSsoIdentifier); + /// + /// Sets the initial master password and account keys for the specified user. + /// + /// User to set the master password for + /// Initial master password setup data + /// A task that completes when the operation succeeds + /// + /// Thrown if the user's master password is already set, the organization is not found, + /// the user is not a member of the organization, or the account keys are missing. + /// + public Task SetInitialMasterPasswordAsync(User user, SetInitialMasterPasswordDataModel masterPasswordDataModel); } diff --git a/src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ISetInitialMasterPasswordCommandV1.cs b/src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ISetInitialMasterPasswordCommandV1.cs new file mode 100644 index 0000000000..7f4818a535 --- /dev/null +++ b/src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ISetInitialMasterPasswordCommandV1.cs @@ -0,0 +1,21 @@ +using Bit.Core.Entities; +using Microsoft.AspNetCore.Identity; + +namespace Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; + +/// +/// Manages the setting of the initial master password for a in an organization. +/// This class is primarily invoked in two scenarios: +/// 1) In organizations configured with Single Sign-On (SSO) and master password decryption: +/// just in time (JIT) provisioned users logging in via SSO are required to set a master password. +/// 2) In organizations configured with SSO and trusted devices decryption: +/// Users who are upgraded to have admin account recovery permissions must set a master password +/// to ensure their ability to reset other users' accounts. +/// +// TODO removed with https://bitwarden.atlassian.net/browse/PM-27327 +[Obsolete("Use ISetInitialMasterPasswordCommand instead")] +public interface ISetInitialMasterPasswordCommandV1 +{ + public Task SetInitialMasterPasswordAsync(User user, string masterPassword, string key, + string orgSsoIdentifier); +} diff --git a/src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ITdeSetPasswordCommand.cs b/src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ITdeSetPasswordCommand.cs new file mode 100644 index 0000000000..4815aea8a0 --- /dev/null +++ b/src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ITdeSetPasswordCommand.cs @@ -0,0 +1,26 @@ +using Bit.Core.Auth.Models.Data; +using Bit.Core.Entities; +using Bit.Core.Exceptions; + +namespace Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; + +/// +/// Manages the setting of the master password for a TDE in an organization. +/// In organizations configured with SSO and trusted devices decryption: +/// Users who are upgraded to have admin account recovery permissions must set a master password +/// to ensure their ability to reset other users' accounts. +/// +public interface ITdeSetPasswordCommand +{ + /// + /// Sets the master password for the specified TDE user. + /// + /// User to set the master password for + /// Master password setup data + /// A task that completes when the operation succeeds + /// + /// Thrown if the user's master password is already set, the organization is not found, + /// the user is not a member of the organization, or the user is a TDE user without account keys set. + /// + Task SetMasterPasswordAsync(User user, SetInitialMasterPasswordDataModel masterPasswordDataModel); +} diff --git a/src/Core/Auth/UserFeatures/UserMasterPassword/SetInitialMasterPasswordCommand.cs b/src/Core/Auth/UserFeatures/UserMasterPassword/SetInitialMasterPasswordCommand.cs index 32966f5710..b91ac61f7f 100644 --- a/src/Core/Auth/UserFeatures/UserMasterPassword/SetInitialMasterPasswordCommand.cs +++ b/src/Core/Auth/UserFeatures/UserMasterPassword/SetInitialMasterPasswordCommand.cs @@ -1,4 +1,5 @@ -using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -6,98 +7,74 @@ using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Logging; namespace Bit.Core.Auth.UserFeatures.UserMasterPassword; public class SetInitialMasterPasswordCommand : ISetInitialMasterPasswordCommand { - private readonly ILogger _logger; - private readonly IdentityErrorDescriber _identityErrorDescriber; private readonly IUserService _userService; private readonly IUserRepository _userRepository; - private readonly IEventService _eventService; private readonly IAcceptOrgUserCommand _acceptOrgUserCommand; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationRepository _organizationRepository; + private readonly IPasswordHasher _passwordHasher; + private readonly IEventService _eventService; - - public SetInitialMasterPasswordCommand( - ILogger logger, - IdentityErrorDescriber identityErrorDescriber, - IUserService userService, - IUserRepository userRepository, - IEventService eventService, - IAcceptOrgUserCommand acceptOrgUserCommand, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository) + public SetInitialMasterPasswordCommand(IUserService userService, IUserRepository userRepository, + IAcceptOrgUserCommand acceptOrgUserCommand, IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, IPasswordHasher passwordHasher, + IEventService eventService) { - _logger = logger; - _identityErrorDescriber = identityErrorDescriber; _userService = userService; _userRepository = userRepository; - _eventService = eventService; _acceptOrgUserCommand = acceptOrgUserCommand; _organizationUserRepository = organizationUserRepository; _organizationRepository = organizationRepository; + _passwordHasher = passwordHasher; + _eventService = eventService; } - public async Task SetInitialMasterPasswordAsync(User user, string masterPassword, string key, - string orgSsoIdentifier) + public async Task SetInitialMasterPasswordAsync(User user, + SetInitialMasterPasswordDataModel masterPasswordDataModel) { - if (user == null) + if (user.Key != null) { - throw new ArgumentNullException(nameof(user)); + throw new BadRequestException("User already has a master password set."); } - if (!string.IsNullOrWhiteSpace(user.MasterPassword)) + if (masterPasswordDataModel.AccountKeys == null) { - _logger.LogWarning("Change password failed for user {userId} - already has password.", user.Id); - return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword()); + throw new BadRequestException("Account keys are required."); } - var result = await _userService.UpdatePasswordHash(user, masterPassword, validatePassword: true, refreshStamp: false); - if (!result.Succeeded) - { - return result; - } - - user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; - user.Key = key; - - await _userRepository.ReplaceAsync(user); - await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword); - - - if (string.IsNullOrWhiteSpace(orgSsoIdentifier)) - { - throw new BadRequestException("Organization SSO Identifier required."); - } - - var org = await _organizationRepository.GetByIdentifierAsync(orgSsoIdentifier); + // Prevent a de-synced salt value from creating an un-decryptable unlock method + masterPasswordDataModel.MasterPasswordAuthentication.ValidateSaltUnchangedForUser(user); + masterPasswordDataModel.MasterPasswordUnlock.ValidateSaltUnchangedForUser(user); + var org = await _organizationRepository.GetByIdentifierAsync(masterPasswordDataModel.OrgSsoIdentifier); if (org == null) { - throw new BadRequestException("Organization invalid."); + throw new BadRequestException("Organization SSO identifier is invalid."); } var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id); - if (orgUser == null) { throw new BadRequestException("User not found within organization."); } - // TDE users who go from a user without admin acct recovery permission to having it will be - // required to set a MP for the first time and we don't want to re-execute the accept logic - // as they are already confirmed. - // TLDR: only accept post SSO user if they are invited - if (orgUser.Status == OrganizationUserStatusType.Invited) - { - await _acceptOrgUserCommand.AcceptOrgUserAsync(orgUser, user, _userService); - } + // Hash the provided user master password authentication hash on the server side + var serverSideHashedMasterPasswordAuthenticationHash = _passwordHasher.HashPassword(user, + masterPasswordDataModel.MasterPasswordAuthentication.MasterPasswordAuthenticationHash); - return IdentityResult.Success; + var setMasterPasswordTask = _userRepository.SetMasterPassword(user.Id, + masterPasswordDataModel.MasterPasswordUnlock, serverSideHashedMasterPasswordAuthenticationHash, + masterPasswordDataModel.MasterPasswordHint); + await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, masterPasswordDataModel.AccountKeys, + [setMasterPasswordTask]); + + await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword); + + await _acceptOrgUserCommand.AcceptOrgUserAsync(orgUser, user, _userService); } - } diff --git a/src/Core/Auth/UserFeatures/UserMasterPassword/SetInitialMasterPasswordCommandV1.cs b/src/Core/Auth/UserFeatures/UserMasterPassword/SetInitialMasterPasswordCommandV1.cs new file mode 100644 index 0000000000..df5f0d02f7 --- /dev/null +++ b/src/Core/Auth/UserFeatures/UserMasterPassword/SetInitialMasterPasswordCommandV1.cs @@ -0,0 +1,103 @@ +using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Auth.UserFeatures.UserMasterPassword; + +public class SetInitialMasterPasswordCommandV1 : ISetInitialMasterPasswordCommandV1 +{ + private readonly ILogger _logger; + private readonly IdentityErrorDescriber _identityErrorDescriber; + private readonly IUserService _userService; + private readonly IUserRepository _userRepository; + private readonly IEventService _eventService; + private readonly IAcceptOrgUserCommand _acceptOrgUserCommand; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationRepository _organizationRepository; + + + public SetInitialMasterPasswordCommandV1( + ILogger logger, + IdentityErrorDescriber identityErrorDescriber, + IUserService userService, + IUserRepository userRepository, + IEventService eventService, + IAcceptOrgUserCommand acceptOrgUserCommand, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository) + { + _logger = logger; + _identityErrorDescriber = identityErrorDescriber; + _userService = userService; + _userRepository = userRepository; + _eventService = eventService; + _acceptOrgUserCommand = acceptOrgUserCommand; + _organizationUserRepository = organizationUserRepository; + _organizationRepository = organizationRepository; + } + + public async Task SetInitialMasterPasswordAsync(User user, string masterPassword, string key, + string orgSsoIdentifier) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (!string.IsNullOrWhiteSpace(user.MasterPassword)) + { + _logger.LogWarning("Change password failed for user {userId} - already has password.", user.Id); + return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword()); + } + + var result = await _userService.UpdatePasswordHash(user, masterPassword, validatePassword: true, refreshStamp: false); + if (!result.Succeeded) + { + return result; + } + + user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; + user.Key = key; + + await _userRepository.ReplaceAsync(user); + await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword); + + + if (string.IsNullOrWhiteSpace(orgSsoIdentifier)) + { + throw new BadRequestException("Organization SSO Identifier required."); + } + + var org = await _organizationRepository.GetByIdentifierAsync(orgSsoIdentifier); + + if (org == null) + { + throw new BadRequestException("Organization invalid."); + } + + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id); + + if (orgUser == null) + { + throw new BadRequestException("User not found within organization."); + } + + // TDE users who go from a user without admin acct recovery permission to having it will be + // required to set a MP for the first time and we don't want to re-execute the accept logic + // as they are already confirmed. + // TLDR: only accept post SSO user if they are invited + if (orgUser.Status == OrganizationUserStatusType.Invited) + { + await _acceptOrgUserCommand.AcceptOrgUserAsync(orgUser, user, _userService); + } + + return IdentityResult.Success; + } + +} diff --git a/src/Core/Auth/UserFeatures/UserMasterPassword/TdeSetPasswordCommand.cs b/src/Core/Auth/UserFeatures/UserMasterPassword/TdeSetPasswordCommand.cs new file mode 100644 index 0000000000..afd28e95d9 --- /dev/null +++ b/src/Core/Auth/UserFeatures/UserMasterPassword/TdeSetPasswordCommand.cs @@ -0,0 +1,70 @@ +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Identity; + +namespace Bit.Core.Auth.UserFeatures.UserMasterPassword; + +public class TdeSetPasswordCommand : ITdeSetPasswordCommand +{ + private readonly IUserRepository _userRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IPasswordHasher _passwordHasher; + private readonly IEventService _eventService; + + public TdeSetPasswordCommand(IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository, + IPasswordHasher passwordHasher, IEventService eventService) + { + _userRepository = userRepository; + _organizationUserRepository = organizationUserRepository; + _organizationRepository = organizationRepository; + _passwordHasher = passwordHasher; + _eventService = eventService; + } + + public async Task SetMasterPasswordAsync(User user, SetInitialMasterPasswordDataModel masterPasswordDataModel) + { + if (user.Key != null) + { + throw new BadRequestException("User already has a master password set."); + } + + if (user.PublicKey == null || user.PrivateKey == null) + { + throw new BadRequestException("TDE user account keys must be set before setting initial master password."); + } + + // Prevent a de-synced salt value from creating an un-decryptable unlock method + masterPasswordDataModel.MasterPasswordAuthentication.ValidateSaltUnchangedForUser(user); + masterPasswordDataModel.MasterPasswordUnlock.ValidateSaltUnchangedForUser(user); + + var org = await _organizationRepository.GetByIdentifierAsync(masterPasswordDataModel.OrgSsoIdentifier); + if (org == null) + { + throw new BadRequestException("Organization SSO identifier is invalid."); + } + + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id); + if (orgUser == null) + { + throw new BadRequestException("User not found within organization."); + } + + // Hash the provided user master password authentication hash on the server side + var serverSideHashedMasterPasswordAuthenticationHash = _passwordHasher.HashPassword(user, + masterPasswordDataModel.MasterPasswordAuthentication.MasterPasswordAuthenticationHash); + + var setMasterPasswordTask = _userRepository.SetMasterPassword(user.Id, + masterPasswordDataModel.MasterPasswordUnlock, serverSideHashedMasterPasswordAuthenticationHash, + masterPasswordDataModel.MasterPasswordHint); + await _userRepository.UpdateUserDataAsync([setMasterPasswordTask]); + + await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword); + } +} diff --git a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs index 7c50f7f17b..6249d1cb1c 100644 --- a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs +++ b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs @@ -44,6 +44,8 @@ public static class UserServiceCollectionExtensions private static void AddUserPasswordCommands(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } private static void AddTdeOffboardingPasswordCommands(this IServiceCollection services) diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index dc128127ae..d25962a7ba 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -67,6 +67,10 @@ public static class StripeConstants public const string BraintreeCustomerId = "btCustomerId"; public const string InvoiceApproved = "invoice_approved"; public const string OrganizationId = "organizationId"; + public const string PreviousAdditionalStorage = "previous_additional_storage"; + public const string PreviousPeriodEndDate = "previous_period_end_date"; + public const string PreviousPremiumPriceId = "previous_premium_price_id"; + public const string PreviousPremiumUserId = "previous_premium_user_id"; public const string ProviderId = "providerId"; public const string Region = "region"; public const string RetiredBraintreeCustomerId = "btCustomerId_old"; diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 5ceefed603..d121ab04aa 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Bit.Core.Billing.Caches; using Bit.Core.Billing.Caches.Implementations; +using Bit.Core.Billing.Licenses; using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Billing.Organizations.Commands; using Bit.Core.Billing.Organizations.Queries; @@ -28,6 +29,7 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddTransient(); services.AddLicenseServices(); + services.AddLicenseOperations(); services.AddPricingClient(); services.AddPaymentOperations(); services.AddOrganizationLicenseCommandsQueries(); @@ -51,6 +53,8 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddTransient(); + services.AddScoped(); + services.AddScoped(); } private static void AddPremiumQueries(this IServiceCollection services) diff --git a/src/Core/Billing/Licenses/LicenseConstants.cs b/src/Core/Billing/Licenses/LicenseConstants.cs index 727bcbc229..e7188c5d6f 100644 --- a/src/Core/Billing/Licenses/LicenseConstants.cs +++ b/src/Core/Billing/Licenses/LicenseConstants.cs @@ -44,6 +44,7 @@ public static class OrganizationLicenseConstants public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies); public const string UseOrganizationDomains = nameof(UseOrganizationDomains); public const string UseAutomaticUserConfirmation = nameof(UseAutomaticUserConfirmation); + public const string UseDisableSmAdsForUsers = nameof(UseDisableSmAdsForUsers); public const string UsePhishingBlocker = nameof(UsePhishingBlocker); } diff --git a/src/Core/Billing/Licenses/Models/Api/Response/LicenseResponseModel.cs b/src/Core/Billing/Licenses/Models/Api/Response/LicenseResponseModel.cs new file mode 100644 index 0000000000..60f8f0e81a --- /dev/null +++ b/src/Core/Billing/Licenses/Models/Api/Response/LicenseResponseModel.cs @@ -0,0 +1,44 @@ +using System.Security.Claims; +using Bit.Core.Billing.Licenses.Extensions; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Models.Api; + +namespace Bit.Core.Billing.Licenses.Models.Api.Response; + +/// +/// Response model containing user license information. +/// Separated from subscription data to maintain separation of concerns. +/// +public class LicenseResponseModel : ResponseModel +{ + public LicenseResponseModel(UserLicense license, ClaimsPrincipal? claimsPrincipal) + : base("license") + { + License = license; + + // CRITICAL: When a license has a Token (JWT), ALWAYS use the expiration from the token claim + // The token's expiration is cryptographically secured and cannot be tampered with + // The file's Expires property can be manually edited and should NOT be trusted for display + if (claimsPrincipal != null) + { + Expiration = claimsPrincipal.GetValue(UserLicenseConstants.Expires); + } + else + { + // No token - use the license file expiration (for older licenses without tokens) + Expiration = license.Expires; + } + } + + /// + /// The user's license containing feature entitlements and metadata. + /// + public UserLicense License { get; set; } + + /// + /// The license expiration date. + /// Extracted from the cryptographically secured JWT token when available, + /// otherwise falls back to the license file's expiration date. + /// + public DateTime? Expiration { get; set; } +} diff --git a/src/Core/Billing/Licenses/Queries/GetUserLicenseQuery.cs b/src/Core/Billing/Licenses/Queries/GetUserLicenseQuery.cs new file mode 100644 index 0000000000..16344116cb --- /dev/null +++ b/src/Core/Billing/Licenses/Queries/GetUserLicenseQuery.cs @@ -0,0 +1,23 @@ +using Bit.Core.Billing.Licenses.Models.Api.Response; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Services; + +namespace Bit.Core.Billing.Licenses.Queries; + +public interface IGetUserLicenseQuery +{ + Task Run(User user); +} + +public class GetUserLicenseQuery( + IUserService userService, + ILicensingService licensingService) : IGetUserLicenseQuery +{ + public async Task Run(User user) + { + var license = await userService.GenerateLicenseAsync(user); + var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license); + return new LicenseResponseModel(license, claimsPrincipal); + } +} diff --git a/src/Core/Billing/Licenses/Registrations.cs b/src/Core/Billing/Licenses/Registrations.cs new file mode 100644 index 0000000000..74c449a355 --- /dev/null +++ b/src/Core/Billing/Licenses/Registrations.cs @@ -0,0 +1,13 @@ +using Bit.Core.Billing.Licenses.Queries; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Billing.Licenses; + +public static class Registrations +{ + public static void AddLicenseOperations(this IServiceCollection services) + { + // Queries + services.AddTransient(); + } +} diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs index 4a4771857e..67e47a742a 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -57,6 +57,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory p.Name) .Select(p => $"{p.Name}:{Core.Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}") @@ -425,6 +427,7 @@ public class OrganizationLicense : ILicense var useAdminSponsoredFamilies = claimsPrincipal.GetValue(nameof(UseAdminSponsoredFamilies)); var useOrganizationDomains = claimsPrincipal.GetValue(nameof(UseOrganizationDomains)); var useAutomaticUserConfirmation = claimsPrincipal.GetValue(nameof(UseAutomaticUserConfirmation)); + var useDisableSmAdsForUsers = claimsPrincipal.GetValue(nameof(UseDisableSmAdsForUsers)); var claimedPlanType = claimsPrincipal.GetValue(nameof(PlanType)); @@ -461,7 +464,8 @@ public class OrganizationLicense : ILicense smServiceAccounts == organization.SmServiceAccounts && useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies && useOrganizationDomains == organization.UseOrganizationDomains && - useAutomaticUserConfirmation == organization.UseAutomaticUserConfirmation; + useAutomaticUserConfirmation == organization.UseAutomaticUserConfirmation && + useDisableSmAdsForUsers == organization.UseDisableSmAdsForUsers; } diff --git a/src/Core/Billing/Premium/Commands/UpdatePremiumStorageCommand.cs b/src/Core/Billing/Premium/Commands/UpdatePremiumStorageCommand.cs new file mode 100644 index 0000000000..610c112e08 --- /dev/null +++ b/src/Core/Billing/Premium/Commands/UpdatePremiumStorageCommand.cs @@ -0,0 +1,144 @@ +using Bit.Core.Billing.Commands; +using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.Extensions.Logging; +using OneOf.Types; +using Stripe; + +namespace Bit.Core.Billing.Premium.Commands; + +/// +/// Updates the storage allocation for a premium user's subscription. +/// Handles both increases and decreases in storage in an idempotent manner. +/// +public interface IUpdatePremiumStorageCommand +{ + /// + /// Updates the user's storage by the specified additional amount. + /// + /// The premium user whose storage should be updated. + /// The additional storage amount in GB beyond base storage. + /// A billing command result indicating success or failure. + Task> Run(User user, short additionalStorageGb); +} + +public class UpdatePremiumStorageCommand( + IStripeAdapter stripeAdapter, + IUserService userService, + IPricingClient pricingClient, + ILogger logger) + : BaseBillingCommand(logger), IUpdatePremiumStorageCommand +{ + public Task> Run(User user, short additionalStorageGb) => HandleAsync(async () => + { + if (!user.Premium) + { + return new BadRequest("User does not have a premium subscription."); + } + + if (!user.MaxStorageGb.HasValue) + { + return new BadRequest("No access to storage."); + } + + // Fetch all premium plans and the user's subscription to find which plan they're on + var premiumPlans = await pricingClient.ListPremiumPlans(); + var subscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId); + + // Find the password manager subscription item (seat, not storage) and match it to a plan + var passwordManagerItem = subscription.Items.Data.FirstOrDefault(i => + premiumPlans.Any(p => p.Seat.StripePriceId == i.Price.Id)); + + if (passwordManagerItem == null) + { + return new BadRequest("Premium subscription item not found."); + } + + var premiumPlan = premiumPlans.First(p => p.Seat.StripePriceId == passwordManagerItem.Price.Id); + + var baseStorageGb = (short)premiumPlan.Storage.Provided; + + if (additionalStorageGb < 0) + { + return new BadRequest("Additional storage cannot be negative."); + } + + var newTotalStorageGb = (short)(baseStorageGb + additionalStorageGb); + + if (newTotalStorageGb > 100) + { + return new BadRequest("Maximum storage is 100 GB."); + } + + // Idempotency check: if user already has the requested storage, return success + if (user.MaxStorageGb == newTotalStorageGb) + { + return new None(); + } + + var remainingStorage = user.StorageBytesRemaining(newTotalStorageGb); + if (remainingStorage < 0) + { + return new BadRequest( + $"You are currently using {CoreHelpers.ReadableBytesSize(user.Storage.GetValueOrDefault(0))} of storage. " + + "Delete some stored data first."); + } + + // Find the storage line item in the subscription + var storageItem = subscription.Items.Data.FirstOrDefault(i => i.Price.Id == premiumPlan.Storage.StripePriceId); + + var subscriptionItemOptions = new List(); + + if (additionalStorageGb > 0) + { + if (storageItem != null) + { + // Update existing storage item + subscriptionItemOptions.Add(new SubscriptionItemOptions + { + Id = storageItem.Id, + Price = premiumPlan.Storage.StripePriceId, + Quantity = additionalStorageGb + }); + } + else + { + // Add new storage item + subscriptionItemOptions.Add(new SubscriptionItemOptions + { + Price = premiumPlan.Storage.StripePriceId, + Quantity = additionalStorageGb + }); + } + } + else if (storageItem != null) + { + // Remove storage item if setting to 0 + subscriptionItemOptions.Add(new SubscriptionItemOptions + { + Id = storageItem.Id, + Deleted = true + }); + } + + // Update subscription with prorations + // Storage is billed annually, so we create prorations and invoice immediately + var subscriptionUpdateOptions = new SubscriptionUpdateOptions + { + Items = subscriptionItemOptions, + ProrationBehavior = Core.Constants.CreateProrations + }; + + await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, subscriptionUpdateOptions); + + // Update the user's max storage + user.MaxStorageGb = newTotalStorageGb; + await userService.SaveUserAsync(user); + + // No payment intent needed - the subscription update will automatically create and finalize the invoice + return new None(); + }); +} diff --git a/src/Core/Billing/Premium/Commands/UpgradePremiumToOrganizationCommand.cs b/src/Core/Billing/Premium/Commands/UpgradePremiumToOrganizationCommand.cs new file mode 100644 index 0000000000..81bc5c9e2c --- /dev/null +++ b/src/Core/Billing/Premium/Commands/UpgradePremiumToOrganizationCommand.cs @@ -0,0 +1,228 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Commands; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.Extensions.Logging; +using OneOf.Types; +using Stripe; + +namespace Bit.Core.Billing.Premium.Commands; +/// +/// Upgrades a user's Premium subscription to an Organization plan by creating a new Organization +/// and transferring the subscription from the User to the Organization. +/// +public interface IUpgradePremiumToOrganizationCommand +{ + /// + /// Upgrades a Premium subscription to an Organization subscription. + /// + /// The user with an active Premium subscription to upgrade. + /// The name for the new organization. + /// The encrypted organization key for the owner. + /// The target organization plan type to upgrade to. + /// A billing command result indicating success or failure with appropriate error details. + Task> Run( + User user, + string organizationName, + string key, + PlanType targetPlanType); +} + +public class UpgradePremiumToOrganizationCommand( + ILogger logger, + IPricingClient pricingClient, + IStripeAdapter stripeAdapter, + IUserService userService, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationApiKeyRepository organizationApiKeyRepository, + IApplicationCacheService applicationCacheService) + : BaseBillingCommand(logger), IUpgradePremiumToOrganizationCommand +{ + public Task> Run( + User user, + string organizationName, + string key, + PlanType targetPlanType) => HandleAsync(async () => + { + // Validate that the user has an active Premium subscription + if (user is not { Premium: true, GatewaySubscriptionId: not null and not "" }) + { + return new BadRequest("User does not have an active Premium subscription."); + } + + // Hardcode seats to 1 for upgrade flow + const int seats = 1; + + // Fetch the current Premium subscription from Stripe + var currentSubscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId); + + // Fetch all premium plans to find which specific plan the user is on + var premiumPlans = await pricingClient.ListPremiumPlans(); + + // Find the password manager subscription item (seat, not storage) and match it to a plan + var passwordManagerItem = currentSubscription.Items.Data.FirstOrDefault(i => + premiumPlans.Any(p => p.Seat.StripePriceId == i.Price.Id)); + + if (passwordManagerItem == null) + { + return new BadRequest("Premium subscription item not found."); + } + + var usersPremiumPlan = premiumPlans.First(p => p.Seat.StripePriceId == passwordManagerItem.Price.Id); + + // Get the target organization plan + var targetPlan = await pricingClient.GetPlanOrThrow(targetPlanType); + + // Build the list of subscription item updates + var subscriptionItemOptions = new List(); + + // Delete the user's specific password manager item + subscriptionItemOptions.Add(new SubscriptionItemOptions + { + Id = passwordManagerItem.Id, + Deleted = true + }); + + // Delete the storage item if it exists for this user's plan + var storageItem = currentSubscription.Items.Data.FirstOrDefault(i => + i.Price.Id == usersPremiumPlan.Storage.StripePriceId); + + // Capture the previous additional storage quantity for potential revert + var previousAdditionalStorage = storageItem?.Quantity ?? 0; + + if (storageItem != null) + { + subscriptionItemOptions.Add(new SubscriptionItemOptions + { + Id = storageItem.Id, + Deleted = true + }); + } + + // Add new organization subscription items + if (targetPlan.HasNonSeatBasedPasswordManagerPlan()) + { + subscriptionItemOptions.Add(new SubscriptionItemOptions + { + Price = targetPlan.PasswordManager.StripePlanId, + Quantity = 1 + }); + } + else + { + subscriptionItemOptions.Add(new SubscriptionItemOptions + { + Price = targetPlan.PasswordManager.StripeSeatPlanId, + Quantity = seats + }); + } + + // Generate organization ID early to include in metadata + var organizationId = CoreHelpers.GenerateComb(); + + // Build the subscription update options + var subscriptionUpdateOptions = new SubscriptionUpdateOptions + { + Items = subscriptionItemOptions, + ProrationBehavior = StripeConstants.ProrationBehavior.None, + Metadata = new Dictionary + { + [StripeConstants.MetadataKeys.OrganizationId] = organizationId.ToString(), + [StripeConstants.MetadataKeys.PreviousPremiumPriceId] = usersPremiumPlan.Seat.StripePriceId, + [StripeConstants.MetadataKeys.PreviousPeriodEndDate] = currentSubscription.GetCurrentPeriodEnd()?.ToString("O") ?? string.Empty, + [StripeConstants.MetadataKeys.PreviousAdditionalStorage] = previousAdditionalStorage.ToString(), + [StripeConstants.MetadataKeys.PreviousPremiumUserId] = user.Id.ToString(), + [StripeConstants.MetadataKeys.UserId] = string.Empty // Remove userId to unlink subscription from User + } + }; + + // Create the Organization entity + var organization = new Organization + { + Id = organizationId, + Name = organizationName, + BillingEmail = user.Email, + PlanType = targetPlan.Type, + Seats = (short)seats, + MaxCollections = targetPlan.PasswordManager.MaxCollections, + MaxStorageGb = targetPlan.PasswordManager.BaseStorageGb, + UsePolicies = targetPlan.HasPolicies, + UseSso = targetPlan.HasSso, + UseGroups = targetPlan.HasGroups, + UseEvents = targetPlan.HasEvents, + UseDirectory = targetPlan.HasDirectory, + UseTotp = targetPlan.HasTotp, + Use2fa = targetPlan.Has2fa, + UseApi = targetPlan.HasApi, + UseResetPassword = targetPlan.HasResetPassword, + SelfHost = targetPlan.HasSelfHost, + UsersGetPremium = targetPlan.UsersGetPremium, + UseCustomPermissions = targetPlan.HasCustomPermissions, + UseScim = targetPlan.HasScim, + Plan = targetPlan.Name, + Gateway = GatewayType.Stripe, + Enabled = true, + LicenseKey = CoreHelpers.SecureRandomString(20), + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Status = OrganizationStatusType.Created, + UsePasswordManager = true, + UseSecretsManager = false, + UseOrganizationDomains = targetPlan.HasOrganizationDomains, + GatewayCustomerId = user.GatewayCustomerId, + GatewaySubscriptionId = currentSubscription.Id + }; + + // Update the subscription in Stripe + await stripeAdapter.UpdateSubscriptionAsync(currentSubscription.Id, subscriptionUpdateOptions); + + // Save the organization + await organizationRepository.CreateAsync(organization); + + // Create organization API key + await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey + { + OrganizationId = organization.Id, + ApiKey = CoreHelpers.SecureRandomString(30), + Type = OrganizationApiKeyType.Default, + RevisionDate = DateTime.UtcNow, + }); + + // Update cache + await applicationCacheService.UpsertOrganizationAbilityAsync(organization); + + // Create OrganizationUser for the upgrading user as owner + var organizationUser = new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Key = key, + AccessSecretsManager = false, + Type = OrganizationUserType.Owner, + Status = OrganizationUserStatusType.Confirmed, + CreationDate = organization.CreationDate, + RevisionDate = organization.CreationDate + }; + organizationUser.SetNewId(); + await organizationUserRepository.CreateAsync(organizationUser); + + // Remove subscription from user + user.Premium = false; + user.PremiumExpirationDate = null; + user.GatewaySubscriptionId = null; + user.GatewayCustomerId = null; + user.RevisionDate = DateTime.UtcNow; + await userService.SaveUserAsync(user); + + return new None(); + }); +} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index c3c009a2d5..373107bb66 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -163,32 +163,23 @@ public static class FeatureFlagKeys "pm-23174-manage-account-recovery-permission-drives-the-need-to-set-master-password"; public const string MJMLBasedEmailTemplates = "mjml-based-email-templates"; public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email"; + public const string OrganizationConfirmationEmail = "pm-28402-update-confirmed-to-org-email-template"; public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow"; public const string RedirectOnSsoRequired = "pm-1632-redirect-on-sso-required"; + public const string PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin"; /* Autofill Team */ - public const string IdpAutoSubmitLogin = "idp-auto-submit-login"; - public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; - public const string InlineMenuFieldQualification = "inline-menu-field-qualification"; - public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; public const string SSHAgent = "ssh-agent"; public const string SSHAgentV2 = "ssh-agent-v2"; public const string SSHVersionCheckQAOverride = "ssh-version-check-qa-override"; - public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor"; - public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2"; - public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements"; - public const string BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain"; public const string NotificationRefresh = "notification-refresh"; - public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill"; public const string MacOsNativeCredentialSync = "macos-native-credential-sync"; - public const string InlineMenuTotp = "inline-menu-totp"; public const string WindowsDesktopAutotype = "windows-desktop-autotype"; public const string WindowsDesktopAutotypeGA = "windows-desktop-autotype-ga"; /* Billing Team */ public const string TrialPayment = "PM-8163-trial-payment"; public const string PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure"; - public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog"; public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button"; public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog"; public const string PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service"; @@ -196,6 +187,7 @@ public static class FeatureFlagKeys public const string PM26462_Milestone_3 = "pm-26462-milestone-3"; public const string PM28265_EnableReconcileAdditionalStorageJob = "pm-28265-enable-reconcile-additional-storage-job"; public const string PM28265_ReconcileAdditionalStorageJobEnableLiveMode = "pm-28265-reconcile-additional-storage-job-enable-live-mode"; + public const string PM29594_UpdateIndividualSubscriptionPage = "pm-29594-update-individual-subscription-page"; /* Key Management Team */ public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; @@ -212,6 +204,7 @@ public static class FeatureFlagKeys public const string V2RegistrationTDEJIT = "pm-27279-v2-registration-tde-jit"; public const string DataRecoveryTool = "pm-28813-data-recovery-tool"; public const string EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration"; + public const string EnableAccountEncryptionV2JitPasswordRegistration = "enable-account-encryption-v2-jit-password-registration"; /* Mobile Team */ public const string AndroidImportLoginsFlow = "import-logins-flow"; @@ -234,6 +227,10 @@ public static class FeatureFlagKeys public const string PushNotificationsWhenInactive = "pm-25130-receive-push-notifications-for-inactive-users"; /* Tools Team */ + /// + /// Enable this flag to share the send view used by the web and browser clients + /// on the desktop client. + /// public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; public const string UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators"; public const string UseChromiumImporter = "pm-23982-chromium-importer"; @@ -241,6 +238,16 @@ public static class FeatureFlagKeys public const string SendUIRefresh = "pm-28175-send-ui-refresh"; public const string SendEmailOTP = "pm-19051-send-email-verification"; + /// + /// Enable this flag to output email/OTP authenticated sends from the `GET sends` endpoint. When + /// this flag is disabled, the `GET sends` endpoint omits email/OTP authenticated sends. + /// + /// + /// This flag is server-side only, and only inhibits the endpoint returning all sends. + /// Email/OTP sends can still be created and downloaded through other endpoints. + /// + public const string PM19051_ListEmailOtpSends = "tools-send-email-otp-listing"; + /* Vault Team */ public const string CipherKeyEncryption = "cipher-key-encryption"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; @@ -252,18 +259,21 @@ public static class FeatureFlagKeys public const string VaultLoadingSkeletons = "pm-25081-vault-skeleton-loaders"; public const string BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight"; public const string MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems"; + public const string PM27632_CipherCrudOperationsToSdk = "pm-27632-cipher-crud-operations-to-sdk"; /* Innovation Team */ public const string ArchiveVaultItems = "pm-19148-innovation-archive"; /* DIRT Team */ - public const string PM22887_RiskInsightsActivityTab = "pm-22887-risk-insights-activity-tab"; public const string EventManagementForDataDogAndCrowdStrike = "event-management-for-datadog-and-crowdstrike"; public const string EventDiagnosticLogging = "pm-27666-siem-event-log-debugging"; /* UIF Team */ public const string RouterFocusManagement = "router-focus-management"; + /* Secrets Manager Team */ + public const string SM1719_RemoveSecretsManagerAds = "sm-1719-remove-secrets-manager-ads"; + public static List GetAllKeys() { return typeof(FeatureFlagKeys).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 52c0a641ab..0783e84cc4 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -36,7 +36,7 @@ - + diff --git a/src/Core/AdminConsole/Entities/OrganizationIntegration.cs b/src/Core/Dirt/Entities/OrganizationIntegration.cs similarity index 83% rename from src/Core/AdminConsole/Entities/OrganizationIntegration.cs rename to src/Core/Dirt/Entities/OrganizationIntegration.cs index f1c96c8b98..42b4e89e27 100644 --- a/src/Core/AdminConsole/Entities/OrganizationIntegration.cs +++ b/src/Core/Dirt/Entities/OrganizationIntegration.cs @@ -1,8 +1,8 @@ -using Bit.Core.Entities; -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; +using Bit.Core.Entities; using Bit.Core.Utilities; -namespace Bit.Core.AdminConsole.Entities; +namespace Bit.Core.Dirt.Entities; public class OrganizationIntegration : ITableObject { diff --git a/src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs b/src/Core/Dirt/Entities/OrganizationIntegrationConfiguration.cs similarity index 93% rename from src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs rename to src/Core/Dirt/Entities/OrganizationIntegrationConfiguration.cs index a9ce676062..2b8dbf9220 100644 --- a/src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs +++ b/src/Core/Dirt/Entities/OrganizationIntegrationConfiguration.cs @@ -2,7 +2,7 @@ using Bit.Core.Enums; using Bit.Core.Utilities; -namespace Bit.Core.AdminConsole.Entities; +namespace Bit.Core.Dirt.Entities; public class OrganizationIntegrationConfiguration : ITableObject { diff --git a/src/Core/Dirt/Enums/EventType.cs b/src/Core/Dirt/Enums/EventType.cs index 916f408fe6..61372fc4e0 100644 --- a/src/Core/Dirt/Enums/EventType.cs +++ b/src/Core/Dirt/Enums/EventType.cs @@ -61,6 +61,7 @@ public enum EventType : int OrganizationUser_Deleted = 1515, // Both user and organization user data were deleted OrganizationUser_Left = 1516, // User voluntarily left the organization OrganizationUser_AutomaticallyConfirmed = 1517, + OrganizationUser_SelfRevoked = 1518, // User self-revoked due to declining organization data ownership policy Organization_Updated = 1600, Organization_PurgedVault = 1601, diff --git a/src/Core/AdminConsole/Enums/IntegrationType.cs b/src/Core/Dirt/Enums/IntegrationType.cs similarity index 96% rename from src/Core/AdminConsole/Enums/IntegrationType.cs rename to src/Core/Dirt/Enums/IntegrationType.cs index 84e4de94e9..767f2feb06 100644 --- a/src/Core/AdminConsole/Enums/IntegrationType.cs +++ b/src/Core/Dirt/Enums/IntegrationType.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Enums; +namespace Bit.Core.Dirt.Enums; public enum IntegrationType : int { diff --git a/src/Core/AdminConsole/Enums/OrganizationIntegrationStatus.cs b/src/Core/Dirt/Enums/OrganizationIntegrationStatus.cs similarity index 66% rename from src/Core/AdminConsole/Enums/OrganizationIntegrationStatus.cs rename to src/Core/Dirt/Enums/OrganizationIntegrationStatus.cs index 78a7bc6d63..aad0530971 100644 --- a/src/Core/AdminConsole/Enums/OrganizationIntegrationStatus.cs +++ b/src/Core/Dirt/Enums/OrganizationIntegrationStatus.cs @@ -1,4 +1,4 @@ -namespace Bit.Api.AdminConsole.Models.Response.Organizations; +namespace Bit.Core.Dirt.Enums; public enum OrganizationIntegrationStatus : int { diff --git a/src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs b/src/Core/Dirt/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs similarity index 98% rename from src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs rename to src/Core/Dirt/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs index ebeef44484..b03a68cfa6 100644 --- a/src/Core/AdminConsole/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs +++ b/src/Core/Dirt/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs @@ -1,13 +1,15 @@ using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.AdminConsole.Models.Teams; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Services; -using Bit.Core.AdminConsole.Services.NoopImplementations; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.Teams; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; +using Bit.Core.Dirt.Services.Implementations; +using Bit.Core.Dirt.Services.NoopImplementations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs similarity index 89% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs index cb3ce8b9ea..478b43bb7e 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs @@ -1,13 +1,13 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; -using Bit.Core.AdminConsole.Services; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.DependencyInjection; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; /// /// Command implementation for creating organization integration configurations with validation and cache invalidation support. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs similarity index 90% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs index 78768fd0d4..d6369f1b1b 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs @@ -1,11 +1,11 @@ -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.DependencyInjection; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; /// /// Command implementation for deleting organization integration configurations with cache invalidation support. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs similarity index 78% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs index a2078c3c98..6dfe2949a4 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs @@ -1,9 +1,9 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; /// /// Query implementation for retrieving organization integration configurations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs similarity index 88% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs index 140cc79d1a..629a1ee8ed 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; /// /// Command interface for creating organization integration configurations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs similarity index 89% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs index 3970676d40..d6866443c2 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; /// /// Command interface for deleting organization integration configurations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs similarity index 85% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs index 2bf806c458..a6635cb3be 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; /// /// Query interface for retrieving organization integration configurations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs similarity index 90% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs index 3e60a0af07..3ed680b808 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; /// /// Command interface for updating organization integration configurations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs similarity index 92% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs index f619e2ddf2..69c28f3e7e 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs @@ -1,13 +1,13 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; -using Bit.Core.AdminConsole.Services; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Dirt.Services; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.DependencyInjection; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations; /// /// Command implementation for updating organization integration configurations with validation and cache invalidation support. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs similarity index 85% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs index 376451977c..4423c103f9 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs @@ -1,12 +1,12 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.DependencyInjection; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; /// /// Command implementation for creating organization integrations with cache invalidation support. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs similarity index 85% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs index 614693cd82..dc1e7fb1dc 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs @@ -1,11 +1,11 @@ -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.DependencyInjection; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; /// /// Command implementation for deleting organization integrations with cache invalidation support. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs similarity index 68% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs index f7bbaadb4a..807f0b0b59 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; -using Bit.Core.Repositories; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Repositories; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; /// /// Query implementation for retrieving organization integrations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs similarity index 83% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs index e7b79eab13..0b06d79bdb 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; /// /// Command interface for creating an OrganizationIntegration. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs similarity index 87% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs index be22b4e482..8640f03ec8 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; /// /// Command interface for deleting organization integrations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs similarity index 80% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs index 8cdea7f301..1f378abe9b 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; /// /// Query interface for retrieving organization integrations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs similarity index 87% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs index f40086600d..ddba2bd233 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; /// /// Command interface for updating organization integrations. diff --git a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs similarity index 86% rename from src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs rename to src/Core/Dirt/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs index 12a8620926..77a3448276 100644 --- a/src/Core/AdminConsole/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs +++ b/src/Core/Dirt/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs @@ -1,12 +1,12 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces; +using Bit.Core.Dirt.Repositories; using Bit.Core.Exceptions; -using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.DependencyInjection; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations; +namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations; /// /// Command implementation for updating organization integrations with cache invalidation support. diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md b/src/Core/Dirt/EventIntegrations/README.md similarity index 100% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md rename to src/Core/Dirt/EventIntegrations/README.md diff --git a/src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegration.cs new file mode 100644 index 0000000000..69a4deb66b --- /dev/null +++ b/src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegration.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; + +public record DatadogIntegration(string ApiKey, Uri Uri); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs b/src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs similarity index 54% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs index 07aafa4bd8..ed91c3828b 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record DatadogIntegrationConfigurationDetails(string ApiKey, Uri Uri); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs similarity index 91% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs index 1c74826791..ce35e29927 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs @@ -1,7 +1,7 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class DatadogListenerConfiguration(GlobalSettings globalSettings) : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/HecIntegration.cs similarity index 58% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/HecIntegration.cs index 33ae5dadbe..df943e0bfc 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/HecIntegration.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record HecIntegration(Uri Uri, string Scheme, string Token, string? Service = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/HecListenerConfiguration.cs similarity index 91% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/HecListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/HecListenerConfiguration.cs index 37a0d68beb..5ceb42be64 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/HecListenerConfiguration.cs @@ -1,7 +1,7 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class HecListenerConfiguration(GlobalSettings globalSettings) : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IEventListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IEventListenerConfiguration.cs similarity index 80% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IEventListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IEventListenerConfiguration.cs index 7df1459941..206dc2cc0b 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IEventListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IEventListenerConfiguration.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public interface IEventListenerConfiguration { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs similarity index 86% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs index 30401bb072..1fbfefa420 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs @@ -1,6 +1,6 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public interface IIntegrationListenerConfiguration : IEventListenerConfiguration { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationMessage.cs similarity index 77% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationMessage.cs index 5b6bfe2e53..2d333dfee4 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationMessage.cs @@ -1,6 +1,6 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public interface IIntegrationMessage { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFailureCategory.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFailureCategory.cs similarity index 93% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFailureCategory.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFailureCategory.cs index 544e671d51..f9d8f2ab68 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFailureCategory.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFailureCategory.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; /// /// Categories of event integration failures used for classification and retry logic. diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterGroup.cs similarity index 76% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterGroup.cs index 276ca3a14b..0c129883cf 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterGroup.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class IntegrationFilterGroup { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterOperation.cs similarity index 61% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterOperation.cs index fddf630e26..d98ab1e13e 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterOperation.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public enum IntegrationFilterOperation { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterRule.cs similarity index 76% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterRule.cs index b5f90f5e63..9ac3ef753e 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterRule.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class IntegrationFilterRule { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationHandlerResult.cs similarity index 97% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationHandlerResult.cs index 375f2489cb..bbdce50ec0 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationHandlerResult.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; /// /// Represents the result of an integration handler operation, including success status, diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationMessage.cs similarity index 93% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationMessage.cs index b0fc2161ba..edf31a2a1f 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationMessage.cs @@ -1,7 +1,7 @@ using System.Text.Json; -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class IntegrationMessage : IIntegrationMessage { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationOAuthState.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationOAuthState.cs similarity index 95% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationOAuthState.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationOAuthState.cs index 3b29bbebb4..d75780d6c6 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationOAuthState.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationOAuthState.cs @@ -1,8 +1,8 @@ using System.Security.Cryptography; using System.Text; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class IntegrationOAuthState { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationTemplateContext.cs similarity index 97% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/IntegrationTemplateContext.cs index c44e550d15..3b527469fa 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/IntegrationTemplateContext.cs @@ -4,7 +4,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class IntegrationTemplateContext(EventMessage eventMessage) { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/ListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/ListenerConfiguration.cs similarity index 94% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/ListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/ListenerConfiguration.cs index 40eb2b3e77..2a970ce670 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/ListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/ListenerConfiguration.cs @@ -1,6 +1,6 @@ using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public abstract class ListenerConfiguration { diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs b/src/Core/Dirt/Models/Data/EventIntegrations/OrganizationIntegrationConfigurationDetails.cs similarity index 95% rename from src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/OrganizationIntegrationConfigurationDetails.cs index 5fdc760c90..6517ceccf0 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/OrganizationIntegrationConfigurationDetails.cs @@ -1,9 +1,8 @@ using System.Text.Json.Nodes; +using Bit.Core.Dirt.Enums; using Bit.Core.Enums; -#nullable enable - -namespace Bit.Core.Models.Data.Organizations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class OrganizationIntegrationConfigurationDetails { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs similarity index 87% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs index 118b3a17fe..20299dd651 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs @@ -1,6 +1,6 @@ using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class RepositoryListenerConfiguration(GlobalSettings globalSettings) : ListenerConfiguration(globalSettings), IEventListenerConfiguration diff --git a/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegration.cs new file mode 100644 index 0000000000..fcfd07f574 --- /dev/null +++ b/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegration.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; + +public record SlackIntegration(string Token); diff --git a/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs new file mode 100644 index 0000000000..164a132e8c --- /dev/null +++ b/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; + +public record SlackIntegrationConfiguration(string ChannelId); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs b/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs similarity index 56% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs index d22f43bb92..b81617118d 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record SlackIntegrationConfigurationDetails(string ChannelId, string Token); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/SlackListenerConfiguration.cs similarity index 91% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/SlackListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/SlackListenerConfiguration.cs index 7dd834f51e..ef2cf83837 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/SlackListenerConfiguration.cs @@ -1,7 +1,7 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class SlackListenerConfiguration(GlobalSettings globalSettings) : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegration.cs similarity index 71% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegration.cs index 8390022839..fcb42a5261 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegration.cs @@ -1,6 +1,6 @@ -using Bit.Core.Models.Teams; +using Bit.Core.Dirt.Models.Data.Teams; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record TeamsIntegration( string TenantId, diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs b/src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs similarity index 56% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs index 66fe558dff..a890f553f5 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record TeamsIntegrationConfigurationDetails(string ChannelId, Uri ServiceUrl); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs similarity index 91% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs index 24cf674648..4111c96601 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs @@ -1,7 +1,7 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class TeamsListenerConfiguration(GlobalSettings globalSettings) : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegration.cs similarity index 57% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegration.cs index dcda4caa92..d12ea16ee1 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegration.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record WebhookIntegration(Uri Uri, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs similarity index 60% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs index 851bd3f411..8d7bf90e2c 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record WebhookIntegrationConfiguration(Uri Uri, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs similarity index 62% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs index dba9b1714d..49508f8454 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public record WebhookIntegrationConfigurationDetails(Uri Uri, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs similarity index 91% rename from src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs rename to src/Core/Dirt/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs index 9d5bf811c7..9afc26168c 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs +++ b/src/Core/Dirt/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs @@ -1,7 +1,7 @@ -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; using Bit.Core.Settings; -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.Dirt.Models.Data.EventIntegrations; public class WebhookListenerConfiguration(GlobalSettings globalSettings) : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration diff --git a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs b/src/Core/Dirt/Models/Data/Slack/SlackApiResponse.cs similarity index 97% rename from src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs rename to src/Core/Dirt/Models/Data/Slack/SlackApiResponse.cs index 3c811e2b28..a70e623ae3 100644 --- a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs +++ b/src/Core/Dirt/Models/Data/Slack/SlackApiResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Bit.Core.Models.Slack; +namespace Bit.Core.Dirt.Models.Data.Slack; public abstract class SlackApiResponse { diff --git a/src/Core/AdminConsole/Models/Teams/TeamsApiResponse.cs b/src/Core/Dirt/Models/Data/Teams/TeamsApiResponse.cs similarity index 97% rename from src/Core/AdminConsole/Models/Teams/TeamsApiResponse.cs rename to src/Core/Dirt/Models/Data/Teams/TeamsApiResponse.cs index 131e45264f..b4b6a2542d 100644 --- a/src/Core/AdminConsole/Models/Teams/TeamsApiResponse.cs +++ b/src/Core/Dirt/Models/Data/Teams/TeamsApiResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Bit.Core.Models.Teams; +namespace Bit.Core.Dirt.Models.Data.Teams; /// Represents the response returned by the Microsoft OAuth 2.0 token endpoint. /// See Microsoft identity platform and OAuth 2.0 diff --git a/src/Core/AdminConsole/Models/Teams/TeamsBotCredentialProvider.cs b/src/Core/Dirt/Models/Data/Teams/TeamsBotCredentialProvider.cs similarity index 94% rename from src/Core/AdminConsole/Models/Teams/TeamsBotCredentialProvider.cs rename to src/Core/Dirt/Models/Data/Teams/TeamsBotCredentialProvider.cs index eeb17131a3..d8740f9e90 100644 --- a/src/Core/AdminConsole/Models/Teams/TeamsBotCredentialProvider.cs +++ b/src/Core/Dirt/Models/Data/Teams/TeamsBotCredentialProvider.cs @@ -1,6 +1,6 @@ using Microsoft.Bot.Connector.Authentication; -namespace Bit.Core.AdminConsole.Models.Teams; +namespace Bit.Core.Dirt.Models.Data.Teams; public class TeamsBotCredentialProvider(string clientId, string clientSecret) : ICredentialProvider { diff --git a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs b/src/Core/Dirt/Repositories/IOrganizationIntegrationConfigurationRepository.cs similarity index 88% rename from src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs rename to src/Core/Dirt/Repositories/IOrganizationIntegrationConfigurationRepository.cs index fb42ffa000..f6f90c7c9f 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs +++ b/src/Core/Dirt/Repositories/IOrganizationIntegrationConfigurationRepository.cs @@ -1,8 +1,10 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Enums; -using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; -namespace Bit.Core.Repositories; +namespace Bit.Core.Dirt.Repositories; public interface IOrganizationIntegrationConfigurationRepository : IRepository { diff --git a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs b/src/Core/Dirt/Repositories/IOrganizationIntegrationRepository.cs similarity index 74% rename from src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs rename to src/Core/Dirt/Repositories/IOrganizationIntegrationRepository.cs index 1d8b8be0ec..03775e8d20 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs +++ b/src/Core/Dirt/Repositories/IOrganizationIntegrationRepository.cs @@ -1,6 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; +using Bit.Core.Repositories; -namespace Bit.Core.Repositories; +namespace Bit.Core.Dirt.Repositories; public interface IOrganizationIntegrationRepository : IRepository { diff --git a/src/Core/AdminConsole/Services/IAzureServiceBusService.cs b/src/Core/Dirt/Services/IAzureServiceBusService.cs similarity index 77% rename from src/Core/AdminConsole/Services/IAzureServiceBusService.cs rename to src/Core/Dirt/Services/IAzureServiceBusService.cs index 75864255c2..6b425511ab 100644 --- a/src/Core/AdminConsole/Services/IAzureServiceBusService.cs +++ b/src/Core/Dirt/Services/IAzureServiceBusService.cs @@ -1,7 +1,7 @@ using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; public interface IAzureServiceBusService : IEventIntegrationPublisher, IAsyncDisposable { diff --git a/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs b/src/Core/Dirt/Services/IEventIntegrationPublisher.cs similarity index 67% rename from src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs rename to src/Core/Dirt/Services/IEventIntegrationPublisher.cs index 4d95707e90..583c2448fe 100644 --- a/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs +++ b/src/Core/Dirt/Services/IEventIntegrationPublisher.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; public interface IEventIntegrationPublisher : IAsyncDisposable { diff --git a/src/Core/AdminConsole/Services/IEventMessageHandler.cs b/src/Core/Dirt/Services/IEventMessageHandler.cs similarity index 85% rename from src/Core/AdminConsole/Services/IEventMessageHandler.cs rename to src/Core/Dirt/Services/IEventMessageHandler.cs index 83c5e33ecb..9b1385129b 100644 --- a/src/Core/AdminConsole/Services/IEventMessageHandler.cs +++ b/src/Core/Dirt/Services/IEventMessageHandler.cs @@ -1,6 +1,6 @@ using Bit.Core.Models.Data; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; public interface IEventMessageHandler { diff --git a/src/Core/AdminConsole/Services/IIntegrationFilterService.cs b/src/Core/Dirt/Services/IIntegrationFilterService.cs similarity index 67% rename from src/Core/AdminConsole/Services/IIntegrationFilterService.cs rename to src/Core/Dirt/Services/IIntegrationFilterService.cs index 5bc035d468..f46ab83f54 100644 --- a/src/Core/AdminConsole/Services/IIntegrationFilterService.cs +++ b/src/Core/Dirt/Services/IIntegrationFilterService.cs @@ -1,9 +1,9 @@ #nullable enable -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Models.Data; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; public interface IIntegrationFilterService { diff --git a/src/Core/AdminConsole/Services/IIntegrationHandler.cs b/src/Core/Dirt/Services/IIntegrationHandler.cs similarity index 98% rename from src/Core/AdminConsole/Services/IIntegrationHandler.cs rename to src/Core/Dirt/Services/IIntegrationHandler.cs index c36081cb52..81103b453d 100644 --- a/src/Core/AdminConsole/Services/IIntegrationHandler.cs +++ b/src/Core/Dirt/Services/IIntegrationHandler.cs @@ -1,8 +1,8 @@ using System.Globalization; using System.Net; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; public interface IIntegrationHandler { diff --git a/src/Core/AdminConsole/Services/IOrganizationIntegrationConfigurationValidator.cs b/src/Core/Dirt/Services/IOrganizationIntegrationConfigurationValidator.cs similarity index 86% rename from src/Core/AdminConsole/Services/IOrganizationIntegrationConfigurationValidator.cs rename to src/Core/Dirt/Services/IOrganizationIntegrationConfigurationValidator.cs index 48346cbae7..4a3a089f26 100644 --- a/src/Core/AdminConsole/Services/IOrganizationIntegrationConfigurationValidator.cs +++ b/src/Core/Dirt/Services/IOrganizationIntegrationConfigurationValidator.cs @@ -1,7 +1,7 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; -namespace Bit.Core.AdminConsole.Services; +namespace Bit.Core.Dirt.Services; public interface IOrganizationIntegrationConfigurationValidator { diff --git a/src/Core/AdminConsole/Services/IRabbitMqService.cs b/src/Core/Dirt/Services/IRabbitMqService.cs similarity index 89% rename from src/Core/AdminConsole/Services/IRabbitMqService.cs rename to src/Core/Dirt/Services/IRabbitMqService.cs index 12c40c3b98..b9f824506f 100644 --- a/src/Core/AdminConsole/Services/IRabbitMqService.cs +++ b/src/Core/Dirt/Services/IRabbitMqService.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; public interface IRabbitMqService : IEventIntegrationPublisher { diff --git a/src/Core/AdminConsole/Services/ISlackService.cs b/src/Core/Dirt/Services/ISlackService.cs similarity index 97% rename from src/Core/AdminConsole/Services/ISlackService.cs rename to src/Core/Dirt/Services/ISlackService.cs index 60d3da8af4..111fcb5440 100644 --- a/src/Core/AdminConsole/Services/ISlackService.cs +++ b/src/Core/Dirt/Services/ISlackService.cs @@ -1,6 +1,7 @@ -using Bit.Core.Models.Slack; +using Bit.Core.Dirt.Models.Data.Slack; +using Bit.Core.Dirt.Services.Implementations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; /// Defines operations for interacting with Slack, including OAuth authentication, channel discovery, /// and sending messages. diff --git a/src/Core/AdminConsole/Services/ITeamsService.cs b/src/Core/Dirt/Services/ITeamsService.cs similarity index 95% rename from src/Core/AdminConsole/Services/ITeamsService.cs rename to src/Core/Dirt/Services/ITeamsService.cs index e3757987c3..30a324f9a4 100644 --- a/src/Core/AdminConsole/Services/ITeamsService.cs +++ b/src/Core/Dirt/Services/ITeamsService.cs @@ -1,6 +1,7 @@ -using Bit.Core.Models.Teams; +using Bit.Core.Dirt.Models.Data.Teams; +using Bit.Core.Dirt.Services.Implementations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services; /// /// Service that provides functionality relating to the Microsoft Teams integration including OAuth, diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs b/src/Core/Dirt/Services/Implementations/AzureServiceBusEventListenerService.cs similarity index 89% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs rename to src/Core/Dirt/Services/Implementations/AzureServiceBusEventListenerService.cs index a589211687..6175374e2f 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs +++ b/src/Core/Dirt/Services/Implementations/AzureServiceBusEventListenerService.cs @@ -1,9 +1,9 @@ using System.Text; using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Microsoft.Extensions.Logging; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class AzureServiceBusEventListenerService : EventLoggingListenerService where TConfiguration : IEventListenerConfiguration @@ -42,7 +42,7 @@ public class AzureServiceBusEventListenerService : EventLoggingL private static ILogger CreateLogger(ILoggerFactory loggerFactory, TConfiguration configuration) { return loggerFactory.CreateLogger( - categoryName: $"Bit.Core.Services.AzureServiceBusEventListenerService.{configuration.EventSubscriptionName}"); + categoryName: $"Bit.Core.Dirt.Services.Implementations.AzureServiceBusEventListenerService.{configuration.EventSubscriptionName}"); } internal Task ProcessErrorAsync(ProcessErrorEventArgs args) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs b/src/Core/Dirt/Services/Implementations/AzureServiceBusIntegrationListenerService.cs similarity index 94% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs rename to src/Core/Dirt/Services/Implementations/AzureServiceBusIntegrationListenerService.cs index c97c5f7efe..32132ddb37 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs +++ b/src/Core/Dirt/Services/Implementations/AzureServiceBusIntegrationListenerService.cs @@ -1,9 +1,9 @@ using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class AzureServiceBusIntegrationListenerService : BackgroundService where TConfiguration : IIntegrationListenerConfiguration @@ -23,7 +23,7 @@ public class AzureServiceBusIntegrationListenerService : Backgro { _handler = handler; _logger = loggerFactory.CreateLogger( - categoryName: $"Bit.Core.Services.AzureServiceBusIntegrationListenerService.{configuration.IntegrationSubscriptionName}"); + categoryName: $"Bit.Core.Dirt.Services.Implementations.AzureServiceBusIntegrationListenerService.{configuration.IntegrationSubscriptionName}"); _maxRetries = configuration.MaxRetries; _serviceBusService = serviceBusService; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs b/src/Core/Dirt/Services/Implementations/AzureServiceBusService.cs similarity index 94% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs rename to src/Core/Dirt/Services/Implementations/AzureServiceBusService.cs index 953a9bb56e..7b87850fe3 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs +++ b/src/Core/Dirt/Services/Implementations/AzureServiceBusService.cs @@ -1,9 +1,9 @@ using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Settings; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class AzureServiceBusService : IAzureServiceBusService { diff --git a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs b/src/Core/Dirt/Services/Implementations/AzureTableStorageEventHandler.cs similarity index 84% rename from src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs rename to src/Core/Dirt/Services/Implementations/AzureTableStorageEventHandler.cs index 578dde9485..73d22b21a7 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs +++ b/src/Core/Dirt/Services/Implementations/AzureTableStorageEventHandler.cs @@ -1,9 +1,8 @@ -#nullable enable - -using Bit.Core.Models.Data; +using Bit.Core.Models.Data; +using Bit.Core.Services; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class AzureTableStorageEventHandler( [FromKeyedServices("persistent")] IEventWriteService eventWriteService) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/DatadogIntegrationHandler.cs b/src/Core/Dirt/Services/Implementations/DatadogIntegrationHandler.cs similarity index 90% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/DatadogIntegrationHandler.cs rename to src/Core/Dirt/Services/Implementations/DatadogIntegrationHandler.cs index 45bb5b6d7d..e5c684ceec 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/DatadogIntegrationHandler.cs +++ b/src/Core/Dirt/Services/Implementations/DatadogIntegrationHandler.cs @@ -1,7 +1,7 @@ using System.Text; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class DatadogIntegrationHandler( IHttpClientFactory httpClientFactory, diff --git a/src/Core/Dirt/Services/Implementations/EventIntegrationEventWriteService.cs b/src/Core/Dirt/Services/Implementations/EventIntegrationEventWriteService.cs index 4ac97df763..44e0513ee0 100644 --- a/src/Core/Dirt/Services/Implementations/EventIntegrationEventWriteService.cs +++ b/src/Core/Dirt/Services/Implementations/EventIntegrationEventWriteService.cs @@ -1,7 +1,8 @@ using System.Text.Json; using Bit.Core.Models.Data; +using Bit.Core.Services; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class EventIntegrationEventWriteService : IEventWriteService, IAsyncDisposable { private readonly IEventIntegrationPublisher _eventIntegrationPublisher; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs b/src/Core/Dirt/Services/Implementations/EventIntegrationHandler.cs similarity index 97% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs rename to src/Core/Dirt/Services/Implementations/EventIntegrationHandler.cs index b4246884f7..bcd1f1dd8c 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs +++ b/src/Core/Dirt/Services/Implementations/EventIntegrationHandler.cs @@ -1,18 +1,18 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Utilities; -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Repositories; using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Extensions.Logging; using ZiggyCreatures.Caching.Fusion; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class EventIntegrationHandler( IntegrationType integrationType, diff --git a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs b/src/Core/Dirt/Services/Implementations/EventLoggingListenerService.cs similarity index 97% rename from src/Core/AdminConsole/Services/EventLoggingListenerService.cs rename to src/Core/Dirt/Services/Implementations/EventLoggingListenerService.cs index 84a862ce94..29e3f8dec3 100644 --- a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs +++ b/src/Core/Dirt/Services/Implementations/EventLoggingListenerService.cs @@ -1,11 +1,9 @@ -#nullable enable - -using System.Text.Json; +using System.Text.Json; using Bit.Core.Models.Data; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public abstract class EventLoggingListenerService : BackgroundService { diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRepositoryHandler.cs b/src/Core/Dirt/Services/Implementations/EventRepositoryHandler.cs similarity index 87% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRepositoryHandler.cs rename to src/Core/Dirt/Services/Implementations/EventRepositoryHandler.cs index ee3a2d5db2..32173b8da0 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRepositoryHandler.cs +++ b/src/Core/Dirt/Services/Implementations/EventRepositoryHandler.cs @@ -1,7 +1,8 @@ using Bit.Core.Models.Data; +using Bit.Core.Services; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class EventRepositoryHandler( [FromKeyedServices("persistent")] IEventWriteService eventWriteService) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs b/src/Core/Dirt/Services/Implementations/IntegrationFilterFactory.cs similarity index 97% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs rename to src/Core/Dirt/Services/Implementations/IntegrationFilterFactory.cs index d28ac910b7..8c25c80208 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs +++ b/src/Core/Dirt/Services/Implementations/IntegrationFilterFactory.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using Bit.Core.Models.Data; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public delegate bool IntegrationFilter(EventMessage message, object? value); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs b/src/Core/Dirt/Services/Implementations/IntegrationFilterService.cs similarity index 97% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs rename to src/Core/Dirt/Services/Implementations/IntegrationFilterService.cs index 1c8fae4000..7d56b7c7ce 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs +++ b/src/Core/Dirt/Services/Implementations/IntegrationFilterService.cs @@ -1,8 +1,8 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Models.Data; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class IntegrationFilterService : IIntegrationFilterService { diff --git a/src/Core/AdminConsole/Services/OrganizationIntegrationConfigurationValidator.cs b/src/Core/Dirt/Services/Implementations/OrganizationIntegrationConfigurationValidator.cs similarity index 92% rename from src/Core/AdminConsole/Services/OrganizationIntegrationConfigurationValidator.cs rename to src/Core/Dirt/Services/Implementations/OrganizationIntegrationConfigurationValidator.cs index 2769565675..7b6ab320b8 100644 --- a/src/Core/AdminConsole/Services/OrganizationIntegrationConfigurationValidator.cs +++ b/src/Core/Dirt/Services/Implementations/OrganizationIntegrationConfigurationValidator.cs @@ -1,9 +1,9 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.AdminConsole.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class OrganizationIntegrationConfigurationValidator : IOrganizationIntegrationConfigurationValidator { diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs b/src/Core/Dirt/Services/Implementations/RabbitMqEventListenerService.cs similarity index 91% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs rename to src/Core/Dirt/Services/Implementations/RabbitMqEventListenerService.cs index 430540a2f7..ca7cd5ef16 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs +++ b/src/Core/Dirt/Services/Implementations/RabbitMqEventListenerService.cs @@ -1,10 +1,10 @@ using System.Text; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class RabbitMqEventListenerService : EventLoggingListenerService where TConfiguration : IEventListenerConfiguration @@ -69,6 +69,6 @@ public class RabbitMqEventListenerService : EventLoggingListener private static ILogger CreateLogger(ILoggerFactory loggerFactory, TConfiguration configuration) { return loggerFactory.CreateLogger( - categoryName: $"Bit.Core.Services.RabbitMqEventListenerService.{configuration.EventQueueName}"); + categoryName: $"Bit.Core.Dirt.Services.Implementations.RabbitMqEventListenerService.{configuration.EventQueueName}"); } } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs b/src/Core/Dirt/Services/Implementations/RabbitMqIntegrationListenerService.cs similarity index 96% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs rename to src/Core/Dirt/Services/Implementations/RabbitMqIntegrationListenerService.cs index 0762edc040..eced9131bb 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs +++ b/src/Core/Dirt/Services/Implementations/RabbitMqIntegrationListenerService.cs @@ -1,12 +1,12 @@ using System.Text; using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class RabbitMqIntegrationListenerService : BackgroundService where TConfiguration : IIntegrationListenerConfiguration @@ -37,7 +37,7 @@ public class RabbitMqIntegrationListenerService : BackgroundServ _timeProvider = timeProvider; _lazyChannel = new Lazy>(() => _rabbitMqService.CreateChannelAsync()); _logger = loggerFactory.CreateLogger( - categoryName: $"Bit.Core.Services.RabbitMqIntegrationListenerService.{configuration.IntegrationQueueName}"); ; + categoryName: $"Bit.Core.Dirt.Services.Implementations.RabbitMqIntegrationListenerService.{configuration.IntegrationQueueName}"); ; } public override async Task StartAsync(CancellationToken cancellationToken) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs b/src/Core/Dirt/Services/Implementations/RabbitMqService.cs similarity index 98% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs rename to src/Core/Dirt/Services/Implementations/RabbitMqService.cs index 8976530cf4..c27fb37d08 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs +++ b/src/Core/Dirt/Services/Implementations/RabbitMqService.cs @@ -1,11 +1,11 @@ using System.Text; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Enums; +using Bit.Core.Dirt.Enums; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Bit.Core.Settings; using RabbitMQ.Client; using RabbitMQ.Client.Events; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class RabbitMqService : IRabbitMqService { diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs b/src/Core/Dirt/Services/Implementations/SlackIntegrationHandler.cs similarity index 96% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs rename to src/Core/Dirt/Services/Implementations/SlackIntegrationHandler.cs index e681140afe..6c6a4dd356 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs +++ b/src/Core/Dirt/Services/Implementations/SlackIntegrationHandler.cs @@ -1,6 +1,6 @@ -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class SlackIntegrationHandler( ISlackService slackService) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs b/src/Core/Dirt/Services/Implementations/SlackService.cs similarity index 98% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs rename to src/Core/Dirt/Services/Implementations/SlackService.cs index 7eec2ec374..7683f718b5 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs +++ b/src/Core/Dirt/Services/Implementations/SlackService.cs @@ -2,11 +2,11 @@ using System.Net.Http.Json; using System.Text.Json; using System.Web; -using Bit.Core.Models.Slack; +using Bit.Core.Dirt.Models.Data.Slack; using Bit.Core.Settings; using Microsoft.Extensions.Logging; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class SlackService( IHttpClientFactory httpClientFactory, diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs b/src/Core/Dirt/Services/Implementations/TeamsIntegrationHandler.cs similarity index 94% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs rename to src/Core/Dirt/Services/Implementations/TeamsIntegrationHandler.cs index 9e3645a99f..7aaed6c647 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs +++ b/src/Core/Dirt/Services/Implementations/TeamsIntegrationHandler.cs @@ -1,8 +1,8 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; using Microsoft.Rest; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class TeamsIntegrationHandler( ITeamsService teamsService) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsService.cs b/src/Core/Dirt/Services/Implementations/TeamsService.cs similarity index 96% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsService.cs rename to src/Core/Dirt/Services/Implementations/TeamsService.cs index f9911760bb..edb43bf85e 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsService.cs +++ b/src/Core/Dirt/Services/Implementations/TeamsService.cs @@ -2,9 +2,9 @@ using System.Net.Http.Json; using System.Text.Json; using System.Web; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Models.Teams; -using Bit.Core.Repositories; +using Bit.Core.Dirt.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.Teams; +using Bit.Core.Dirt.Repositories; using Bit.Core.Settings; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Teams; @@ -12,9 +12,9 @@ using Microsoft.Bot.Connector; using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Schema; using Microsoft.Extensions.Logging; -using TeamInfo = Bit.Core.Models.Teams.TeamInfo; +using TeamInfo = Bit.Core.Dirt.Models.Data.Teams.TeamInfo; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class TeamsService( IHttpClientFactory httpClientFactory, diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs b/src/Core/Dirt/Services/Implementations/WebhookIntegrationHandler.cs similarity index 92% rename from src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs rename to src/Core/Dirt/Services/Implementations/WebhookIntegrationHandler.cs index 0599f6e9d4..6caa1b9a6e 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs +++ b/src/Core/Dirt/Services/Implementations/WebhookIntegrationHandler.cs @@ -1,8 +1,8 @@ using System.Net.Http.Headers; using System.Text; -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Dirt.Models.Data.EventIntegrations; -namespace Bit.Core.Services; +namespace Bit.Core.Dirt.Services.Implementations; public class WebhookIntegrationHandler( IHttpClientFactory httpClientFactory, diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopSlackService.cs b/src/Core/Dirt/Services/NoopImplementations/NoopSlackService.cs similarity index 88% rename from src/Core/AdminConsole/Services/NoopImplementations/NoopSlackService.cs rename to src/Core/Dirt/Services/NoopImplementations/NoopSlackService.cs index a54df94814..30b68186bc 100644 --- a/src/Core/AdminConsole/Services/NoopImplementations/NoopSlackService.cs +++ b/src/Core/Dirt/Services/NoopImplementations/NoopSlackService.cs @@ -1,7 +1,6 @@ -using Bit.Core.Models.Slack; -using Bit.Core.Services; +using Bit.Core.Dirt.Models.Data.Slack; -namespace Bit.Core.AdminConsole.Services.NoopImplementations; +namespace Bit.Core.Dirt.Services.NoopImplementations; public class NoopSlackService : ISlackService { diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopTeamsService.cs b/src/Core/Dirt/Services/NoopImplementations/NoopTeamsService.cs similarity index 83% rename from src/Core/AdminConsole/Services/NoopImplementations/NoopTeamsService.cs rename to src/Core/Dirt/Services/NoopImplementations/NoopTeamsService.cs index fafb23f570..3ebd58d996 100644 --- a/src/Core/AdminConsole/Services/NoopImplementations/NoopTeamsService.cs +++ b/src/Core/Dirt/Services/NoopImplementations/NoopTeamsService.cs @@ -1,7 +1,6 @@ -using Bit.Core.Models.Teams; -using Bit.Core.Services; +using Bit.Core.Dirt.Models.Data.Teams; -namespace Bit.Core.AdminConsole.Services.NoopImplementations; +namespace Bit.Core.Dirt.Services.NoopImplementations; public class NoopTeamsService : ITeamsService { diff --git a/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs index f9cc04f73e..7d30fdcbe4 100644 --- a/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs +++ b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs @@ -378,12 +378,12 @@
+ - +
-

+

© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa Barbara, CA, USA

Always confirm you are on a trusted Bitwarden domain before logging in:
- bitwarden.com | - Learn why we include this + bitwarden.com | + Learn why we include this

- + -
+
- +
@@ -471,8 +471,8 @@ - -
- + +
@@ -488,13 +488,13 @@
- +
+ - @@ -511,13 +511,13 @@ -
+ - +
- +
+ - @@ -534,13 +534,13 @@ -
+ - +
- +
+ - @@ -557,13 +557,13 @@ -
+ - +
- +
+ - @@ -580,13 +580,13 @@ -
+ - +
- +
+ - @@ -603,13 +603,13 @@ -
+ - +
- +
+ - @@ -626,13 +626,13 @@ -
+ - +
- +
+ - @@ -653,15 +653,15 @@ diff --git a/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs b/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs index 9c4b2406d4..b2b957f849 100644 --- a/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs +++ b/src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs @@ -30,6 +30,14 @@ + + + + + + + + + + + + + + + + + +
+ - +
-

+

© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa Barbara, CA, USA

Always confirm you are on a trusted Bitwarden domain before logging in:
- bitwarden.com | - Learn why we include this + bitwarden.com | + Learn why we include this