1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 17:53:39 +00:00

Merge branch 'main' into auth/pm-8111/browser-refresh-login-component

This commit is contained in:
Alec Rippberger
2024-10-02 09:45:07 -05:00
172 changed files with 4678 additions and 919 deletions

View File

@@ -1,28 +0,0 @@
import { View } from "@bitwarden/common/models/view/view";
interface SelectionResponseLike {
id: string;
readOnly: boolean;
hidePasswords: boolean;
manage: boolean;
}
export class CollectionAccessSelectionView extends View {
readonly id: string;
readonly readOnly: boolean;
readonly hidePasswords: boolean;
readonly manage: boolean;
constructor(response?: SelectionResponseLike) {
super();
if (!response) {
return;
}
this.id = response.id;
this.readOnly = response.readOnly;
this.hidePasswords = response.hidePasswords;
this.manage = response.manage;
}
}

View File

@@ -1,9 +1,8 @@
import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common";
import { View } from "@bitwarden/common/src/models/view/view";
import { GroupDetailsResponse, GroupResponse } from "../services/group/responses/group.response";
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
export class GroupView implements View {
id: string;
organizationId: string;

View File

@@ -1,4 +1,3 @@
export * from "./collection-access-selection.view";
export * from "./group.view";
export * from "./organization-user.view";
export * from "./organization-user-admin-view";

View File

@@ -1,11 +1,10 @@
import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common";
import {
OrganizationUserStatusType,
OrganizationUserType,
} from "@bitwarden/common/admin-console/enums";
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
export class OrganizationUserAdminView {
id: string;
userId: string;

View File

@@ -1,12 +1,13 @@
import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common";
import {
OrganizationUserUserDetailsResponse,
CollectionAccessSelectionView,
} from "@bitwarden/admin-console/common";
import {
OrganizationUserStatusType,
OrganizationUserType,
} from "@bitwarden/common/admin-console/enums";
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
export class OrganizationUserView {
id: string;
userId: string;

View File

@@ -78,7 +78,7 @@ export class EntityEventsComponent implements OnInit {
async load() {
try {
if (this.showUser) {
const response = await this.organizationUserApiService.getAllUsers(
const response = await this.organizationUserApiService.getAllMiniUserDetails(
this.params.organizationId,
);
response.data.forEach((u) => {

View File

@@ -83,7 +83,9 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
}
async load() {
const response = await this.organizationUserApiService.getAllUsers(this.organizationId);
const response = await this.organizationUserApiService.getAllMiniUserDetails(
this.organizationId,
);
response.data.forEach((u) => {
const name = this.userNamePipe.transform(u);
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });

View File

@@ -14,7 +14,11 @@ import {
takeUntil,
} from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import {
CollectionAdminService,
CollectionAdminView,
OrganizationUserApiService,
} from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@@ -26,8 +30,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService, ToastService } from "@bitwarden/components";
import { CollectionAdminService } from "../../../vault/core/collection-admin.service";
import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view";
import { InternalGroupService as GroupService, GroupView } from "../core";
import {
AccessItemType,
@@ -131,7 +133,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
);
private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {
return from(this.organizationUserApiService.getAllUsers(this.organizationId)).pipe(
return from(this.organizationUserApiService.getAllMiniUserDetails(this.organizationId)).pipe(
map((response) =>
response.data.map((m) => ({
id: m.id,

View File

@@ -8,6 +8,7 @@ import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-conso
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
@@ -31,6 +32,7 @@ export abstract class BaseBulkConfirmComponent implements OnInit {
protected constructor(
protected cryptoService: CryptoService,
protected encryptService: EncryptService,
protected i18nService: I18nService,
) {}
@@ -67,7 +69,7 @@ export abstract class BaseBulkConfirmComponent implements OnInit {
if (publicKey == null) {
continue;
}
const encryptedKey = await this.cryptoService.rsaEncrypt(key.key, publicKey);
const encryptedKey = await this.encryptService.rsaEncrypt(key.key, publicKey);
userIdsWithKeys.push({
id: user.id,
key: encryptedKey.encryptedString,

View File

@@ -8,6 +8,7 @@ import {
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
@@ -41,6 +42,7 @@ export class BulkConfirmComponent implements OnInit {
constructor(
@Inject(DIALOG_DATA) protected data: BulkConfirmDialogData,
protected cryptoService: CryptoService,
protected encryptService: EncryptService,
protected apiService: ApiService,
private organizationUserApiService: OrganizationUserApiService,
private i18nService: I18nService,
@@ -81,7 +83,7 @@ export class BulkConfirmComponent implements OnInit {
if (publicKey == null) {
continue;
}
const encryptedKey = await this.cryptoService.rsaEncrypt(key.key, publicKey);
const encryptedKey = await this.encryptService.rsaEncrypt(key.key, publicKey);
userIdsWithKeys.push({
id: user.id,
key: encryptedKey.encryptedString,

View File

@@ -13,7 +13,12 @@ import {
takeUntil,
} from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import {
CollectionAccessSelectionView,
CollectionAdminService,
CollectionAdminView,
OrganizationUserApiService,
} from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import {
OrganizationUserStatusType,
@@ -24,14 +29,10 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { DialogService, ToastService } from "@bitwarden/components";
import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service";
import { CollectionAdminView } from "../../../../../vault/core/views/collection-admin.view";
import {
CollectionAccessSelectionView,
GroupService,
GroupView,
OrganizationUserAdminView,
@@ -133,7 +134,6 @@ export class MemberDialogComponent implements OnDestroy {
@Inject(DIALOG_DATA) protected params: MemberDialogParams,
private dialogRef: DialogRef<MemberDialogResult>,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private formBuilder: FormBuilder,
// TODO: We should really look into consolidating naming conventions for these services
private collectionAdminService: CollectionAdminService,

View File

@@ -39,6 +39,7 @@ import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@@ -107,6 +108,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
i18nService: I18nService,
organizationManagementPreferencesService: OrganizationManagementPreferencesService,
cryptoService: CryptoService,
private encryptService: EncryptService,
validationService: ValidationService,
logService: LogService,
userNamePipe: UserNamePipe,
@@ -289,7 +291,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise<void> {
const orgKey = await this.cryptoService.getOrgKey(this.organization.id);
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey);
const key = await this.encryptService.rsaEncrypt(orgKey.key, publicKey);
const request = new OrganizationUserConfirmRequest();
request.key = key.encryptedString;
await this.organizationUserApiService.postOrganizationUserConfirm(

View File

@@ -71,7 +71,7 @@ describe("OrganizationUserResetPasswordService", () => {
const mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
cryptoService.getUserKey.mockResolvedValue(mockUserKey);
cryptoService.rsaEncrypt.mockResolvedValue(
encryptService.rsaEncrypt.mockResolvedValue(
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"),
);
});
@@ -103,7 +103,7 @@ describe("OrganizationUserResetPasswordService", () => {
it("should rsa encrypt the user key", async () => {
await sut.buildRecoveryKey(mockOrgId);
expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(expect.anything(), expect.anything());
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(expect.anything(), expect.anything());
});
});
@@ -128,7 +128,7 @@ describe("OrganizationUserResetPasswordService", () => {
cryptoService.getOrgKey.mockResolvedValue(mockOrgKey);
encryptService.decryptToBytes.mockResolvedValue(mockRandomBytes);
cryptoService.rsaDecrypt.mockResolvedValue(mockRandomBytes);
encryptService.rsaDecrypt.mockResolvedValue(mockRandomBytes);
const mockMasterKey = new SymmetricCryptoKey(mockRandomBytes) as MasterKey;
cryptoService.makeMasterKey.mockResolvedValue(mockMasterKey);
cryptoService.hashMasterKey.mockResolvedValue("test-master-key-hash");
@@ -172,7 +172,7 @@ describe("OrganizationUserResetPasswordService", () => {
publicKey: "test-public-key",
}),
);
cryptoService.rsaEncrypt.mockResolvedValue(
encryptService.rsaEncrypt.mockResolvedValue(
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"),
);
});

View File

@@ -57,7 +57,7 @@ export class OrganizationUserResetPasswordService
if (userKey == null) {
throw new Error("No user key found");
}
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey);
return encryptedKey.encryptedString;
}
@@ -96,7 +96,10 @@ export class OrganizationUserResetPasswordService
);
// Decrypt User's Reset Password Key to get UserKey
const decValue = await this.cryptoService.rsaDecrypt(response.resetPasswordKey, decPrivateKey);
const decValue = await this.encryptService.rsaDecrypt(
new EncString(response.resetPasswordKey),
decPrivateKey,
);
const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey;
// determine Kdf Algorithm

View File

@@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { CollectionAdminService } from "@bitwarden/admin-console/common";
import {
canAccessVaultTab,
OrganizationService,
@@ -11,7 +12,6 @@ import { ImportComponent } from "@bitwarden/importer/ui";
import { LooseComponentsModule, SharedModule } from "../../../shared";
import { ImportCollectionAdminService } from "../../../tools/import/import-collection-admin.service";
import { CollectionAdminService } from "../../../vault/core/collection-admin.service";
@Component({
templateUrl: "org-import.component.html",

View File

@@ -1,11 +1,14 @@
import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common";
import {
CollectionAccessSelectionView,
OrganizationUserUserDetailsResponse,
} from "@bitwarden/admin-console/common";
import {
OrganizationUserStatusType,
OrganizationUserType,
} from "@bitwarden/common/admin-console/enums";
import { SelectItemView } from "@bitwarden/components";
import { CollectionAccessSelectionView, GroupView } from "../../../core";
import { GroupView } from "../../../core";
/**
* Permission options that replace/correspond with manage, readOnly, and hidePassword server fields.

View File

@@ -2,3 +2,4 @@ export * from "./login";
export * from "./webauthn-login";
export * from "./set-password-jit";
export * from "./registration";
export * from "./web-lock-component.service";

View File

@@ -35,7 +35,7 @@ describe("RotateableKeySetService", () => {
const encryptedPrivateKey = Symbol();
cryptoService.makeKeyPair.mockResolvedValue(["publicKey", encryptedPrivateKey as any]);
cryptoService.getUserKey.mockResolvedValue({ key: userKey.key } as any);
cryptoService.rsaEncrypt.mockResolvedValue(encryptedUserKey as any);
encryptService.rsaEncrypt.mockResolvedValue(encryptedUserKey as any);
encryptService.encrypt.mockResolvedValue(encryptedPublicKey as any);
const result = await service.createKeySet(externalKey as any);

View File

@@ -25,7 +25,7 @@ export class RotateableKeySetService {
const userKey = await this.cryptoService.getUserKey();
const rawPublicKey = Utils.fromB64ToArray(publicKey);
const encryptedUserKey = await this.cryptoService.rsaEncrypt(userKey.key, rawPublicKey);
const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, rawPublicKey);
const encryptedPublicKey = await this.encryptService.encrypt(rawPublicKey, userKey);
return new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey);
}

View File

@@ -0,0 +1,94 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { UserId } from "@bitwarden/common/types/guid";
import { WebLockComponentService } from "./web-lock-component.service";
describe("WebLockComponentService", () => {
let service: WebLockComponentService;
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
beforeEach(() => {
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
TestBed.configureTestingModule({
providers: [
WebLockComponentService,
{
provide: UserDecryptionOptionsServiceAbstraction,
useValue: userDecryptionOptionsService,
},
],
});
service = TestBed.inject(WebLockComponentService);
});
it("instantiates", () => {
expect(service).not.toBeFalsy();
});
describe("getBiometricsError", () => {
it("throws an error when given a null input", () => {
expect(() => service.getBiometricsError(null)).toThrow(
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
);
});
it("throws an error when given a non-null input", () => {
expect(() => service.getBiometricsError("error")).toThrow(
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
);
});
});
describe("getPreviousUrl", () => {
it("returns null", () => {
expect(service.getPreviousUrl()).toBeNull();
});
});
describe("isWindowVisible", () => {
it("throws an error", async () => {
await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented.");
});
});
describe("getBiometricsUnlockBtnText", () => {
it("throws an error", () => {
expect(() => service.getBiometricsUnlockBtnText()).toThrow(
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
);
});
});
describe("getAvailableUnlockOptions$", () => {
it("returns an observable of unlock options", async () => {
const userId = "user-id" as UserId;
const userDecryptionOptions = {
hasMasterPassword: true,
};
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValueOnce(
of(userDecryptionOptions),
);
const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId));
expect(unlockOptions).toEqual({
masterPassword: {
enabled: true,
},
pin: {
enabled: false,
},
biometrics: {
enabled: false,
disableReason: null,
},
});
});
});
});

View File

@@ -0,0 +1,55 @@
import { inject } from "@angular/core";
import { map, Observable } from "rxjs";
import { LockComponentService, UnlockOptions } from "@bitwarden/auth/angular";
import {
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { UserId } from "@bitwarden/common/types/guid";
export class WebLockComponentService implements LockComponentService {
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
constructor() {}
getBiometricsError(error: any): string | null {
throw new Error(
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
);
}
getPreviousUrl(): string | null {
return null;
}
async isWindowVisible(): Promise<boolean> {
throw new Error("Method not implemented.");
}
getBiometricsUnlockBtnText(): string {
throw new Error(
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
);
}
getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions> {
return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe(
map((userDecryptionOptions: UserDecryptionOptions) => {
const unlockOpts: UnlockOptions = {
masterPassword: {
enabled: userDecryptionOptions.hasMasterPassword,
},
pin: {
enabled: false,
},
biometrics: {
enabled: false,
disableReason: null,
},
};
return unlockOpts;
}),
);
}
}

View File

@@ -132,7 +132,7 @@ describe("EmergencyAccessService", () => {
cryptoService.getUserKey.mockResolvedValueOnce(mockUserKey);
apiService.getUserPublicKey.mockResolvedValueOnce(mockUserPublicKeyResponse);
cryptoService.rsaEncrypt.mockResolvedValueOnce(mockUserPublicKeyEncryptedUserKey);
encryptService.rsaEncrypt.mockResolvedValueOnce(mockUserPublicKeyEncryptedUserKey);
emergencyAccessApiService.postEmergencyAccessConfirm.mockResolvedValueOnce();
@@ -162,7 +162,7 @@ describe("EmergencyAccessService", () => {
const mockDecryptedGrantorUserKey = new Uint8Array(64);
cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64));
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey);
encryptService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey);
const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
@@ -200,7 +200,7 @@ describe("EmergencyAccessService", () => {
});
it("should not post a new password if decryption fails", async () => {
cryptoService.rsaDecrypt.mockResolvedValueOnce(null);
encryptService.rsaDecrypt.mockResolvedValueOnce(null);
emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce({
keyEncrypted: "EncryptedKey",
kdf: KdfType.PBKDF2_SHA256,
@@ -259,7 +259,7 @@ describe("EmergencyAccessService", () => {
publicKey: "mockPublicKey",
} as UserKeyResponse);
cryptoService.rsaEncrypt.mockImplementation((plainValue, publicKey) => {
encryptService.rsaEncrypt.mockImplementation((plainValue, publicKey) => {
return Promise.resolve(
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "Encrypted: " + plainValue),
);

View File

@@ -17,7 +17,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { KdfType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
@@ -224,8 +224,8 @@ export class EmergencyAccessService
throw new Error("Active user does not have a private key, cannot get view only ciphers.");
}
const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(
response.keyEncrypted,
const grantorKeyBuffer = await this.encryptService.rsaDecrypt(
new EncString(response.keyEncrypted),
activeUserPrivateKey,
);
const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey;
@@ -261,8 +261,8 @@ export class EmergencyAccessService
throw new Error("Active user does not have a private key, cannot complete a takeover.");
}
const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(
takeoverResponse.keyEncrypted,
const grantorKeyBuffer = await this.encryptService.rsaDecrypt(
new EncString(takeoverResponse.keyEncrypted),
activeUserPrivateKey,
);
if (grantorKeyBuffer == null) {
@@ -355,6 +355,6 @@ export class EmergencyAccessService
}
private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise<EncryptedString> {
return (await this.cryptoService.rsaEncrypt(userKey.key, publicKey)).encryptedString;
return (await this.encryptService.rsaEncrypt(userKey.key, publicKey)).encryptedString;
}
}

View File

@@ -184,7 +184,7 @@ export class AcceptOrganizationInviteService {
// RSA Encrypt user's encKey.key with organization public key
const userKey = await this.cryptoService.getUserKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey);
// Add reset password key to accept request
request.resetPasswordKey = encryptedKey.encryptedString;

View File

@@ -42,7 +42,7 @@
<dl>
<dt>{{ "status" | i18n }}</dt>
<dd>
<span class="tw-capitalize">{{ (subscription && subscription.status) || "-" }}</span>
<span class="tw-capitalize">{{ (subscription && subscriptionStatus) || "-" }}</span>
<span bitBadge variant="warning" *ngIf="subscriptionMarkedForCancel">{{
"pendingCancellation" | i18n
}}</span>

View File

@@ -35,8 +35,6 @@ import { UpdateLicenseDialogResult } from "../shared/update-license-types";
export class UserSubscriptionComponent implements OnInit {
loading = false;
firstLoaded = false;
adjustStorageAdd = true;
showUpdateLicense = false;
sub: SubscriptionResponse;
selfHosted = false;
cloudWebVaultUrl: string;
@@ -65,7 +63,7 @@ export class UserSubscriptionComponent implements OnInit {
private toastService: ToastService,
private configService: ConfigService,
) {
this.selfHosted = platformUtilsService.isSelfHost();
this.selfHosted = this.platformUtilsService.isSelfHost();
}
async ngOnInit() {
@@ -216,11 +214,28 @@ export class UserSubscriptionComponent implements OnInit {
: 0;
}
get storageProgressWidth() {
return this.storagePercentage < 5 ? 5 : 0;
}
get title(): string {
return this.i18nService.t(this.selfHosted ? "subscription" : "premiumMembership");
}
get subscriptionStatus(): string | null {
if (!this.subscription) {
return null;
} else {
/*
Premium users who sign up with PayPal will have their subscription activated by a webhook.
This is an arbitrary 15-second grace period where we show their subscription as active rather than
incomplete while we wait for our webhook to process the `invoice.created` event.
*/
if (this.subscription.status === "incomplete") {
const periodStartMS = new Date(this.subscription.periodStartDate).getTime();
const nowMS = new Date().getTime();
return nowMS - periodStartMS <= 15000
? this.i18nService.t("active")
: this.subscription.status;
}
return this.subscription.status;
}
}
}

View File

@@ -1,7 +1,7 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog>
<h1 bitDialogTitle>
{{ (hasBillingToken ? "viewBillingSyncToken" : "generateBillingSyncToken") | i18n }}
{{ (hasBillingToken ? "viewBillingToken" : "generateBillingToken") | i18n }}
</h1>
<div bitDialogContent>
<app-user-verification formControlName="verification" *ngIf="!clientSecret">

View File

@@ -1,7 +1,7 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog>
<h1 bitDialogTitle>
{{ "manageBillingSync" | i18n }}
{{ "manageBillingTokenSync" | i18n }}
</h1>
<div bitDialogContent>
<p>{{ "billingSyncKeyDesc" | i18n }}</p>

View File

@@ -280,7 +280,7 @@
(click)="manageBillingSync()"
*ngIf="canManageBillingSync"
>
{{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }}
{{ (hasBillingSyncToken ? "viewBillingToken" : "setUpBillingSync") | i18n }}
</button>
</div>
<ng-container *ngIf="userOrg.canEditSubscription">

View File

@@ -90,7 +90,7 @@
</a>
</bit-label>
<bit-hint>
{{ "billingSyncDesc" | i18n }}
{{ "automaticBillingSyncDesc" | i18n }}
</bit-hint>
</bit-radio-button>
<ng-container *ngIf="updateMethod === licenseOptions.SYNC">
@@ -100,7 +100,7 @@
type="button"
(click)="manageBillingSyncSelfHosted()"
>
{{ "manageBillingSync" | i18n }}
{{ "manageBillingTokenSync" | i18n }}
</button>
<button
bitButton
@@ -121,7 +121,7 @@
>
<bit-label>{{ "manualUpload" | i18n }}</bit-label>
<bit-hint>
{{ "manualUploadDesc" | i18n }}
{{ "manualBillingTokenUploadDesc" | i18n }}
</bit-hint>
</bit-radio-button>
<ng-container *ngIf="updateMethod === licenseOptions.UPLOAD">

View File

@@ -1,7 +1,11 @@
import { CommonModule } from "@angular/common";
import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import {
CollectionAdminService,
DefaultCollectionAdminService,
OrganizationUserApiService,
} from "@bitwarden/admin-console/common";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import {
CLIENT_TYPE,
@@ -21,6 +25,7 @@ import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/serv
import {
RegistrationFinishService as RegistrationFinishServiceAbstraction,
LoginComponentService,
LockComponentService,
SetPasswordJitService,
} from "@bitwarden/auth/angular";
import {
@@ -42,6 +47,7 @@ import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.ser
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -66,6 +72,7 @@ import {
} from "@bitwarden/common/platform/theming/theme-state.service";
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { BiometricsService } from "@bitwarden/key-management";
import { PolicyListService } from "../admin-console/core/policy-list.service";
@@ -73,6 +80,7 @@ import {
WebSetPasswordJitService,
WebRegistrationFinishService,
WebLoginComponentService,
WebLockComponentService,
} from "../auth";
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
import { HtmlStorageService } from "../core/html-storage.service";
@@ -81,7 +89,6 @@ import { WebBiometricsService } from "../key-management/web-biometric.service";
import { WebEnvironmentService } from "../platform/web-environment.service";
import { WebMigrationRunner } from "../platform/web-migration-runner";
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
import { CollectionAdminService } from "../vault/core/collection-admin.service";
import { EventService } from "./event.service";
import { InitService } from "./init.service";
@@ -155,7 +162,6 @@ const safeProviders: SafeProvider[] = [
useClass: WebFileDownloadService,
useAngularDecorators: true,
}),
safeProvider(CollectionAdminService),
safeProvider({
provide: WindowStorageService,
useFactory: () => new WindowStorageService(window.localStorage),
@@ -208,12 +214,18 @@ const safeProviders: SafeProvider[] = [
PolicyService,
],
}),
safeProvider({
provide: LockComponentService,
useClass: WebLockComponentService,
deps: [],
}),
safeProvider({
provide: SetPasswordJitService,
useClass: WebSetPasswordJitService,
deps: [
ApiService,
CryptoServiceAbstraction,
EncryptService,
I18nServiceAbstraction,
KdfConfigService,
InternalMasterPasswordServiceAbstraction,
@@ -243,6 +255,11 @@ const safeProviders: SafeProvider[] = [
useClass: LoginEmailService,
deps: [AccountService, AuthService, StateProvider],
}),
safeProvider({
provide: CollectionAdminService,
useClass: DefaultCollectionAdminService,
deps: [ApiService, CryptoServiceAbstraction, EncryptService, CollectionService],
}),
];
@NgModule({

View File

@@ -10,6 +10,7 @@ import {
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap";
import {
AnonLayoutWrapperComponent,
AnonLayoutWrapperData,
@@ -22,6 +23,7 @@ import {
RegistrationLinkExpiredComponent,
LoginComponent,
LoginSecondaryContentComponent,
LockV2Component,
LockIcon,
UserLockIcon,
} from "@bitwarden/auth/angular";
@@ -367,20 +369,58 @@ const routes: Routes = [
],
},
{
path: "lock",
canActivate: [deepLinkGuard(), lockGuard()],
path: "login",
canActivate: [unauthGuardFn()],
children: [
{
path: "",
component: LockComponent,
component: LoginComponent,
},
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
data: {
pageTitle: "yourVaultIsLockedV2",
pageIcon: LockIcon,
showReadonlyHostname: true,
} satisfies AnonLayoutWrapperData,
pageTitle: "logIn",
},
},
...extensionRefreshSwap(
LockComponent,
LockV2Component,
{
path: "lock",
canActivate: [deepLinkGuard(), lockGuard()],
children: [
{
path: "",
component: LockComponent,
},
],
data: {
pageTitle: "yourVaultIsLockedV2",
pageIcon: LockIcon,
showReadonlyHostname: true,
} satisfies AnonLayoutWrapperData,
},
{
path: "lock",
canActivate: [deepLinkGuard(), lockGuard()],
children: [
{
path: "",
component: LockV2Component,
},
],
data: {
pageTitle: "yourAccountIsLocked",
pageIcon: LockIcon,
showReadonlyHostname: true,
} satisfies AnonLayoutWrapperData,
},
),
{
path: "2fa",
canActivate: [unauthGuardFn()],

View File

@@ -1,8 +1,8 @@
import { Injectable } from "@angular/core";
import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common";
import { ImportCollectionServiceAbstraction } from "../../../../../../libs/importer/src/services/import-collection.service.abstraction";
import { CollectionAdminService } from "../../vault/core/collection-admin.service";
import { CollectionAdminView } from "../../vault/core/views/collection-admin.view";
@Injectable()
export class ImportCollectionAdminService implements ImportCollectionServiceAbstraction {

View File

@@ -14,8 +14,11 @@ import {
import { first } from "rxjs/operators";
import {
CollectionAccessSelectionView,
CollectionAdminService,
CollectionAdminView,
OrganizationUserApiService,
OrganizationUserUserDetailsResponse,
OrganizationUserUserMiniResponse,
} from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@@ -26,11 +29,7 @@ import { CollectionResponse } from "@bitwarden/common/vault/models/response/coll
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { BitValidators, DialogService } from "@bitwarden/components";
import {
CollectionAccessSelectionView,
GroupService,
GroupView,
} from "../../../admin-console/organizations/core";
import { GroupService, GroupView } from "../../../admin-console/organizations/core";
import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component";
import {
AccessItemType,
@@ -40,8 +39,6 @@ import {
convertToPermission,
convertToSelectionView,
} from "../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
import { CollectionAdminService } from "../../core/collection-admin.service";
import { CollectionAdminView } from "../../core/views/collection-admin.view";
export enum CollectionDialogTabType {
Info = 0,
@@ -156,15 +153,23 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
organization: organization$,
collections: this.collectionAdminService.getAll(orgId),
groups: groups$,
// Collection(s) needed to map readonlypermission for (potential) access selector disabled state
users: this.organizationUserApiService.getAllUsers(orgId, { includeCollections: true }),
users: this.organizationUserApiService.getAllMiniUserDetails(orgId),
})
.pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$))
.subscribe(({ organization, collections: allCollections, groups, users }) => {
this.organization = organization;
if (this.params.collectionId) {
this.collection = allCollections.find((c) => c.id === this.collectionId);
if (!this.collection) {
throw new Error("Could not find collection to edit.");
}
}
this.accessItems = [].concat(
groups.map((group) => mapGroupToAccessItemView(group, this.collectionId)),
users.data.map((user) => mapUserToAccessItemView(user, this.collectionId)),
groups.map((group) => mapGroupToAccessItemView(group, this.collection)),
users.data.map((user) => mapUserToAccessItemView(user, this.collection)),
);
// Force change detection to update the access selector's items
@@ -174,15 +179,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
? allCollections.filter((c) => c.manage)
: allCollections;
if (this.params.collectionId) {
this.collection = allCollections.find((c) => c.id === this.collectionId);
if (this.collection) {
// Ensure we don't allow nesting the current collection within itself
this.nestOptions = this.nestOptions.filter((c) => c.id !== this.collectionId);
if (!this.collection) {
throw new Error("Could not find collection to edit.");
}
// Parse the name to find its parent name
const { name, parent: parentName } = parseName(this.collection);
@@ -423,7 +423,10 @@ function validateCanManagePermission(control: AbstractControl) {
* @param collectionId Current collection being viewed/edited
* @returns AccessItemView customized to set a readonlyPermission to be displayed if the access selector is in a disabled state
*/
function mapGroupToAccessItemView(group: GroupView, collectionId: string): AccessItemView {
function mapGroupToAccessItemView(
group: GroupView,
collection: CollectionAdminView,
): AccessItemView {
return {
id: group.id,
type: AccessItemType.Group,
@@ -431,8 +434,8 @@ function mapGroupToAccessItemView(group: GroupView, collectionId: string): Acces
labelName: group.name,
readonly: false,
readonlyPermission:
collectionId != null
? convertToPermission(group.collections.find((gc) => gc.id == collectionId))
collection != null
? convertToPermission(collection.groups.find((g) => g.id === group.id))
: undefined,
};
}
@@ -444,8 +447,8 @@ function mapGroupToAccessItemView(group: GroupView, collectionId: string): Acces
* @returns AccessItemView customized to set a readonlyPermission to be displayed if the access selector is in a disabled state
*/
function mapUserToAccessItemView(
user: OrganizationUserUserDetailsResponse,
collectionId: string,
user: OrganizationUserUserMiniResponse,
collection: CollectionAdminView,
): AccessItemView {
return {
id: user.id,
@@ -457,9 +460,9 @@ function mapUserToAccessItemView(
status: user.status,
readonly: false,
readonlyPermission:
collectionId != null
collection != null
? convertToPermission(
new CollectionAccessSelectionView(user.collections.find((uc) => uc.id == collectionId)),
new CollectionAccessSelectionView(collection.users.find((u) => u.id === user.id)),
)
: undefined,
};

View File

@@ -1,12 +1,11 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { CollectionAdminView, Unassigned } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { GroupView } from "../../../admin-console/organizations/core";
import { CollectionAdminView } from "../../core/views/collection-admin.view";
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
import {
convertToPermission,

View File

@@ -1,13 +1,13 @@
import { SelectionModel } from "@angular/cdk/collections";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Unassigned } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { TableDataSource } from "@bitwarden/components";
import { GroupView } from "../../../admin-console/organizations/core";
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
import { VaultItem } from "./vault-item";
import { VaultItemEvent } from "./vault-item-event";

View File

@@ -3,6 +3,11 @@ import { RouterModule } from "@angular/router";
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { BehaviorSubject, of } from "rxjs";
import {
CollectionAccessSelectionView,
CollectionAdminView,
Unassigned,
} from "@bitwarden/admin-console/common";
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@@ -19,13 +24,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import {
CollectionAccessSelectionView,
GroupView,
} from "../../../admin-console/organizations/core";
import { GroupView } from "../../../admin-console/organizations/core";
import { PreloadedEnglishI18nModule } from "../../../core/tests";
import { CollectionAdminView } from "../../core/views/collection-admin.view";
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
import { VaultItemsComponent } from "./vault-items.component";
import { VaultItemsModule } from "./vault-items.module";

View File

@@ -1,7 +0,0 @@
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
export class BulkCollectionAccessRequest {
collectionIds: string[];
users: SelectionReadOnlyRequest[];
groups: SelectionReadOnlyRequest[];
}

View File

@@ -1,170 +0,0 @@
import { Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
import { CollectionRequest } from "@bitwarden/common/vault/models/request/collection.request";
import {
CollectionAccessDetailsResponse,
CollectionDetailsResponse,
CollectionResponse,
} from "@bitwarden/common/vault/models/response/collection.response";
import { CollectionAccessSelectionView } from "../../admin-console/organizations/core";
import { BulkCollectionAccessRequest } from "./bulk-collection-access.request";
import { CollectionAdminView } from "./views/collection-admin.view";
@Injectable()
export class CollectionAdminService {
constructor(
private apiService: ApiService,
private cryptoService: CryptoService,
private encryptService: EncryptService,
private collectionService: CollectionService,
) {}
async getAll(organizationId: string): Promise<CollectionAdminView[]> {
const collectionResponse =
await this.apiService.getManyCollectionsWithAccessDetails(organizationId);
if (collectionResponse?.data == null || collectionResponse.data.length === 0) {
return [];
}
return await this.decryptMany(organizationId, collectionResponse.data);
}
async get(
organizationId: string,
collectionId: string,
): Promise<CollectionAdminView | undefined> {
const collectionResponse = await this.apiService.getCollectionAccessDetails(
organizationId,
collectionId,
);
if (collectionResponse == null) {
return undefined;
}
const [view] = await this.decryptMany(organizationId, [collectionResponse]);
return view;
}
async save(collection: CollectionAdminView): Promise<CollectionDetailsResponse> {
const request = await this.encrypt(collection);
let response: CollectionDetailsResponse;
if (collection.id == null) {
response = await this.apiService.postCollection(collection.organizationId, request);
collection.id = response.id;
} else {
response = await this.apiService.putCollection(
collection.organizationId,
collection.id,
request,
);
}
if (response.assigned) {
await this.collectionService.upsert(new CollectionData(response));
} else {
await this.collectionService.delete(collection.id);
}
return response;
}
async delete(organizationId: string, collectionId: string): Promise<void> {
await this.apiService.deleteCollection(organizationId, collectionId);
}
async bulkAssignAccess(
organizationId: string,
collectionIds: string[],
users: CollectionAccessSelectionView[],
groups: CollectionAccessSelectionView[],
): Promise<void> {
const request = new BulkCollectionAccessRequest();
request.collectionIds = collectionIds;
request.users = users.map(
(u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage),
);
request.groups = groups.map(
(g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords, g.manage),
);
await this.apiService.send(
"POST",
`/organizations/${organizationId}/collections/bulk-access`,
request,
true,
false,
);
}
private async decryptMany(
organizationId: string,
collections: CollectionResponse[] | CollectionAccessDetailsResponse[],
): Promise<CollectionAdminView[]> {
const orgKey = await this.cryptoService.getOrgKey(organizationId);
const promises = collections.map(async (c) => {
const view = new CollectionAdminView();
view.id = c.id;
view.name = await this.encryptService.decryptToUtf8(new EncString(c.name), orgKey);
view.externalId = c.externalId;
view.organizationId = c.organizationId;
if (isCollectionAccessDetailsResponse(c)) {
view.groups = c.groups;
view.users = c.users;
view.assigned = c.assigned;
view.readOnly = c.readOnly;
view.hidePasswords = c.hidePasswords;
view.manage = c.manage;
view.unmanaged = c.unmanaged;
}
return view;
});
return await Promise.all(promises);
}
private async encrypt(model: CollectionAdminView): Promise<CollectionRequest> {
if (model.organizationId == null) {
throw new Error("Collection has no organization id.");
}
const key = await this.cryptoService.getOrgKey(model.organizationId);
if (key == null) {
throw new Error("No key for this collection's organization.");
}
const collection = new CollectionRequest();
collection.externalId = model.externalId;
collection.name = (await this.encryptService.encrypt(model.name, key)).encryptedString;
collection.groups = model.groups.map(
(group) =>
new SelectionReadOnlyRequest(group.id, group.readOnly, group.hidePasswords, group.manage),
);
collection.users = model.users.map(
(user) =>
new SelectionReadOnlyRequest(user.id, user.readOnly, user.hidePasswords, user.manage),
);
return collection;
}
}
function isCollectionAccessDetailsResponse(
response: CollectionResponse | CollectionAccessDetailsResponse,
): response is CollectionAccessDetailsResponse {
const anyResponse = response as any;
return anyResponse?.groups instanceof Array && anyResponse?.users instanceof Array;
}

View File

@@ -1,96 +0,0 @@
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { CollectionAccessSelectionView } from "../../../admin-console/organizations/core/views/collection-access-selection.view";
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
export class CollectionAdminView extends CollectionView {
groups: CollectionAccessSelectionView[] = [];
users: CollectionAccessSelectionView[] = [];
/**
* Flag indicating the collection has no active user or group assigned to it with CanManage permissions
* In this case, the collection can be managed by admins/owners or custom users with appropriate permissions
*/
unmanaged: boolean;
/**
* Flag indicating the user has been explicitly assigned to this Collection
*/
assigned: boolean;
constructor(response?: CollectionAccessDetailsResponse) {
super(response);
if (!response) {
return;
}
this.groups = response.groups
? response.groups.map((g) => new CollectionAccessSelectionView(g))
: [];
this.users = response.users
? response.users.map((g) => new CollectionAccessSelectionView(g))
: [];
this.assigned = response.assigned;
}
/**
* Returns true if the user can edit a collection (including user and group access) from the Admin Console.
*/
override canEdit(org: Organization): boolean {
return (
org?.canEditAnyCollection ||
(this.unmanaged && org?.canEditUnmanagedCollections) ||
super.canEdit(org)
);
}
/**
* Returns true if the user can delete a collection from the Admin Console.
*/
override canDelete(org: Organization): boolean {
return org?.canDeleteAnyCollection || super.canDelete(org);
}
/**
* Whether the user can modify user access to this collection
*/
canEditUserAccess(org: Organization): boolean {
return (
(org.permissions.manageUsers && org.allowAdminAccessToAllCollectionItems) || this.canEdit(org)
);
}
/**
* Whether the user can modify group access to this collection
*/
canEditGroupAccess(org: Organization): boolean {
return (
(org.permissions.manageGroups && org.allowAdminAccessToAllCollectionItems) ||
this.canEdit(org)
);
}
/**
* Returns true if the user can view collection info and access in a read-only state from the Admin Console
*/
override canViewCollectionInfo(org: Organization | undefined): boolean {
if (this.isUnassignedCollection) {
return false;
}
return this.manage || org?.isAdmin || org?.permissions.editAnyCollection;
}
/**
* True if this collection represents the pseudo "Unassigned" collection
* This is different from the "unmanaged" flag, which indicates that no users or groups have access to the collection
*/
get isUnassignedCollection() {
return this.id === Unassigned;
}
}

View File

@@ -1,13 +1,12 @@
import { Component, Input, OnChanges } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { Unassigned } from "@bitwarden/admin-console/common";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { Unassigned } from "../vault-filter/shared/models/routed-vault-filter.model";
@Component({
selector: "app-org-badge",
templateUrl: "organization-name-badge.component.html",

View File

@@ -1,11 +1,11 @@
import { Observable } from "rxjs";
import { CollectionAdminView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CollectionView } from "@bitwarden/common/src/vault/models/view/collection.view";
import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CollectionAdminView } from "../../../../core/views/collection-admin.view";
import {
CipherTypeFilter,
CollectionFilter,

View File

@@ -2,15 +2,12 @@ import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { combineLatest, map, Observable } from "rxjs";
import { Unassigned } from "@bitwarden/admin-console/common";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { RoutedVaultFilterBridge } from "../shared/models/routed-vault-filter-bridge.model";
import {
RoutedVaultFilterModel,
Unassigned,
All,
} from "../shared/models/routed-vault-filter.model";
import { RoutedVaultFilterModel, All } from "../shared/models/routed-vault-filter.model";
import { VaultFilter } from "../shared/models/vault-filter.model";
import {
CipherTypeFilter,

View File

@@ -10,6 +10,7 @@ import {
switchMap,
} from "rxjs";
import { CollectionAdminView } from "@bitwarden/admin-console/common";
import { OrganizationService } 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";
@@ -26,7 +27,6 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state";
import { CollectionAdminView } from "../../../core/views/collection-admin.view";
import {
CipherTypeFilter,
CollectionFilter,

View File

@@ -1,8 +1,9 @@
import { Unassigned } from "@bitwarden/admin-console/common";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { createFilterFunction } from "./filter-function";
import { Unassigned, All } from "./routed-vault-filter.model";
import { All } from "./routed-vault-filter.model";
describe("createFilter", () => {
describe("given a generic cipher", () => {

View File

@@ -1,7 +1,8 @@
import { Unassigned } from "@bitwarden/admin-console/common";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { All, RoutedVaultFilterModel, Unassigned } from "./routed-vault-filter.model";
import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
export type FilterFunction = (cipher: CipherView) => boolean;

View File

@@ -1,3 +1,4 @@
import { Unassigned } from "@bitwarden/admin-console/common";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
@@ -8,7 +9,6 @@ import {
isRoutedVaultFilterItemType,
RoutedVaultFilterItemType,
RoutedVaultFilterModel,
Unassigned,
} from "./routed-vault-filter.model";
import { VaultFilter, VaultFilterFunction } from "./vault-filter.model";
import {

View File

@@ -1,5 +1,3 @@
export const Unassigned = "unassigned";
export const All = "all";
// TODO: Remove `All` when moving to vertical navigation.

View File

@@ -1,10 +1,9 @@
import { CollectionAdminView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
import { CollectionAdminView } from "../../../../core/views/collection-admin.view";
export type CipherStatus = "all" | "favorites" | "trash" | CipherType;
export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string };

View File

@@ -9,6 +9,7 @@ import {
} from "@angular/core";
import { firstValueFrom } from "rxjs";
import { Unassigned } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -26,7 +27,6 @@ import { PipesModule } from "../pipes/pipes.module";
import {
All,
RoutedVaultFilterModel,
Unassigned,
} from "../vault-filter/shared/models/routed-vault-filter.model";
@Component({

View File

@@ -28,6 +28,7 @@ import {
tap,
} from "rxjs/operators";
import { Unassigned } from "@bitwarden/admin-console/common";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -118,7 +119,6 @@ import { createFilterFunction } from "./vault-filter/shared/models/filter-functi
import {
All,
RoutedVaultFilterModel,
Unassigned,
} from "./vault-filter/shared/models/routed-vault-filter.model";
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type";

View File

@@ -3,7 +3,10 @@ import { Component, Inject, OnDestroy } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import {
CollectionAdminService,
OrganizationUserApiService,
} from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -23,7 +26,6 @@ import {
PermissionMode,
} from "../../../admin-console/organizations/shared/components/access-selector";
import { SharedModule } from "../../../shared";
import { CollectionAdminService } from "../../core/collection-admin.service";
export interface BulkCollectionsDialogParams {
organizationId: string;
@@ -79,7 +81,7 @@ export class BulkCollectionsDialogComponent implements OnDestroy {
combineLatest([
organization$,
groups$,
this.organizationUserApiService.getAllUsers(this.params.organizationId),
this.organizationUserApiService.getAllMiniUserDetails(this.params.organizationId),
])
.pipe(takeUntil(this.destroy$))
.subscribe(([organization, groups, users]) => {

View File

@@ -1,6 +1,7 @@
import { Injectable, OnDestroy } from "@angular/core";
import { map, Observable, ReplaySubject, Subject } from "rxjs";
import { CollectionAdminView } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -10,7 +11,6 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view";
import { VaultFilterService as BaseVaultFilterService } from "../../individual-vault/vault-filter/services/vault-filter.service";
import { CollectionFilter } from "../../individual-vault/vault-filter/shared/models/vault-filter.type";

View File

@@ -3,6 +3,11 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import {
CollectionAdminService,
CollectionAdminView,
Unassigned,
} from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@@ -22,13 +27,10 @@ import {
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared";
import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view";
import { CollectionDialogTabType } from "../../components/collection-dialog";
import { CollectionAdminService } from "../../core/collection-admin.service";
import {
All,
RoutedVaultFilterModel,
Unassigned,
} from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
@Component({

View File

@@ -31,8 +31,9 @@ import {
} from "rxjs/operators";
import {
OrganizationUserApiService,
OrganizationUserUserDetailsResponse,
CollectionAdminService,
CollectionAdminView,
Unassigned,
} from "@bitwarden/admin-console/common";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@@ -76,8 +77,6 @@ import {
} from "../components/collection-dialog";
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
import { CollectionAdminService } from "../core/collection-admin.service";
import { CollectionAdminView } from "../core/views/collection-admin.view";
import {
BulkDeleteDialogResult,
openBulkDeleteDialog,
@@ -89,7 +88,6 @@ import { createFilterFunction } from "../individual-vault/vault-filter/shared/mo
import {
All,
RoutedVaultFilterModel,
Unassigned,
} from "../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
import {
openViewCipherDialog,
@@ -168,8 +166,6 @@ export class VaultComponent implements OnInit, OnDestroy {
protected editableCollections$: Observable<CollectionAdminView[]>;
protected allCollectionsWithoutUnassigned$: Observable<CollectionAdminView[]>;
protected orgRevokedUsers: OrganizationUserUserDetailsResponse[];
protected get hideVaultFilters(): boolean {
return this.organization?.isProviderUser && !this.organization?.isMember;
}
@@ -206,7 +202,6 @@ export class VaultComponent implements OnInit, OnDestroy {
private totpService: TotpService,
private apiService: ApiService,
private collectionService: CollectionService,
private organizationUserApiService: OrganizationUserApiService,
private toastService: ToastService,
private accountService: AccountService,
) {}
@@ -358,13 +353,6 @@ export class VaultComponent implements OnInit, OnDestroy {
shareReplay({ refCount: true, bufferSize: 1 }),
);
// This will be passed into the usersCanManage call
this.orgRevokedUsers = (
await this.organizationUserApiService.getAllUsers(await firstValueFrom(organizationId$))
).data.filter((user: OrganizationUserUserDetailsResponse) => {
return user.status === -1;
});
const collections$ = combineLatest([
nestedCollections$,
filter$,

View File

@@ -1,3 +1,4 @@
import { CollectionAdminView } from "@bitwarden/admin-console/common";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import {
CollectionView,
@@ -5,8 +6,6 @@ import {
} from "@bitwarden/common/vault/models/view/collection.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { CollectionAdminView } from "../../vault/core/views/collection-admin.view";
export function getNestedCollectionTree(
collections: CollectionAdminView[],
): TreeNode<CollectionAdminView>[];

View File

@@ -1111,8 +1111,11 @@
"yourVaultIsLockedV2": {
"message": "Your vault is locked"
},
"uuid": {
"message": "UUID"
"yourAccountIsLocked": {
"message": "Your account is locked"
},
"uuid":{
"message" : "UUID"
},
"unlock": {
"message": "Unlock"
@@ -2576,6 +2579,9 @@
"downloadLicense": {
"message": "Download license"
},
"viewBillingToken": {
"message": "View Billing Token"
},
"updateLicense": {
"message": "Update license"
},
@@ -3181,6 +3187,10 @@
"incorrectPin": {
"message": "Incorrect PIN"
},
"pin": {
"message": "PIN",
"description": "PIN code. Ex. The short code (often numeric) that you use to unlock a device."
},
"exportedVault": {
"message": "Vault exported"
},
@@ -6006,8 +6016,8 @@
"viewBillingSyncToken": {
"message": "View billing sync token"
},
"generateBillingSyncToken": {
"message": "Generate billing sync token"
"generateBillingToken": {
"message": "Generate billing token"
},
"copyPasteBillingSync": {
"message": "Copy and paste this token into the billing sync settings of your self-hosted organization."
@@ -6015,8 +6025,8 @@
"billingSyncCanAccess": {
"message": "Your billing sync token can access and edit this organization's subscription settings."
},
"manageBillingSync": {
"message": "Manage billing sync"
"manageBillingTokenSync": {
"message": "Manage Billing Token"
},
"setUpBillingSync": {
"message": "Set up billing sync"
@@ -6075,15 +6085,15 @@
"billingSyncApiKeyRotated": {
"message": "Token rotated"
},
"billingSyncDesc": {
"message": "Billing sync unlocks Families sponsorships and automatic license syncing on your server. After making updates in the Bitwarden cloud server, select Sync License to apply changes."
},
"billingSyncKeyDesc": {
"message": "A billing sync token from your cloud organization's subscription settings is required to complete this form."
},
"billingSyncKey": {
"message": "Billing sync token"
},
"automaticBillingSyncDesc": {
"message": "Automatic sync unlocks Families sponsorships and allows you to sync your license without uploading a file. After making updates in the Bitwarden cloud server, select Sync License to apply changes."
},
"active": {
"message": "Active"
},
@@ -6603,6 +6613,18 @@
}
}
},
"singleFieldNeedsAttention": {
"message": "1 field needs your attention."
},
"multipleFieldsNeedAttention": {
"message": "$COUNT$ fields need your attention.",
"placeholders": {
"count": {
"content": "$1",
"example": "2"
}
}
},
"duoHealthCheckResultsInNullAuthUrlError": {
"message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance."
},
@@ -7463,6 +7485,15 @@
"or": {
"message": "or"
},
"unlockWithBiometrics": {
"message": "Unlock with biometrics"
},
"unlockWithPin": {
"message": "Unlock with PIN"
},
"unlockWithMasterPassword": {
"message": "Unlock with master password"
},
"licenseAndBillingManagement": {
"message": "License and billing management"
},
@@ -7472,11 +7503,11 @@
"manualUpload": {
"message": "Manual upload"
},
"manualUploadDesc": {
"message": "If you do not want to opt into billing sync, manually upload your license here."
"manualBillingTokenUploadDesc": {
"message": "If you do not want to opt into billing sync, manually upload your license here. This will not automatically unlock Families sponsorships."
},
"syncLicense": {
"message": "Sync license"
"message": "Sync License"
},
"licenseSyncSuccess": {
"message": "Successfully synced license"