1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 22:44:11 +00:00

Merge branch 'main' into ps/extension-refresh

This commit is contained in:
Vicki League
2024-10-22 14:01:06 -04:00
84 changed files with 1194 additions and 1207 deletions

View File

@@ -81,7 +81,6 @@
"cross-env",
"del",
"gulp",
"gulp-filter",
"gulp-if",
"gulp-json-editor",
"gulp-replace",

View File

@@ -14,23 +14,9 @@ const betaBuild = process.env.BETA_BUILD === "1";
const paths = {
build: "./build/",
dist: "./dist/",
node_modules: "./node_modules/",
popupDir: "./src/popup/",
cssDir: "./src/popup/css/",
safari: "./src/safari/",
};
const filters = {
fonts: [
"!build/popup/fonts/*",
"build/popup/fonts/dm-sans.woff2",
"build/popup/fonts/bwi-font.woff2",
"build/popup/fonts/bwi-font.woff",
"build/popup/fonts/bwi-font.ttf",
],
safari: ["!build/safari/**/*"],
};
/**
* Converts a number to a tuple containing two Uint16's
* @param num {number} This number is expected to be a integer style number with no decimals
@@ -64,11 +50,9 @@ function distFileName(browserName, ext) {
async function dist(browserName, manifest) {
const { default: zip } = await import("gulp-zip");
const { default: filter } = await import("gulp-filter");
return gulp
.src(paths.build + "**/*")
.pipe(filter(["**"].concat(filters.fonts).concat(filters.safari)))
.pipe(gulpif("popup/index.html", replace("__BROWSER__", "browser_" + browserName)))
.pipe(gulpif("manifest.json", jeditor(manifest)))
.pipe(zip(distFileName(browserName, "zip")))
@@ -192,8 +176,6 @@ function distSafariApp(cb, subBuildPath) {
return new Promise((resolve) => proc.on("close", resolve));
})
.then(async () => {
const { default: filter } = await import("gulp-filter");
const libs = fs
.readdirSync(builtAppexFrameworkPath)
.filter((p) => p.endsWith(".dylib"))
@@ -237,13 +219,10 @@ function safariCopyAssets(source, dest) {
}
async function safariCopyBuild(source, dest) {
const { default: filter } = await import("gulp-filter");
return new Promise((resolve, reject) => {
gulp
.src(source)
.on("error", reject)
.pipe(filter(["**"].concat(filters.fonts)))
.pipe(gulpif("popup/index.html", replace("__BROWSER__", "browser_safari")))
.pipe(
gulpif(

View File

@@ -131,31 +131,35 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
return;
}
if (data.pageTitle) {
this.pageTitle = this.handleStringOrTranslation(data.pageTitle);
// Null emissions are used to reset the page data as all fields are optional.
if (data.pageTitle !== undefined) {
this.pageTitle =
data.pageTitle !== null ? this.handleStringOrTranslation(data.pageTitle) : null;
}
if (data.pageSubtitle) {
this.pageSubtitle = this.handleStringOrTranslation(data.pageSubtitle);
if (data.pageSubtitle !== undefined) {
this.pageSubtitle =
data.pageSubtitle !== null ? this.handleStringOrTranslation(data.pageSubtitle) : null;
}
if (data.pageIcon) {
this.pageIcon = data.pageIcon;
if (data.pageIcon !== undefined) {
this.pageIcon = data.pageIcon !== null ? data.pageIcon : null;
}
if (data.showReadonlyHostname != null) {
if (data.showReadonlyHostname !== undefined) {
this.showReadonlyHostname = data.showReadonlyHostname;
}
if (data.showAcctSwitcher != null) {
if (data.showAcctSwitcher !== undefined) {
this.showAcctSwitcher = data.showAcctSwitcher;
}
if (data.showBackButton != null) {
if (data.showBackButton !== undefined) {
this.showBackButton = data.showBackButton;
}
if (data.showLogo != null) {
if (data.showLogo !== undefined) {
this.showLogo = data.showLogo;
}
}

View File

@@ -177,6 +177,10 @@ import { InternalFolderService as InternalFolderServiceAbstraction } from "@bitw
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
CipherAuthorizationService,
DefaultCipherAuthorizationService,
} from "@bitwarden/common/vault/services/cipher-authorization.service";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
@@ -369,6 +373,7 @@ export default class MainBackground {
themeStateService: DefaultThemeStateService;
autoSubmitLoginBackground: AutoSubmitLoginBackground;
sdkService: SdkService;
cipherAuthorizationService: CipherAuthorizationService;
onUpdatedRan: boolean;
onReplacedRan: boolean;
@@ -1265,6 +1270,11 @@ export default class MainBackground {
}
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
this.cipherAuthorizationService = new DefaultCipherAuthorizationService(
this.collectionService,
this.organizationService,
);
}
async bootstrap() {

View File

@@ -114,8 +114,8 @@ export class SendAddEditComponent {
/**
* Handles the event when the send is updated.
*/
onSendUpdated(send: SendView) {
this.location.back();
async onSendUpdated(_: SendView) {
await this.router.navigate(["/tabs/send"]);
}
deleteSend = async () => {

View File

@@ -4,7 +4,7 @@
slot="header"
[pageTitle]="'createdSend' | i18n"
showBackButton
[backAction]="close.bind(this)"
[backAction]="goToEditSend.bind(this)"
>
<ng-container slot="end">
<app-pop-out></app-pop-out>
@@ -15,7 +15,9 @@
class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-center tw-items-center tw-gap-2 tw-h-full tw-px-5"
>
<bit-icon [icon]="sendCreatedIcon"></bit-icon>
<h3 class="tw-font-semibold">{{ "createdSendSuccessfully" | i18n }}</h3>
<h3 tabindex="0" appAutofocus class="tw-font-semibold">
{{ "createdSendSuccessfully" | i18n }}
</h3>
<p class="tw-text-center">
{{ formatExpirationDate() }}
</p>
@@ -27,7 +29,7 @@
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
<b>{{ "copyLink" | i18n }}</b>
</button>
<button bitButton type="button" buttonType="secondary" (click)="close()">
<button bitButton type="button" buttonType="secondary" (click)="goBack()">
{{ "close" | i18n }}
</button>
</popup-footer>

View File

@@ -11,6 +11,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { ButtonModule, I18nMockService, IconModule, ToastService } from "@bitwarden/components";
@@ -50,6 +51,7 @@ describe("SendCreatedComponent", () => {
sendView = {
id: sendId,
deletionDate: new Date(),
type: SendType.Text,
accessId: "abc",
urlB64Key: "123",
} as SendView;
@@ -129,9 +131,11 @@ describe("SendCreatedComponent", () => {
expect(component["hoursAvailable"]).toBe(0);
});
it("should navigate back to send list on close", async () => {
await component.close();
expect(router.navigate).toHaveBeenCalledWith(["/tabs/send"]);
it("should navigate back to the edit send form on close", async () => {
await component.goToEditSend();
expect(router.navigate).toHaveBeenCalledWith(["/edit-send"], {
queryParams: { sendId: "test-send-id", type: SendType.Text },
});
});
describe("getHoursAvailable", () => {

View File

@@ -77,7 +77,13 @@ export class SendCreatedComponent {
return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60)));
}
async close() {
async goToEditSend() {
await this.router.navigate([`/edit-send`], {
queryParams: { sendId: this.send.id, type: this.send.type },
});
}
async goBack() {
await this.router.navigate(["/tabs/send"]);
}

View File

@@ -1,4 +1,4 @@
<popup-page @slideIn>
<popup-page>
<popup-header
slot="header"
[backAction]="close"

View File

@@ -1,4 +1,3 @@
import { animate, group, style, transition, trigger } from "@angular/animations";
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Overlay } from "@angular/cdk/overlay";
import { CommonModule } from "@angular/common";
@@ -26,16 +25,6 @@ export enum GeneratorDialogAction {
Canceled = "canceled",
}
const slideIn = trigger("slideIn", [
transition(":enter", [
style({ opacity: 0, transform: "translateY(100vh)" }),
group([
animate("0.15s linear", style({ opacity: 1 })),
animate("0.3s ease-out", style({ transform: "none" })),
]),
]),
]);
@Component({
selector: "app-vault-generator-dialog",
templateUrl: "./vault-generator-dialog.component.html",
@@ -48,7 +37,6 @@ const slideIn = trigger("slideIn", [
CipherFormGeneratorComponent,
ButtonModule,
],
animations: [slideIn],
})
export class VaultGeneratorDialogComponent {
protected title = this.i18nService.t(this.isPassword ? "passwordGenerator" : "usernameGenerator");

View File

@@ -28,7 +28,7 @@
<button
slot="end"
*ngIf="cipher.edit"
*ngIf="canDeleteCipher$ | async"
[bitAction]="delete"
type="button"
buttonType="danger"

View File

@@ -15,6 +15,7 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
@@ -81,6 +82,12 @@ describe("ViewV2Component", () => {
provide: AccountService,
useValue: accountService,
},
{
provide: CipherAuthorizationService,
useValue: {
canDeleteCipher$: jest.fn().mockReturnValue(true),
},
},
],
}).compileComponents();

View File

@@ -19,6 +19,7 @@ import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import {
AsyncActionsModule,
ButtonModule,
@@ -68,6 +69,7 @@ export class ViewV2Component {
cipher: CipherView;
organization$: Observable<Organization>;
folder$: Observable<FolderView>;
canDeleteCipher$: Observable<boolean>;
collections$: Observable<CollectionView[]>;
loadAction: typeof AUTOFILL_ID | typeof SHOW_AUTOFILL_BUTTON;
@@ -83,6 +85,7 @@ export class ViewV2Component {
private accountService: AccountService,
private eventCollectionService: EventCollectionService,
private popupRouterCacheService: PopupRouterCacheService,
protected cipherAuthorizationService: CipherAuthorizationService,
) {
this.subscribeToParams();
}
@@ -101,6 +104,8 @@ export class ViewV2Component {
await this.vaultPopupAutofillService.doAutofill(this.cipher);
}
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(cipher);
await this.eventCollectionService.collect(
EventType.Cipher_ClientViewed,
cipher.id,

View File

@@ -779,7 +779,7 @@
</div>
</div>
</div>
<div class="box list" *ngIf="editMode && !cloneMode && !(!cipher.edit && editMode)">
<div class="box list" *ngIf="editMode && !cloneMode && (canDeleteCipher$ | async)">
<div class="box-content single-line">
<button
type="button"

View File

@@ -24,6 +24,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -72,6 +73,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
datePipe: DatePipe,
configService: ConfigService,
private fido2UserVerificationService: Fido2UserVerificationService,
cipherAuthorizationService: CipherAuthorizationService,
) {
super(
cipherService,
@@ -92,6 +94,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
window,
datePipe,
configService,
cipherAuthorizationService,
);
}
@@ -107,6 +110,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
this.folderId = params.folderId;
}
if (params.collectionId) {
this.collectionId = params.collectionId;
const collection = this.writeableCollections.find((c) => c.id === params.collectionId);
if (collection != null) {
this.collectionIds = [collection.id];

View File

@@ -198,7 +198,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], {
queryParams: { cipherId: cipher.id },
queryParams: { cipherId: cipher.id, collectionId: this.collectionId },
});
}
this.preventSelected = false;

View File

@@ -644,7 +644,7 @@
class="box-content-row"
appStopClick
(click)="delete()"
*ngIf="cipher.edit"
*ngIf="canDeleteCipher$ | async"
>
<div class="row-main text-danger">
<div class="icon text-danger" aria-hidden="true">

View File

@@ -26,6 +26,7 @@ import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/a
import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -102,6 +103,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
datePipe: DatePipe,
accountService: AccountService,
billingAccountProfileStateService: BillingAccountProfileStateService,
cipherAuthorizationService: CipherAuthorizationService,
) {
super(
cipherService,
@@ -127,6 +129,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
datePipe,
accountService,
billingAccountProfileStateService,
cipherAuthorizationService,
);
}
@@ -143,7 +146,13 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.cipherId) {
this.cipherId = params.cipherId;
} else {
}
if (params.collectionId) {
this.collectionId = params.collectionId;
}
if (!params.cipherId) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.close();
@@ -197,7 +206,12 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/edit-cipher"], {
queryParams: { cipherId: this.cipher.id, type: this.cipher.type, isNew: false },
queryParams: {
cipherId: this.cipher.id,
type: this.cipher.type,
isNew: false,
collectionId: this.collectionId,
},
});
return true;
}

View File

@@ -113,6 +113,7 @@ export class OssServeConfigurator {
this.serviceContainer.apiService,
this.serviceContainer.folderApiService,
this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.cipherAuthorizationService,
);
this.confirmCommand = new ConfirmCommand(
this.serviceContainer.apiService,

View File

@@ -129,6 +129,10 @@ import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-st
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import {
CipherAuthorizationService,
DefaultCipherAuthorizationService,
} from "@bitwarden/common/vault/services/cipher-authorization.service";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
@@ -255,6 +259,7 @@ export class ServiceContainer {
kdfConfigService: KdfConfigServiceAbstraction;
taskSchedulerService: TaskSchedulerService;
sdkService: SdkService;
cipherAuthorizationService: CipherAuthorizationService;
constructor() {
let p = null;
@@ -805,6 +810,11 @@ export class ServiceContainer {
this.apiService,
this.configService,
);
this.cipherAuthorizationService = new DefaultCipherAuthorizationService(
this.collectionService,
this.organizationService,
);
}
async logout() {

View File

@@ -319,6 +319,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.apiService,
this.serviceContainer.folderApiService,
this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.cipherAuthorizationService,
);
const response = await command.run(object, id, cmd);
this.processResponse(response);

View File

@@ -6,6 +6,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { Response } from "../models/response";
import { CliUtils } from "../utils";
@@ -17,6 +18,7 @@ export class DeleteCommand {
private apiService: ApiService,
private folderApiService: FolderApiServiceAbstraction,
private accountProfileService: BillingAccountProfileStateService,
private cipherAuthorizationService: CipherAuthorizationService,
) {}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
@@ -45,6 +47,14 @@ export class DeleteCommand {
return Response.notFound();
}
const canDeleteCipher = await firstValueFrom(
this.cipherAuthorizationService.canDeleteCipher$(cipher),
);
if (!canDeleteCipher) {
return Response.error("You do not have permission to delete this item.");
}
try {
if (options.permanent) {
await this.cipherService.deleteWithServer(id);

View File

@@ -710,7 +710,7 @@
(click)="delete()"
class="danger"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode && !cloneMode"
*ngIf="editMode && !cloneMode && (canDeleteCipher$ | async)"
[disabled]="$any(deleteBtn).loading"
[appApiAction]="deletePromise"
>

View File

@@ -18,6 +18,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -50,6 +51,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
dialogService: DialogService,
datePipe: DatePipe,
configService: ConfigService,
cipherAuthorizationService: CipherAuthorizationService,
) {
super(
cipherService,
@@ -70,6 +72,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
window,
datePipe,
configService,
cipherAuthorizationService,
);
}

View File

@@ -14,6 +14,7 @@
class="details"
*ngIf="cipherId && action === 'view'"
[cipherId]="cipherId"
[collectionId]="activeFilter?.selectedCollectionId"
(onCloneCipher)="cloneCipherWithoutPasswordPrompt($event)"
(onEditCipher)="editCipherWithoutPasswordPrompt($event)"
(onViewCipherPasswordHistory)="viewCipherPasswordHistory($event)"
@@ -29,6 +30,7 @@
[folderId]="action === 'add' && folderId !== 'none' ? folderId : null"
[organizationId]="action === 'add' ? addOrganizationId : null"
[collectionIds]="action === 'add' ? addCollectionIds : null"
[collectionId]="activeFilter?.selectedCollectionId"
[type]="action === 'add' ? (addType ? addType : type) : null"
[cipherId]="action === 'edit' || action === 'clone' ? cipherId : null"
(onSavedCipher)="savedCipher($event)"

View File

@@ -566,7 +566,7 @@
>
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
</button>
<div class="right" *ngIf="cipher.edit">
<div class="right" *ngIf="canDeleteCipher$ | async">
<button
type="button"
(click)="delete()"

View File

@@ -30,6 +30,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -66,6 +67,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
datePipe: DatePipe,
billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
cipherAuthorizationService: CipherAuthorizationService,
) {
super(
cipherService,
@@ -91,6 +93,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
datePipe,
accountService,
billingAccountProfileStateService,
cipherAuthorizationService,
);
}
ngOnInit() {

View File

@@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
"version": "2024.10.2",
"version": "2024.10.3",
"scripts": {
"build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",

View File

@@ -0,0 +1,87 @@
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { firstValueFrom, map, Observable, switchMap } from "rxjs";
import {
OrganizationUserApiService,
OrganizationUserBulkConfirmRequest,
OrganizationUserBulkPublicKeyResponse,
OrganizationUserBulkResponse,
} from "@bitwarden/admin-console/common";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response";
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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { StateProvider } from "@bitwarden/common/platform/state";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { DialogService } from "@bitwarden/components";
import { BaseBulkConfirmComponent } from "./base-bulk-confirm.component";
import { BulkUserDetails } from "./bulk-status.component";
type BulkConfirmDialogParams = {
organizationId: string;
users: BulkUserDetails[];
};
@Component({
templateUrl: "bulk-confirm-dialog.component.html",
})
export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
organizationId: string;
organizationKey$: Observable<OrgKey>;
users: BulkUserDetails[];
constructor(
protected cryptoService: CryptoService,
@Inject(DIALOG_DATA) protected dialogParams: BulkConfirmDialogParams,
protected encryptService: EncryptService,
private organizationUserApiService: OrganizationUserApiService,
protected i18nService: I18nService,
private stateProvider: StateProvider,
) {
super(cryptoService, encryptService, i18nService);
this.organizationId = dialogParams.organizationId;
this.organizationKey$ = this.stateProvider.activeUserId$.pipe(
switchMap((userId) => this.cryptoService.orgKeys$(userId)),
map((organizationKeysById) => organizationKeysById[this.organizationId as OrganizationId]),
takeUntilDestroyed(),
);
this.users = dialogParams.users;
}
protected getCryptoKey = async (): Promise<SymmetricCryptoKey> =>
await firstValueFrom(this.organizationKey$);
protected getPublicKeys = async (): Promise<
ListResponse<OrganizationUserBulkPublicKeyResponse | ProviderUserBulkPublicKeyResponse>
> =>
await this.organizationUserApiService.postOrganizationUsersPublicKey(
this.organizationId,
this.filteredUsers.map((user) => user.id),
);
protected isAccepted = (user: BulkUserDetails) =>
user.status === OrganizationUserStatusType.Accepted;
protected postConfirmRequest = async (
userIdsWithKeys: { id: string; key: string }[],
): Promise<ListResponse<OrganizationUserBulkResponse | ProviderUserBulkResponse>> => {
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
return await this.organizationUserApiService.postOrganizationUserBulkConfirm(
this.organizationId,
request,
);
};
static open(dialogService: DialogService, config: DialogConfig<BulkConfirmDialogParams>) {
return dialogService.open(BulkConfirmDialogComponent, config);
}
}

View File

@@ -1,132 +0,0 @@
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import {
OrganizationUserApiService,
OrganizationUserBulkConfirmRequest,
} from "@bitwarden/admin-console/common";
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";
import { DialogService } from "@bitwarden/components";
import { BulkUserDetails } from "./bulk-status.component";
type BulkConfirmDialogData = {
organizationId: string;
users: BulkUserDetails[];
};
@Component({
selector: "app-bulk-confirm",
templateUrl: "bulk-confirm.component.html",
})
export class BulkConfirmComponent implements OnInit {
organizationId: string;
users: BulkUserDetails[];
excludedUsers: BulkUserDetails[];
filteredUsers: BulkUserDetails[];
publicKeys: Map<string, Uint8Array> = new Map();
fingerprints: Map<string, string> = new Map();
statuses: Map<string, string> = new Map();
loading = true;
done = false;
error: string;
constructor(
@Inject(DIALOG_DATA) protected data: BulkConfirmDialogData,
protected cryptoService: CryptoService,
protected encryptService: EncryptService,
protected apiService: ApiService,
private organizationUserApiService: OrganizationUserApiService,
private i18nService: I18nService,
) {
this.organizationId = data.organizationId;
this.users = data.users;
}
async ngOnInit() {
this.excludedUsers = this.users.filter((u) => !this.isAccepted(u));
this.filteredUsers = this.users.filter((u) => this.isAccepted(u));
if (this.filteredUsers.length <= 0) {
this.done = true;
}
const response = await this.getPublicKeys();
for (const entry of response.data) {
const publicKey = Utils.fromB64ToArray(entry.key);
const fingerprint = await this.cryptoService.getFingerprint(entry.userId, publicKey);
if (fingerprint != null) {
this.publicKeys.set(entry.id, publicKey);
this.fingerprints.set(entry.id, fingerprint.join("-"));
}
}
this.loading = false;
}
async submit() {
this.loading = true;
try {
const key = await this.getCryptoKey();
const userIdsWithKeys: any[] = [];
for (const user of this.filteredUsers) {
const publicKey = this.publicKeys.get(user.id);
if (publicKey == null) {
continue;
}
const encryptedKey = await this.encryptService.rsaEncrypt(key.key, publicKey);
userIdsWithKeys.push({
id: user.id,
key: encryptedKey.encryptedString,
});
}
const response = await this.postConfirmRequest(userIdsWithKeys);
response.data.forEach((entry) => {
const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkConfirmMessage");
this.statuses.set(entry.id, error);
});
this.done = true;
} catch (e) {
this.error = e.message;
}
this.loading = false;
}
protected isAccepted(user: BulkUserDetails) {
return user.status === OrganizationUserStatusType.Accepted;
}
protected async getPublicKeys() {
return await this.organizationUserApiService.postOrganizationUsersPublicKey(
this.organizationId,
this.filteredUsers.map((user) => user.id),
);
}
protected getCryptoKey(): Promise<SymmetricCryptoKey> {
return this.cryptoService.getOrgKey(this.organizationId);
}
protected async postConfirmRequest(userIdsWithKeys: any[]) {
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
return await this.organizationUserApiService.postOrganizationUserBulkConfirm(
this.organizationId,
request,
);
}
static open(dialogService: DialogService, config: DialogConfig<BulkConfirmDialogData>) {
return dialogService.open(BulkConfirmComponent, config);
}
}

View File

@@ -0,0 +1,54 @@
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import {
OrganizationUserApiService,
OrganizationUserBulkResponse,
} from "@bitwarden/admin-console/common";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "@bitwarden/components";
import { BaseBulkRemoveComponent } from "./base-bulk-remove.component";
import { BulkUserDetails } from "./bulk-status.component";
type BulkRemoveDialogParams = {
organizationId: string;
users: BulkUserDetails[];
};
@Component({
templateUrl: "bulk-remove-dialog.component.html",
})
export class BulkRemoveDialogComponent extends BaseBulkRemoveComponent {
organizationId: string;
users: BulkUserDetails[];
constructor(
@Inject(DIALOG_DATA) protected dialogParams: BulkRemoveDialogParams,
protected i18nService: I18nService,
private organizationUserApiService: OrganizationUserApiService,
) {
super(i18nService);
this.organizationId = dialogParams.organizationId;
this.users = dialogParams.users;
this.showNoMasterPasswordWarning = this.users.some(
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
);
}
protected deleteUsers = (): Promise<ListResponse<OrganizationUserBulkResponse>> =>
this.organizationUserApiService.removeManyOrganizationUsers(
this.organizationId,
this.users.map((user) => user.id),
);
protected get removeUsersWarning() {
return this.i18nService.t("removeOrgUsersConfirmation");
}
static open(dialogService: DialogService, config: DialogConfig<BulkRemoveDialogParams>) {
return dialogService.open(BulkRemoveDialogComponent, config);
}
}

View File

@@ -1,76 +0,0 @@
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "@bitwarden/components";
import { BulkUserDetails } from "./bulk-status.component";
type BulkRemoveDialogData = {
organizationId: string;
users: BulkUserDetails[];
};
@Component({
selector: "app-bulk-remove",
templateUrl: "bulk-remove.component.html",
})
export class BulkRemoveComponent {
organizationId: string;
users: BulkUserDetails[];
statuses: Map<string, string> = new Map();
loading = false;
done = false;
error: string;
showNoMasterPasswordWarning = false;
constructor(
@Inject(DIALOG_DATA) protected data: BulkRemoveDialogData,
protected apiService: ApiService,
protected i18nService: I18nService,
private organizationUserApiService: OrganizationUserApiService,
) {
this.organizationId = data.organizationId;
this.users = data.users;
this.showNoMasterPasswordWarning = this.users.some(
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
);
}
submit = async () => {
this.loading = true;
try {
const response = await this.removeUsers();
response.data.forEach((entry) => {
const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkRemovedMessage");
this.statuses.set(entry.id, error);
});
this.done = true;
} catch (e) {
this.error = e.message;
}
this.loading = false;
};
protected async removeUsers() {
return await this.organizationUserApiService.removeManyOrganizationUsers(
this.organizationId,
this.users.map((user) => user.id),
);
}
protected get removeUsersWarning() {
return this.i18nService.t("removeOrgUsersConfirmation");
}
static open(dialogService: DialogService, config: DialogConfig<BulkRemoveDialogData>) {
return dialogService.open(BulkRemoveComponent, config);
}
}

View File

@@ -60,9 +60,9 @@ import { GroupService } from "../core";
import { OrganizationUserView } from "../core/views/organization-user.view";
import { openEntityEventsDialog } from "../manage/entity-events.component";
import { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component";
import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component";
import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component";
import { BulkRemoveComponent } from "./components/bulk/bulk-remove.component";
import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component";
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
import { BulkStatusComponent } from "./components/bulk/bulk-status.component";
import {
@@ -541,7 +541,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return;
}
const dialogRef = BulkRemoveComponent.open(this.dialogService, {
const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, {
data: {
organizationId: this.organization.id,
users: this.dataSource.getCheckedUsers(),
@@ -620,7 +620,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return;
}
const dialogRef = BulkConfirmComponent.open(this.dialogService, {
const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, {
data: {
organizationId: this.organization.id,
users: this.dataSource.getCheckedUsers(),

View File

@@ -7,9 +7,9 @@ import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
import { LooseComponentsModule } from "../../../shared";
import { SharedOrganizationModule } from "../shared";
import { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component";
import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component";
import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component";
import { BulkRemoveComponent } from "./components/bulk/bulk-remove.component";
import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component";
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
import { BulkStatusComponent } from "./components/bulk/bulk-status.component";
import { UserDialogModule } from "./components/member-dialog";
@@ -28,9 +28,9 @@ import { MembersComponent } from "./members.component";
PasswordStrengthV2Component,
],
declarations: [
BulkConfirmComponent,
BulkConfirmDialogComponent,
BulkEnableSecretsManagerDialogComponent,
BulkRemoveComponent,
BulkRemoveDialogComponent,
BulkRestoreRevokeComponent,
BulkStatusComponent,
MembersComponent,

View File

@@ -18,6 +18,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -54,6 +55,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
datePipe: DatePipe,
configService: ConfigService,
billingAccountProfileStateService: BillingAccountProfileStateService,
cipherAuthorizationService: CipherAuthorizationService,
) {
super(
cipherService,
@@ -76,6 +78,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
datePipe,
configService,
billingAccountProfileStateService,
cipherAuthorizationService,
);
}

View File

@@ -1,25 +0,0 @@
import { svgIcon } from "@bitwarden/components";
export const ManageBilling = svgIcon`
<svg width="213" height="231" viewBox="0 0 213 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130.089 85.6617C129.868 85.4299 129.604 85.2456 129.31 85.1197C129.016 84.9937 128.7 84.9299 128.381 84.9317H84.5811C84.2617 84.9299 83.9441 84.9937 83.6503 85.1197C83.3565 85.2456 83.0919 85.4299 82.8729 85.6617C82.6411 85.8807 82.4568 86.1471 82.3308 86.441C82.2049 86.7348 82.141 87.0505 82.1429 87.3699V116.57C82.152 118.793 82.5827 120.994 83.4131 123.056C84.2033 125.091 85.2654 127.011 86.5703 128.761C87.9117 130.515 89.4137 132.137 91.0562 133.612C92.58 135.01 94.186 136.318 95.8632 137.528C97.3232 138.565 98.8562 139.547 100.462 140.474C102.068 141.401 103.202 142.027 103.864 142.353C104.532 142.682 105.072 142.941 105.474 143.113C105.788 143.264 106.132 143.339 106.481 143.332C106.824 143.337 107.164 143.257 107.47 143.102C107.879 142.923 108.412 142.671 109.087 142.343C109.762 142.014 110.912 141.386 112.489 140.463C114.066 139.539 115.617 138.554 117.088 137.517C118.767 136.305 120.375 134.999 121.902 133.601C123.547 132.128 125.049 130.504 126.388 128.75C127.691 126.998 128.753 125.08 129.545 123.045C130.378 120.983 130.808 118.782 130.816 116.559V87.3589C130.817 87.0414 130.754 86.7275 130.628 86.4355C130.502 86.1435 130.319 85.8807 130.089 85.6617ZM124.443 116.836C124.443 127.421 106.481 136.513 106.481 136.513V91.1878H124.443V116.836Z" fill="#212529"/>
<path d="M62.7328 163.392C62.7328 168.149 51.6616 166.263 46.761 166.263C41.8605 166.263 22.5074 161.096 20.7328 153.058C23.6946 151.005 16.0004 143.298 31.9722 142.724C33.1529 141.759 44.9083 148.712 46.761 149.039C51.6616 149.039 62.7328 158.636 62.7328 163.392Z" fill="#E5E5E5"/>
<path d="M21.3544 122.3C21.4472 123.4 22.4147 124.217 23.5153 124.125C24.616 124.032 25.433 123.064 25.3402 121.964L21.3544 122.3ZM148.234 45.7444C149.303 45.4678 149.946 44.3767 149.669 43.3073L145.162 25.8808C144.885 24.8114 143.794 24.1687 142.725 24.4453C141.655 24.7219 141.013 25.813 141.289 26.8824L145.296 42.3726L129.805 46.3792C128.736 46.6558 128.093 47.7469 128.37 48.8163C128.647 49.8857 129.738 50.5283 130.807 50.2517L148.234 45.7444ZM25.3402 121.964C23.4116 99.0873 31.1986 75.5542 48.6989 58.0539L45.8705 55.2255C27.5023 73.5937 19.331 98.2998 21.3544 122.3L25.3402 121.964ZM48.6989 58.0539C75.2732 31.4796 115.769 27.3025 146.718 45.5314L148.748 42.0848C116.267 22.9532 73.7654 27.3305 45.8705 55.2255L48.6989 58.0539Z" fill="#212529"/>
<path d="M64.2075 185.062C63.1417 185.352 62.5129 186.451 62.8029 187.517L67.5298 204.885C67.8199 205.951 68.919 206.58 69.9848 206.29C71.0507 205.999 71.6795 204.9 71.3895 203.834L67.1878 188.396L82.6262 184.194C83.692 183.904 84.3209 182.805 84.0308 181.739C83.7408 180.674 82.6416 180.045 81.5758 180.335L64.2075 185.062ZM189.211 100.283C189.018 99.1952 187.98 98.4697 186.893 98.6625C185.805 98.8552 185.08 99.8931 185.272 100.981L189.211 100.283ZM162.871 172.225C136.546 198.55 96.5599 202.897 65.726 185.255L63.7396 188.727C96.0997 207.242 138.066 202.687 165.699 175.054L162.871 172.225ZM185.272 100.981C189.718 126.07 182.249 152.847 162.871 172.225L165.699 175.054C186.04 154.713 193.875 126.603 189.211 100.283L185.272 100.981Z" fill="#212529"/>
<path d="M34.4588 108.132C36.0159 92.1931 42.8984 76.6765 55.1062 64.4686C72.0222 47.5527 95.2911 40.8618 117.233 44.396" stroke="#212529" stroke-width="2" stroke-linecap="round"/>
<path d="M177.328 119.132C176.386 136.119 169.426 152.834 156.449 165.811C141.173 181.088 120.715 188.025 100.733 186.623" stroke="#212529" stroke-width="2" stroke-linecap="round"/>
<rect x="150.233" y="56.1318" width="49" height="34" rx="2.5" stroke="#212529" stroke-width="3"/>
<path d="M150.233 63.6318V63.6318C150.233 66.9455 152.919 69.6318 156.233 69.6318H169.242M199.233 63.6318V63.6318C199.233 66.9455 196.546 69.6318 193.233 69.6318H180.224" stroke="#212529" stroke-width="3"/>
<mask id="path-9-inside-1_873_6447" fill="white">
<rect x="168.733" y="65.6318" width="12" height="9" rx="1.25"/>
</mask>
<rect x="168.733" y="65.6318" width="12" height="9" rx="1.25" stroke="#212529" stroke-width="6" mask="url(#path-9-inside-1_873_6447)"/>
<path d="M183.733 54.6318C183.733 54.6318 183.733 53.6318 183.733 52.6318C183.733 51.6318 182.785 50.6318 181.838 50.6318C180.891 50.6318 168.575 50.6318 167.628 50.6318C166.68 50.6318 165.733 51.6318 165.733 52.6318C165.733 53.6318 165.733 54.6318 165.733 54.6318" stroke="#212529" stroke-width="3"/>
<circle cx="48.7328" cy="142.632" r="10.5" fill="white" stroke="#212529" stroke-width="3"/>
<path d="M65.7263 170.132H65.6454H65.5646H65.484H65.4036H65.3233H65.2432H65.1632H65.0834H65.0037H64.9242H64.8449H64.7657H64.6866H64.6077H64.529H64.4504H64.372H64.2937H64.2155H64.1376H64.0597H63.982H63.9045H63.8271H63.7498H63.6727H63.5957H63.5189H63.4422H63.3657H63.2893H63.213H63.1369H63.0609H62.985H62.9093H62.8338H62.7583H62.683H62.6079H62.5329H62.458H62.3832H62.3086H62.2341H62.1597H62.0855H62.0114H61.9374H61.8636H61.7899H61.7163H61.6428H61.5695H61.4963H61.4232H61.3503H61.2774H61.2047H61.1321H61.0597H60.9873H60.9151H60.843H60.771H60.6992H60.6274H60.5558H60.4843H60.4129H60.3416H60.2704H60.1994H60.1284H60.0576H59.9869H59.9163H59.8458H59.7754H59.7052H59.635H59.5649H59.495H59.4252H59.3554H59.2858H59.2163H59.1469H59.0776H59.0084H58.9393H58.8703H58.8013H58.7325H58.6638H58.5952H58.5267H58.4583H58.39H58.3218H58.2537H58.1857H58.1178H58.0499H57.9822H57.9146H57.847H57.7796H57.7122H57.6449H57.5777H57.5106H57.4436H57.3767H57.3099H57.2431H57.1765H57.1099H57.0434H56.977H56.9107H56.8444H56.7783H56.7122H56.6462H56.5803H56.5145H56.4487H56.383H56.3174H56.2519H56.1865H56.1211H56.0558H55.9906H55.9254H55.8603H55.7953H55.7304H55.6655H55.6008H55.536H55.4714H55.4068H55.3423H55.2778H55.2135H55.1492H55.0849H55.0207H54.9566H54.8925H54.8286H54.7646H54.7008H54.6369H54.5732H54.5095H54.4459H54.3823H54.3188H54.2553H54.1919H54.1286H54.0653H54.0021H53.9389H53.8758H53.8127H53.7497H53.6867H53.6238H53.5609H53.4981H53.4353H53.3726H53.3099H53.2473H53.1847H53.1222H53.0597H52.9972H52.9348H52.8725H52.8102H52.7479H52.6856H52.6234H52.5613H52.4992H52.4371H52.375H52.313H52.2511H52.1891H52.1272H52.0654H52.0036H51.9418H51.88H51.8183H51.7566H51.6949H51.6333H51.5717H51.5101H51.4485H51.387H51.3255H51.264H51.2026H51.1412H51.0798H51.0184H50.9571H50.8957H50.8344H50.7731H50.7119H50.6506H50.5894H50.5282H50.467H50.4058H50.3447H50.2836H50.2224H50.1613H50.1002H50.0392H49.9781H49.917H49.856H49.795H49.7339H49.6729H49.6119H49.5509H49.4899H49.429H49.368H49.307H49.246H49.1851H49.1241H49.0632H49.0022H48.9413H48.8803H48.8194H48.7584H48.6975H48.6365H48.5756H48.5146H48.4537H48.3927H48.3318H48.2708H48.2098H48.1488H48.0878H48.0268H47.9658H47.9048H47.8438H47.7828H47.7217H47.6607H47.5996H47.5385H47.4774H47.4163H47.3552H47.294H47.2329H47.1717H47.1105H47.0493H46.9881H46.9268H46.8656H46.8043H46.743H46.6816H46.6203H46.5589H46.4975H46.4361H46.3746H46.3132H46.2517H46.1901H46.1286H46.067H46.0054H45.9437H45.8821H45.8203H45.7586H45.6968H45.635H45.5732H45.5113H45.4494H45.3875H45.3255H45.2635H45.2015H45.1394H45.0772H45.0151H44.9529H44.8906H44.8283H44.766H44.7036H44.6412H44.5788H44.5163H44.4537H44.3911H44.3285H44.2658H44.2031H44.1403H44.0775H44.0146H43.9517H43.8887H43.8256H43.7626H43.6994H43.6362H43.573H43.5097H43.4463H43.3829H43.3195H43.2559H43.1924H43.1287H43.065H43.0013H42.9374H42.8736H42.8096H42.7456H42.6815H42.6174H42.5532H42.4889H42.4246H42.3602H42.2958H42.2312H42.1666H42.102H42.0373H41.9724H41.9076H41.8426H41.7776H41.7125H41.6474H41.5821H41.5168H41.4514H41.386H41.3204H41.2548H41.1891H41.1233H41.0575H40.9916H40.9255H40.8594H40.7933H40.727H40.6607H40.5943H40.5277H40.4612H40.3945H40.3277H40.2609H40.1939H40.1269H40.0598H39.9926H39.9253H39.8579H39.7904H39.7229H39.6552H39.5874H39.5196H39.4517H39.3836H39.3155H39.2473H39.1789H39.1105H39.042H38.9734H38.9046H38.8358H38.7669H38.6979H38.6288H38.5595H38.4902H38.4208H38.3512H38.2816H38.2118H38.142H38.072H38.0019H37.9317H37.8615H37.7911H37.7205H37.6499H37.5792H37.5083H37.4374H37.3663H37.2951H37.2238H37.1524H37.0809H37.0092H36.9374H36.8655H36.7935H36.7214H36.6492H36.5768H36.5043H36.4317H36.359H36.2861H36.2131H36.14H36.0668H35.9934H35.9199H35.8463H35.7726H35.6987H35.6247H35.5506H35.4764H35.402H35.3274H35.2528H35.178H35.1031H35.028H34.9528H34.8775H34.8021H34.7265H34.6507H34.5749H34.4989H34.4227H34.3464H34.27H34.1934H34.1167H34.0398H33.9628H33.8857H33.8084H33.731H33.6534H33.5757H33.4978H33.4198H33.3416H33.2633H33.1848H33.1062H33.0274H32.9485H32.8694H32.7902H32.7108H32.6313H32.5516H32.4718H32.3918H32.3116H32.2313H32.1508H32.0702H31.9894H31.9085H31.8273H31.7461H31.6646H31.583H31.5013H31.4194H31.3373H31.255H31.1726H31.09H31.0073H30.9243C30.7817 170.132 30.7021 170.098 30.6492 170.065C30.5881 170.026 30.5107 169.954 30.4348 169.823C30.2689 169.538 30.1936 169.112 30.2525 168.743C31.6563 159.954 39.3802 153.206 48.7252 153.206C58.0703 153.206 65.7943 159.954 67.198 168.743C67.3079 169.431 67.1364 169.686 67.0452 169.781C66.9216 169.91 66.5692 170.132 65.7263 170.132Z" fill="white" stroke="#212529" stroke-width="3"/>
<circle cx="20.7328" cy="142.632" r="10.5" fill="white" stroke="#212529" stroke-width="3"/>
<path d="M37.7263 170.132H37.6454H37.5646H37.484H37.4036H37.3233H37.2432H37.1632H37.0834H37.0037H36.9242H36.8449H36.7657H36.6866H36.6077H36.529H36.4504H36.372H36.2937H36.2155H36.1376H36.0597H35.982H35.9045H35.8271H35.7498H35.6727H35.5957H35.5189H35.4422H35.3657H35.2893H35.213H35.1369H35.0609H34.985H34.9093H34.8338H34.7583H34.683H34.6079H34.5329H34.458H34.3832H34.3086H34.2341H34.1597H34.0855H34.0114H33.9374H33.8636H33.7899H33.7163H33.6428H33.5695H33.4963H33.4232H33.3503H33.2774H33.2047H33.1321H33.0597H32.9873H32.9151H32.843H32.771H32.6992H32.6274H32.5558H32.4843H32.4129H32.3416H32.2704H32.1994H32.1284H32.0576H31.9869H31.9163H31.8458H31.7754H31.7052H31.635H31.5649H31.495H31.4252H31.3554H31.2858H31.2163H31.1469H31.0776H31.0084H30.9393H30.8703H30.8013H30.7325H30.6638H30.5952H30.5267H30.4583H30.39H30.3218H30.2537H30.1857H30.1178H30.0499H29.9822H29.9146H29.847H29.7796H29.7122H29.6449H29.5777H29.5106H29.4436H29.3767H29.3099H29.2431H29.1765H29.1099H29.0434H28.977H28.9107H28.8444H28.7783H28.7122H28.6462H28.5803H28.5145H28.4487H28.383H28.3174H28.2519H28.1865H28.1211H28.0558H27.9906H27.9254H27.8603H27.7953H27.7304H27.6655H27.6008H27.536H27.4714H27.4068H27.3423H27.2778H27.2135H27.1492H27.0849H27.0207H26.9566H26.8925H26.8286H26.7646H26.7008H26.6369H26.5732H26.5095H26.4459H26.3823H26.3188H26.2553H26.1919H26.1286H26.0653H26.0021H25.9389H25.8758H25.8127H25.7497H25.6867H25.6238H25.5609H25.4981H25.4353H25.3726H25.3099H25.2473H25.1847H25.1222H25.0597H24.9972H24.9348H24.8725H24.8102H24.7479H24.6856H24.6234H24.5613H24.4992H24.4371H24.375H24.313H24.2511H24.1891H24.1272H24.0654H24.0036H23.9418H23.88H23.8183H23.7566H23.6949H23.6333H23.5717H23.5101H23.4485H23.387H23.3255H23.264H23.2026H23.1412H23.0798H23.0184H22.9571H22.8957H22.8344H22.7731H22.7119H22.6506H22.5894H22.5282H22.467H22.4058H22.3447H22.2836H22.2224H22.1613H22.1002H22.0392H21.9781H21.917H21.856H21.795H21.7339H21.6729H21.6119H21.5509H21.4899H21.429H21.368H21.307H21.246H21.1851H21.1241H21.0632H21.0022H20.9413H20.8803H20.8194H20.7584H20.6975H20.6365H20.5756H20.5146H20.4537H20.3927H20.3318H20.2708H20.2098H20.1488H20.0878H20.0268H19.9658H19.9048H19.8438H19.7828H19.7217H19.6607H19.5996H19.5385H19.4774H19.4163H19.3552H19.294H19.2329H19.1717H19.1105H19.0493H18.9881H18.9268H18.8656H18.8043H18.743H18.6816H18.6203H18.5589H18.4975H18.4361H18.3746H18.3132H18.2517H18.1901H18.1286H18.067H18.0054H17.9437H17.8821H17.8203H17.7586H17.6968H17.635H17.5732H17.5113H17.4494H17.3875H17.3255H17.2635H17.2015H17.1394H17.0772H17.0151H16.9529H16.8906H16.8283H16.766H16.7036H16.6412H16.5788H16.5163H16.4537H16.3911H16.3285H16.2658H16.2031H16.1403H16.0775H16.0146H15.9517H15.8887H15.8256H15.7626H15.6994H15.6362H15.573H15.5097H15.4463H15.3829H15.3195H15.2559H15.1924H15.1287H15.065H15.0013H14.9374H14.8736H14.8096H14.7456H14.6815H14.6174H14.5532H14.4889H14.4246H14.3602H14.2958H14.2312H14.1666H14.102H14.0373H13.9724H13.9076H13.8426H13.7776H13.7125H13.6474H13.5821H13.5168H13.4514H13.386H13.3204H13.2548H13.1891H13.1233H13.0575H12.9916H12.9255H12.8594H12.7933H12.727H12.6607H12.5943H12.5277H12.4612H12.3945H12.3277H12.2609H12.1939H12.1269H12.0598H11.9926H11.9253H11.8579H11.7904H11.7229H11.6552H11.5874H11.5196H11.4517H11.3836H11.3155H11.2473H11.1789H11.1105H11.042H10.9734H10.9046H10.8358H10.7669H10.6979H10.6288H10.5595H10.4902H10.4208H10.3512H10.2816H10.2118H10.142H10.072H10.0019H9.93175H9.86145H9.79105H9.72054H9.64992H9.57918H9.50834H9.43738H9.3663H9.29511H9.22381H9.15239H9.08085H9.0092H8.93743H8.86554H8.79354H8.72141H8.64916H8.5768H8.50431H8.4317H8.35896H8.28611H8.21312H8.14002H8.06679H7.99343H7.91995H7.84634H7.7726H7.69873H7.62473H7.55061H7.47635H7.40196H7.32744H7.25279H7.17801H7.10309H7.02804H6.95285H6.87753H6.80207H6.72647H6.65074H6.57487H6.49886H6.42271H6.34642H6.26998H6.19341H6.1167H6.03984H5.96284H5.8857H5.80841H5.73098H5.6534H5.57567H5.4978H5.41978H5.34161H5.26329H5.18482H5.1062H5.02743H4.94851H4.86944H4.79021H4.71083H4.6313H4.55161H4.47177H4.39177H4.31161H4.2313H4.15082H4.07019H3.9894H3.90845H3.82734H3.74607H3.66464H3.58304H3.50128H3.41936H3.33727H3.25501H3.1726H3.09001H3.00726H2.92434C2.78171 170.132 2.70206 170.098 2.64924 170.065C2.5881 170.026 2.51071 169.954 2.43479 169.823C2.26892 169.538 2.19357 169.112 2.25253 168.743C3.65626 159.954 11.3802 153.206 20.7252 153.206C30.0703 153.206 37.7943 159.954 39.198 168.743C39.3079 169.431 39.1364 169.686 39.0452 169.781C38.9216 169.91 38.5692 170.132 37.7263 170.132Z" fill="white" stroke="#212529" stroke-width="3"/>
<circle cx="34.7328" cy="155.632" r="10.5" fill="white" stroke="#212529" stroke-width="3"/>
<path d="M51.7263 183.132H51.6454H51.5646H51.484H51.4036H51.3233H51.2432H51.1632H51.0834H51.0037H50.9242H50.8449H50.7657H50.6866H50.6077H50.529H50.4504H50.372H50.2937H50.2155H50.1376H50.0597H49.982H49.9045H49.8271H49.7498H49.6727H49.5957H49.5189H49.4422H49.3657H49.2893H49.213H49.1369H49.0609H48.985H48.9093H48.8338H48.7583H48.683H48.6079H48.5329H48.458H48.3832H48.3086H48.2341H48.1597H48.0855H48.0114H47.9374H47.8636H47.7899H47.7163H47.6428H47.5695H47.4963H47.4232H47.3503H47.2774H47.2047H47.1321H47.0597H46.9873H46.9151H46.843H46.771H46.6992H46.6274H46.5558H46.4843H46.4129H46.3416H46.2704H46.1994H46.1284H46.0576H45.9869H45.9163H45.8458H45.7754H45.7052H45.635H45.5649H45.495H45.4252H45.3554H45.2858H45.2163H45.1469H45.0776H45.0084H44.9393H44.8703H44.8013H44.7325H44.6638H44.5952H44.5267H44.4583H44.39H44.3218H44.2537H44.1857H44.1178H44.0499H43.9822H43.9146H43.847H43.7796H43.7122H43.6449H43.5777H43.5106H43.4436H43.3767H43.3099H43.2431H43.1765H43.1099H43.0434H42.977H42.9107H42.8444H42.7783H42.7122H42.6462H42.5803H42.5145H42.4487H42.383H42.3174H42.2519H42.1865H42.1211H42.0558H41.9906H41.9254H41.8603H41.7953H41.7304H41.6655H41.6008H41.536H41.4714H41.4068H41.3423H41.2778H41.2135H41.1492H41.0849H41.0207H40.9566H40.8925H40.8286H40.7646H40.7008H40.6369H40.5732H40.5095H40.4459H40.3823H40.3188H40.2553H40.1919H40.1286H40.0653H40.0021H39.9389H39.8758H39.8127H39.7497H39.6867H39.6238H39.5609H39.4981H39.4353H39.3726H39.3099H39.2473H39.1847H39.1222H39.0597H38.9972H38.9348H38.8725H38.8102H38.7479H38.6856H38.6234H38.5613H38.4992H38.4371H38.375H38.313H38.2511H38.1891H38.1272H38.0654H38.0036H37.9418H37.88H37.8183H37.7566H37.6949H37.6333H37.5717H37.5101H37.4485H37.387H37.3255H37.264H37.2026H37.1412H37.0798H37.0184H36.9571H36.8957H36.8344H36.7731H36.7119H36.6506H36.5894H36.5282H36.467H36.4058H36.3447H36.2836H36.2224H36.1613H36.1002H36.0392H35.9781H35.917H35.856H35.795H35.7339H35.6729H35.6119H35.5509H35.4899H35.429H35.368H35.307H35.246H35.1851H35.1241H35.0632H35.0022H34.9413H34.8803H34.8194H34.7584H34.6975H34.6365H34.5756H34.5146H34.4537H34.3927H34.3318H34.2708H34.2098H34.1488H34.0878H34.0268H33.9658H33.9048H33.8438H33.7828H33.7217H33.6607H33.5996H33.5385H33.4774H33.4163H33.3552H33.294H33.2329H33.1717H33.1105H33.0493H32.9881H32.9268H32.8656H32.8043H32.743H32.6816H32.6203H32.5589H32.4975H32.4361H32.3746H32.3132H32.2517H32.1901H32.1286H32.067H32.0054H31.9437H31.8821H31.8203H31.7586H31.6968H31.635H31.5732H31.5113H31.4494H31.3875H31.3255H31.2635H31.2015H31.1394H31.0772H31.0151H30.9529H30.8906H30.8283H30.766H30.7036H30.6412H30.5788H30.5163H30.4537H30.3911H30.3285H30.2658H30.2031H30.1403H30.0775H30.0146H29.9517H29.8887H29.8256H29.7626H29.6994H29.6362H29.573H29.5097H29.4463H29.3829H29.3195H29.2559H29.1924H29.1287H29.065H29.0013H28.9374H28.8736H28.8096H28.7456H28.6815H28.6174H28.5532H28.4889H28.4246H28.3602H28.2958H28.2312H28.1666H28.102H28.0373H27.9724H27.9076H27.8426H27.7776H27.7125H27.6474H27.5821H27.5168H27.4514H27.386H27.3204H27.2548H27.1891H27.1233H27.0575H26.9916H26.9255H26.8594H26.7933H26.727H26.6607H26.5943H26.5277H26.4612H26.3945H26.3277H26.2609H26.1939H26.1269H26.0598H25.9926H25.9253H25.8579H25.7904H25.7229H25.6552H25.5874H25.5196H25.4517H25.3836H25.3155H25.2473H25.1789H25.1105H25.042H24.9734H24.9046H24.8358H24.7669H24.6979H24.6288H24.5595H24.4902H24.4208H24.3512H24.2816H24.2118H24.142H24.072H24.0019H23.9317H23.8615H23.7911H23.7205H23.6499H23.5792H23.5083H23.4374H23.3663H23.2951H23.2238H23.1524H23.0809H23.0092H22.9374H22.8655H22.7935H22.7214H22.6492H22.5768H22.5043H22.4317H22.359H22.2861H22.2131H22.14H22.0668H21.9934H21.9199H21.8463H21.7726H21.6987H21.6247H21.5506H21.4764H21.402H21.3274H21.2528H21.178H21.1031H21.028H20.9528H20.8775H20.8021H20.7265H20.6507H20.5749H20.4989H20.4227H20.3464H20.27H20.1934H20.1167H20.0398H19.9628H19.8857H19.8084H19.731H19.6534H19.5757H19.4978H19.4198H19.3416H19.2633H19.1848H19.1062H19.0274H18.9485H18.8694H18.7902H18.7108H18.6313H18.5516H18.4718H18.3918H18.3116H18.2313H18.1508H18.0702H17.9894H17.9085H17.8273H17.7461H17.6646H17.583H17.5013H17.4194H17.3373H17.255H17.1726H17.09H17.0073H16.9243C16.7778 183.132 16.6956 183.097 16.642 183.064C16.5807 183.026 16.5047 182.955 16.4306 182.829C16.2682 182.553 16.1944 182.141 16.2521 181.785C17.6523 173.127 25.3653 166.455 34.7252 166.455C44.0852 166.455 51.7982 173.127 53.1984 181.785C53.3068 182.454 53.138 182.695 53.0518 182.784C52.929 182.91 52.5741 183.132 51.7263 183.132Z" fill="white" stroke="#212529" stroke-width="3"/>
</svg>
`;

View File

@@ -0,0 +1,24 @@
import { Icon, svgIcon } from "@bitwarden/components";
export const SubscriptionHiddenIcon: Icon = svgIcon`
<svg width="216" height="231" viewBox="0 0 216 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M133.356 85.6618C133.136 85.43 132.871 85.2457 132.577 85.1198C132.283 84.9939 131.968 84.93 131.648 84.9318H87.8482C87.5289 84.93 87.2113 84.9939 86.9175 85.1198C86.6237 85.2457 86.359 85.43 86.14 85.6618C85.9083 85.8808 85.7239 86.1473 85.598 86.4411C85.4721 86.7349 85.4082 87.0506 85.41 87.37V116.57C85.4192 118.793 85.8499 120.994 86.6802 123.056C87.4705 125.091 88.5326 127.011 89.8375 128.761C91.1789 130.515 92.6808 132.137 94.3233 133.612C95.8472 135.01 97.4532 136.318 99.1304 137.528C100.59 138.565 102.123 139.547 103.729 140.474C105.335 141.401 106.469 142.027 107.131 142.354C107.799 142.682 108.339 142.941 108.741 143.113C109.055 143.264 109.4 143.339 109.748 143.332C110.091 143.337 110.431 143.257 110.737 143.102C111.146 142.923 111.679 142.671 112.354 142.343C113.03 142.014 114.179 141.386 115.756 140.463C117.333 139.539 118.884 138.554 120.355 137.517C122.034 136.306 123.642 134.999 125.169 133.601C126.814 132.128 128.316 130.504 129.655 128.75C130.958 126.998 132.021 125.08 132.813 123.045C133.645 120.983 134.075 118.782 134.083 116.559V87.3591C134.085 87.0415 134.021 86.7276 133.895 86.4356C133.769 86.1436 133.586 85.8808 133.356 85.6618ZM127.71 116.836C127.71 127.421 109.748 136.514 109.748 136.514V91.1879H127.71V116.836Z" fill="rgb(var(--color-secondary-700))"/>
<path d="M24.6216 122.3C24.7144 123.4 25.6819 124.217 26.7825 124.125C27.8832 124.032 28.7002 123.064 28.6074 121.964L24.6216 122.3ZM151.501 45.7445C152.57 45.4679 153.213 44.3768 152.936 43.3074L148.429 25.8809C148.152 24.8115 147.061 24.1688 145.992 24.4454C144.922 24.722 144.28 25.8131 144.556 26.8825L148.563 42.3728L133.073 46.3793C132.003 46.6559 131.361 47.747 131.637 48.8164C131.914 49.8858 133.005 50.5285 134.074 50.2519L151.501 45.7445ZM28.6074 121.964C26.6788 99.0874 34.4658 75.5543 51.9661 58.054L49.1377 55.2256C30.7695 73.5938 22.5982 98.2999 24.6216 122.3L28.6074 121.964ZM51.9661 58.054C78.5404 31.4797 119.036 27.3026 149.985 45.5315L152.015 42.0849C119.534 22.9534 77.0327 27.3306 49.1377 55.2256L51.9661 58.054Z" fill="rgb(var(--color-secondary-700))"/>
<path d="M67.4747 185.062C66.4089 185.352 65.7801 186.451 66.0701 187.517L70.797 204.885C71.0871 205.951 72.1862 206.58 73.252 206.29C74.3179 205.999 74.9467 204.9 74.6567 203.834L70.455 188.396L85.8934 184.194C86.9592 183.904 87.5881 182.805 87.298 181.739C87.008 180.674 85.9088 180.045 84.843 180.335L67.4747 185.062ZM192.478 100.283C192.286 99.1952 191.248 98.4697 190.16 98.6625C189.072 98.8552 188.347 99.8931 188.54 100.981L192.478 100.283ZM166.138 172.225C139.813 198.55 99.8271 202.897 68.9932 185.255L67.0068 188.727C99.3669 207.242 141.333 202.687 168.966 175.054L166.138 172.225ZM188.54 100.981C192.985 126.07 185.516 152.847 166.138 172.225L168.966 175.054C189.307 154.713 197.142 126.603 192.478 100.283L188.54 100.981Z" fill="rgb(var(--color-secondary-700))"/>
<path d="M37.726 108.132C39.283 92.1931 46.1655 76.6765 58.3734 64.4686C75.2893 47.5527 98.5583 40.8618 120.5 44.396" stroke="rgb(var(--color-secondary-700))" stroke-width="2" stroke-linecap="round"/>
<path d="M180.595 119.132C179.653 136.119 172.693 152.834 159.717 165.811C144.44 181.088 123.982 188.025 104 186.623" stroke="rgb(var(--color-secondary-700))" stroke-width="2" stroke-linecap="round"/>
<rect x="153.5" y="56.1317" width="49" height="34" rx="2.5" stroke="rgb(var(--color-secondary-700))" stroke-width="3"/>
<path d="M153.5 63.6317V63.6317C153.5 66.9454 156.186 69.6317 159.5 69.6317H172.509M202.5 63.6317V63.6317C202.5 66.9454 199.814 69.6317 196.5 69.6317H183.491" stroke="rgb(var(--color-secondary-700))" stroke-width="3"/>
<mask id="path-8-inside-1_1000_26057" fill="white">
<rect x="172" y="65.6317" width="12" height="9" rx="1.25"/>
</mask>
<rect x="172" y="65.6317" width="12" height="9" rx="1.25" stroke="rgb(var(--color-secondary-700))" stroke-width="6" mask="url(#path-8-inside-1_1000_26057)"/>
<path d="M187 54.6317C187 54.6317 187 53.6317 187 52.6317C187 51.6317 186.053 50.6317 185.105 50.6317C184.158 50.6317 171.842 50.6317 170.895 50.6317C169.947 50.6317 169 51.6317 169 52.6317C169 53.6317 169 54.6317 169 54.6317" stroke="rgb(var(--color-secondary-700))" stroke-width="3"/>
<circle cx="48" cy="141" r="10.5" fill="rgb(var(--color-background))" stroke="rgb(var(--color-secondary-700))" stroke-width="3"/>
<path d="M64.9935 168.5H64.9126H64.8318H64.7512H64.6708H64.5905H64.5104H64.4304H64.3506H64.2709H64.1914H64.1121H64.0329H63.9538H63.8749H63.7962H63.7176H63.6392H63.5609H63.4828H63.4048H63.3269H63.2492H63.1717H63.0943H63.017H62.9399H62.8629H62.7861H62.7094H62.6329H62.5565H62.4802H62.4041H62.3281H62.2523H62.1766H62.101H62.0256H61.9503H61.8751H61.8001H61.7252H61.6504H61.5758H61.5013H61.4269H61.3527H61.2786H61.2046H61.1308H61.0571H60.9835H60.91H60.8367H60.7635H60.6904H60.6175H60.5446H60.4719H60.3993H60.3269H60.2545H60.1823H60.1102H60.0382H59.9664H59.8946H59.823H59.7515H59.6801H59.6088H59.5376H59.4666H59.3956H59.3248H59.2541H59.1835H59.113H59.0426H58.9724H58.9022H58.8322H58.7622H58.6924H58.6226H58.553H58.4835H58.4141H58.3448H58.2756H58.2065H58.1375H58.0686H57.9998H57.9311H57.8625H57.794H57.7256H57.6572H57.589H57.5209H57.4529H57.385H57.3172H57.2494H57.1818H57.1142H57.0468H56.9794H56.9121H56.8449H56.7779H56.7108H56.6439H56.5771H56.5103H56.4437H56.3771H56.3106H56.2442H56.1779H56.1117H56.0455H55.9794H55.9134H55.8475H55.7817H55.7159H55.6502H55.5846H55.5191H55.4537H55.3883H55.323H55.2578H55.1926H55.1275H55.0625H54.9976H54.9328H54.868H54.8032H54.7386H54.674H54.6095H54.5451H54.4807H54.4164H54.3521H54.2879H54.2238H54.1598H54.0958H54.0318H53.968H53.9042H53.8404H53.7767H53.7131H53.6495H53.586H53.5226H53.4592H53.3958H53.3325H53.2693H53.2061H53.143H53.0799H53.0169H52.9539H52.891H52.8281H52.7653H52.7025H52.6398H52.5771H52.5145H52.4519H52.3894H52.3269H52.2645H52.202H52.1397H52.0774H52.0151H51.9528H51.8907H51.8285H51.7664H51.7043H51.6423H51.5803H51.5183H51.4564H51.3945H51.3326H51.2708H51.209H51.1472H51.0855H51.0238H50.9621H50.9005H50.8389H50.7773H50.7157H50.6542H50.5927H50.5312H50.4698H50.4084H50.347H50.2856H50.2243H50.1629H50.1016H50.0404H49.9791H49.9178H49.8566H49.7954H49.7342H49.6731H49.6119H49.5508H49.4896H49.4285H49.3674H49.3064H49.2453H49.1842H49.1232H49.0622H49.0011H48.9401H48.8791H48.8181H48.7571H48.6962H48.6352H48.5742H48.5133H48.4523H48.3913H48.3304H48.2694H48.2085H48.1475H48.0866H48.0257H47.9647H47.9038H47.8428H47.7819H47.7209H47.6599H47.599H47.538H47.477H47.416H47.3551H47.2941H47.2331H47.172H47.111H47.05H46.9889H46.9279H46.8668H46.8057H46.7446H46.6835H46.6224H46.5612H46.5001H46.4389H46.3777H46.3165H46.2553H46.194H46.1328H46.0715H46.0102H45.9489H45.8875H45.8261H45.7647H45.7033H45.6418H45.5804H45.5189H45.4573H45.3958H45.3342H45.2726H45.2109H45.1493H45.0876H45.0258H44.9641H44.9023H44.8404H44.7786H44.7166H44.6547H44.5927H44.5307H44.4687H44.4066H44.3445H44.2823H44.2201H44.1578H44.0956H44.0332H43.9709H43.9084H43.846H43.7835H43.7209H43.6583H43.5957H43.533H43.4703H43.4075H43.3447H43.2818H43.2189H43.1559H43.0929H43.0298H42.9666H42.9034H42.8402H42.7769H42.7135H42.6501H42.5867H42.5231H42.4596H42.3959H42.3322H42.2685H42.2046H42.1408H42.0768H42.0128H41.9487H41.8846H41.8204H41.7562H41.6918H41.6274H41.563H41.4985H41.4339H41.3692H41.3045H41.2397H41.1748H41.1098H41.0448H40.9797H40.9146H40.8493H40.784H40.7186H40.6532H40.5876H40.522H40.4563H40.3905H40.3247H40.2588H40.1928H40.1267H40.0605H39.9942H39.9279H39.8615H39.795H39.7284H39.6617H39.5949H39.5281H39.4611H39.3941H39.327H39.2598H39.1925H39.1251H39.0576H38.9901H38.9224H38.8547H38.7868H38.7189H38.6508H38.5827H38.5145H38.4461H38.3777H38.3092H38.2406H38.1719H38.103H38.0341H37.9651H37.896H37.8267H37.7574H37.688H37.6184H37.5488H37.479H37.4092H37.3392H37.2691H37.199H37.1287H37.0583H36.9878H36.9171H36.8464H36.7755H36.7046H36.6335H36.5623H36.491H36.4196H36.3481H36.2764H36.2046H36.1328H36.0607H35.9886H35.9164H35.844H35.7715H35.6989H35.6262H35.5533H35.4803H35.4072H35.334H35.2606H35.1872H35.1135H35.0398H34.9659H34.8919H34.8178H34.7436H34.6692H34.5947H34.52H34.4452H34.3703H34.2952H34.2201H34.1447H34.0693H33.9937H33.9179H33.8421H33.7661H33.6899H33.6136H33.5372H33.4606H33.3839H33.3071H33.2301H33.1529H33.0756H32.9982H32.9206H32.8429H32.765H32.687H32.6088H32.5305H32.452H32.3734H32.2946H32.2157H32.1367H32.0574H31.978H31.8985H31.8188H31.739H31.659H31.5788H31.4985H31.418H31.3374H31.2566H31.1757H31.0946H31.0133H30.9318H30.8503H30.7685H30.6866H30.6045H30.5222H30.4398H30.3572H30.2745H30.1915C30.0489 168.5 29.9693 168.466 29.9164 168.433C29.8553 168.394 29.7779 168.322 29.702 168.192C29.5361 167.906 29.4608 167.48 29.5197 167.111C30.9235 158.322 38.6474 151.574 47.9925 151.574C57.3375 151.574 65.0615 158.322 66.4652 167.111C66.5751 167.799 66.4037 168.054 66.3124 168.149C66.1888 168.278 65.8364 168.5 64.9935 168.5Z" fill="rgb(var(--color-background))" stroke="rgb(var(--color-secondary-700))" stroke-width="3"/>
<circle cx="20" cy="141" r="10.5" fill="rgb(var(--color-background))" stroke="rgb(var(--color-secondary-700))" stroke-width="3"/>
<path d="M36.9935 168.5H36.9126H36.8318H36.7512H36.6708H36.5905H36.5104H36.4304H36.3506H36.2709H36.1914H36.1121H36.0329H35.9538H35.8749H35.7962H35.7176H35.6392H35.5609H35.4828H35.4048H35.3269H35.2492H35.1717H35.0943H35.017H34.9399H34.8629H34.7861H34.7094H34.6329H34.5565H34.4802H34.4041H34.3281H34.2523H34.1766H34.101H34.0256H33.9503H33.8751H33.8001H33.7252H33.6504H33.5758H33.5013H33.4269H33.3527H33.2786H33.2046H33.1308H33.0571H32.9835H32.91H32.8367H32.7635H32.6904H32.6175H32.5446H32.4719H32.3993H32.3269H32.2545H32.1823H32.1102H32.0382H31.9664H31.8946H31.823H31.7515H31.6801H31.6088H31.5376H31.4666H31.3956H31.3248H31.2541H31.1835H31.113H31.0426H30.9724H30.9022H30.8322H30.7622H30.6924H30.6226H30.553H30.4835H30.4141H30.3448H30.2756H30.2065H30.1375H30.0686H29.9998H29.9311H29.8625H29.794H29.7256H29.6572H29.589H29.5209H29.4529H29.385H29.3172H29.2494H29.1818H29.1142H29.0468H28.9794H28.9121H28.8449H28.7779H28.7108H28.6439H28.5771H28.5103H28.4437H28.3771H28.3106H28.2442H28.1779H28.1117H28.0455H27.9794H27.9134H27.8475H27.7817H27.7159H27.6502H27.5846H27.5191H27.4537H27.3883H27.323H27.2578H27.1926H27.1275H27.0625H26.9976H26.9328H26.868H26.8032H26.7386H26.674H26.6095H26.5451H26.4807H26.4164H26.3521H26.2879H26.2238H26.1598H26.0958H26.0318H25.968H25.9042H25.8404H25.7767H25.7131H25.6495H25.586H25.5226H25.4592H25.3958H25.3325H25.2693H25.2061H25.143H25.0799H25.0169H24.9539H24.891H24.8281H24.7653H24.7025H24.6398H24.5771H24.5145H24.4519H24.3894H24.3269H24.2645H24.202H24.1397H24.0774H24.0151H23.9528H23.8907H23.8285H23.7664H23.7043H23.6423H23.5803H23.5183H23.4564H23.3945H23.3326H23.2708H23.209H23.1472H23.0855H23.0238H22.9621H22.9005H22.8389H22.7773H22.7157H22.6542H22.5927H22.5312H22.4698H22.4084H22.347H22.2856H22.2243H22.1629H22.1016H22.0404H21.9791H21.9178H21.8566H21.7954H21.7342H21.6731H21.6119H21.5508H21.4896H21.4285H21.3674H21.3064H21.2453H21.1842H21.1232H21.0622H21.0011H20.9401H20.8791H20.8181H20.7571H20.6962H20.6352H20.5742H20.5133H20.4523H20.3913H20.3304H20.2694H20.2085H20.1475H20.0866H20.0257H19.9647H19.9038H19.8428H19.7819H19.7209H19.6599H19.599H19.538H19.477H19.416H19.3551H19.2941H19.2331H19.172H19.111H19.05H18.9889H18.9279H18.8668H18.8057H18.7446H18.6835H18.6224H18.5612H18.5001H18.4389H18.3777H18.3165H18.2553H18.194H18.1328H18.0715H18.0102H17.9489H17.8875H17.8261H17.7647H17.7033H17.6418H17.5804H17.5189H17.4573H17.3958H17.3342H17.2726H17.2109H17.1493H17.0876H17.0258H16.9641H16.9023H16.8404H16.7786H16.7166H16.6547H16.5927H16.5307H16.4687H16.4066H16.3445H16.2823H16.2201H16.1578H16.0956H16.0332H15.9709H15.9084H15.846H15.7835H15.7209H15.6583H15.5957H15.533H15.4703H15.4075H15.3447H15.2818H15.2189H15.1559H15.0929H15.0298H14.9666H14.9034H14.8402H14.7769H14.7135H14.6501H14.5867H14.5231H14.4596H14.3959H14.3322H14.2685H14.2046H14.1408H14.0768H14.0128H13.9487H13.8846H13.8204H13.7562H13.6918H13.6274H13.563H13.4985H13.4339H13.3692H13.3045H13.2397H13.1748H13.1098H13.0448H12.9797H12.9146H12.8493H12.784H12.7186H12.6532H12.5876H12.522H12.4563H12.3905H12.3247H12.2588H12.1928H12.1267H12.0605H11.9942H11.9279H11.8615H11.795H11.7284H11.6617H11.5949H11.5281H11.4611H11.3941H11.327H11.2598H11.1925H11.1251H11.0576H10.9901H10.9224H10.8547H10.7868H10.7189H10.6508H10.5827H10.5145H10.4461H10.3777H10.3092H10.2406H10.1719H10.103H10.0341H9.9651H9.89597H9.82674H9.75741H9.68798H9.61843H9.54879H9.47904H9.40918H9.33921H9.26914H9.19896H9.12867H9.05826H8.98775H8.91713H8.8464H8.77555H8.70459H8.63351H8.56232H8.49102H8.4196H8.34807H8.27641H8.20464H8.13276H8.06075H7.98862H7.91638H7.84401H7.77152H7.69891H7.62617H7.55332H7.48034H7.40723H7.334H7.26064H7.18716H7.11355H7.03981H6.96594H6.89195H6.81782H6.74356H6.66918H6.59466H6.52H6.44522H6.3703H6.29525H6.22006H6.14474H6.06928H5.99368H5.91795H5.84208H5.76607H5.68992H5.61363H5.5372H5.46062H5.38391H5.30705H5.23005H5.15291H5.07562H4.99819H4.92061H4.84288H4.76501H4.68699H4.60882H4.5305H4.45203H4.37342H4.29465H4.21573H4.13665H4.05743H3.97805H3.89851H3.81882H3.73898H3.65898H3.57882H3.49851H3.41804H3.33741H3.25662H3.17566H3.09455H3.01328H2.93185H2.85025H2.76849H2.68657H2.60448H2.52223H2.43981H2.35722H2.27447H2.19155C2.04893 168.5 1.96927 168.466 1.91645 168.433C1.85532 168.394 1.77792 168.322 1.702 168.192C1.53613 167.906 1.46078 167.48 1.51975 167.111C2.92347 158.322 10.6474 151.574 19.9925 151.574C29.3375 151.574 37.0615 158.322 38.4652 167.111C38.5751 167.799 38.4037 168.054 38.3124 168.149C38.1888 168.278 37.8364 168.5 36.9935 168.5Z" fill="rgb(var(--color-background))" stroke="rgb(var(--color-secondary-700))" stroke-width="3"/>
<circle cx="34" cy="154" r="10.5" fill="rgb(var(--color-background))" stroke="rgb(var(--color-secondary-700))" stroke-width="3"/>
<path d="M50.9935 181.5H50.9126H50.8318H50.7512H50.6708H50.5905H50.5104H50.4304H50.3506H50.2709H50.1914H50.1121H50.0329H49.9538H49.8749H49.7962H49.7176H49.6392H49.5609H49.4828H49.4048H49.3269H49.2492H49.1717H49.0943H49.017H48.9399H48.8629H48.7861H48.7094H48.6329H48.5565H48.4802H48.4041H48.3281H48.2523H48.1766H48.101H48.0256H47.9503H47.8751H47.8001H47.7252H47.6504H47.5758H47.5013H47.4269H47.3527H47.2786H47.2046H47.1308H47.0571H46.9835H46.91H46.8367H46.7635H46.6904H46.6175H46.5446H46.4719H46.3993H46.3269H46.2545H46.1823H46.1102H46.0382H45.9664H45.8946H45.823H45.7515H45.6801H45.6088H45.5376H45.4666H45.3956H45.3248H45.2541H45.1835H45.113H45.0426H44.9724H44.9022H44.8322H44.7622H44.6924H44.6226H44.553H44.4835H44.4141H44.3448H44.2756H44.2065H44.1375H44.0686H43.9998H43.9311H43.8625H43.794H43.7256H43.6572H43.589H43.5209H43.4529H43.385H43.3172H43.2494H43.1818H43.1142H43.0468H42.9794H42.9121H42.8449H42.7779H42.7108H42.6439H42.5771H42.5103H42.4437H42.3771H42.3106H42.2442H42.1779H42.1117H42.0455H41.9794H41.9134H41.8475H41.7817H41.7159H41.6502H41.5846H41.5191H41.4537H41.3883H41.323H41.2578H41.1926H41.1275H41.0625H40.9976H40.9328H40.868H40.8032H40.7386H40.674H40.6095H40.5451H40.4807H40.4164H40.3521H40.2879H40.2238H40.1598H40.0958H40.0318H39.968H39.9042H39.8404H39.7767H39.7131H39.6495H39.586H39.5226H39.4592H39.3958H39.3325H39.2693H39.2061H39.143H39.0799H39.0169H38.9539H38.891H38.8281H38.7653H38.7025H38.6398H38.5771H38.5145H38.4519H38.3894H38.3269H38.2645H38.202H38.1397H38.0774H38.0151H37.9528H37.8907H37.8285H37.7664H37.7043H37.6423H37.5803H37.5183H37.4564H37.3945H37.3326H37.2708H37.209H37.1472H37.0855H37.0238H36.9621H36.9005H36.8389H36.7773H36.7157H36.6542H36.5927H36.5312H36.4698H36.4084H36.347H36.2856H36.2243H36.1629H36.1016H36.0404H35.9791H35.9178H35.8566H35.7954H35.7342H35.6731H35.6119H35.5508H35.4896H35.4285H35.3674H35.3064H35.2453H35.1842H35.1232H35.0622H35.0011H34.9401H34.8791H34.8181H34.7571H34.6962H34.6352H34.5742H34.5133H34.4523H34.3913H34.3304H34.2694H34.2085H34.1475H34.0866H34.0257H33.9647H33.9038H33.8428H33.7819H33.7209H33.6599H33.599H33.538H33.477H33.416H33.3551H33.2941H33.2331H33.172H33.111H33.05H32.9889H32.9279H32.8668H32.8057H32.7446H32.6835H32.6224H32.5612H32.5001H32.4389H32.3777H32.3165H32.2553H32.194H32.1328H32.0715H32.0102H31.9489H31.8875H31.8261H31.7647H31.7033H31.6418H31.5804H31.5189H31.4573H31.3958H31.3342H31.2726H31.2109H31.1493H31.0876H31.0258H30.9641H30.9023H30.8404H30.7786H30.7166H30.6547H30.5927H30.5307H30.4687H30.4066H30.3445H30.2823H30.2201H30.1578H30.0956H30.0332H29.9709H29.9084H29.846H29.7835H29.7209H29.6583H29.5957H29.533H29.4703H29.4075H29.3447H29.2818H29.2189H29.1559H29.0929H29.0298H28.9666H28.9034H28.8402H28.7769H28.7135H28.6501H28.5867H28.5231H28.4596H28.3959H28.3322H28.2685H28.2046H28.1408H28.0768H28.0128H27.9487H27.8846H27.8204H27.7562H27.6918H27.6274H27.563H27.4985H27.4339H27.3692H27.3045H27.2397H27.1748H27.1098H27.0448H26.9797H26.9146H26.8493H26.784H26.7186H26.6532H26.5876H26.522H26.4563H26.3905H26.3247H26.2588H26.1928H26.1267H26.0605H25.9942H25.9279H25.8615H25.795H25.7284H25.6617H25.5949H25.5281H25.4611H25.3941H25.327H25.2598H25.1925H25.1251H25.0576H24.9901H24.9224H24.8547H24.7868H24.7189H24.6508H24.5827H24.5145H24.4461H24.3777H24.3092H24.2406H24.1719H24.103H24.0341H23.9651H23.896H23.8267H23.7574H23.688H23.6184H23.5488H23.479H23.4092H23.3392H23.2691H23.199H23.1287H23.0583H22.9878H22.9171H22.8464H22.7755H22.7046H22.6335H22.5623H22.491H22.4196H22.3481H22.2764H22.2046H22.1328H22.0607H21.9886H21.9164H21.844H21.7715H21.6989H21.6262H21.5533H21.4803H21.4072H21.334H21.2606H21.1872H21.1135H21.0398H20.9659H20.8919H20.8178H20.7436H20.6692H20.5947H20.52H20.4452H20.3703H20.2952H20.2201H20.1447H20.0693H19.9937H19.9179H19.8421H19.7661H19.6899H19.6136H19.5372H19.4606H19.3839H19.3071H19.2301H19.1529H19.0756H18.9982H18.9206H18.8429H18.765H18.687H18.6088H18.5305H18.452H18.3734H18.2946H18.2157H18.1367H18.0574H17.978H17.8985H17.8188H17.739H17.659H17.5788H17.4985H17.418H17.3374H17.2566H17.1757H17.0946H17.0133H16.9318H16.8503H16.7685H16.6866H16.6045H16.5222H16.4398H16.3572H16.2745H16.1915C16.045 181.5 15.9628 181.465 15.9092 181.432C15.8479 181.394 15.772 181.324 15.6978 181.198C15.5354 180.922 15.4617 180.509 15.5193 180.153C16.9196 171.496 24.6325 164.823 33.9925 164.823C43.3524 164.823 51.0654 171.496 52.4657 180.153C52.574 180.823 52.4052 181.064 52.319 181.152C52.1962 181.279 51.8413 181.5 50.9935 181.5Z" fill="rgb(var(--color-background))" stroke="rgb(var(--color-secondary-700))" stroke-width="3"/>
</svg>
`;

View File

@@ -46,7 +46,7 @@
*ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans"
class="tw-pt-6"
>
<bit-section>
<bit-section [ngClass]="{ 'tw-hidden': !createOrganization }">
<app-org-info
(changedBusinessOwned)="changedOwnedBusiness()"
[formGroup]="formGroup"

View File

@@ -1,17 +1,12 @@
<app-header></app-header>
<bit-container *ngIf="!isManagedByConsolidatedBillingMSP">
<ng-container *ngIf="!firstLoaded && loading">
<i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<app-org-subscription-hidden
*ngIf="firstLoaded && !userOrg.canViewSubscription"
[providerName]="userOrg.providerName"
></app-org-subscription-hidden>
<ng-container *ngIf="sub && firstLoaded">
<bit-container *ngIf="showSubscription; else hideSubscription">
<ng-container *ngIf="sub && !loading">
<ng-container *ngIf="!(showUpdatedSubscriptionStatusSection$ | async)">
<bit-callout
type="warning"
@@ -247,42 +242,7 @@
></app-sm-adjust-subscription>
</ng-container>
</ng-container>
<h2 bitTypography="h2" *ngIf="shownSelfHost()" class="tw-mt-7">
{{ "selfHostingTitle" | i18n }}
</h2>
<p bitTypography="body1" *ngIf="shownSelfHost()">
{{ "selfHostingEnterpriseOrganizationSectionCopy" | i18n }}
<a
href="https://bitwarden.com/help/licensing-on-premise/#retrieve-organization-license"
target="_blank"
rel="noreferrer"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i
></a>
</p>
<div class="tw-flex tw-space-x-2">
<button
bitButton
buttonType="secondary"
type="button"
(click)="downloadLicense()"
*ngIf="canDownloadLicense && shownSelfHost()"
[disabled]="showDownloadLicense"
>
{{ "downloadLicense" | i18n }}
</button>
<button
bitButton
buttonType="secondary"
type="button"
(click)="manageBillingSync()"
*ngIf="canManageBillingSync"
>
{{ (hasBillingSyncToken ? "viewBillingToken" : "setUpBillingSync") | i18n }}
</button>
</div>
<ng-container *ngTemplateOutlet="setupSelfHost"></ng-container>
<ng-container *ngIf="userOrg.canEditSubscription">
<h2 bitTypography="h2" class="tw-mt-7">{{ "additionalOptions" | i18n }}</h2>
<p bitTypography="body1">
@@ -302,13 +262,50 @@
</ng-container>
</ng-container>
</bit-container>
<bit-container *ngIf="isManagedByConsolidatedBillingMSP">
<div
class="tw-mx-auto tw-flex tw-flex-col tw-items-center tw-justify-center tw-pt-24 tw-text-center tw-font-bold"
>
<bit-icon [icon]="manageBillingFromProviderPortal"></bit-icon>
<ng-container slot="description">{{
"manageBillingFromProviderPortalMessage" | i18n
}}</ng-container>
</div>
</bit-container>
<ng-template #hideSubscription>
<bit-container *ngIf="!loading">
<ng-container *ngIf="enableConsolidatedBilling$ | async; else consolidatedBillingDisabled">
<h2 bitTypography="h2">{{ "manageSubscription" | i18n }}</h2>
<p bitTypography="body1" *ngIf="userOrg.isProviderUser; else isOrganizationOwner">
{{ "manageSubscriptionFromThe" | i18n }}
<a [routerLink]="['/providers', userOrg.providerId, 'manage-client-organizations']">{{
"providerPortal" | i18n
}}</a
>.
</p>
<ng-template #isOrganizationOwner>
<p>
{{ "billingManagedByProvider" | i18n: userOrg.providerName }}.
{{ "billingContactProviderForAssistance" | i18n }}.
</p>
</ng-template>
<ng-container *ngTemplateOutlet="setupSelfHost"></ng-container>
</ng-container>
<ng-template #consolidatedBillingDisabled>
<div class="tw-flex tw-flex-col tw-items-center tw-text-info">
<bit-icon [icon]="subscriptionHiddenIcon"></bit-icon>
<p class="tw-font-bold">{{ "billingManagedByProvider" | i18n: userOrg.providerName }}</p>
<p>{{ "billingContactProviderForAssistance" | i18n }}</p>
</div>
</ng-template>
</bit-container>
</ng-template>
<ng-template #setupSelfHost>
<ng-container *ngIf="showSelfHost">
<h2 bitTypography="h2" class="tw-mt-7">
{{ "selfHostingTitleProper" | i18n }}
</h2>
<p bitTypography="body1">
{{ "toHostBitwardenOnYourOwnServer" | i18n }}
</p>
<div class="tw-flex tw-space-x-2">
<button bitButton buttonType="secondary" type="button" (click)="downloadLicense()">
{{ "downloadLicense" | i18n }}
</button>
<button bitButton buttonType="secondary" type="button" (click)="manageBillingSync()">
{{ (hasBillingSyncToken ? "viewBillingToken" : "setUpBillingSync") | i18n }}
</button>
</div>
</ng-container>
</ng-template>

View File

@@ -5,9 +5,9 @@ import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUnti
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { OrganizationApiKeyType, ProviderStatusType } from "@bitwarden/common/admin-console/enums";
import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { BillingSubscriptionItemResponse } from "@bitwarden/common/billing/models/response/subscription.response";
@@ -15,7 +15,6 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ToastService } from "@bitwarden/components";
import {
@@ -34,7 +33,7 @@ import {
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
import { ChangePlanDialogResultType, openChangePlanDialog } from "./change-plan-dialog.component";
import { DownloadLicenceDialogComponent } from "./download-license.component";
import { ManageBilling } from "./icons/manage-billing.icon";
import { SubscriptionHiddenIcon } from "./icons/subscription-hidden.icon";
import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component";
@Component({
@@ -50,19 +49,17 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
hasBillingSyncToken: boolean;
showAdjustSecretsManager = false;
showSecretsManagerSubscribe = false;
firstLoaded = false;
loading: boolean;
loading = true;
locale: string;
showUpdatedSubscriptionStatusSection$: Observable<boolean>;
manageBillingFromProviderPortal = ManageBilling;
isManagedByConsolidatedBillingMSP = false;
enableTimeThreshold: boolean;
preSelectedProductTier: ProductTierType = ProductTierType.Free;
showSubscription = true;
showSelfHost = false;
protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon;
protected readonly teamsStarter = ProductTierType.TeamsStarter;
private destroy$ = new Subject<void>();
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableConsolidatedBilling,
);
@@ -71,7 +68,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
FeatureFlag.EnableTimeThreshold,
);
protected EnableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$(
protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableUpgradePasswordManagerSub,
);
@@ -79,9 +76,10 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
);
private destroy$ = new Subject<void>();
constructor(
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private logService: LogService,
private organizationService: OrganizationService,
@@ -89,15 +87,13 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
private route: ActivatedRoute,
private dialogService: DialogService,
private configService: ConfigService,
private providerService: ProviderService,
private toastService: ToastService,
private billingApiService: BillingApiServiceAbstraction,
) {}
async ngOnInit() {
if (this.route.snapshot.queryParamMap.get("upgrade")) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.changePlan();
await this.changePlan();
const productTierTypeStr = this.route.snapshot.queryParamMap.get("productTierType");
if (productTierTypeStr != null) {
const productTier = Number(productTierTypeStr);
@@ -112,7 +108,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
concatMap(async (params) => {
this.organizationId = params.organizationId;
await this.load();
this.firstLoaded = true;
}),
takeUntil(this.destroy$),
)
@@ -130,21 +125,34 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
}
async load() {
if (this.loading) {
return;
}
this.loading = true;
this.locale = await firstValueFrom(this.i18nService.locale$);
this.userOrg = await this.organizationService.get(this.organizationId);
if (this.userOrg.canViewSubscription) {
const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$);
const provider = await this.providerService.get(this.userOrg.providerId);
this.isManagedByConsolidatedBillingMSP =
enableConsolidatedBilling &&
this.userOrg.hasProvider &&
provider?.providerStatus == ProviderStatusType.Billable;
/*
+--------------------+--------------+----------------------+--------------+
| User Type | Has Provider | Consolidated Billing | Subscription |
+--------------------+--------------+----------------------+--------------+
| Organization Owner | False | N/A | Shown |
| Organization Owner | True | N/A | Hidden |
| Provider User | True | False | Shown |
| Provider User | True | True | Hidden |
+--------------------+--------------+----------------------+--------------+
*/
const consolidatedBillingEnabled = await firstValueFrom(this.enableConsolidatedBilling$);
this.showSubscription =
(!this.userOrg.hasProvider && this.userOrg.isOwner) ||
(this.userOrg.hasProvider && this.userOrg.isProviderUser && !consolidatedBillingEnabled);
const metadata = await this.billingApiService.getOrganizationBillingMetadata(
this.organizationId,
);
this.showSelfHost = metadata.isEligibleForSelfHost;
if (this.showSubscription) {
this.sub = await this.organizationApiService.getSubscription(this.organizationId);
this.lineItems = this.sub?.subscription?.items;
@@ -277,26 +285,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
return this.sub.subscription?.items.some((i) => i.sponsoredSubscriptionItem);
}
get canDownloadLicense() {
return (
(this.sub.planType !== PlanType.Free && this.subscription == null) ||
(this.subscription != null && !this.subscription.cancelled)
);
}
get canManageBillingSync() {
return (
this.sub.planType === PlanType.EnterpriseAnnually ||
this.sub.planType === PlanType.EnterpriseMonthly ||
this.sub.planType === PlanType.EnterpriseAnnually2023 ||
this.sub.planType === PlanType.EnterpriseMonthly2023 ||
this.sub.planType === PlanType.EnterpriseAnnually2020 ||
this.sub.planType === PlanType.EnterpriseMonthly2020 ||
this.sub.planType === PlanType.EnterpriseAnnually2019 ||
this.sub.planType === PlanType.EnterpriseMonthly2019
);
}
get subscriptionDesc() {
if (this.sub.planType === PlanType.Free) {
return this.i18nService.t("subscriptionFreePlan", this.sub.seats.toString());
@@ -353,13 +341,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
);
}
shownSelfHost(): boolean {
return (
this.sub?.plan.productTier !== ProductTierType.Teams &&
this.sub?.plan.productTier !== ProductTierType.Free
);
}
cancelSubscription = async () => {
const reference = openOffboardingSurvey(this.dialogService, {
data: {
@@ -399,9 +380,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
title: null,
message: this.i18nService.t("reinstated"),
});
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
await this.load();
} catch (e) {
this.logService.error(e);
}
@@ -409,7 +388,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
async changePlan() {
const EnableUpgradePasswordManagerSub = await firstValueFrom(
this.EnableUpgradePasswordManagerSub$,
this.enableUpgradePasswordManagerSub$,
);
if (EnableUpgradePasswordManagerSub) {
const reference = openChangePlanDialog(this.dialogService, {
@@ -458,24 +437,15 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
});
await firstValueFrom(dialogRef.closed);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
await this.load();
}
closeDownloadLicense() {
this.showDownloadLicense = false;
}
subscriptionAdjusted() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
async subscriptionAdjusted() {
await this.load();
}
calculateTotalAppliedDiscount(total: number) {
const discountedTotal = total / (1 - this.customerDiscount?.percentOff / 100);
return discountedTotal;
return total / (1 - this.customerDiscount?.percentOff / 100);
}
adjustStorage = (add: boolean) => {

View File

@@ -72,7 +72,7 @@
buttonType="danger"
[appA11yTitle]="'delete' | i18n"
[bitAction]="delete"
[disabled]="!canDelete"
[disabled]="!(canDeleteCipher$ | async)"
data-testid="delete-cipher-btn"
></button>
</div>

View File

@@ -2,7 +2,7 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, Subject } from "rxjs";
import { firstValueFrom, Observable, Subject } from "rxjs";
import { map } from "rxjs/operators";
import { CollectionView } from "@bitwarden/admin-console/common";
@@ -12,12 +12,13 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherId, CollectionId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import {
AsyncActionsModule,
ButtonModule,
@@ -63,6 +64,16 @@ export interface VaultItemDialogParams {
* If true, the "edit" button will be disabled in the dialog.
*/
disableForm?: boolean;
/**
* The ID of the active collection. This is know the collection filter selected by the user.
*/
activeCollectionId?: CollectionId;
/**
* If true, the dialog is being opened from the admin console.
*/
isAdminConsoleAction?: boolean;
}
export enum VaultItemDialogResult {
@@ -204,6 +215,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
protected formConfig: CipherFormConfig = this.params.formConfig;
protected canDeleteCipher$: Observable<boolean>;
constructor(
@Inject(DIALOG_DATA) protected params: VaultItemDialogParams,
private dialogRef: DialogRef<VaultItemDialogResult>,
@@ -217,6 +230,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
private router: Router,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private premiumUpgradeService: PremiumUpgradePromptService,
private cipherAuthorizationService: CipherAuthorizationService,
) {
this.updateTitle();
}
@@ -231,6 +245,12 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
this.organization = this.formConfig.organizations.find(
(o) => o.id === this.cipher.organizationId,
);
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(
this.cipher,
[this.params.activeCollectionId],
this.params.isAdminConsoleAction,
);
}
this.performingInitialLoad = false;

View File

@@ -69,7 +69,11 @@
></app-collection-badge>
</td>
<td bitCell [ngClass]="RowHeightClass" *ngIf="showGroups"></td>
<td bitCell [ngClass]="RowHeightClass" *ngIf="viewingOrgVault"></td>
<td bitCell [ngClass]="RowHeightClass" *ngIf="viewingOrgVault">
<p class="tw-mb-0 tw-text-muted">
{{ permissionText }}
</p>
</td>
<td bitCell [ngClass]="RowHeightClass" class="tw-text-right">
<button
[disabled]="disabled || disableMenu"
@@ -132,7 +136,7 @@
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
{{ "restore" | i18n }}
</button>
<button bitMenuItem *ngIf="canEditCipher" (click)="deleteCipher()" type="button">
<button bitMenuItem *ngIf="canManageCollection" (click)="deleteCipher()" type="button">
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}

View File

@@ -5,9 +5,14 @@ import { CollectionView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
convertToPermission,
getPermissionList,
} from "./../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
import { VaultItemEvent } from "./vault-item-event";
import { RowHeightClass } from "./vault-items.component";
@@ -35,6 +40,7 @@ export class VaultCipherRowComponent implements OnInit {
@Input() collections: CollectionView[];
@Input() viewingOrgVault: boolean;
@Input() canEditCipher: boolean;
@Input() canManageCollection: boolean;
@Output() onEvent = new EventEmitter<VaultItemEvent>();
@@ -42,9 +48,20 @@ export class VaultCipherRowComponent implements OnInit {
@Output() checkedToggled = new EventEmitter<void>();
protected CipherType = CipherType;
private permissionList = getPermissionList();
private permissionPriority = [
"canManage",
"canEdit",
"canEditExceptPass",
"canView",
"canViewExceptPass",
];
protected organization?: Organization;
constructor(private configService: ConfigService) {}
constructor(
private configService: ConfigService,
private i18nService: I18nService,
) {}
/**
* Lifecycle hook for component initialization.
@@ -90,6 +107,40 @@ export class VaultCipherRowComponent implements OnInit {
return this.cipher.type === this.CipherType.Login && !this.cipher.isDeleted;
}
protected get permissionText() {
if (!this.cipher.organizationId || this.cipher.collectionIds.length === 0) {
return this.i18nService.t("canManage");
}
const filteredCollections = this.collections.filter((collection) => {
if (collection.assigned) {
return this.cipher.collectionIds.find((id) => {
if (collection.id === id) {
return collection;
}
});
}
});
if (filteredCollections?.length === 1) {
return this.i18nService.t(
this.permissionList.find((p) => p.perm === convertToPermission(filteredCollections[0]))
?.labelId,
);
}
if (filteredCollections?.length > 1) {
const labels = filteredCollections.map((collection) => {
return this.permissionList.find((p) => p.perm === convertToPermission(collection))?.labelId;
});
const highestPerm = this.permissionPriority.find((perm) => labels.includes(perm));
return this.i18nService.t(highestPerm);
}
return this.i18nService.t("noAccess");
}
protected get showCopyPassword(): boolean {
return this.isNotDeletedLoginCipher && this.cipher.viewPassword;
}

View File

@@ -64,12 +64,7 @@
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
{{ "restoreSelected" | i18n }}
</button>
<button
*ngIf="showDelete() || showBulkTrashOptions"
type="button"
bitMenuItem
(click)="bulkDelete()"
>
<button *ngIf="showDelete" type="button" bitMenuItem (click)="bulkDelete()">
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{ (showBulkTrashOptions ? "permanentlyDeleteSelected" : "delete") | i18n }}
@@ -123,6 +118,7 @@
[collections]="allCollections"
[checked]="selection.isSelected(item)"
[canEditCipher]="canEditCipher(item.cipher)"
[canManageCollection]="canManageCollection(item.cipher)"
(checkedToggled)="selection.toggle(item)"
(onEvent)="event($event)"
></tr>

View File

@@ -45,6 +45,7 @@ export class VaultItemsComponent {
@Input() viewingOrgVault: boolean;
@Input() addAccessStatus: number;
@Input() addAccessToggle: boolean;
@Input() activeCollection: CollectionView | undefined;
private _ciphers?: CipherView[] = [];
@Input() get ciphers(): CipherView[] {
@@ -90,11 +91,39 @@ export class VaultItemsComponent {
);
}
get showDelete(): boolean {
if (this.selection.selected.length === 0) {
return true;
}
const hasPersonalItems = this.hasPersonalItems();
const uniqueCipherOrgIds = this.getUniqueOrganizationIds();
const canManageCollectionCiphers = this.selection.selected
.filter((item) => item.cipher)
.every(({ cipher }) => this.canManageCollection(cipher));
const canDeleteCollections = this.selection.selected
.filter((item) => item.collection)
.every((item) => item.collection && this.canDeleteCollection(item.collection));
const userCanDeleteAccess = canManageCollectionCiphers && canDeleteCollections;
if (
userCanDeleteAccess ||
(hasPersonalItems && (!uniqueCipherOrgIds.size || userCanDeleteAccess))
) {
return true;
}
return false;
}
get disableMenu() {
return (
!this.bulkMoveAllowed &&
!this.showAssignToCollections() &&
!this.showDelete() &&
!this.showDelete &&
!this.showBulkEditCollectionAccess
);
}
@@ -198,6 +227,37 @@ export class VaultItemsComponent {
return (organization.canEditAllCiphers && this.viewingOrgVault) || cipher.edit;
}
protected canManageCollection(cipher: CipherView) {
// If the cipher is not part of an organization (personal item), user can manage it
if (cipher.organizationId == null) {
return true;
}
// Check for admin access in AC vault
if (this.showAdminActions) {
const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId);
// If the user is an admin, they can delete an unassigned cipher
if (cipher.collectionIds.length === 0) {
return organization?.canEditUnmanagedCollections === true;
}
if (
organization?.permissions.editAnyCollection ||
(organization?.allowAdminAccessToAllCollectionItems && organization.isAdmin)
) {
return true;
}
}
if (this.activeCollection) {
return this.activeCollection.manage === true;
}
return this.allCollections
.filter((c) => cipher.collectionIds.includes(c.id))
.some((collection) => collection.manage);
}
private refreshItems() {
const collections: VaultItem[] = this.collections.map((collection) => ({ collection }));
const ciphers: VaultItem[] = this.ciphers.map((cipher) => ({ cipher }));
@@ -267,37 +327,6 @@ export class VaultItemsComponent {
return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected;
}
protected showDelete(): boolean {
if (this.selection.selected.length === 0) {
return true;
}
const hasPersonalItems = this.hasPersonalItems();
const uniqueCipherOrgIds = this.getUniqueOrganizationIds();
const organizations = Array.from(uniqueCipherOrgIds, (orgId) =>
this.allOrganizations.find((o) => o.id === orgId),
);
const canEditOrManageAllCiphers =
organizations.length > 0 && organizations.every((org) => org?.canEditAllCiphers);
const canDeleteCollections = this.selection.selected
.filter((item) => item.collection)
.every((item) => item.collection && this.canDeleteCollection(item.collection));
const userCanDeleteAccess =
(canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && canDeleteCollections;
if (
userCanDeleteAccess ||
(hasPersonalItems && (!uniqueCipherOrgIds.size || userCanDeleteAccess))
) {
return true;
}
return false;
}
private hasPersonalItems(): boolean {
return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null);
}

View File

@@ -995,7 +995,7 @@
(click)="delete()"
class="btn btn-outline-danger"
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
*ngIf="editMode && !cloneMode && !(!cipher.edit && editMode)"
*ngIf="editMode && !cloneMode && (canDeleteCipher$ | async)"
[disabled]="$any(deleteBtn).loading"
[appApiAction]="deletePromise"
>

View File

@@ -25,6 +25,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -71,6 +72,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
datePipe: DatePipe,
configService: ConfigService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
cipherAuthorizationService: CipherAuthorizationService,
) {
super(
cipherService,
@@ -91,6 +93,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
window,
datePipe,
configService,
cipherAuthorizationService,
);
}

View File

@@ -55,7 +55,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -173,6 +173,7 @@ export class VaultComponent implements OnInit, OnDestroy {
protected selectedCollection: TreeNode<CollectionView> | undefined;
protected canCreateCollections = false;
protected currentSearchText$: Observable<string>;
private activeUserId: UserId;
private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null);
private destroy$ = new Subject<void>();
@@ -219,6 +220,10 @@ export class VaultComponent implements OnInit, OnDestroy {
: "trashCleanupWarning",
);
this.activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const firstSetup$ = this.route.queryParams.pipe(
first(),
switchMap(async (params: Params) => {
@@ -603,11 +608,17 @@ export class VaultComponent implements OnInit, OnDestroy {
* Open the combined view / edit dialog for a cipher.
* @param mode - Starting mode of the dialog.
* @param formConfig - Configuration for the form when editing/adding a cipher.
* @param activeCollectionId - The active collection ID.
*/
async openVaultItemDialog(mode: VaultItemDialogMode, formConfig: CipherFormConfig) {
async openVaultItemDialog(
mode: VaultItemDialogMode,
formConfig: CipherFormConfig,
activeCollectionId?: CollectionId,
) {
this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, {
mode,
formConfig,
activeCollectionId,
});
const result = await lastValueFrom(this.vaultItemDialogRef.closed);
@@ -713,6 +724,8 @@ export class VaultComponent implements OnInit, OnDestroy {
this.cipherAddEditModalRef,
(comp) => {
comp.cipherId = id;
comp.collectionId = this.selectedCollection?.node.id;
comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
@@ -787,7 +800,11 @@ export class VaultComponent implements OnInit, OnDestroy {
cipher.type,
);
await this.openVaultItemDialog("view", cipherFormConfig);
await this.openVaultItemDialog(
"view",
cipherFormConfig,
this.selectedCollection?.node.id as CollectionId,
);
}
async addCollection() {

View File

@@ -22,7 +22,7 @@
buttonType="danger"
[appA11yTitle]="'delete' | i18n"
[bitAction]="delete"
[disabled]="!cipher.edit"
[disabled]="!(canDeleteCipher$ | async)"
>
<i class="bwi bwi-trash bwi-lg bwi-fw" aria-hidden="true"></i>
</button>

View File

@@ -14,6 +14,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { ViewCipherDialogParams, ViewCipherDialogResult, ViewComponent } from "./view.component";
@@ -62,6 +63,12 @@ describe("ViewComponent", () => {
useValue: mock<BillingAccountProfileStateService>(),
},
{ provide: ConfigService, useValue: mock<ConfigService>() },
{
provide: CipherAuthorizationService,
useValue: {
canDeleteCipher$: jest.fn().mockReturnValue(true),
},
},
],
}).compileComponents();

View File

@@ -1,6 +1,7 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Inject, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import { CollectionView } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -8,10 +9,12 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { CollectionId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import {
AsyncActionsModule,
DialogModule,
@@ -34,6 +37,11 @@ export interface ViewCipherDialogParams {
*/
collections?: CollectionView[];
/**
* Optional collection ID used to know the collection filter selected.
*/
activeCollectionId?: CollectionId;
/**
* If true, the edit button will be disabled in the dialog.
*/
@@ -71,6 +79,8 @@ export class ViewComponent implements OnInit {
cipherTypeString: string;
organization: Organization;
canDeleteCipher$: Observable<boolean>;
constructor(
@Inject(DIALOG_DATA) public params: ViewCipherDialogParams,
private dialogRef: DialogRef<ViewCipherDialogCloseResult>,
@@ -81,6 +91,7 @@ export class ViewComponent implements OnInit {
private cipherService: CipherService,
private toastService: ToastService,
private organizationService: OrganizationService,
private cipherAuthorizationService: CipherAuthorizationService,
) {}
/**
@@ -93,6 +104,10 @@ export class ViewComponent implements OnInit {
if (this.cipher.organizationId) {
this.organization = await this.organizationService.get(this.cipher.organizationId);
}
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
this.params.activeCollectionId,
]);
}
/**

View File

@@ -21,6 +21,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -57,6 +58,7 @@ export class AddEditComponent extends BaseAddEditComponent {
datePipe: DatePipe,
configService: ConfigService,
billingAccountProfileStateService: BillingAccountProfileStateService,
cipherAuthorizationService: CipherAuthorizationService,
) {
super(
cipherService,
@@ -79,6 +81,7 @@ export class AddEditComponent extends BaseAddEditComponent {
datePipe,
configService,
billingAccountProfileStateService,
cipherAuthorizationService,
);
}
@@ -90,6 +93,7 @@ export class AddEditComponent extends BaseAddEditComponent {
}
protected async loadCipher() {
this.isAdminConsoleAction = true;
// Calling loadCipher first to assess if the cipher is unassigned. If null use apiService getCipherAdmin
const firstCipherCheck = await super.loadCipher();

View File

@@ -70,6 +70,7 @@
[viewingOrgVault]="true"
[addAccessStatus]="addAccessStatus$ | async"
[addAccessToggle]="showAddAccessToggle"
[activeCollection]="selectedCollection?.node"
>
</app-vault-items>
<ng-container *ngIf="!performingInitialLoad && isEmpty">

View File

@@ -347,6 +347,7 @@ export class VaultComponent implements OnInit, OnDestroy {
// If the user can edit all ciphers for the organization then fetch them ALL.
if (organization.canEditAllCiphers) {
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
ciphers?.forEach((c) => (c.edit = true));
} else {
// Otherwise, only fetch ciphers they have access to (includes unassigned for admins).
ciphers = await this.cipherService.getManyFromApiForOrganization(organization.id);
@@ -828,6 +829,7 @@ export class VaultComponent implements OnInit, OnDestroy {
comp.organization = this.organization;
comp.organizationId = this.organization.id;
comp.cipherId = cipher?.id;
comp.collectionId = this.activeFilter.collectionId;
comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
@@ -897,7 +899,12 @@ export class VaultComponent implements OnInit, OnDestroy {
cipher.type,
);
await this.openVaultItemDialog("view", cipherFormConfig, cipher);
await this.openVaultItemDialog(
"view",
cipherFormConfig,
cipher,
this.activeFilter.collectionId as CollectionId,
);
}
/**
@@ -907,6 +914,7 @@ export class VaultComponent implements OnInit, OnDestroy {
mode: VaultItemDialogMode,
formConfig: CipherFormConfig,
cipher?: CipherView,
activeCollectionId?: CollectionId,
) {
const disableForm = cipher ? !cipher.edit && !this.organization.canEditAllCiphers : false;
// If the form is disabled, force the mode into `view`
@@ -915,6 +923,8 @@ export class VaultComponent implements OnInit, OnDestroy {
mode: dialogMode,
formConfig,
disableForm,
activeCollectionId,
isAdminConsoleAction: true,
});
const result = await lastValueFrom(this.vaultItemDialogRef.closed);

View File

@@ -9457,5 +9457,15 @@
},
"permanentlyDeleteAttachmentConfirmation": {
"message": "Are you sure you want to permanently delete this attachment?"
},
"manageSubscriptionFromThe": {
"message": "Manage subscription from the",
"description": "This represents the beginning of a sentence. The full sentence will be 'Manage subscription from the Provider Portal', but 'Provider Portal' will be a link and thus cannot be included in the translation file."
},
"toHostBitwardenOnYourOwnServer": {
"message": "To host Bitwarden on your own server, you will need to upload your license file. To support Free Families plans and advanced billing capabilities for your self-hosted organization, you will need to set up automatic sync in your self-hosted organization."
},
"selfHostingTitleProper": {
"message": "Self-Hosting"
}
}

View File

@@ -0,0 +1,16 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
export class MemberCipherDetailsResponse extends BaseResponse {
userName: string;
email: string;
useKeyConnector: boolean;
cipherIds: string[] = [];
constructor(response: any) {
super(response);
this.userName = this.getResponseProperty("UserName");
this.email = this.getResponseProperty("Email");
this.useKeyConnector = this.getResponseProperty("UseKeyConnector");
this.cipherIds = this.getResponseProperty("CipherIds");
}
}

View File

@@ -0,0 +1,105 @@
import { mock } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service";
const mockMemberCipherDetails: any = {
data: [
{
userName: "David Brent",
email: "david.brent@wernhamhogg.uk",
usesKeyConnector: true,
cipherIds: [
"cbea34a8-bde4-46ad-9d19-b05001228ab1",
"cbea34a8-bde4-46ad-9d19-b05001228ab2",
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
"cbea34a8-bde4-46ad-9d19-b05001227nm5",
],
},
{
userName: "Tim Canterbury",
email: "tim.canterbury@wernhamhogg.uk",
usesKeyConnector: false,
cipherIds: [
"cbea34a8-bde4-46ad-9d19-b05001228ab2",
"cbea34a8-bde4-46ad-9d19-b05001228cd3",
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
"cbea34a8-bde4-46ad-9d19-b05001227nm5",
],
},
{
userName: "Gareth Keenan",
email: "gareth.keenan@wernhamhogg.uk",
usesKeyConnector: true,
cipherIds: [
"cbea34a8-bde4-46ad-9d19-b05001228cd3",
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
"cbea34a8-bde4-46ad-9d19-b05001227nm5",
"cbea34a8-bde4-46ad-9d19-b05001227nm7",
],
},
{
userName: "Dawn Tinsley",
email: "dawn.tinsley@wernhamhogg.uk",
usesKeyConnector: true,
cipherIds: [
"cbea34a8-bde4-46ad-9d19-b05001228ab2",
"cbea34a8-bde4-46ad-9d19-b05001228cd3",
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
],
},
{
userName: "Keith Bishop",
email: "keith.bishop@wernhamhogg.uk",
usesKeyConnector: false,
cipherIds: [
"cbea34a8-bde4-46ad-9d19-b05001228ab1",
"cbea34a8-bde4-46ad-9d19-b05001228cd3",
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
"cbea34a8-bde4-46ad-9d19-b05001227nm5",
],
},
{
userName: "Chris Finch",
email: "chris.finch@wernhamhogg.uk",
usesKeyConnector: true,
cipherIds: [
"cbea34a8-bde4-46ad-9d19-b05001228ab2",
"cbea34a8-bde4-46ad-9d19-b05001228cd3",
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
],
},
],
};
describe("Member Cipher Details API Service", () => {
let memberCipherDetailsApiService: MemberCipherDetailsApiService;
const apiService = mock<ApiService>();
beforeEach(() => {
memberCipherDetailsApiService = new MemberCipherDetailsApiService(apiService);
jest.resetAllMocks();
});
it("instantiates", () => {
expect(memberCipherDetailsApiService).not.toBeFalsy();
});
it("getMemberCipherDetails retrieves data", async () => {
apiService.send.mockResolvedValue(mockMemberCipherDetails);
const orgId = "1234";
const result = await memberCipherDetailsApiService.getMemberCipherDetails(orgId);
expect(result).not.toBeNull();
expect(result).toHaveLength(6);
expect(apiService.send).toHaveBeenCalledWith(
"GET",
"/reports/member-cipher-details/" + orgId,
null,
true,
true,
);
});
});

View File

@@ -0,0 +1,27 @@
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { MemberCipherDetailsResponse } from "../response/member-cipher-details.response";
export class MemberCipherDetailsApiService {
constructor(private apiService: ApiService) {}
/**
* Returns a list of organization members with their assigned
* cipherIds
* @param orgId OrganizationId to get member cipher details for
* @returns List of organization members and assigned cipherIds
*/
async getMemberCipherDetails(orgId: string): Promise<MemberCipherDetailsResponse[]> {
const response = await this.apiService.send(
"GET",
"/reports/member-cipher-details/" + orgId,
null,
true,
true,
);
const listResponse = new ListResponse(response, MemberCipherDetailsResponse);
return listResponse.data.map((r) => new MemberCipherDetailsResponse(r));
}
}

View File

@@ -1,37 +0,0 @@
import { Component, Input } from "@angular/core";
import { ProviderUserStatusType } from "@bitwarden/common/admin-console/enums";
import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk-confirm.request";
import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-confirm.component";
import { BulkUserDetails } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component";
/**
* @deprecated Please use the {@link BulkConfirmDialogComponent} instead.
*/
@Component({
templateUrl:
"../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.html",
})
export class BulkConfirmComponent extends OrganizationBulkConfirmComponent {
@Input() providerId: string;
protected override isAccepted(user: BulkUserDetails) {
return user.status === ProviderUserStatusType.Accepted;
}
protected override async getPublicKeys() {
const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id));
return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
}
protected override getCryptoKey(): Promise<SymmetricCryptoKey> {
return this.cryptoService.getProviderKey(this.providerId);
}
protected override async postConfirmRequest(userIdsWithKeys: any[]) {
const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys);
return await this.apiService.postProviderUserBulkConfirm(this.providerId, request);
}
}

View File

@@ -1,24 +0,0 @@
import { Component, Input } from "@angular/core";
import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request";
import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-remove.component";
/**
* @deprecated Please use the {@link BulkRemoveDialogComponent} instead.
*/
@Component({
templateUrl:
"../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.html",
})
export class BulkRemoveComponent extends OrganizationBulkRemoveComponent {
@Input() providerId: string;
async deleteUsers() {
const request = new ProviderUserBulkRequest(this.users.map((user) => user.id));
return await this.apiService.deleteManyProviderUsers(this.providerId, request);
}
protected get removeUsersWarning() {
return this.i18nService.t("removeUsersWarning");
}
}

View File

@@ -27,7 +27,7 @@ type BulkConfirmDialogParams = {
@Component({
templateUrl:
"../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.html",
"../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.html",
})
export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
providerId: string;

View File

@@ -17,7 +17,7 @@ type BulkRemoveDialogParams = {
@Component({
templateUrl:
"../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.html",
"../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html",
})
export class BulkRemoveDialogComponent extends BaseBulkRemoveComponent {
providerId: string;

View File

@@ -1,203 +0,0 @@
<app-header>
<bit-search
class="tw-grow"
[formControl]="searchControl"
[placeholder]="'search' | i18n"
></bit-search>
<button type="button" bitButton buttonType="primary" (click)="invite()">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "inviteMember" | i18n }}
</button>
</app-header>
<div class="ml-auto d-flex tw-mb-4">
<bit-toggle-group
[selected]="status"
(selectedChange)="filter($event)"
[attr.aria-label]="'memberStatusFilter' | i18n"
>
<bit-toggle [value]="null">
{{ "all" | i18n }}
<span bitBadge variant="info" *ngIf="allCount">{{ allCount }}</span>
</bit-toggle>
<bit-toggle [value]="userStatusType.Invited">
{{ "invited" | i18n }}
<span bitBadge variant="info" *ngIf="invitedCount">{{ invitedCount }}</span>
</bit-toggle>
<bit-toggle [value]="userStatusType.Accepted">
{{ "needsConfirmation" | i18n }}
<span bitBadge variant="warning" *ngIf="acceptedCount">{{ acceptedCount }}</span>
</bit-toggle>
</bit-toggle-group>
<div class="dropdown ml-3" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
id="bulkActionsButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
<button type="button" class="dropdown-item" appStopClick (click)="bulkReinvite()">
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "reinviteSelected" | i18n }}
</button>
<button
type="button"
class="dropdown-item text-success"
appStopClick
(click)="bulkConfirm()"
*ngIf="showBulkConfirmUsers"
>
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirmSelected" | i18n }}
</button>
<button type="button" class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</button>
<div class="dropdown-divider"></div>
<button type="button" class="dropdown-item" appStopClick (click)="selectAll(true)">
<i class="bwi bwi-fw bwi-check-square" aria-hidden="true"></i>
{{ "selectAll" | i18n }}
</button>
<button type="button" class="dropdown-item" appStopClick (click)="selectAll(false)">
<i class="bwi bwi-fw bwi-minus-square" aria-hidden="true"></i>
{{ "unselectAll" | i18n }}
</button>
</div>
</div>
</div>
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container
*ngIf="
!loading &&
((isPaging$ | async)
? pagedUsers
: (users | search: searchControl.value : 'name' : 'email' : 'id')) as searchedUsers
"
>
<p *ngIf="!searchedUsers.length">{{ "noUsersInList" | i18n }}</p>
<ng-container *ngIf="searchedUsers.length">
<app-callout
type="info"
title="{{ 'confirmUsers' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="showConfirmUsers"
>
{{ "providerUsersNeedConfirmed" | i18n }}
</app-callout>
<table
class="table table-hover table-list"
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!(isPaging$ | async)"
(scrolled)="loadMore()"
>
<tbody>
<tr *ngFor="let u of searchedUsers">
<td (click)="checkUser(u)" class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="$any(u).checked" appStopProp />
</td>
<td width="30">
<bit-avatar [text]="u | userName" [id]="u.userId" size="small"></bit-avatar>
</td>
<td>
<a href="#" appStopClick (click)="edit(u)">{{ u.email }}</a>
<span bitBadge variant="secondary" *ngIf="u.status === userStatusType.Invited">{{
"invited" | i18n
}}</span>
<span bitBadge variant="warning" *ngIf="u.status === userStatusType.Accepted">{{
"needsConfirmation" | i18n
}}</span>
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
</td>
<td>
<ng-container *ngIf="$any(u).twoFactorEnabled">
<i
class="bwi bwi-lock"
title="{{ 'userUsingTwoStep' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "userUsingTwoStep" | i18n }}</span>
</ng-container>
</td>
<td>
<span *ngIf="u.type === userType.ProviderAdmin">{{ "providerAdmin" | i18n }}</span>
<span *ngIf="u.type === userType.ServiceUser">{{ "serviceUser" | i18n }}</span>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a
class="dropdown-item"
href="#"
appStopClick
(click)="reinvite(u)"
*ngIf="u.status === userStatusType.Invited"
>
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "resendInvitation" | i18n }}
</a>
<a
class="dropdown-item text-success"
href="#"
appStopClick
(click)="confirm(u)"
*ngIf="u.status === userStatusType.Accepted"
>
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirm" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="events(u)"
*ngIf="accessEvents && u.status === userStatusType.Confirmed"
>
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
{{ "eventLogs" | i18n }}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
</ng-container>
<ng-template #addEdit></ng-template>
<ng-template #confirmTemplate></ng-template>
<ng-template #bulkStatusTemplate></ng-template>
<ng-template #bulkConfirmTemplate></ng-template>
<ng-template #bulkRemoveTemplate></ng-template>

View File

@@ -1,267 +0,0 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { lastValueFrom } from "rxjs";
import { first } from "rxjs/operators";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderUserStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request";
import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-confirm.request";
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { BasePeopleComponent } from "@bitwarden/web-vault/app/admin-console/common/base.people.component";
import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component";
import { BulkStatusComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component";
import { BulkConfirmComponent } from "./bulk/bulk-confirm.component";
import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
import { UserAddEditComponent } from "./user-add-edit.component";
/**
* @deprecated Please use the {@link MembersComponent} instead.
*/
@Component({
selector: "provider-people",
templateUrl: "people.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class PeopleComponent
extends BasePeopleComponent<ProviderUserUserDetailsResponse>
implements OnInit
{
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
groupsModalRef: ViewContainerRef;
@ViewChild("bulkStatusTemplate", { read: ViewContainerRef, static: true })
bulkStatusModalRef: ViewContainerRef;
@ViewChild("bulkConfirmTemplate", { read: ViewContainerRef, static: true })
bulkConfirmModalRef: ViewContainerRef;
@ViewChild("bulkRemoveTemplate", { read: ViewContainerRef, static: true })
bulkRemoveModalRef: ViewContainerRef;
userType = ProviderUserType;
userStatusType = ProviderUserStatusType;
status: ProviderUserStatusType = null;
providerId: string;
accessEvents = false;
constructor(
apiService: ApiService,
private route: ActivatedRoute,
i18nService: I18nService,
modalService: ModalService,
platformUtilsService: PlatformUtilsService,
cryptoService: CryptoService,
private encryptService: EncryptService,
private router: Router,
searchService: SearchService,
validationService: ValidationService,
logService: LogService,
searchPipe: SearchPipe,
userNamePipe: UserNamePipe,
private providerService: ProviderService,
dialogService: DialogService,
organizationManagementPreferencesService: OrganizationManagementPreferencesService,
private configService: ConfigService,
protected toastService: ToastService,
) {
super(
apiService,
searchService,
i18nService,
platformUtilsService,
cryptoService,
validationService,
modalService,
logService,
searchPipe,
userNamePipe,
dialogService,
organizationManagementPreferencesService,
toastService,
);
}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
const provider = await this.providerService.get(this.providerId);
if (!provider.canManageUsers) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["../"], { relativeTo: this.route });
return;
}
this.accessEvents = provider.useEvents;
await this.load();
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchControl.setValue(qParams.search);
if (qParams.viewEvents != null) {
const user = this.users.filter((u) => u.id === qParams.viewEvents);
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.events(user[0]);
}
}
});
});
}
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
return this.apiService.getProviderUsers(this.providerId);
}
deleteUser(id: string): Promise<any> {
return this.apiService.deleteProviderUser(this.providerId, id);
}
revokeUser(id: string): Promise<any> {
// Not implemented.
return null;
}
restoreUser(id: string): Promise<any> {
// Not implemented.
return null;
}
reinviteUser(id: string): Promise<any> {
return this.apiService.postProviderUserReinvite(this.providerId, id);
}
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
const key = await this.encryptService.rsaEncrypt(providerKey.key, publicKey);
const request = new ProviderUserConfirmRequest();
request.key = key.encryptedString;
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
}
async edit(user: ProviderUserUserDetailsResponse) {
const [modal] = await this.modalService.openViewRef(
UserAddEditComponent,
this.addEditModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId;
comp.providerUserId = user != null ? user.id : null;
comp.savedUser.subscribe(() => {
modal.close();
this.load();
});
comp.deletedUser.subscribe(() => {
modal.close();
this.removeUser(user);
});
},
);
}
async events(user: ProviderUserUserDetailsResponse) {
await openEntityEventsDialog(this.dialogService, {
data: {
name: this.userNamePipe.transform(user),
providerId: this.providerId,
entityId: user.id,
showUser: false,
entity: "user",
},
});
}
async bulkRemove() {
if (this.actionPromise != null) {
return;
}
const [modal] = await this.modalService.openViewRef(
BulkRemoveComponent,
this.bulkRemoveModalRef,
(comp) => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
},
);
await modal.onClosedPromise();
await this.load();
}
async bulkReinvite() {
if (this.actionPromise != null) {
return;
}
const users = this.getCheckedUsers();
const filteredUsers = users.filter((u) => u.status === ProviderUserStatusType.Invited);
if (filteredUsers.length <= 0) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("noSelectedUsersApplicable"),
});
return;
}
try {
const request = new ProviderUserBulkRequest(filteredUsers.map((user) => user.id));
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
// Bulk Status component open
const dialogRef = BulkStatusComponent.open(this.dialogService, {
data: {
users: users,
filteredUsers: filteredUsers,
request: response,
successfulMessage: this.i18nService.t("bulkReinviteMessage"),
},
});
await lastValueFrom(dialogRef.closed);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async bulkConfirm() {
if (this.actionPromise != null) {
return;
}
const [modal] = await this.modalService.openViewRef(
BulkConfirmComponent,
this.bulkConfirmModalRef,
(comp) => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
},
);
await modal.onClosedPromise();
await this.load();
}
}

View File

@@ -13,7 +13,7 @@
route="manage"
*ngIf="showManageTab(provider)"
>
<bit-nav-item [text]="'people' | i18n" route="manage/people"></bit-nav-item>
<bit-nav-item [text]="'members' | i18n" route="manage/members"></bit-nav-item>
<bit-nav-item
[text]="'eventLogs' | i18n"
route="manage/events"

View File

@@ -2,10 +2,8 @@ import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { authGuard } from "@bitwarden/angular/auth/guards";
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ProvidersComponent } from "@bitwarden/web-vault/app/admin-console/providers/providers.component";
import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component";
import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component";
@@ -23,7 +21,6 @@ import { providerPermissionsGuard } from "./guards/provider-permissions.guard";
import { AcceptProviderComponent } from "./manage/accept-provider.component";
import { EventsComponent } from "./manage/events.component";
import { MembersComponent } from "./manage/members.component";
import { PeopleComponent } from "./manage/people.component";
import { ProvidersLayoutComponent } from "./providers-layout.component";
import { AccountComponent } from "./settings/account.component";
import { SetupProviderComponent } from "./setup/setup-provider.component";
@@ -98,22 +95,18 @@ const routes: Routes = [
{
path: "",
pathMatch: "full",
redirectTo: "people",
redirectTo: "members",
},
...featureFlaggedRoute({
defaultComponent: PeopleComponent,
flaggedComponent: MembersComponent,
featureFlag: FeatureFlag.AC2828_ProviderPortalMembersPage,
routeOptions: {
path: "people",
canActivate: [
providerPermissionsGuard((provider: Provider) => provider.canManageUsers),
],
data: {
titleId: "people",
},
{
path: "members",
component: MembersComponent,
canActivate: [
providerPermissionsGuard((provider: Provider) => provider.canManageUsers),
],
data: {
titleId: "members",
},
}),
},
{
path: "events",
component: EventsComponent,

View File

@@ -24,14 +24,11 @@ import { AddOrganizationComponent } from "./clients/add-organization.component";
import { ClientsComponent } from "./clients/clients.component";
import { CreateOrganizationComponent } from "./clients/create-organization.component";
import { AcceptProviderComponent } from "./manage/accept-provider.component";
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component";
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component";
import { AddEditMemberDialogComponent } from "./manage/dialogs/add-edit-member-dialog.component";
import { BulkConfirmDialogComponent } from "./manage/dialogs/bulk-confirm-dialog.component";
import { BulkRemoveDialogComponent } from "./manage/dialogs/bulk-remove-dialog.component";
import { EventsComponent } from "./manage/events.component";
import { MembersComponent } from "./manage/members.component";
import { PeopleComponent } from "./manage/people.component";
import { UserAddEditComponent } from "./manage/user-add-edit.component";
import { ProvidersLayoutComponent } from "./providers-layout.component";
import { ProvidersRoutingModule } from "./providers-routing.module";
@@ -58,14 +55,11 @@ import { SetupComponent } from "./setup/setup.component";
AcceptProviderComponent,
AccountComponent,
AddOrganizationComponent,
BulkConfirmComponent,
BulkConfirmDialogComponent,
BulkRemoveComponent,
BulkRemoveDialogComponent,
ClientsComponent,
CreateOrganizationComponent,
EventsComponent,
PeopleComponent,
MembersComponent,
SetupComponent,
SetupProviderComponent,

View File

@@ -242,6 +242,10 @@ import {
} from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import {
CipherAuthorizationService,
DefaultCipherAuthorizationService,
} from "@bitwarden/common/vault/services/cipher-authorization.service";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
@@ -1340,6 +1344,11 @@ const safeProviders: SafeProvider[] = [
ApiServiceAbstraction,
],
}),
safeProvider({
provide: CipherAuthorizationService,
useClass: DefaultCipherAuthorizationService,
deps: [CollectionService, OrganizationServiceAbstraction],
}),
];
@NgModule({

View File

@@ -23,7 +23,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
@@ -36,6 +36,7 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -47,6 +48,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
@Input() type: CipherType;
@Input() collectionIds: string[];
@Input() organizationId: string = null;
@Input() collectionId: string = null;
@Output() onSavedCipher = new EventEmitter<CipherView>();
@Output() onDeletedCipher = new EventEmitter<CipherView>();
@Output() onRestoredCipher = new EventEmitter<CipherView>();
@@ -57,6 +59,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
@Output() onGeneratePassword = new EventEmitter();
@Output() onGenerateUsername = new EventEmitter();
canDeleteCipher$: Observable<boolean>;
editMode = false;
cipher: CipherView;
folders$: Observable<FolderView[]>;
@@ -83,6 +87,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
reprompt = false;
canUseReprompt = true;
organization: Organization;
/**
* Flag to determine if the action is being performed from the admin console.
*/
isAdminConsoleAction: boolean = false;
protected componentName = "";
protected destroy$ = new Subject<void>();
@@ -118,6 +126,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected win: Window,
protected datePipe: DatePipe,
protected configService: ConfigService,
protected cipherAuthorizationService: CipherAuthorizationService,
) {
this.typeOptions = [
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
@@ -314,6 +323,12 @@ export class AddEditComponent implements OnInit, OnDestroy {
if (this.reprompt) {
this.cipher.login.autofillOnPageLoad = this.autofillOnPageLoadOptions[2].value;
}
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(
this.cipher,
[this.collectionId as CollectionId],
this.isAdminConsoleAction,
);
}
async submit(): Promise<boolean> {

View File

@@ -9,7 +9,7 @@ import {
OnInit,
Output,
} from "@angular/core";
import { firstValueFrom, map } from "rxjs";
import { firstValueFrom, map, Observable } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
@@ -28,6 +28,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { CollectionId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
@@ -37,6 +38,7 @@ import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -45,12 +47,14 @@ const BroadcasterSubscriptionId = "ViewComponent";
@Directive()
export class ViewComponent implements OnDestroy, OnInit {
@Input() cipherId: string;
@Input() collectionId: string;
@Output() onEditCipher = new EventEmitter<CipherView>();
@Output() onCloneCipher = new EventEmitter<CipherView>();
@Output() onShareCipher = new EventEmitter<CipherView>();
@Output() onDeletedCipher = new EventEmitter<CipherView>();
@Output() onRestoredCipher = new EventEmitter<CipherView>();
canDeleteCipher$: Observable<boolean>;
cipher: CipherView;
showPassword: boolean;
showPasswordCount: boolean;
@@ -105,6 +109,7 @@ export class ViewComponent implements OnDestroy, OnInit {
protected datePipe: DatePipe,
protected accountService: AccountService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private cipherAuthorizationService: CipherAuthorizationService,
) {}
ngOnInit() {
@@ -144,6 +149,9 @@ export class ViewComponent implements OnDestroy, OnInit {
);
this.showPremiumRequiredTotp =
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
this.collectionId as CollectionId,
]);
if (this.cipher.folderId) {
this.folder = await (

View File

@@ -14,17 +14,17 @@ export interface AnonLayoutWrapperData {
* If a string is provided, it will be presented as is (ex: Organization name)
* If a Translation object (supports placeholders) is provided, it will be translated
*/
pageTitle?: string | Translation;
pageTitle?: string | Translation | null;
/**
* The optional subtitle of the page.
* If a string is provided, it will be presented as is (ex: user's email)
* If a Translation object (supports placeholders) is provided, it will be translated
*/
pageSubtitle?: string | Translation;
pageSubtitle?: string | Translation | null;
/**
* The optional icon to display on the page.
*/
pageIcon?: Icon;
pageIcon?: Icon | null;
/**
* Optional flag to either show the optional environment selector (false) or just a readonly hostname (true).
*/
@@ -114,19 +114,23 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
return;
}
if (data.pageTitle) {
this.pageTitle = this.handleStringOrTranslation(data.pageTitle);
// Null emissions are used to reset the page data as all fields are optional.
if (data.pageTitle !== undefined) {
this.pageTitle =
data.pageTitle !== null ? this.handleStringOrTranslation(data.pageTitle) : null;
}
if (data.pageSubtitle) {
this.pageSubtitle = this.handleStringOrTranslation(data.pageSubtitle);
if (data.pageSubtitle !== undefined) {
this.pageSubtitle =
data.pageSubtitle !== null ? this.handleStringOrTranslation(data.pageSubtitle) : null;
}
if (data.pageIcon) {
this.pageIcon = data.pageIcon;
if (data.pageIcon !== undefined) {
this.pageIcon = data.pageIcon !== null ? data.pageIcon : null;
}
if (data.showReadonlyHostname != null) {
if (data.showReadonlyHostname !== undefined) {
this.showReadonlyHostname = data.showReadonlyHostname;
}

View File

@@ -1,10 +1,12 @@
import { BaseResponse } from "../../../models/response/base.response";
export class OrganizationBillingMetadataResponse extends BaseResponse {
isEligibleForSelfHost: boolean;
isOnSecretsManagerStandalone: boolean;
constructor(response: any) {
super(response);
this.isEligibleForSelfHost = this.getResponseProperty("IsEligibleForSelfHost");
this.isOnSecretsManagerStandalone = this.getResponseProperty("IsOnSecretsManagerStandalone");
}
}

View File

@@ -20,7 +20,7 @@ export enum FeatureFlag {
EnableTimeThreshold = "PM-5864-dollar-threshold",
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page",
VaultBulkManagementAction = "vault-bulk-management-action",
IdpAutoSubmitLogin = "idp-auto-submit-login",
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub",
@@ -67,7 +67,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.EnableTimeThreshold]: FALSE,
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
[FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE,
[FeatureFlag.VaultBulkManagementAction]: FALSE,
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
[FeatureFlag.EnableUpgradePasswordManagerSub]: FALSE,

View File

@@ -0,0 +1,200 @@
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { CollectionService, CollectionView } 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 { CollectionId } from "@bitwarden/common/types/guid";
import { CipherView } from "../models/view/cipher.view";
import {
CipherAuthorizationService,
DefaultCipherAuthorizationService,
} from "./cipher-authorization.service";
describe("CipherAuthorizationService", () => {
let cipherAuthorizationService: CipherAuthorizationService;
const mockCollectionService = mock<CollectionService>();
const mockOrganizationService = mock<OrganizationService>();
// Mock factories
const createMockCipher = (
organizationId: string | null,
collectionIds: string[],
edit: boolean = true,
) => ({
organizationId,
collectionIds,
edit,
});
const createMockCollection = (id: string, manage: boolean) => ({
id,
manage,
});
const createMockOrganization = ({
allowAdminAccessToAllCollectionItems = false,
canEditAllCiphers = false,
canEditUnassignedCiphers = false,
} = {}) => ({
allowAdminAccessToAllCollectionItems,
canEditAllCiphers,
canEditUnassignedCiphers,
});
beforeEach(() => {
jest.clearAllMocks();
cipherAuthorizationService = new DefaultCipherAuthorizationService(
mockCollectionService,
mockOrganizationService,
);
});
describe("canDeleteCipher$", () => {
it("should return true if cipher has no organizationId", (done) => {
const cipher = createMockCipher(null, []) as CipherView;
cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => {
expect(result).toBe(true);
done();
});
});
it("should return true if isAdminConsoleAction is true and cipher is unassigned", (done) => {
const cipher = createMockCipher("org1", []) as CipherView;
const organization = createMockOrganization({ canEditUnassignedCiphers: true });
mockOrganizationService.get$.mockReturnValue(of(organization as Organization));
cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => {
expect(result).toBe(true);
done();
});
});
it("should return true if isAdminConsoleAction is true and user can edit all ciphers in the org", (done) => {
const cipher = createMockCipher("org1", ["col1"]) as CipherView;
const organization = createMockOrganization({ canEditAllCiphers: true });
mockOrganizationService.get$.mockReturnValue(of(organization as Organization));
cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => {
expect(result).toBe(true);
expect(mockOrganizationService.get$).toHaveBeenCalledWith("org1");
done();
});
});
it("should return false if isAdminConsoleAction is true but user does not have permission to edit unassigned ciphers", (done) => {
const cipher = createMockCipher("org1", []) as CipherView;
const organization = createMockOrganization({ canEditUnassignedCiphers: false });
mockOrganizationService.get$.mockReturnValue(of(organization as Organization));
cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => {
expect(result).toBe(false);
done();
});
});
it("should return true if activeCollectionId is provided and has manage permission", (done) => {
const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView;
const activeCollectionId = "col1" as CollectionId;
const org = createMockOrganization();
mockOrganizationService.get$.mockReturnValue(of(org as Organization));
const allCollections = [
createMockCollection("col1", true),
createMockCollection("col2", false),
];
mockCollectionService.decryptedCollectionViews$.mockReturnValue(
of(allCollections as CollectionView[]),
);
cipherAuthorizationService
.canDeleteCipher$(cipher, [activeCollectionId])
.subscribe((result) => {
expect(result).toBe(true);
expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([
"col1",
"col2",
] as CollectionId[]);
done();
});
});
it("should return false if activeCollectionId is provided and manage permission is not present", (done) => {
const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView;
const activeCollectionId = "col1" as CollectionId;
const org = createMockOrganization();
mockOrganizationService.get$.mockReturnValue(of(org as Organization));
const allCollections = [
createMockCollection("col1", false),
createMockCollection("col2", true),
];
mockCollectionService.decryptedCollectionViews$.mockReturnValue(
of(allCollections as CollectionView[]),
);
cipherAuthorizationService
.canDeleteCipher$(cipher, [activeCollectionId])
.subscribe((result) => {
expect(result).toBe(false);
expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([
"col1",
"col2",
] as CollectionId[]);
done();
});
});
it("should return true if any collection has manage permission", (done) => {
const cipher = createMockCipher("org1", ["col1", "col2", "col3"]) as CipherView;
const org = createMockOrganization();
mockOrganizationService.get$.mockReturnValue(of(org as Organization));
const allCollections = [
createMockCollection("col1", false),
createMockCollection("col2", true),
createMockCollection("col3", false),
];
mockCollectionService.decryptedCollectionViews$.mockReturnValue(
of(allCollections as CollectionView[]),
);
cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => {
expect(result).toBe(true);
expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([
"col1",
"col2",
"col3",
] as CollectionId[]);
done();
});
});
it("should return false if no collection has manage permission", (done) => {
const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView;
const org = createMockOrganization();
mockOrganizationService.get$.mockReturnValue(of(org as Organization));
const allCollections = [
createMockCollection("col1", false),
createMockCollection("col2", false),
];
mockCollectionService.decryptedCollectionViews$.mockReturnValue(
of(allCollections as CollectionView[]),
);
cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => {
expect(result).toBe(false);
expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([
"col1",
"col2",
] as CollectionId[]);
done();
});
});
});
});

View File

@@ -0,0 +1,86 @@
import { map, Observable, of, switchMap } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { CollectionId } from "@bitwarden/common/types/guid";
import { Cipher } from "../models/domain/cipher";
import { CipherView } from "../models/view/cipher.view";
/**
* Represents either a cipher or a cipher view.
*/
type CipherLike = Cipher | CipherView;
/**
* Service for managing user cipher authorization.
*/
export abstract class CipherAuthorizationService {
/**
* Determines if the user can delete the specified cipher.
*
* @param {CipherLike} cipher - The cipher object to evaluate for deletion permissions.
* @param {CollectionId[]} [allowedCollections] - Optional. The selected collection id from the vault filter.
* @param {boolean} isAdminConsoleAction - Optional. A flag indicating if the action is being performed from the admin console.
*
* @returns {Observable<boolean>} - An observable that emits a boolean value indicating if the user can delete the cipher.
*/
canDeleteCipher$: (
cipher: CipherLike,
allowedCollections?: CollectionId[],
isAdminConsoleAction?: boolean,
) => Observable<boolean>;
}
/**
* {@link CipherAuthorizationService}
*/
export class DefaultCipherAuthorizationService implements CipherAuthorizationService {
constructor(
private collectionService: CollectionService,
private organizationService: OrganizationService,
) {}
/**
*
* {@link CipherAuthorizationService.canDeleteCipher$}
*/
canDeleteCipher$(
cipher: CipherLike,
allowedCollections?: CollectionId[],
isAdminConsoleAction?: boolean,
): Observable<boolean> {
if (cipher.organizationId == null) {
return of(true);
}
return this.organizationService.get$(cipher.organizationId).pipe(
switchMap((organization) => {
if (isAdminConsoleAction) {
// If the user is an admin, they can delete an unassigned cipher
if (!cipher.collectionIds || cipher.collectionIds.length === 0) {
return of(organization?.canEditUnassignedCiphers === true);
}
if (organization?.canEditAllCiphers) {
return of(true);
}
}
return this.collectionService
.decryptedCollectionViews$(cipher.collectionIds as CollectionId[])
.pipe(
map((allCollections) => {
const shouldFilter = allowedCollections?.some(Boolean);
const collections = shouldFilter
? allCollections.filter((c) => allowedCollections.includes(c.id as CollectionId))
: allCollections;
return collections.some((collection) => collection.manage);
}),
);
}),
);
}
}

View File

@@ -12,33 +12,44 @@
>
</bit-form-field>
<bit-form-field>
<bit-label *ngIf="!shouldShowNewPassword">{{ "password" | i18n }}</bit-label>
<bit-label *ngIf="shouldShowNewPassword">{{ "newPassword" | i18n }}</bit-label>
<bit-label>{{ "password" | i18n }}</bit-label>
<input bitInput type="password" formControlName="password" />
<ng-container *ngIf="!hasPassword">
<button
data-testid="toggle-visibility-for-password"
type="button"
bitIconButton
bitSuffix
bitPasswordInputToggle
></button>
<button
type="button"
bitIconButton="bwi-generate"
bitSuffix
[appA11yTitle]="'generatePassword' | i18n"
[disabled]="!config.areSendsAllowed"
(click)="generatePassword()"
data-testid="generate-password"
></button>
<button
type="button"
bitIconButton="bwi-clone"
bitSuffix
[appA11yTitle]="'copyPassword' | i18n"
[disabled]="!config.areSendsAllowed || !sendOptionsForm.get('password').value"
[valueLabel]="'password' | i18n"
[appCopyClick]="sendOptionsForm.get('password').value"
showToast
></button>
</ng-container>
<button
data-testid="toggle-visibility-for-password"
*ngIf="hasPassword"
class="tw-border-l-0 last:tw-rounded-r focus-visible:tw-border-l focus-visible:tw-ml-[-1px]"
type="button"
bitIconButton
bitSuffix
bitPasswordInputToggle
></button>
<button
type="button"
bitIconButton="bwi-generate"
bitSuffix
[appA11yTitle]="'generatePassword' | i18n"
[disabled]="!config.areSendsAllowed"
(click)="generatePassword()"
data-testid="generate-password"
></button>
<button
type="button"
bitIconButton="bwi-clone"
bitSuffix
[appA11yTitle]="'copyPassword' | i18n"
[disabled]="!config.areSendsAllowed || !sendOptionsForm.get('password').value"
[valueLabel]="'password' | i18n"
[appCopyClick]="sendOptionsForm.get('password').value"
buttonType="danger"
bitIconButton="bwi-minus-circle"
[appA11yTitle]="'removePassword' | i18n"
[bitAction]="removePassword"
showToast
></button>
<bit-hint>{{ "sendPasswordDescV3" | i18n }}</bit-hint>

View File

@@ -7,14 +7,20 @@ import { firstValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import {
AsyncActionsModule,
ButtonModule,
CardComponent,
CheckboxModule,
DialogService,
FormFieldModule,
IconButtonModule,
SectionComponent,
SectionHeaderComponent,
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { CredentialGeneratorService, Generators } from "@bitwarden/generator-core";
@@ -27,6 +33,8 @@ import { SendFormContainer } from "../../send-form-container";
templateUrl: "./send-options.component.html",
standalone: true,
imports: [
AsyncActionsModule,
ButtonModule,
CardComponent,
CheckboxModule,
CommonModule,
@@ -53,7 +61,7 @@ export class SendOptionsComponent implements OnInit {
hideEmail: [false as boolean],
});
get shouldShowNewPassword(): boolean {
get hasPassword(): boolean {
return this.originalSendView && this.originalSendView.password !== null;
}
@@ -71,8 +79,12 @@ export class SendOptionsComponent implements OnInit {
constructor(
private sendFormContainer: SendFormContainer,
private dialogService: DialogService,
private sendApiService: SendApiService,
private formBuilder: FormBuilder,
private policyService: PolicyService,
private i18nService: I18nService,
private toastService: ToastService,
private generatorService: CredentialGeneratorService,
) {
this.sendFormContainer.registerChildForm("sendOptionsForm", this.sendOptionsForm);
@@ -110,16 +122,49 @@ export class SendOptionsComponent implements OnInit {
});
};
removePassword = async () => {
if (!this.originalSendView || !this.originalSendView.password) {
return;
}
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "removePassword" },
content: { key: "removePasswordConfirmation" },
type: "warning",
});
if (!confirmed) {
return false;
}
await this.sendApiService.removePassword(this.originalSendView.id);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("removedPassword"),
});
this.originalSendView.password = null;
this.sendOptionsForm.patchValue({
password: null,
});
this.sendOptionsForm.get("password")?.enable();
};
ngOnInit() {
if (this.sendFormContainer.originalSendView) {
this.sendOptionsForm.patchValue({
maxAccessCount: this.sendFormContainer.originalSendView.maxAccessCount,
accessCount: this.sendFormContainer.originalSendView.accessCount,
password: null,
password: this.hasPassword ? "************" : null, // 12 masked characters as a placeholder
hideEmail: this.sendFormContainer.originalSendView.hideEmail,
notes: this.sendFormContainer.originalSendView.notes,
});
}
if (this.hasPassword) {
this.sendOptionsForm.get("password")?.disable();
}
if (!this.config.areSendsAllowed) {
this.sendOptionsForm.disable();
}

View File

@@ -6,7 +6,7 @@
<bit-card>
<bit-form-field>
<bit-label>{{ "name" | i18n }}</bit-label>
<input bitInput type="text" formControlName="name" />
<input appAutofocus bitInput type="text" formControlName="name" />
</bit-form-field>
<tools-send-text-details

123
package-lock.json generated
View File

@@ -114,7 +114,7 @@
"@types/node-fetch": "2.6.4",
"@types/node-forge": "1.3.11",
"@types/node-ipc": "9.2.3",
"@types/papaparse": "5.3.14",
"@types/papaparse": "5.3.15",
"@types/proper-lockfile": "4.1.4",
"@types/retry": "0.12.5",
"@types/zxcvbn": "4.4.5",
@@ -146,7 +146,6 @@
"eslint-plugin-storybook": "0.8.0",
"eslint-plugin-tailwindcss": "3.17.4",
"gulp": "4.0.2",
"gulp-filter": "9.0.1",
"gulp-if": "3.0.0",
"gulp-json-editor": "2.6.0",
"gulp-replace": "1.1.4",
@@ -248,7 +247,7 @@
},
"apps/web": {
"name": "@bitwarden/web-vault",
"version": "2024.10.2"
"version": "2024.10.3"
},
"libs/admin-console": {
"name": "@bitwarden/admin-console",
@@ -9698,9 +9697,9 @@
}
},
"node_modules/@types/papaparse": {
"version": "5.3.14",
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz",
"integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==",
"version": "5.3.15",
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.15.tgz",
"integrity": "sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12146,19 +12145,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/array-differ": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/array-differ/-/array-differ-4.0.0.tgz",
"integrity": "sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/array-each": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
@@ -20884,47 +20870,6 @@
"object.assign": "^4.1.0"
}
},
"node_modules/gulp-filter": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-9.0.1.tgz",
"integrity": "sha512-knVYL8h9bfYIeft3VokVTkuaWJkQJMrFCS3yVjZQC6BGg+1dZFoeUY++B9D2X4eFpeNTx9StWK0qnDby3NO3PA==",
"dev": true,
"license": "MIT",
"dependencies": {
"multimatch": "^7.0.0",
"plugin-error": "^2.0.1",
"slash": "^5.1.0",
"streamfilter": "^3.0.0",
"to-absolute-glob": "^3.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
},
"peerDependencies": {
"gulp": ">=4"
},
"peerDependenciesMeta": {
"gulp": {
"optional": true
}
}
},
"node_modules/gulp-filter/node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gulp-if": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-3.0.0.tgz",
@@ -28422,37 +28367,6 @@
"multicast-dns": "cli.js"
}
},
"node_modules/multimatch": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/multimatch/-/multimatch-7.0.0.tgz",
"integrity": "sha512-SYU3HBAdF4psHEL/+jXDKHO95/m5P2RvboHT2Y0WtTttvJLP4H/2WS9WlQPFvF6C8d6SpLw8vjCnQOnVIVOSJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"array-differ": "^4.0.0",
"array-union": "^3.0.1",
"minimatch": "^9.0.3"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/multimatch/node_modules/array-union": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz",
"integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/multistream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz",
@@ -35367,19 +35281,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/streamfilter": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/streamfilter/-/streamfilter-3.0.0.tgz",
"integrity": "sha512-kvKNfXCmUyC8lAXSSHCIXBUlo/lhsLcCU/OmzACZYpRUdtKIH68xYhm/+HI15jFJYtNJGYtCgn2wmIiExY1VwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readable-stream": "^3.0.6"
},
"engines": {
"node": ">=8.12.0"
}
},
"node_modules/streaming-json-stringify": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/streaming-json-stringify/-/streaming-json-stringify-3.1.0.tgz",
@@ -36573,20 +36474,6 @@
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/to-absolute-glob": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-3.0.0.tgz",
"integrity": "sha512-loO/XEWTRqpfcpI7+Jr2RR2Umaaozx1t6OSVWtMi0oy5F/Fxg3IC+D/TToDnxyAGs7uZBGT/6XmyDUxgsObJXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-absolute": "^1.0.0",
"is-negated-glob": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",

View File

@@ -27,7 +27,7 @@
"storybook": "ng run components:storybook",
"build-storybook": "ng run components:build-storybook",
"build-storybook:ci": "ng run components:build-storybook --webpack-stats-json",
"postinstall": "patch-package"
"postinstall": "patch-package && rimraf ./node_modules/@types/glob && rimraf ./node_modules/@types/minimatch"
},
"workspaces": [
"apps/*",
@@ -75,7 +75,7 @@
"@types/node-fetch": "2.6.4",
"@types/node-forge": "1.3.11",
"@types/node-ipc": "9.2.3",
"@types/papaparse": "5.3.14",
"@types/papaparse": "5.3.15",
"@types/proper-lockfile": "4.1.4",
"@types/retry": "0.12.5",
"@types/zxcvbn": "4.4.5",
@@ -107,7 +107,6 @@
"eslint-plugin-storybook": "0.8.0",
"eslint-plugin-tailwindcss": "3.17.4",
"gulp": "4.0.2",
"gulp-filter": "9.0.1",
"gulp-if": "3.0.0",
"gulp-json-editor": "2.6.0",
"gulp-replace": "1.1.4",
@@ -212,11 +211,7 @@
"@storybook/angular": {
"zone.js": "$zone.js"
},
"replacestream": "4.0.3",
"@types/minimatch": "3.0.5",
"@electron/asar": {
"@types/glob": "7.1.3"
}
"replacestream": "4.0.3"
},
"lint-staged": {
"*": "prettier --cache --ignore-unknown --write",