diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 585942d7537..25c7b344982 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -14,7 +14,14 @@ import { timeout, } from "rxjs"; -import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common"; +import { + CollectionService, + DefaultCollectionService, + DefaultOrganizationUserApiService, + DefaultOrganizationUserService, + OrganizationUserApiService, + OrganizationUserService, +} from "@bitwarden/admin-console/common"; import { AuthRequestApiServiceAbstraction, AuthRequestService, @@ -27,6 +34,10 @@ import { LogoutReason, UserDecryptionOptionsService, } from "@bitwarden/auth/common"; +import { + AutomaticUserConfirmationService, + DefaultAutomaticUserConfirmationService, +} from "@bitwarden/auto-confirm"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -487,6 +498,9 @@ export default class MainBackground { onUpdatedRan: boolean; onReplacedRan: boolean; loginToAutoFill: CipherView = null; + organizationUserService: OrganizationUserService; + organizationUserApiService: OrganizationUserApiService; + autoConfirmService: AutomaticUserConfirmationService; private commandsBackground: CommandsBackground; private contextMenusBackground: ContextMenusBackground; @@ -763,6 +777,15 @@ export default class MainBackground { { createRequest: (url, request) => new Request(url, request) }, ); + this.organizationUserApiService = new DefaultOrganizationUserApiService(this.apiService); + this.organizationUserService = new DefaultOrganizationUserService( + this.keyService, + this.encryptService, + this.organizationUserApiService, + this.accountService, + this.i18nService, + ); + this.hibpApiService = new HibpApiService(this.apiService); this.fileUploadService = new FileUploadService(this.logService, this.apiService); this.cipherFileUploadService = new CipherFileUploadService( @@ -804,6 +827,16 @@ export default class MainBackground { this.authService, ); + this.autoConfirmService = new DefaultAutomaticUserConfirmationService( + this.configService, + this.apiService, + this.organizationUserService, + this.stateProvider, + this.organizationService, + this.organizationUserApiService, + this.policyService, + ); + const sdkClientFactory = flagEnabled("sdk") ? new DefaultSdkClientFactory() : new NoopSdkClientFactory(); @@ -1219,6 +1252,7 @@ export default class MainBackground { this.authRequestAnsweringService, this.configService, this.policyService, + this.autoConfirmService, ); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 4e14d1171fd..0d85743bba7 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -42,7 +42,7 @@ import { TwoFactorAuthComponent, TwoFactorAuthGuard, } from "@bitwarden/auth/angular"; -import { canAccessAutoConfirmSettings } from "@bitwarden/auto-confirm"; +import { canAccessAutoConfirmSettings } from "@bitwarden/auto-confirm/angular"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components"; import { LockComponent, diff --git a/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts b/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts index 70affd73ef3..f48b08566a1 100644 --- a/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault/vault.component.spec.ts @@ -12,7 +12,7 @@ import { NudgeType, NudgesService } from "@bitwarden/angular/vault"; import { AutoConfirmExtensionSetupDialogComponent, AutomaticUserConfirmationService, -} from "@bitwarden/auto-confirm"; +} from "@bitwarden/auto-confirm/angular"; import { CurrentAccountComponent } from "@bitwarden/browser/auth/popup/account-switching/current-account.component"; import AutofillService from "@bitwarden/browser/autofill/services/autofill.service"; import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; diff --git a/apps/browser/src/vault/popup/components/vault/vault.component.ts b/apps/browser/src/vault/popup/components/vault/vault.component.ts index 281abc5f180..cb3cb5f5eec 100644 --- a/apps/browser/src/vault/popup/components/vault/vault.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault.component.ts @@ -28,7 +28,7 @@ import { AutoConfirmExtensionSetupDialogComponent, AutoConfirmState, AutomaticUserConfirmationService, -} from "@bitwarden/auto-confirm"; +} from "@bitwarden/auto-confirm/angular"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; diff --git a/apps/browser/src/vault/popup/settings/admin-settings.component.ts b/apps/browser/src/vault/popup/settings/admin-settings.component.ts index e4b676525ed..52da4318047 100644 --- a/apps/browser/src/vault/popup/settings/admin-settings.component.ts +++ b/apps/browser/src/vault/popup/settings/admin-settings.component.ts @@ -16,7 +16,7 @@ import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotligh import { AutoConfirmWarningDialogComponent, AutomaticUserConfirmationService, -} from "@bitwarden/auto-confirm"; +} from "@bitwarden/auto-confirm/angular"; import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "@bitwarden/browser/platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "@bitwarden/browser/platform/popup/layout/popup-page.component"; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 0f857e67247..2fbf55bf6c5 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -56,6 +56,7 @@ import { UserDecryptionOptionsService, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -1079,6 +1080,7 @@ const safeProviders: SafeProvider[] = [ AuthRequestAnsweringService, ConfigService, InternalPolicyService, + AutomaticUserConfirmationService, ], }), safeProvider({ diff --git a/libs/auto-confirm/src/abstractions/auto-confirm.service.abstraction.ts b/libs/auto-confirm/src/abstractions/auto-confirm.service.abstraction.ts index 9ce6cb9c1a4..1ef3be4ff4e 100644 --- a/libs/auto-confirm/src/abstractions/auto-confirm.service.abstraction.ts +++ b/libs/auto-confirm/src/abstractions/auto-confirm.service.abstraction.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/user-core"; import { AutoConfirmState } from "../models/auto-confirm-state.model"; @@ -27,12 +27,12 @@ export abstract class AutomaticUserConfirmationService { /** * Calls the API endpoint to initiate automatic user confirmation. * @param userId The userId of the logged in admin performing auto confirmation. This is neccesary to perform the key exchange and for permissions checks. - * @param confirmingUserId The userId of the user being confirmed. - * @param organization the organization the user is being auto confirmed to. + * @param confirmedUserId The userId of the member being confirmed. + * @param organization the organization the member is being auto confirmed to. **/ abstract autoConfirmUser( userId: UserId, - confirmingUserId: UserId, - organization: Organization, + confirmedUserId: UserId, + organization: OrganizationId, ): Promise; } diff --git a/libs/auto-confirm/src/components/auto-confirm-extension-dialog.component.ts b/libs/auto-confirm/src/angular/components/auto-confirm-extension-dialog.component.ts similarity index 100% rename from libs/auto-confirm/src/components/auto-confirm-extension-dialog.component.ts rename to libs/auto-confirm/src/angular/components/auto-confirm-extension-dialog.component.ts diff --git a/libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.html b/libs/auto-confirm/src/angular/components/auto-confirm-warning-dialog.component.html similarity index 100% rename from libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.html rename to libs/auto-confirm/src/angular/components/auto-confirm-warning-dialog.component.html diff --git a/libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.ts b/libs/auto-confirm/src/angular/components/auto-confirm-warning-dialog.component.ts similarity index 100% rename from libs/auto-confirm/src/components/auto-confirm-warning-dialog.component.ts rename to libs/auto-confirm/src/angular/components/auto-confirm-warning-dialog.component.ts diff --git a/libs/auto-confirm/src/components/index.ts b/libs/auto-confirm/src/angular/components/index.ts similarity index 100% rename from libs/auto-confirm/src/components/index.ts rename to libs/auto-confirm/src/angular/components/index.ts diff --git a/libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.spec.ts b/libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.spec.ts similarity index 97% rename from libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.spec.ts rename to libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.spec.ts index aca51edb8dc..0261a1a86dc 100644 --- a/libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.spec.ts +++ b/libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.spec.ts @@ -3,14 +3,13 @@ import { Router, UrlTree } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { newGuid } from "@bitwarden/guid"; -import { AutomaticUserConfirmationService } from "../abstractions"; - import { canAccessAutoConfirmSettings } from "./automatic-user-confirmation-settings.guard"; describe("canAccessAutoConfirmSettings", () => { diff --git a/libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.ts b/libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.ts similarity index 94% rename from libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.ts rename to libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.ts index 77f01ba2801..3ae6b5b4c52 100644 --- a/libs/auto-confirm/src/guards/automatic-user-confirmation-settings.guard.ts +++ b/libs/auto-confirm/src/angular/guards/automatic-user-confirmation-settings.guard.ts @@ -2,13 +2,12 @@ import { inject } from "@angular/core"; import { CanActivateFn, Router } from "@angular/router"; import { map, switchMap } from "rxjs"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { ToastService } from "@bitwarden/components"; -import { AutomaticUserConfirmationService } from "../abstractions"; - export const canAccessAutoConfirmSettings: CanActivateFn = () => { const accountService = inject(AccountService); const autoConfirmService = inject(AutomaticUserConfirmationService); diff --git a/libs/auto-confirm/src/guards/index.ts b/libs/auto-confirm/src/angular/guards/index.ts similarity index 100% rename from libs/auto-confirm/src/guards/index.ts rename to libs/auto-confirm/src/angular/guards/index.ts diff --git a/libs/auto-confirm/src/angular/index.ts b/libs/auto-confirm/src/angular/index.ts new file mode 100644 index 00000000000..ff2d69248b4 --- /dev/null +++ b/libs/auto-confirm/src/angular/index.ts @@ -0,0 +1,8 @@ +// Re-export core auto-confirm functionality for convenience +export * from "../abstractions"; +export * from "../models"; +export * from "../services"; + +// Angular-specific exports +export * from "./components"; +export * from "./guards"; diff --git a/libs/auto-confirm/src/index.ts b/libs/auto-confirm/src/index.ts index 56b9d0b0285..9187ccd39cf 100644 --- a/libs/auto-confirm/src/index.ts +++ b/libs/auto-confirm/src/index.ts @@ -1,5 +1,3 @@ export * from "./abstractions"; -export * from "./components"; -export * from "./guards"; export * from "./models"; export * from "./services"; diff --git a/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts b/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts index 1d37378b96c..0ea3ca9c23a 100644 --- a/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts +++ b/libs/auto-confirm/src/services/default-auto-confirm.service.spec.ts @@ -377,48 +377,70 @@ describe("DefaultAutomaticUserConfirmationService", () => { defaultUserCollectionName: "encrypted-collection", } as OrganizationUserConfirmRequest; - beforeEach(() => { + beforeEach(async () => { const organizations$ = new BehaviorSubject([mockOrganization]); organizationService.organizations$.mockReturnValue(organizations$); configService.getFeatureFlag$.mockReturnValue(of(true)); policyService.policyAppliesToUser$.mockReturnValue(of(true)); + // Enable auto-confirm configuration for the user + const enabledConfig = new AutoConfirmState(); + enabledConfig.enabled = true; + await stateProvider.setUserState( + AUTO_CONFIRM_STATE, + { [mockUserId]: enabledConfig }, + mockUserId, + ); + apiService.getUserPublicKey.mockResolvedValue({ publicKey: mockPublicKey, } as UserKeyResponse); jest.spyOn(Utils, "fromB64ToArray").mockReturnValue(mockPublicKeyArray); organizationUserService.buildConfirmRequest.mockReturnValue(of(mockConfirmRequest)); - organizationUserApiService.postOrganizationUserConfirm.mockResolvedValue(undefined); + organizationUserApiService.postOrganizationUserAutoConfirm.mockResolvedValue(undefined); }); - it("should successfully auto-confirm a user", async () => { - await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization); + it("should successfully auto-confirm a user with organizationId", async () => { + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); expect(apiService.getUserPublicKey).toHaveBeenCalledWith(mockUserId); expect(organizationUserService.buildConfirmRequest).toHaveBeenCalledWith( mockOrganization, mockPublicKeyArray, ); - expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith( + expect(organizationUserApiService.postOrganizationUserAutoConfirm).toHaveBeenCalledWith( mockOrganizationId, mockConfirmingUserId, mockConfirmRequest, ); }); - it("should not confirm user when canManageAutoConfirm returns false", async () => { + it("should return early when canManageAutoConfirm returns false", async () => { configService.getFeatureFlag$.mockReturnValue(of(false)); - await expect( - service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization), - ).rejects.toThrow("Cannot automatically confirm user (insufficient permissions)"); + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); expect(apiService.getUserPublicKey).not.toHaveBeenCalled(); - expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); + }); + + it("should return early when auto-confirm is disabled in configuration", async () => { + const disabledConfig = new AutoConfirmState(); + disabledConfig.enabled = false; + await stateProvider.setUserState( + AUTO_CONFIRM_STATE, + { [mockUserId]: disabledConfig }, + mockUserId, + ); + + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); + + expect(apiService.getUserPublicKey).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); }); it("should build confirm request with organization and public key", async () => { - await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization); + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); expect(organizationUserService.buildConfirmRequest).toHaveBeenCalledWith( mockOrganization, @@ -427,10 +449,10 @@ describe("DefaultAutomaticUserConfirmationService", () => { }); it("should call API with correct parameters", async () => { - await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization); + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId); - expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith( - mockOrganization.id, + expect(organizationUserApiService.postOrganizationUserAutoConfirm).toHaveBeenCalledWith( + mockOrganizationId, mockConfirmingUserId, mockConfirmRequest, ); @@ -441,10 +463,10 @@ describe("DefaultAutomaticUserConfirmationService", () => { apiService.getUserPublicKey.mockRejectedValue(apiError); await expect( - service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization), + service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId), ).rejects.toThrow("API Error"); - expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); }); it("should handle buildConfirmRequest errors gracefully", async () => { @@ -452,10 +474,10 @@ describe("DefaultAutomaticUserConfirmationService", () => { organizationUserService.buildConfirmRequest.mockReturnValue(throwError(() => buildError)); await expect( - service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization), + service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganizationId), ).rejects.toThrow("Build Error"); - expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAutoConfirm).not.toHaveBeenCalled(); }); }); }); diff --git a/libs/auto-confirm/src/services/default-auto-confirm.service.ts b/libs/auto-confirm/src/services/default-auto-confirm.service.ts index 109ccb6c9db..821340a0a9c 100644 --- a/libs/auto-confirm/src/services/default-auto-confirm.service.ts +++ b/libs/auto-confirm/src/services/default-auto-confirm.service.ts @@ -8,10 +8,11 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { getById } from "@bitwarden/common/platform/misc"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { StateProvider } from "@bitwarden/state"; import { UserId } from "@bitwarden/user-core"; @@ -66,26 +67,44 @@ export class DefaultAutomaticUserConfirmationService implements AutomaticUserCon async autoConfirmUser( userId: UserId, - confirmingUserId: UserId, - organization: Organization, + confirmedUserId: UserId, + organizationId: OrganizationId, ): Promise { + const canManage = await firstValueFrom(this.canManageAutoConfirm$(userId)); + + if (!canManage) { + return; + } + + // Only initiate auto confirmation if the local client setting has been turned on + const autoConfirmEnabled = await firstValueFrom( + this.configuration$(userId).pipe(map((state) => state.enabled)), + ); + + if (!autoConfirmEnabled) { + return; + } + + const organization$ = this.organizationService.organizations$(userId).pipe( + getById(organizationId), + map((organization) => { + if (organization == null) { + throw new Error("Organization not found"); + } + return organization; + }), + ); + + const publicKeyResponse = await this.apiService.getUserPublicKey(userId); + const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); + await firstValueFrom( - this.canManageAutoConfirm$(userId).pipe( - map((canManage) => { - if (!canManage) { - throw new Error("Cannot automatically confirm user (insufficient permissions)"); - } - return canManage; - }), - switchMap(() => this.apiService.getUserPublicKey(userId)), - map((publicKeyResponse) => Utils.fromB64ToArray(publicKeyResponse.publicKey)), - switchMap((publicKey) => - this.organizationUserService.buildConfirmRequest(organization, publicKey), - ), + organization$.pipe( + switchMap((org) => this.organizationUserService.buildConfirmRequest(org, publicKey)), switchMap((request) => - this.organizationUserApiService.postOrganizationUserConfirm( - organization.id, - confirmingUserId, + this.organizationUserApiService.postOrganizationUserAutoConfirm( + organizationId, + confirmedUserId, request, ), ), diff --git a/libs/common/src/enums/notification-type.enum.ts b/libs/common/src/enums/notification-type.enum.ts index a10e6bf4448..d323dda4d74 100644 --- a/libs/common/src/enums/notification-type.enum.ts +++ b/libs/common/src/enums/notification-type.enum.ts @@ -35,4 +35,5 @@ export enum NotificationType { ProviderBankAccountVerified = 24, SyncPolicy = 25, + AutoConfirmMember = 26, } diff --git a/libs/common/src/models/response/notification.response.spec.ts b/libs/common/src/models/response/notification.response.spec.ts new file mode 100644 index 00000000000..91a1390bfdb --- /dev/null +++ b/libs/common/src/models/response/notification.response.spec.ts @@ -0,0 +1,63 @@ +import { NotificationType } from "../../enums"; + +import { AutoConfirmMemberNotification, NotificationResponse } from "./notification.response"; + +describe("NotificationResponse", () => { + describe("AutoConfirmMemberNotification", () => { + it("should parse AutoConfirmMemberNotification payload", () => { + const response = { + ContextId: "context-123", + Type: NotificationType.AutoConfirmMember, + Payload: { + TargetUserId: "target-user-id", + UserId: "user-id", + OrganizationId: "org-id", + }, + }; + + const notification = new NotificationResponse(response); + + expect(notification.type).toBe(NotificationType.AutoConfirmMember); + expect(notification.payload).toBeInstanceOf(AutoConfirmMemberNotification); + expect(notification.payload.targetUserId).toBe("target-user-id"); + expect(notification.payload.userId).toBe("user-id"); + expect(notification.payload.organizationId).toBe("org-id"); + }); + + it("should handle stringified JSON payload", () => { + const response = { + ContextId: "context-123", + Type: NotificationType.AutoConfirmMember, + Payload: JSON.stringify({ + TargetUserId: "target-user-id-2", + UserId: "user-id-2", + OrganizationId: "org-id-2", + }), + }; + + const notification = new NotificationResponse(response); + + expect(notification.type).toBe(NotificationType.AutoConfirmMember); + expect(notification.payload).toBeInstanceOf(AutoConfirmMemberNotification); + expect(notification.payload.targetUserId).toBe("target-user-id-2"); + expect(notification.payload.userId).toBe("user-id-2"); + expect(notification.payload.organizationId).toBe("org-id-2"); + }); + }); + + describe("AutoConfirmMemberNotification constructor", () => { + it("should extract all properties from response", () => { + const response = { + TargetUserId: "target-user-id", + UserId: "user-id", + OrganizationId: "org-id", + }; + + const notification = new AutoConfirmMemberNotification(response); + + expect(notification.targetUserId).toBe("target-user-id"); + expect(notification.userId).toBe("user-id"); + expect(notification.organizationId).toBe("org-id"); + }); + }); +}); diff --git a/libs/common/src/models/response/notification.response.ts b/libs/common/src/models/response/notification.response.ts index 2c0c0aae3f1..27232696d2e 100644 --- a/libs/common/src/models/response/notification.response.ts +++ b/libs/common/src/models/response/notification.response.ts @@ -75,6 +75,9 @@ export class NotificationResponse extends BaseResponse { case NotificationType.SyncPolicy: this.payload = new SyncPolicyNotification(payload); break; + case NotificationType.AutoConfirmMember: + this.payload = new AutoConfirmMemberNotification(payload); + break; default: break; } @@ -210,3 +213,16 @@ export class LogOutNotification extends BaseResponse { this.reason = this.getResponseProperty("Reason"); } } + +export class AutoConfirmMemberNotification extends BaseResponse { + userId: string; + targetUserId: string; + organizationId: string; + + constructor(response: any) { + super(response); + this.targetUserId = this.getResponseProperty("TargetUserId"); + this.userId = this.getResponseProperty("UserId"); + this.organizationId = this.getResponseProperty("OrganizationId"); + } +} diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts index 2795e4c3003..70b93c77f1c 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.multiuser.spec.ts @@ -3,6 +3,7 @@ import { BehaviorSubject, bufferCount, firstValueFrom, Subject, ObservedValueOf // eslint-disable-next-line no-restricted-imports import { LogoutReason } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; @@ -36,6 +37,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => { let authRequestAnsweringService: MockProxy; let configService: MockProxy; let policyService: MockProxy; + let autoConfirmService: MockProxy; let activeUserAccount$: BehaviorSubject>; let userAccounts$: BehaviorSubject>; @@ -131,6 +133,8 @@ describe("DefaultServerNotificationsService (multi-user)", () => { policyService = mock(); + autoConfirmService = mock(); + defaultServerNotificationsService = new DefaultServerNotificationsService( mock(), syncService, @@ -145,6 +149,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => { authRequestAnsweringService, configService, policyService, + autoConfirmService, ); }); diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts index f058e8794ac..a54509925ef 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts @@ -4,6 +4,7 @@ import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, of, Subj // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { LogoutReason } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; @@ -45,6 +46,7 @@ describe("NotificationsService", () => { let authRequestAnsweringService: MockProxy; let configService: MockProxy; let policyService: MockProxy; + let autoConfirmService: MockProxy; let activeAccount: BehaviorSubject>; let accounts: BehaviorSubject>; @@ -75,6 +77,7 @@ describe("NotificationsService", () => { authRequestAnsweringService = mock(); configService = mock(); policyService = mock(); + autoConfirmService = mock(); // For these tests, use the active-user implementation (feature flag disabled) configService.getFeatureFlag$.mockImplementation(() => of(true)); @@ -128,6 +131,7 @@ describe("NotificationsService", () => { authRequestAnsweringService, configService, policyService, + autoConfirmService, ); }); @@ -507,5 +511,29 @@ describe("NotificationsService", () => { }); }); }); + + describe("NotificationType.AutoConfirmMember", () => { + it("should call autoConfirmService.autoConfirmUser with correct parameters", async () => { + autoConfirmService.autoConfirmUser.mockResolvedValue(); + + const notification = new NotificationResponse({ + type: NotificationType.AutoConfirmMember, + payload: { + UserId: mockUser1, + TargetUserId: "target-user-id", + OrganizationId: "org-id", + }, + contextId: "different-app-id", + }); + + await sut["processNotification"](notification, mockUser1); + + expect(autoConfirmService.autoConfirmUser).toHaveBeenCalledWith( + mockUser1, + "target-user-id", + "org-id", + ); + }); + }); }); }); diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts index 83ea12bf154..1a43c0edb09 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts @@ -15,6 +15,7 @@ import { // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { LogoutReason } from "@bitwarden/auth/common"; +import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; @@ -49,6 +50,7 @@ export const DISABLED_NOTIFICATIONS_URL = "http://-"; export const AllowedMultiUserNotificationTypes = new Set([ NotificationType.AuthRequest, + NotificationType.AutoConfirmMember, ]); export class DefaultServerNotificationsService implements ServerNotificationsService { @@ -70,6 +72,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer private readonly authRequestAnsweringService: AuthRequestAnsweringService, private readonly configService: ConfigService, private readonly policyService: InternalPolicyService, + private autoConfirmService: AutomaticUserConfirmationService, ) { this.notifications$ = this.accountService.accounts$.pipe( map((accounts: Record): Set => { @@ -292,6 +295,13 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer case NotificationType.SyncPolicy: await this.policyService.syncPolicy(PolicyData.fromPolicy(notification.payload.policy)); break; + case NotificationType.AutoConfirmMember: + await this.autoConfirmService.autoConfirmUser( + notification.payload.userId, + notification.payload.targetUserId, + notification.payload.organizationId, + ); + break; default: break; } diff --git a/tsconfig.base.json b/tsconfig.base.json index 68498cfae01..17f8f6d44fc 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -25,6 +25,7 @@ "@bitwarden/auth/angular": ["./libs/auth/src/angular"], "@bitwarden/auth/common": ["./libs/auth/src/common"], "@bitwarden/auto-confirm": ["libs/auto-confirm/src/index.ts"], + "@bitwarden/auto-confirm/angular": ["libs/auto-confirm/src/angular"], "@bitwarden/billing": ["./libs/billing/src"], "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"], "@bitwarden/browser/*": ["./apps/browser/src/*"],