1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-20 03:13:55 +00:00

[PM-26378] Auto confirm events (#19025)

* 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

* add event logging for auto confirm

* update copy
This commit is contained in:
Brandon Treston
2026-02-19 09:57:52 -05:00
committed by GitHub
parent e66a1f37b5
commit c8ba23e28d
8 changed files with 100 additions and 5 deletions

View File

@@ -7,6 +7,8 @@ import { of } from "rxjs";
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
import { AutoConfirmState, AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
import { PopOutComponent } from "@bitwarden/browser/platform/popup/components/pop-out.component";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { mockAccountServiceWith } from "@bitwarden/common/spec";
@@ -52,6 +54,8 @@ describe("AdminSettingsComponent", () => {
let autoConfirmService: MockProxy<AutomaticUserConfirmationService>;
let nudgesService: MockProxy<NudgesService>;
let mockDialogService: MockProxy<DialogService>;
let eventCollectionService: MockProxy<EventCollectionService>;
let organizationService: MockProxy<InternalOrganizationServiceAbstraction>;
const userId = "test-user-id" as UserId;
const mockAutoConfirmState: AutoConfirmState = {
@@ -64,10 +68,14 @@ describe("AdminSettingsComponent", () => {
autoConfirmService = mock<AutomaticUserConfirmationService>();
nudgesService = mock<NudgesService>();
mockDialogService = mock<DialogService>();
eventCollectionService = mock<EventCollectionService>();
organizationService = mock<InternalOrganizationServiceAbstraction>();
autoConfirmService.configuration$.mockReturnValue(of(mockAutoConfirmState));
autoConfirmService.upsert.mockResolvedValue(undefined);
nudgesService.showNudgeSpotlight$.mockReturnValue(of(false));
eventCollectionService.collect.mockResolvedValue(undefined);
organizationService.organizations$.mockReturnValue(of([]));
await TestBed.configureTestingModule({
imports: [AdminSettingsComponent],
@@ -77,6 +85,11 @@ describe("AdminSettingsComponent", () => {
{ provide: AutomaticUserConfirmationService, useValue: autoConfirmService },
{ provide: DialogService, useValue: mockDialogService },
{ provide: NudgesService, useValue: nudgesService },
{ provide: EventCollectionService, useValue: eventCollectionService },
{
provide: InternalOrganizationServiceAbstraction,
useValue: organizationService,
},
{ provide: I18nService, useValue: { t: (key: string) => key } },
],
})

View File

@@ -20,8 +20,11 @@ import {
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";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EventType } from "@bitwarden/common/enums";
import {
BitIconButtonComponent,
CardComponent,
@@ -69,6 +72,8 @@ export class AdminSettingsComponent implements OnInit {
private destroyRef: DestroyRef,
private dialogService: DialogService,
private nudgesService: NudgesService,
private eventCollectionService: EventCollectionService,
private organizationService: InternalOrganizationServiceAbstraction,
) {}
async ngOnInit() {
@@ -88,14 +93,26 @@ export class AdminSettingsComponent implements OnInit {
}
return of(false);
}),
withLatestFrom(this.autoConfirmService.configuration$(userId)),
switchMap(([newValue, existingState]) =>
this.autoConfirmService.upsert(userId, {
withLatestFrom(
this.autoConfirmService.configuration$(userId),
this.organizationService.organizations$(userId),
),
switchMap(async ([newValue, existingState, organizations]) => {
await this.autoConfirmService.upsert(userId, {
...existingState,
enabled: newValue,
showBrowserNotification: false,
}),
),
});
// Auto-confirm users can only belong to one organization
const organization = organizations[0];
if (organization?.id) {
const eventType = newValue
? EventType.Organization_AutoConfirmEnabled_Admin
: EventType.Organization_AutoConfirmDisabled_Admin;
await this.eventCollectionService.collect(eventType, undefined, true, organization.id);
}
}),
takeUntilDestroyed(this.destroyRef),
)
.subscribe();

View File

@@ -44,6 +44,7 @@ const EVENT_SYSTEM_USER_TO_TRANSLATION: Record<EventSystemUser, string> = {
[EventSystemUser.SCIM]: null, // SCIM acronym not able to be translated so just display SCIM
[EventSystemUser.DomainVerification]: "domainVerification",
[EventSystemUser.PublicApi]: "publicApi",
[EventSystemUser.BitwardenPortal]: "system",
};
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush

View File

@@ -355,6 +355,13 @@ export class EventService {
this.getShortId(ev.organizationUserId),
);
break;
case EventType.OrganizationUser_AutomaticallyConfirmed:
msg = this.i18nService.t("automaticallyConfirmedUserId", this.formatOrgUserId(ev));
humanReadableMsg = this.i18nService.t(
"automaticallyConfirmedUserId",
this.getShortId(ev.organizationUserId),
);
break;
// Org
case EventType.Organization_Updated:
msg = humanReadableMsg = this.i18nService.t("editedOrgSettings");
@@ -458,6 +465,18 @@ export class EventService {
case EventType.Organization_ItemOrganization_Declined:
msg = humanReadableMsg = this.i18nService.t("userDeclinedTransfer");
break;
case EventType.Organization_AutoConfirmEnabled_Admin:
msg = humanReadableMsg = this.i18nService.t("autoConfirmEnabledByAdmin");
break;
case EventType.Organization_AutoConfirmDisabled_Admin:
msg = humanReadableMsg = this.i18nService.t("autoConfirmDisabledByAdmin");
break;
case EventType.Organization_AutoConfirmEnabled_Portal:
msg = humanReadableMsg = this.i18nService.t("autoConfirmEnabledByPortal");
break;
case EventType.Organization_AutoConfirmDisabled_Portal:
msg = humanReadableMsg = this.i18nService.t("autoConfirmDisabledByPortal");
break;
// Policies
case EventType.Policy_Updated: {

View File

@@ -4337,6 +4337,15 @@
}
}
},
"automaticallyConfirmedUserId": {
"message": "Automatically confirmed user $ID$.",
"placeholders": {
"id": {
"content": "$1",
"example": "John Smith"
}
}
},
"editedUserId": {
"message": "Edited user $ID$.",
"placeholders": {
@@ -6142,6 +6151,21 @@
"autoConfirmCheckBoxLabel": {
"message": "I accept these risks and policy updates"
},
"autoConfirmEnabledByAdmin": {
"message": "Turned on Automatic user confirmation setting"
},
"autoConfirmDisabledByAdmin": {
"message": "Turned off Automatic user confirmation setting"
},
"autoConfirmEnabledByPortal": {
"message": "Added Automatic user confirmation policy"
},
"autoConfirmDisabledByPortal": {
"message": "Removed Automatic user confirmation policy"
},
"system": {
"message": "System"
},
"personalOwnership": {
"message": "Remove individual vault"
},

View File

@@ -439,6 +439,21 @@ describe("DefaultAutomaticUserConfirmationService", () => {
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, mockOrganizationId);

View File

@@ -5,4 +5,5 @@ export enum EventSystemUser {
SCIM = 1,
DomainVerification = 2,
PublicApi = 3,
BitwardenPortal = 5,
}

View File

@@ -60,6 +60,7 @@ export enum EventType {
OrganizationUser_RejectedAuthRequest = 1514,
OrganizationUser_Deleted = 1515,
OrganizationUser_Left = 1516,
OrganizationUser_AutomaticallyConfirmed = 1517,
Organization_Updated = 1600,
Organization_PurgedVault = 1601,
@@ -81,6 +82,10 @@ export enum EventType {
Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsDisabled = 1617,
Organization_ItemOrganization_Accepted = 1618,
Organization_ItemOrganization_Declined = 1619,
Organization_AutoConfirmEnabled_Admin = 1620,
Organization_AutoConfirmDisabled_Admin = 1621,
Organization_AutoConfirmEnabled_Portal = 1622,
Organization_AutoConfirmDisabled_Portal = 1623,
Policy_Updated = 1700,