1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-24 08:33:29 +00:00

Merge branch 'main' into auth/pm-27086/input-password-use-new-km-data-types

This commit is contained in:
rr-bw
2026-01-20 14:10:45 -08:00
82 changed files with 4273 additions and 1181 deletions

View File

@@ -264,6 +264,13 @@ export abstract class OrganizationUserApiService {
ids: string[],
): Promise<ListResponse<OrganizationUserBulkResponse>>;
/**
* Revoke the current user's access to the organization
* if they decline an item transfer under the Organization Data Ownership policy.
* @param organizationId - Identifier for the organization the user belongs to
*/
abstract revokeSelf(organizationId: string): Promise<void>;
/**
* Restore an organization user's access to the organization
* @param organizationId - Identifier for the organization the user belongs to

View File

@@ -339,6 +339,16 @@ export class DefaultOrganizationUserApiService implements OrganizationUserApiSer
return new ListResponse(r, OrganizationUserBulkResponse);
}
revokeSelf(organizationId: string): Promise<void> {
return this.apiService.send(
"PUT",
"/organizations/" + organizationId + "/users/revoke-self",
null,
true,
false,
);
}
restoreOrganizationUser(organizationId: string, id: string): Promise<void> {
return this.apiService.send(
"PUT",

View File

@@ -121,13 +121,13 @@ export class CollectionAdminView extends CollectionView {
try {
view.name = await encryptService.decryptString(new EncString(view.name), orgKey);
} catch (e) {
view.name = "[error: cannot decrypt]";
// Note: This should be replaced by the owning team with appropriate, domain-specific behavior.
// eslint-disable-next-line no-console
console.error(
"[CollectionAdminView/fromCollectionAccessDetails] Error decrypting collection name",
e,
);
throw e;
}
view.assigned = collection.assigned;
view.readOnly = collection.readOnly;

View File

@@ -126,7 +126,14 @@ export class CollectionView implements View, ITreeNodeObject {
): Promise<CollectionView> {
const view = new CollectionView({ ...collection, name: "" });
view.name = await encryptService.decryptString(collection.name, key);
try {
view.name = await encryptService.decryptString(collection.name, key);
} catch (e) {
view.name = "[error: cannot decrypt]";
// eslint-disable-next-line no-console
console.error("[CollectionView] Error decrypting collection name", e);
}
view.assigned = true;
view.externalId = collection.externalId;
view.readOnly = collection.readOnly;

View File

@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export class ProviderUserConfirmRequest {
key: string;
protected key: string;
constructor(key: string) {
this.key = key;
}
}

View File

@@ -14,6 +14,7 @@ export enum FeatureFlag {
AutoConfirm = "pm-19934-auto-confirm-organization-users",
BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration",
IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud",
MembersComponentRefactor = "pm-29503-refactor-members-inheritance",
/* Auth */
PM23801_PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin",
@@ -101,6 +102,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.AutoConfirm]: FALSE,
[FeatureFlag.BlockClaimedDomainAccountCreation]: FALSE,
[FeatureFlag.IncreaseBulkReinviteLimitForCloud]: FALSE,
[FeatureFlag.MembersComponentRefactor]: FALSE,
/* Autofill */
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,

View File

