1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 18:33:50 +00:00

[PM-26901] Add notification handler for auto confirm (#18886)

* add notification handler for auto confirm

* add missing state check

* fix test

* isolate angular specific code from shared lib code

* clean up

* use autoconfirm method

* fix test
This commit is contained in:
Brandon Treston
2026-02-13 14:36:11 -05:00
committed by GitHub
parent 10a20a43a3
commit 2912bf05e1
25 changed files with 257 additions and 52 deletions

View File

@@ -35,4 +35,5 @@ export enum NotificationType {
ProviderBankAccountVerified = 24,
SyncPolicy = 25,
AutoConfirmMember = 26,
}

View File

@@ -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");
});
});
});

View File

@@ -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");
}
}

View File

@@ -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<AuthRequestAnsweringService>;
let configService: MockProxy<ConfigService>;
let policyService: MockProxy<InternalPolicyService>;
let autoConfirmService: MockProxy<AutomaticUserConfirmationService>;
let activeUserAccount$: BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>;
let userAccounts$: BehaviorSubject<ObservedValueOf<AccountService["accounts$"]>>;
@@ -131,6 +133,8 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
policyService = mock<InternalPolicyService>();
autoConfirmService = mock<AutomaticUserConfirmationService>();
defaultServerNotificationsService = new DefaultServerNotificationsService(
mock<LogService>(),
syncService,
@@ -145,6 +149,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
authRequestAnsweringService,
configService,
policyService,
autoConfirmService,
);
});

View File

@@ -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<AuthRequestAnsweringService>;
let configService: MockProxy<ConfigService>;
let policyService: MockProxy<InternalPolicyService>;
let autoConfirmService: MockProxy<AutomaticUserConfirmationService>;
let activeAccount: BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>;
let accounts: BehaviorSubject<ObservedValueOf<AccountService["accounts$"]>>;
@@ -75,6 +77,7 @@ describe("NotificationsService", () => {
authRequestAnsweringService = mock<AuthRequestAnsweringService>();
configService = mock<ConfigService>();
policyService = mock<InternalPolicyService>();
autoConfirmService = mock<AutomaticUserConfirmationService>();
// 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",
);
});
});
});
});

View File

@@ -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>([
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<UserId, AccountInfo>): Set<UserId> => {
@@ -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;
}