diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index fa4d027d0f6..8387c53e5e3 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -98,7 +98,7 @@ *ngIf="canAccessExport$ | async" > diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 91c965658a3..6ead83b01d8 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -23,6 +23,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { ProductTierType } from "@bitwarden/common/billing/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { getById } from "@bitwarden/common/platform/misc"; import { BannerModule, IconModule } from "@bitwarden/components"; @@ -49,6 +50,7 @@ export class OrganizationLayoutComponent implements OnInit { protected readonly logo = AdminConsoleLogo; protected orgFilter = (org: Organization) => canAccessOrgAdmin(org); + protected domainVerificationNavigationTextKey: string; protected integrationPageEnabled$: Observable; @@ -67,6 +69,7 @@ export class OrganizationLayoutComponent implements OnInit { private configService: ConfigService, private policyService: PolicyService, private providerService: ProviderService, + private i18nService: I18nService, ) {} async ngOnInit() { @@ -116,6 +119,12 @@ export class OrganizationLayoutComponent implements OnInit { org.productTierType === ProductTierType.Enterprise && featureFlagEnabled, ), ); + + this.domainVerificationNavigationTextKey = (await this.configService.getFeatureFlag( + FeatureFlag.AccountDeprovisioning, + )) + ? "claimedDomains" + : "domainVerification"; } canShowVaultTab(organization: Organization): boolean { diff --git a/apps/web/src/app/core/event.service.ts b/apps/web/src/app/core/event.service.ts index 412423a3a24..aedad9b26ea 100644 --- a/apps/web/src/app/core/event.service.ts +++ b/apps/web/src/app/core/event.service.ts @@ -6,7 +6,9 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { DeviceType, EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EventResponse } from "@bitwarden/common/models/response/event.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @Injectable() @@ -16,6 +18,7 @@ export class EventService { constructor( private i18nService: I18nService, policyService: PolicyService, + private configService: ConfigService, ) { policyService.policies$.subscribe((policies) => { this.policies = policies; @@ -451,10 +454,20 @@ export class EventService { msg = humanReadableMsg = this.i18nService.t("removedDomain", ev.domainName); break; case EventType.OrganizationDomain_Verified: - msg = humanReadableMsg = this.i18nService.t("domainVerifiedEvent", ev.domainName); + msg = humanReadableMsg = this.i18nService.t( + (await this.configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning)) + ? "domainClaimedEvent" + : "domainVerifiedEvent", + ev.domainName, + ); break; case EventType.OrganizationDomain_NotVerified: - msg = humanReadableMsg = this.i18nService.t("domainNotVerifiedEvent", ev.domainName); + msg = humanReadableMsg = this.i18nService.t( + (await this.configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning)) + ? "domainNotClaimedEvent" + : "domainNotVerifiedEvent", + ev.domainName, + ); break; // Secrets Manager case EventType.Secret_Retrieved: diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 06728929912..b1203230688 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9801,8 +9801,8 @@ "selfHostingTitleProper": { "message": "Self-Hosting" }, - "verified-domain-single-org-warning" : { - "message": "Verifying a domain will turn on the single organization policy." + "claim-domain-single-org-warning" : { + "message": "Claiming a domain will turn on the single organization policy." }, "single-org-revoked-user-warning": { "message": "Non-compliant members will be revoked. Administrators can restore members once they leave all other organizations." @@ -9902,5 +9902,62 @@ }, "removeMembers": { "message": "Remove members" + }, + "claimedDomains": { + "message": "Claimed domains" + }, + "claimDomain": { + "message": "Claim domain" + }, + "reclaimDomain": { + "message": "Reclaim domain" + }, + "claimDomainNameInputHint": { + "message": "Example: mydomain.com. Subdomains require separate entries to be claimed." + }, + "automaticClaimedDomains": { + "message": "Automatic Claimed Domains" + }, + "automaticDomainClaimProcess": { + "message": "Bitwarden will attempt to claim the domain 3 times during the first 72 hours. If the domain can’t be claimed, check the DNS record in your host and manually claim. The domain will be removed from your organization in 7 days if it is not claimed." + }, + "domainNotClaimed": { + "message": "$DOMAIN$ not claimed. Check your DNS records.", + "placeholders": { + "DOMAIN": { + "content": "$1", + "example": "bitwarden.com" + } + } + }, + "domainStatusClaimed": { + "message": "Claimed" + }, + "domainStatusUnderVerification": { + "message": "Under verification" + }, + "claimedDomainsDesc": { + "message": "Claim a domain to own all member accounts whose email address matches the domain. Members will be able to skip the SSO identifier when logging in. Administrators will also be able to delete member accounts." + }, + "invalidDomainNameClaimMessage": { + "message": "Input is not a valid format. Format: mydomain.com. Subdomains require separate entries to be claimed." + }, + "domainClaimedEvent": { + "message": "$DOMAIN$ claimed", + "placeholders": { + "DOMAIN": { + "content": "$1", + "example": "bitwarden.com" + } + } + }, + "domainNotClaimedEvent": { + "message": "$DOMAIN$ not claimed", + "placeholders": { + "DOMAIN": { + "content": "$1", + "example": "bitwarden.com" + } + } } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html index 15120eed92a..7226c957598 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html @@ -6,24 +6,37 @@ {{ "newDomain" | i18n }} - {{ "verifyDomain" | i18n }} + + {{ + ((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") | i18n + }} {{ data.orgDomain.domainName }} {{ - "domainStatusUnverified" | i18n + ((accountDeprovisioningEnabled$ | async) + ? "domainStatusUnderVerification" + : "domainStatusUnverified" + ) | i18n }} {{ - "domainStatusVerified" | i18n + ((accountDeprovisioningEnabled$ | async) ? "domainStatusClaimed" : "domainStatusVerified") + | i18n }} {{ "domainName" | i18n }} - {{ "domainNameInputHint" | i18n }} + {{ + ((accountDeprovisioningEnabled$ | async) + ? "claimDomainNameInputHint" + : "domainNameInputHint" + ) | i18n + }} @@ -42,18 +55,29 @@ - {{ "automaticDomainVerificationProcess" | i18n }} + {{ + ((accountDeprovisioningEnabled$ | async) + ? "automaticDomainClaimProcess" + : "automaticDomainVerificationProcess" + ) | i18n + }} {{ "next" | i18n }} {{ - "verifyDomain" | i18n + ((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") | i18n + }} + {{ + ((accountDeprovisioningEnabled$ | async) ? "reclaimDomain" : "reverifyDomain") | i18n }} - {{ "reverifyDomain" | i18n }} {{ "cancel" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts index ae3dfc28d9e..01dc93aa7e1 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts @@ -3,14 +3,16 @@ import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from "@angular/forms"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, takeUntil, Observable, firstValueFrom } from "rxjs"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; import { OrgDomainServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain.service.abstraction"; import { OrganizationDomainResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain.response"; import { OrganizationDomainRequest } from "@bitwarden/common/admin-console/services/organization-domain/requests/organization-domain.request"; import { HttpStatusCode } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -31,20 +33,8 @@ export interface DomainAddEditDialogData { export class DomainAddEditDialogComponent implements OnInit, OnDestroy { private componentDestroyed$: Subject = new Subject(); - domainForm: FormGroup = this.formBuilder.group({ - domainName: [ - "", - [ - Validators.required, - domainNameValidator(this.i18nService.t("invalidDomainNameMessage")), - uniqueInArrayValidator( - this.data.existingDomainNames, - this.i18nService.t("duplicateDomainError"), - ), - ], - ], - txt: [{ value: null, disabled: true }], - }); + accountDeprovisioningEnabled$: Observable; + domainForm: FormGroup; get domainNameCtrl(): FormControl { return this.domainForm.controls.domainName as FormControl; @@ -69,11 +59,34 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy { private validationService: ValidationService, private dialogService: DialogService, private toastService: ToastService, - ) {} + private configService: ConfigService, + ) { + this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.AccountDeprovisioning, + ); + } // Angular Method Implementations async ngOnInit(): Promise { + this.domainForm = this.formBuilder.group({ + domainName: [ + "", + [ + Validators.required, + domainNameValidator( + (await firstValueFrom(this.accountDeprovisioningEnabled$)) + ? this.i18nService.t("invalidDomainNameClaimMessage") + : this.i18nService.t("invalidDomainNameMessage"), + ), + uniqueInArrayValidator( + this.data.existingDomainNames, + this.i18nService.t("duplicateDomainError"), + ), + ], + ], + txt: [{ value: null, disabled: true }], + }); // If we have data.orgDomain, then editing, otherwise creating new domain await this.populateForm(); } @@ -211,13 +224,22 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy { this.toastService.showToast({ variant: "success", title: null, - message: this.i18nService.t("domainVerified"), + message: this.i18nService.t( + (await firstValueFrom(this.accountDeprovisioningEnabled$)) + ? "domainClaimed" + : "domainVerified", + ), }); this.dialogRef.close(); } else { this.domainNameCtrl.setErrors({ errorPassthrough: { - message: this.i18nService.t("domainNotVerified", this.domainNameCtrl.value), + message: this.i18nService.t( + (await firstValueFrom(this.accountDeprovisioningEnabled$)) + ? "domainNotClaimed" + : "domainNotVerified", + this.domainNameCtrl.value, + ), }, }); // For the case where user opens dialog and reverifies when domain name formControl disabled. diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html index dee663eb199..0c28e4b13d5 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html @@ -4,6 +4,20 @@ + + {{ "claimedDomainsDesc" | i18n }} + + + + + {{ - "domainStatusUnverified" | i18n + ((accountDeprovisioningEnabled$ | async) + ? "domainStatusUnderVerification" + : "domainStatusUnverified" + ) | i18n }} {{ - "domainStatusVerified" | i18n + ((accountDeprovisioningEnabled$ | async) + ? "domainStatusClaimed" + : "domainStatusVerified" + ) | i18n }} @@ -70,7 +90,10 @@ type="button" > - {{ "verifyDomain" | i18n }} + {{ + ((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") + | i18n + }} diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts index 26347a515a0..2a2ae73227a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts @@ -43,6 +43,7 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { organizationId: string; orgDomains$: Observable; + accountDeprovisioningEnabled$: Observable; constructor( private route: ActivatedRoute, @@ -54,7 +55,11 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { private toastService: ToastService, private configService: ConfigService, private policyApiService: PolicyApiServiceAbstraction, - ) {} + ) { + this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.AccountDeprovisioning, + ); + } // eslint-disable-next-line @typescript-eslint/no-empty-function async ngOnInit() { @@ -105,7 +110,7 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { organizationDomains.every((domain) => domain.verifiedDate === null) ) { await this.dialogService.openSimpleDialog({ - title: { key: "verified-domain-single-org-warning" }, + title: { key: "claim-domain-single-org-warning" }, content: { key: "single-org-revoked-user-warning" }, cancelButtonText: { key: "cancel" }, acceptButtonText: { key: "confirm" }, @@ -169,13 +174,22 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { this.toastService.showToast({ variant: "success", title: null, - message: this.i18nService.t("domainVerified"), + message: this.i18nService.t( + (await firstValueFrom(this.accountDeprovisioningEnabled$)) + ? "domainClaimed" + : "domainVerified", + ), }); } else { this.toastService.showToast({ variant: "error", title: null, - message: this.i18nService.t("domainNotVerified", domainName), + message: this.i18nService.t( + (await firstValueFrom(this.accountDeprovisioningEnabled$)) + ? "domainNotClaimed" + : "domainNotVerified", + domainName, + ), }); // Update this item so the last checked date gets updated. await this.updateOrgDomain(orgDomainId); diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts index 3a76b92cede..c585e9dacd0 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations-routing.module.ts @@ -1,8 +1,10 @@ -import { NgModule } from "@angular/core"; +import { inject, NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { authGuard } from "@bitwarden/angular/auth/guards"; import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { isEnterpriseOrgGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/is-enterprise-org.guard"; import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard"; import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-console/organizations/layouts/organization-layout.component"; @@ -26,8 +28,13 @@ const routes: Routes = [ path: "domain-verification", component: DomainVerificationComponent, canActivate: [organizationPermissionsGuard((org) => org.canManageDomainVerification)], - data: { - titleId: "domainVerification", + resolve: { + titleId: async () => { + const configService = inject(ConfigService); + return (await configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning)) + ? "claimedDomains" + : "domainVerification"; + }, }, }, { diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html index fa9665ff457..0731820e413 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html @@ -31,7 +31,10 @@ {{ "ssoIdentifierHintPartOne" | i18n }} - {{ "domainVerification" | i18n }} + {{ + ((accountDeprovisioningEnabled$ | async) ? "claimedDomains" : "domainVerification") + | i18n + }} diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index 87f2a3dd9d9..6449ef7a701 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -9,7 +9,7 @@ import { Validators, } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, Subject, takeUntil } from "rxjs"; +import { concatMap, Observable, Subject, takeUntil } from "rxjs"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -28,6 +28,7 @@ import { SsoConfigApi } from "@bitwarden/common/auth/models/api/sso-config.api"; import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/organization-sso.request"; import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response"; import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -185,6 +186,8 @@ export class SsoComponent implements OnInit, OnDestroy { return this.ssoConfigForm?.controls?.configType as FormControl; } + accountDeprovisioningEnabled$: Observable; + constructor( private formBuilder: FormBuilder, private route: ActivatedRoute, @@ -195,7 +198,11 @@ export class SsoComponent implements OnInit, OnDestroy { private organizationApiService: OrganizationApiServiceAbstraction, private configService: ConfigService, private toastService: ToastService, - ) {} + ) { + this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.AccountDeprovisioning, + ); + } async ngOnInit() { this.enabledCtrl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((enabled) => {
+ {{ "claimedDomainsDesc" | i18n }} + + + +