@@ -280,8 +280,7 @@ export abstract class KeyService {
* encrypted private key at all.
*
* @param userId The user id of the user to get the data for.
* @returns An observable stream of the decrypted private key or null.
* @throws Error when decryption of the encrypted private key fails.
* @returns An observable stream of the decrypted private key or null if the private key is not present or fails to decrypt
*/
abstract userPrivateKey$(userId: UserId): Observable<UserPrivateKey | null>;

View File

@@ -437,14 +437,13 @@ describe("keyService", () => {
);
});
it("throws an error if unwrapping encrypted private key fails", async () => {
it("emits null if unwrapping encrypted private key fails", async () => {
encryptService.unwrapDecapsulationKey.mockImplementationOnce(() => {
throw new Error("Unwrapping failed");
});
await expect(firstValueFrom(keyService.userPrivateKey$(mockUserId))).rejects.toThrow(
"Unwrapping failed",
);
const result = await firstValueFrom(keyService.userPrivateKey$(mockUserId));
expect(result).toBeNull();
});
it("returns null if user key is not set", async () => {

View File

@@ -791,7 +791,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$;
}
private userPrivateKeyHelper$(userId: UserId) {
private userPrivateKeyHelper$(userId: UserId): Observable<{
userKey: UserKey;
userPrivateKey: UserPrivateKey | null;
} | null> {
const userKey$ = this.userKey$(userId);
return userKey$.pipe(
switchMap((userKey) => {
@@ -801,18 +804,20 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$.pipe(
switchMap(async (encryptedPrivateKey) => {
try {
return await this.decryptPrivateKey(encryptedPrivateKey, userKey);
} catch (e) {
this.logService.error("Failed to decrypt private key for user ", userId, e);
throw e;
}
return await this.decryptPrivateKey(encryptedPrivateKey, userKey);
}),
// Combine outerscope info with user private key
map((userPrivateKey) => ({
userKey,
userPrivateKey,
})),
catchError((err: unknown) => {
this.logService.error(`Failed to decrypt private key for user ${userId}`);
return of({
userKey,
userPrivateKey: null,
});
}),
);
}),
);

View File

@@ -1,17 +1,8 @@
import { CommonModule } from "@angular/common";
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
input,
output,
} from "@angular/core";
import { ChangeDetectionStrategy, Component, computed, effect, input, output } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { NoResults, NoSendsIcon } from "@bitwarden/assets/svg";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import {
ButtonModule,
@@ -57,8 +48,6 @@ export class SendListComponent {
protected readonly noResultsIcon = NoResults;
protected readonly sendListState = SendListState;
private i18nService = inject(I18nService);
readonly sends = input.required<SendView[]>();
readonly loading = input<boolean>(false);
readonly disableSend = input<boolean>(false);
@@ -70,7 +59,7 @@ export class SendListComponent {
);
protected readonly noSearchResults = computed(
() => this.showSearchBar() && (this.sends().length === 0 || this.searchText().length > 0),
() => this.showSearchBar() && this.sends().length === 0,
);
// Reusable data source instance - updated reactively when sends change

View File

@@ -153,12 +153,12 @@ describe("UriOptionComponent", () => {
component.writeValue({ uri: "https://example.com", matchDetection: UriMatchStrategy.Exact });
fixture.detectChanges();
expect(getToggleMatchDetectionBtn().getAttribute("aria-label")).toBe(
"showMatchDetection https://example.com",
"showMatchDetectionNoPlaceholder",
);
getToggleMatchDetectionBtn().click();
fixture.detectChanges();
expect(getToggleMatchDetectionBtn().getAttribute("aria-label")).toBe(
"hideMatchDetection https://example.com",
"hideMatchDetectionNoPlaceholder",
);
});
});

View File

@@ -165,9 +165,11 @@ export class UriOptionComponent implements ControlValueAccessor {
}
protected get toggleTitle() {
return this.showMatchDetection
? this.i18nService.t("hideMatchDetection", this.uriForm.value.uri)
: this.i18nService.t("showMatchDetection", this.uriForm.value.uri);
return this.i18nService.t(
this.showMatchDetection
? "hideMatchDetectionNoPlaceholder"
: "showMatchDetectionNoPlaceholder",
);
}
// NG_VALUE_ACCESSOR implementation

View File

@@ -41,7 +41,8 @@ export class ArchiveCipherUtilitiesService {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "archiveItem" },
content: { key: "archiveItemConfirmDesc" },
content: { key: "archiveItemDialogContent" },
acceptButtonText: { key: "archiveVerb" },
type: "info",
});

View File

@@ -2,7 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of, Subject } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { CollectionService } from "@bitwarden/admin-console/common";
import { CollectionService, OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -42,6 +42,7 @@ describe("DefaultVaultItemsTransferService", () => {
let mockToastService: MockProxy<ToastService>;
let mockEventCollectionService: MockProxy<EventCollectionService>;
let mockConfigService: MockProxy<ConfigService>;
let mockOrganizationUserApiService: MockProxy<OrganizationUserApiService>;
const userId = "user-id" as UserId;
const organizationId = "org-id" as OrganizationId;
@@ -77,6 +78,7 @@ describe("DefaultVaultItemsTransferService", () => {
mockToastService = mock<ToastService>();
mockEventCollectionService = mock<EventCollectionService>();
mockConfigService = mock<ConfigService>();
mockOrganizationUserApiService = mock<OrganizationUserApiService>();
mockI18nService.t.mockImplementation((key) => key);
transferInProgressValues = [];
@@ -92,6 +94,7 @@ describe("DefaultVaultItemsTransferService", () => {
mockToastService,
mockEventCollectionService,
mockConfigService,
mockOrganizationUserApiService,
);
});
@@ -632,9 +635,15 @@ describe("DefaultVaultItemsTransferService", () => {
mockDialogService.open
.mockReturnValueOnce(createMockDialogRef(TransferItemsDialogResult.Declined))
.mockReturnValueOnce(createMockDialogRef(LeaveConfirmationDialogResult.Confirmed));
mockOrganizationUserApiService.revokeSelf.mockResolvedValue(undefined);
await service.enforceOrganizationDataOwnership(userId);
expect(mockOrganizationUserApiService.revokeSelf).toHaveBeenCalledWith(organizationId);
expect(mockToastService.showToast).toHaveBeenCalledWith({
variant: "success",
message: "leftOrganization",
});
expect(mockCipherService.shareManyWithServer).not.toHaveBeenCalled();
});

View File

@@ -10,7 +10,7 @@ import {
} from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { CollectionService } from "@bitwarden/admin-console/common";
import { CollectionService, OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -53,6 +53,7 @@ export class DefaultVaultItemsTransferService implements VaultItemsTransferServi
private toastService: ToastService,
private eventCollectionService: EventCollectionService,
private configService: ConfigService,
private organizationUserApiService: OrganizationUserApiService,
) {}
private _transferInProgressSubject = new BehaviorSubject(false);
@@ -162,7 +163,12 @@ export class DefaultVaultItemsTransferService implements VaultItemsTransferServi
);
if (!userAcceptedTransfer) {
// TODO: Revoke user from organization if they decline migration and show toast PM-29465
await this.organizationUserApiService.revokeSelf(migrationInfo.enforcingOrganization.id);
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("leftOrganization"),
});
await this.eventCollectionService.collect(
EventType.Organization_ItemOrganization_Declined,