1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-21 20:04:02 +00:00

Merge branch 'main' into autofill/atdb_v2

This commit is contained in:
Colton Hurst
2025-08-06 16:08:41 -04:00
65 changed files with 454 additions and 168 deletions

View File

@@ -1,9 +1,16 @@
import path, { dirname, join } from "path";
import { createRequire } from "module";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";
import type { StorybookConfig } from "@storybook/web-components-webpack5";
import remarkGfm from "remark-gfm";
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
const currentFile = fileURLToPath(import.meta.url);
const currentDirectory = dirname(currentFile);
const require = createRequire(import.meta.url);
const getAbsolutePath = (value: string): string =>
dirname(require.resolve(join(value, "package.json")));
@@ -43,7 +50,7 @@ const config: StorybookConfig = {
if (config.resolve) {
config.resolve.plugins = [
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, "../../../../../tsconfig.json"),
configFile: resolve(currentDirectory, "../../../../../tsconfig.json"),
}),
] as any;
}

View File

@@ -8,6 +8,7 @@ import { Spinner } from "../icons";
export type ActionButtonProps = {
buttonText: string | TemplateResult;
dataTestId?: string;
disabled?: boolean;
isLoading?: boolean;
theme: Theme;
@@ -17,6 +18,7 @@ export type ActionButtonProps = {
export function ActionButton({
buttonText,
dataTestId,
disabled = false,
isLoading = false,
theme,
@@ -32,6 +34,7 @@ export function ActionButton({
return html`
<button
class=${actionButtonStyles({ disabled, fullWidth, isLoading, theme })}
data-testid="${dataTestId}"
title=${buttonText}
type="button"
@click=${handleButtonClick}

View File

@@ -2,6 +2,8 @@ import { Meta, StoryObj } from "@storybook/web-components";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar";
import { getNotificationTestId } from "../../../../../notification/bar";
import {
AtRiskNotification,
AtRiskNotificationProps,
@@ -30,8 +32,10 @@ export default {
},
} as Meta<AtRiskNotificationProps>;
const Template = (args: AtRiskNotificationProps) => AtRiskNotification({ ...args });
const Template = (args: AtRiskNotificationProps) => {
const notificationTestId = getNotificationTestId(NotificationTypes.AtRiskPassword);
return AtRiskNotification({ ...args, notificationTestId });
};
export const Default: StoryObj<AtRiskNotificationProps> = {
render: Template,
};

View File

@@ -18,11 +18,13 @@ export type AtRiskNotificationProps = NotificationBarIframeInitData & {
handleCloseNotification: (e: Event) => void;
} & {
i18n: I18n;
notificationTestId: string;
};
export function AtRiskNotification({
handleCloseNotification,
i18n,
notificationTestId,
theme = ThemeTypes.Light,
params,
}: AtRiskNotificationProps) {
@@ -33,7 +35,7 @@ export function AtRiskNotification({
);
return html`
<div class=${atRiskNotificationContainerStyles(theme)}>
<div data-testid="${notificationTestId}" class=${atRiskNotificationContainerStyles(theme)}>
${NotificationHeader({
handleCloseNotification,
i18n,

View File

@@ -26,6 +26,7 @@ export function AtRiskNotificationFooter({
open(passwordChangeUri, "_blank");
},
buttonText: AdditionalTasksButtonContent({ buttonText: i18n.changePassword, theme }),
dataTestId: "change-password-button",
theme,
fullWidth: false,
})}

View File

@@ -200,7 +200,7 @@ export function getNotificationTestId(
[NotificationTypes.Unlock]: "unlock-notification-bar",
[NotificationTypes.Add]: "save-notification-bar",
[NotificationTypes.Change]: "update-notification-bar",
[NotificationTypes.AtRiskPassword]: "at-risk-password-notification-bar",
[NotificationTypes.AtRiskPassword]: "at-risk-notification-bar",
}[notificationType];
}
@@ -287,6 +287,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
type: notificationBarIframeInitData.type as NotificationType,
theme: resolvedTheme,
i18n,
notificationTestId,
params: initData.params,
handleCloseNotification,
}),

View File

@@ -10,7 +10,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -163,7 +163,7 @@ describe("OpenAttachmentsComponent", () => {
it("sets `cipherIsAPartOfFreeOrg` to true when the cipher is a part of a free organization", async () => {
cipherView.organizationId = "888-333-333";
org.productTierType = ProductTierType.Free;
org.id = cipherView.organizationId;
org.id = cipherView.organizationId as OrganizationId;
await component.ngOnInit();
@@ -173,7 +173,7 @@ describe("OpenAttachmentsComponent", () => {
it("sets `cipherIsAPartOfFreeOrg` to false when the organization is not free", async () => {
cipherView.organizationId = "888-333-333";
org.productTierType = ProductTierType.Families;
org.id = cipherView.organizationId;
org.id = cipherView.organizationId as OrganizationId;
await component.ngOnInit();

View File

@@ -1,13 +1,14 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { SelectionReadOnly } from "../selection-read-only";
export class OrganizationCollectionRequest extends CollectionExport {
static template(): OrganizationCollectionRequest {
const req = new OrganizationCollectionRequest();
req.organizationId = "00000000-0000-0000-0000-000000000000";
req.organizationId = "00000000-0000-0000-0000-000000000000" as OrganizationId;
req.name = "Collection name";
req.externalId = null;
req.groups = [SelectionReadOnly.template(), SelectionReadOnly.template()];

View File

@@ -84,6 +84,7 @@
{{ trashCleanupWarning }}
</bit-callout>
<app-vault-items
#vaultItems
[ciphers]="ciphers"
[collections]="collections"
[allCollections]="allCollections"

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import {
BehaviorSubject,
@@ -81,6 +81,7 @@ import {
} from "@bitwarden/vault";
import { OrganizationResellerRenewalWarningComponent } from "@bitwarden/web-vault/app/billing/warnings/components/organization-reseller-renewal-warning.component";
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/warnings/services/organization-warnings.service";
import { VaultItemsComponent } from "@bitwarden/web-vault/app/vault/components/vault-items/vault-items.component";
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
import {
@@ -204,6 +205,8 @@ export class VaultComponent implements OnInit, OnDestroy {
protected addAccessStatus$ = new BehaviorSubject<AddAccessStatusType>(0);
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
@ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent<CipherView>;
private readonly unpaidSubscriptionDialog$ = this.accountService.activeAccount$.pipe(
map((account) => account?.id),
switchMap((id) =>
@@ -278,9 +281,16 @@ export class VaultComponent implements OnInit, OnDestroy {
);
const filter$ = this.routedVaultFilterService.filter$;
// FIXME: The RoutedVaultFilterModel uses `organizationId: Unassigned` to represent the individual vault,
// but that is never used in Admin Console. This function narrows the type so it doesn't pollute our code here,
// but really we should change to using our own vault filter model that only represents valid states in AC.
const isOrganizationId = (value: OrganizationId | Unassigned): value is OrganizationId =>
value !== Unassigned;
const organizationId$ = filter$.pipe(
map((filter) => filter.organizationId),
filter((filter) => filter !== undefined),
filter(isOrganizationId),
distinctUntilChanged(),
);
@@ -373,9 +383,12 @@ export class VaultComponent implements OnInit, OnDestroy {
this.allCollectionsWithoutUnassigned$,
]).pipe(
map(([organizationId, allCollections]) => {
// FIXME: We should not assert that the Unassigned type is a CollectionId.
// Instead we should consider representing the Unassigned collection as a different object, given that
// it is not actually a collection.
const noneCollection = new CollectionAdminView();
noneCollection.name = this.i18nService.t("unassigned");
noneCollection.id = Unassigned;
noneCollection.id = Unassigned as CollectionId;
noneCollection.organizationId = organizationId;
return allCollections.concat(noneCollection);
}),
@@ -1420,6 +1433,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private refresh() {
this.refresh$.next();
this.vaultItemsComponent?.clearSelection();
}
private go(queryParams: any = null) {

View File

@@ -1,12 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, Input, OnInit } from "@angular/core";
import { UntypedFormControl, UntypedFormGroup } from "@angular/forms";
import { Observable, of } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
export abstract class BasePolicy {
abstract name: string;
@@ -14,38 +14,56 @@ export abstract class BasePolicy {
abstract type: PolicyType;
abstract component: any;
display(organization: Organization) {
return true;
/**
* If true, the description will be reused in the policy edit modal. Set this to false if you
* have more complex requirements that you will implement in your template instead.
**/
showDescription: boolean = true;
display(organization: Organization, configService: ConfigService): Observable<boolean> {
return of(true);
}
}
@Directive()
export abstract class BasePolicyComponent implements OnInit {
@Input() policyResponse: PolicyResponse;
@Input() policy: BasePolicy;
@Input() policyResponse: PolicyResponse | undefined;
@Input() policy: BasePolicy | undefined;
enabled = new UntypedFormControl(false);
data: UntypedFormGroup = null;
data: UntypedFormGroup | undefined;
ngOnInit(): void {
this.enabled.setValue(this.policyResponse.enabled);
this.enabled.setValue(this.policyResponse?.enabled);
if (this.policyResponse.data != null) {
if (this.policyResponse?.data != null) {
this.loadData();
}
}
buildRequest() {
const request = new PolicyRequest();
request.enabled = this.enabled.value;
request.type = this.policy.type;
request.data = this.buildRequestData();
if (!this.policy) {
throw new Error("Policy was not found");
}
const request: PolicyRequest = {
type: this.policy.type,
enabled: this.enabled.value,
data: this.buildRequestData(),
};
return Promise.resolve(request);
}
/**
* Enable optional validation before sumitting a respose for policy submission
* */
confirm(): Promise<boolean> | boolean {
return true;
}
protected loadData() {
this.data.patchValue(this.policyResponse.data ?? {});
this.data?.patchValue(this.policyResponse?.data ?? {});
}
protected buildRequestData() {

View File

@@ -3,6 +3,7 @@ export { BasePolicy, BasePolicyComponent } from "./base-policy.component";
export { DisableSendPolicy } from "./disable-send.component";
export { MasterPasswordPolicy } from "./master-password.component";
export { PasswordGeneratorPolicy } from "./password-generator.component";
export { vNextOrganizationDataOwnershipPolicy } from "./vnext-organization-data-ownership.component";
export { OrganizationDataOwnershipPolicy } from "./organization-data-ownership.component";
export { RequireSsoPolicy } from "./require-sso.component";
export { ResetPasswordPolicy } from "./reset-password.component";

View File

@@ -1,6 +1,10 @@
import { Component } from "@angular/core";
import { map, Observable } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
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 { BasePolicy, BasePolicyComponent } from "./base-policy.component";
@@ -9,6 +13,12 @@ export class OrganizationDataOwnershipPolicy extends BasePolicy {
description = "personalOwnershipPolicyDesc";
type = PolicyType.OrganizationDataOwnership;
component = OrganizationDataOwnershipPolicyComponent;
display(organization: Organization, configService: ConfigService): Observable<boolean> {
return configService
.getFeatureFlag$(FeatureFlag.CreateDefaultLocation)
.pipe(map((enabled) => !enabled));
}
}
@Component({

View File

@@ -1,38 +1,45 @@
<app-header>
@let organization = organization$ | async;
<button
bitBadge
class="!tw-align-middle"
(click)="changePlan(organization)"
*ngIf="isBreadcrumbingEnabled$ | async"
slot="title-suffix"
type="button"
variant="primary"
>
{{ "upgrade" | i18n }}
</button>
@if (isBreadcrumbingEnabled$ | async) {
<button
bitBadge
class="!tw-align-middle"
(click)="changePlan(organization)"
slot="title-suffix"
type="button"
variant="primary"
>
{{ "upgrade" | i18n }}
</button>
}
</app-header>
<bit-container>
<ng-container *ngIf="loading">
@if (loading) {
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<bit-table *ngIf="!loading">
<ng-template body>
<tr bitRow *ngFor="let p of policies">
<td bitCell *ngIf="p.display(organization)" ngPreserveWhitespaces>
<button type="button" bitLink (click)="edit(p)">{{ p.name | i18n }}</button>
<span bitBadge variant="success" *ngIf="policiesEnabledMap.get(p.type)">{{
"on" | i18n
}}</span>
<small class="tw-text-muted tw-block">{{ p.description | i18n }}</small>
</td>
</tr>
</ng-template>
</bit-table>
}
@if (!loading) {
<bit-table>
<ng-template body>
@for (p of policies; track p.name) {
@if (p.display(organization, configService) | async) {
<tr bitRow>
<td bitCell ngPreserveWhitespaces>
<button type="button" bitLink (click)="edit(p)">{{ p.name | i18n }}</button>
@if (policiesEnabledMap.get(p.type)) {
<span bitBadge variant="success">{{ "on" | i18n }}</span>
}
<small class="tw-text-muted tw-block">{{ p.description | i18n }}</small>
</td>
</tr>
}
}
</ng-template>
</bit-table>
}
</bit-container>

View File

@@ -15,7 +15,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { DialogService } from "@bitwarden/components";
import {
@@ -25,7 +24,7 @@ import {
import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
import { PolicyListService } from "../../core/policy-list.service";
import { BasePolicy, RestrictedItemTypesPolicy } from "../policies";
import { BasePolicy } from "../policies";
import { CollectionDialogTabType } from "../shared/components/collection-dialog";
import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component";
@@ -53,7 +52,7 @@ export class PoliciesComponent implements OnInit {
private policyListService: PolicyListService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
private dialogService: DialogService,
private configService: ConfigService,
protected configService: ConfigService,
) {}
async ngOnInit() {
@@ -71,35 +70,31 @@ export class PoliciesComponent implements OnInit {
await this.load();
// Handle policies component launch from Event message
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.policyId != null) {
const policyIdFromEvents: string = qParams.policyId;
for (const orgPolicy of this.orgPolicies) {
if (orgPolicy.id === policyIdFromEvents) {
for (let i = 0; i < this.policies.length; i++) {
if (this.policies[i].type === orgPolicy.type) {
// 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.edit(this.policies[i]);
break;
this.route.queryParams
.pipe(first())
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
.subscribe(async (qParams) => {
if (qParams.policyId != null) {
const policyIdFromEvents: string = qParams.policyId;
for (const orgPolicy of this.orgPolicies) {
if (orgPolicy.id === policyIdFromEvents) {
for (let i = 0; i < this.policies.length; i++) {
if (this.policies[i].type === orgPolicy.type) {
// 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.edit(this.policies[i]);
break;
}
}
break;
}
break;
}
}
}
});
});
});
}
async load() {
if (
(await this.configService.getFeatureFlag(FeatureFlag.RemoveCardItemTypePolicy)) &&
this.policyListService.getPolicies().every((p) => !(p instanceof RestrictedItemTypesPolicy))
) {
this.policyListService.addPolicies([new RestrictedItemTypesPolicy()]);
}
const response = await this.policyApiService.getPolicies(this.organizationId);
this.orgPolicies = response.data != null && response.data.length > 0 ? response.data : [];
this.orgPolicies.forEach((op) => {

View File

@@ -22,7 +22,9 @@
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div [hidden]="loading">
<p bitTypography="body1">{{ policy.description | i18n }}</p>
@if (policy.showDescription) {
<p bitTypography="body1">{{ policy.description | i18n }}</p>
}
<ng-template #policyForm></ng-template>
</div>
</ng-container>

View File

@@ -128,13 +128,20 @@ export class PolicyEditComponent implements AfterViewInit {
}
submit = async () => {
if ((await this.policyComponent.confirm()) == false) {
this.dialogRef.close();
return;
}
let request: PolicyRequest;
try {
request = await this.policyComponent.buildRequest();
} catch (e) {
this.toastService.showToast({ variant: "error", title: null, message: e.message });
return;
}
await this.policyApiService.putPolicy(this.data.organizationId, this.data.policy.type, request);
this.toastService.showToast({
variant: "success",

View File

@@ -1,7 +1,9 @@
import { Component } from "@angular/core";
import { of } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
@@ -11,8 +13,8 @@ export class RequireSsoPolicy extends BasePolicy {
type = PolicyType.RequireSso;
component = RequireSsoPolicyComponent;
display(organization: Organization) {
return organization.useSso;
display(organization: Organization, configService: ConfigService) {
return of(organization.useSso);
}
}

View File

@@ -1,6 +1,6 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { firstValueFrom, of } from "rxjs";
import {
getOrganizationById,
@@ -10,6 +10,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
@@ -19,8 +20,8 @@ export class ResetPasswordPolicy extends BasePolicy {
type = PolicyType.ResetPassword;
component = ResetPasswordPolicyComponent;
display(organization: Organization) {
return organization.useResetPassword;
display(organization: Organization, configService: ConfigService) {
return of(organization.useResetPassword);
}
}
@@ -52,6 +53,10 @@ export class ResetPasswordPolicyComponent extends BasePolicyComponent implements
throw new Error("No user found.");
}
if (!this.policyResponse) {
throw new Error("Policies not found");
}
const organization = await firstValueFrom(
this.organizationService
.organizations$(userId)

View File

@@ -1,6 +1,10 @@
import { Component } from "@angular/core";
import { Observable } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
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 { BasePolicy, BasePolicyComponent } from "./base-policy.component";
@@ -9,6 +13,10 @@ export class RestrictedItemTypesPolicy extends BasePolicy {
description = "restrictedItemTypePolicyDesc";
type = PolicyType.RestrictedItemTypes;
component = RestrictedItemTypesPolicyComponent;
display(organization: Organization, configService: ConfigService): Observable<boolean> {
return configService.getFeatureFlag$(FeatureFlag.RemoveCardItemTypePolicy);
}
}
@Component({

View File

@@ -20,6 +20,9 @@ export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnI
async ngOnInit() {
super.ngOnInit();
if (!this.policyResponse) {
throw new Error("Policies not found");
}
if (!this.policyResponse.canToggleState) {
this.enabled.disable();
}

View File

@@ -0,0 +1,57 @@
<p>
{{ "organizationDataOwnershipContent" | i18n }}
<a
bitLink
href="https://bitwarden.com/resources/credential-lifecycle-management/"
target="_blank"
>
{{ "organizationDataOwnershipContentAnchor" | i18n }}.
</a>
</p>
<bit-form-control>
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
<bit-label>{{ "turnOn" | i18n }}</bit-label>
</bit-form-control>
<ng-template #dialog>
<bit-simple-dialog background="alt">
<span bitDialogTitle>{{ "organizationDataOwnershipWarningTitle" | i18n }}</span>
<ng-container bitDialogContent>
<div class="tw-text-left tw-overflow-hidden">
{{ "organizationDataOwnershipWarningContentTop" | i18n }}
<div class="tw-flex tw-flex-col tw-p-2">
<ul class="tw-list-disc tw-pl-5 tw-space-y-2 tw-break-words tw-mb-0">
<li>
{{ "organizationDataOwnershipWarning1" | i18n }}
</li>
<li>
{{ "organizationDataOwnershipWarning2" | i18n }}
</li>
<li>
{{ "organizationDataOwnershipWarning3" | i18n }}
</li>
</ul>
</div>
{{ "organizationDataOwnershipWarningContentBottom" | i18n }}
<a
bitLink
href="https://bitwarden.com/resources/credential-lifecycle-management/"
target="_blank"
>
{{ "organizationDataOwnershipContentAnchor" | i18n }}.
</a>
</div>
</ng-container>
<ng-container bitDialogFooter>
<span class="tw-flex tw-gap-2">
<button bitButton buttonType="primary" [bitDialogClose]="true" type="submit">
{{ "continue" | i18n }}
</button>
<button bitButton buttonType="secondary" [bitDialogClose]="false" type="button">
{{ "cancel" | i18n }}
</button>
</span>
</ng-container>
</bit-simple-dialog>
</ng-template>

View File

@@ -0,0 +1,50 @@
import { Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
import { lastValueFrom, Observable } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
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 { DialogService } from "@bitwarden/components";
import { SharedModule } from "../../../shared";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
export class vNextOrganizationDataOwnershipPolicy extends BasePolicy {
name = "organizationDataOwnership";
description = "organizationDataOwnershipDesc";
type = PolicyType.OrganizationDataOwnership;
component = vNextOrganizationDataOwnershipPolicyComponent;
showDescription = false;
override display(organization: Organization, configService: ConfigService): Observable<boolean> {
return configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation);
}
}
@Component({
selector: "vnext-policy-organization-data-ownership",
templateUrl: "vnext-organization-data-ownership.component.html",
standalone: true,
imports: [SharedModule],
})
export class vNextOrganizationDataOwnershipPolicyComponent
extends BasePolicyComponent
implements OnInit
{
constructor(private dialogService: DialogService) {
super();
}
@ViewChild("dialog", { static: true }) warningContent!: TemplateRef<unknown>;
override async confirm(): Promise<boolean> {
if (this.policyResponse?.enabled && !this.enabled.value) {
const dialogRef = this.dialogService.open(this.warningContent);
const result = await lastValueFrom(dialogRef.closed);
return Boolean(result);
}
return true;
}
}

View File

@@ -39,6 +39,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { getById } from "@bitwarden/common/platform/misc";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import {
DIALOG_DATA,
DialogConfig,
@@ -87,8 +88,8 @@ enum ButtonType {
}
export interface CollectionDialogParams {
collectionId?: string;
organizationId: string;
collectionId?: CollectionId;
organizationId: OrganizationId;
initialTab?: CollectionDialogTabType;
parentCollectionId?: string;
showOrgSelector?: boolean;
@@ -136,7 +137,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
externalId: { value: "", disabled: true },
parent: undefined as string | undefined,
access: [[] as AccessItemValue[]],
selectedOrg: "",
selectedOrg: "" as OrganizationId,
});
protected PermissionMode = PermissionMode;
protected showDeleteButton = false;

View File

@@ -35,12 +35,14 @@ import {
MasterPasswordPolicy,
PasswordGeneratorPolicy,
OrganizationDataOwnershipPolicy,
vNextOrganizationDataOwnershipPolicy,
RequireSsoPolicy,
ResetPasswordPolicy,
SendOptionsPolicy,
SingleOrgPolicy,
TwoFactorAuthenticationPolicy,
RemoveUnlockWithPinPolicy,
RestrictedItemTypesPolicy,
} from "./admin-console/organizations/policies";
const BroadcasterSubscriptionId = "AppComponent";
@@ -244,8 +246,10 @@ export class AppComponent implements OnDestroy, OnInit {
new SingleOrgPolicy(),
new RequireSsoPolicy(),
new OrganizationDataOwnershipPolicy(),
new vNextOrganizationDataOwnershipPolicy(),
new DisableSendPolicy(),
new SendOptionsPolicy(),
new RestrictedItemTypesPolicy(),
]);
}

View File

@@ -1,8 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Guid } from "@bitwarden/common/types/guid";
import { OrganizationId } from "@bitwarden/common/types/guid";
export class RequestSMAccessRequest {
OrganizationId: Guid;
OrganizationId: OrganizationId;
EmailContent: string;
}

View File

@@ -10,7 +10,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Guid } from "@bitwarden/common/types/guid";
import { NoItemsModule, SearchModule, ToastService } from "@bitwarden/components";
import { HeaderModule } from "../../layouts/header/header.module";
@@ -63,7 +62,7 @@ export class RequestSMAccessComponent implements OnInit {
const formValue = this.requestAccessForm.value;
const request = new RequestSMAccessRequest();
request.OrganizationId = formValue.selectedOrganization.id as Guid;
request.OrganizationId = formValue.selectedOrganization.id;
request.EmailContent = formValue.requestAccessEmailContents;
await this.smLandingApiService.requestSMAccessFromAdmins(request);

View File

@@ -166,6 +166,10 @@ export class VaultItemsComponent<C extends CipherViewLike> {
);
}
clearSelection() {
this.selection.clear();
}
get showExtraColumn() {
return this.showCollections || this.showGroups || this.showOwner;
}

View File

@@ -29,6 +29,7 @@ import {
} from "@bitwarden/common/platform/abstractions/environment.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -262,7 +263,7 @@ export const OrganizationTrash: Story = {
};
const unassignedCollection = new CollectionAdminView();
unassignedCollection.id = Unassigned;
unassignedCollection.id = Unassigned as CollectionId;
unassignedCollection.name = "Unassigned";
export const OrganizationTopLevelCollection: Story = {
args: {
@@ -327,7 +328,7 @@ function createCollectionView(i: number): CollectionAdminView {
const organization = organizations[i % (organizations.length + 1)];
const group = groups[i % (groups.length + 1)];
const view = new CollectionAdminView();
view.id = `collection-${i}`;
view.id = `collection-${i}` as CollectionId;
view.name = `Collection ${i}`;
view.organizationId = organization?.id;
view.manage = true;
@@ -357,7 +358,7 @@ function createGroupView(i: number): GroupView {
function createOrganization(i: number): Organization {
const organization = new Organization();
organization.id = `organization-${i}`;
organization.id = `organization-${i}` as OrganizationId;
organization.name = `Organization ${i}`;
organization.type = OrganizationUserType.Owner;
organization.permissions = new PermissionsApi();

View File

@@ -2,6 +2,8 @@ import { Injectable, OnDestroy } from "@angular/core";
import { ActivatedRoute, NavigationExtras } from "@angular/router";
import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import {
isRoutedVaultFilterItemType,
RoutedVaultFilterModel,
@@ -31,10 +33,12 @@ export class RoutedVaultFilterService implements OnDestroy {
const type = isRoutedVaultFilterItemType(unsafeType) ? unsafeType : undefined;
return {
collectionId: queryParams.get("collectionId") ?? undefined,
collectionId: (queryParams.get("collectionId") as CollectionId) ?? undefined,
folderId: queryParams.get("folderId") ?? undefined,
organizationId:
params.get("organizationId") ?? queryParams.get("organizationId") ?? undefined,
(params.get("organizationId") as OrganizationId) ??
(queryParams.get("organizationId") as OrganizationId) ??
undefined,
organizationIdParamType:
params.get("organizationId") != undefined ? ("path" as const) : ("query" as const),
type,

View File

@@ -28,7 +28,7 @@ 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 { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { OrganizationId, 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 } from "@bitwarden/common/vault/enums";
@@ -209,7 +209,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
protected getOrganizationFilterMyVault(): TreeNode<OrganizationFilter> {
const myVault = new Organization() as OrganizationFilter;
myVault.id = "MyVault";
myVault.id = "MyVault" as OrganizationId;
myVault.icon = "bwi-user";
myVault.enabled = true;
myVault.hideOptions = true;

View File

@@ -1,4 +1,5 @@
import { Unassigned } from "@bitwarden/admin-console/common";
import { CollectionId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
@@ -65,7 +66,7 @@ export class RoutedVaultFilterBridge implements VaultFilter {
let type: RoutedVaultFilterItemType | undefined;
if (value?.node.id === "AllItems" && this.routedFilter.organizationIdParamType === "path") {
type = "all";
type = All;
} else if (
value?.node.id === "AllItems" &&
this.routedFilter.organizationIdParamType === "query"
@@ -98,7 +99,7 @@ export class RoutedVaultFilterBridge implements VaultFilter {
return this.legacyFilter.selectedCollectionNode;
}
set selectedCollectionNode(value: TreeNode<CollectionFilter>) {
let collectionId: string | undefined;
let collectionId: CollectionId | All | Unassigned | undefined;
if (value != null && value.node.id === null) {
collectionId = Unassigned;

View File

@@ -1,4 +1,11 @@
import { Unassigned } from "@bitwarden/admin-console/common";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
/**
* A constant used to represent viewing "all" of a particular filter.
*/
export const All = "all";
export type All = typeof All;
// TODO: Remove `All` when moving to vertical navigation.
const itemTypes = [
@@ -19,9 +26,9 @@ export function isRoutedVaultFilterItemType(value: unknown): value is RoutedVaul
}
export interface RoutedVaultFilterModel {
collectionId?: string;
collectionId?: CollectionId | All | Unassigned;
folderId?: string;
organizationId?: string;
organizationId?: OrganizationId | Unassigned;
type?: RoutedVaultFilterItemType;
organizationIdParamType?: "path" | "query";

View File

@@ -37,6 +37,7 @@
{{ trashCleanupWarning }}
</bit-callout>
<app-vault-items
#vaultItems
[ciphers]="ciphers"
[collections]="collections"
[allCollections]="allCollections"

View File

@@ -108,6 +108,7 @@ import {
} from "../components/vault-item-dialog/vault-item-dialog.component";
import { VaultItem } from "../components/vault-items/vault-item";
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
import { VaultItemsComponent } from "../components/vault-items/vault-items.component";
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
import {
@@ -156,6 +157,7 @@ const SearchTextDebounceInterval = 200;
})
export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestroy {
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
@ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent<C>;
trashCleanupWarning: string = null;
kdfIterations: number;
@@ -1281,6 +1283,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
private refresh() {
this.refresh$.next();
this.vaultItemsComponent?.clearSelection();
}
private async go(queryParams: any = null) {

View File

@@ -5429,6 +5429,37 @@
"organizationDataOwnership": {
"message": "Enforce organization data ownership"
},
"organizationDataOwnershipDesc": {
"message": "Require all items to be owned by an organization, removing the option to store items at the account level.",
"description": "This is the policy description shown in the policy list."
},
"organizationDataOwnershipContent": {
"message": "All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection be available for each member to store items. Learn more about managing the ",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection be available for each member to store items. Learn more about managing the credential lifecycle.'"
},
"organizationDataOwnershipContentAnchor":{
"message": "credential lifecycle",
"description": "This will be used as a hyperlink"
},
"organizationDataOwnershipWarningTitle":{
"message": "Are you sure you want to proceed?"
},
"organizationDataOwnershipWarning1":{
"message": "will remain accessible to members"
},
"organizationDataOwnershipWarning2":{
"message": "will not be automatically selected when creating new items"
},
"organizationDataOwnershipWarning3":{
"message": "cannot be managed from the Admin Console until the user is offboarded"
},
"organizationDataOwnershipWarningContentTop":{
"message": "By turning this policy off, the default collection: "
},
"organizationDataOwnershipWarningContentBottom":{
"message": "Learn more about the ",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'"
},
"personalOwnership": {
"message": "Remove individual vault"
},
@@ -10965,5 +10996,11 @@
},
"unlimitedSecretsAndProjects": {
"message": "Unlimited secrets and projects"
},
"providersubscriptionCanceled": {
"message": "Subscription canceled"
},
"providersubCanceledmessage": {
"message" : "To resubscribe, contact Bitwarden Customer Support."
}
}

View File

@@ -1,7 +1,9 @@
import { Component } from "@angular/core";
import { of } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import {
BasePolicy,
BasePolicyComponent,
@@ -13,8 +15,8 @@ export class ActivateAutofillPolicy extends BasePolicy {
type = PolicyType.ActivateAutofill;
component = ActivateAutofillPolicyComponent;
display(organization: Organization) {
return organization.useActivateAutofillPolicy;
display(organization: Organization, configService: ConfigService) {
return of(organization.useActivateAutofillPolicy);
}
}

View File

@@ -158,7 +158,7 @@ export class ProviderSubscriptionStatusComponent {
}
case "incomplete_expired":
case "canceled": {
const canceledText = this.i18nService.t("canceled");
const canceledText = this.i18nService.t("providersubscriptionCanceled");
return {
status: {
label: defaultStatusLabel,
@@ -171,7 +171,7 @@ export class ProviderSubscriptionStatusComponent {
callout: {
severity: "danger",
header: canceledText,
body: this.i18nService.t("subscriptionCanceled"),
body: this.i18nService.t("providersubCanceledmessage"),
},
};
}

View File

@@ -4,7 +4,10 @@ import { CollectionAccessSelectionView } from "./collection-access-selection.vie
import { CollectionAccessDetailsResponse } from "./collection.response";
import { CollectionView } from "./collection.view";
// TODO: this is used to represent the pseudo "Unassigned" collection as well as
// the user's personal vault (as a pseudo organization). This should be separated out into different values.
export const Unassigned = "unassigned";
export type Unassigned = typeof Unassigned;
export class CollectionAdminView extends CollectionView {
groups: CollectionAccessSelectionView[] = [];

View File

@@ -54,7 +54,7 @@ describe("Collection", () => {
it("Decrypt", async () => {
const collection = new Collection();
collection.id = "id";
collection.id = "id" as CollectionId;
collection.organizationId = "orgId" as OrganizationId;
collection.name = mockEnc("encName");
collection.externalId = "extId";

View File

@@ -1,5 +1,6 @@
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import Domain, { EncryptableKeys } from "@bitwarden/common/platform/models/domain/domain-base";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { CollectionData } from "./collection.data";
@@ -13,8 +14,8 @@ export const CollectionTypes = {
export type CollectionType = (typeof CollectionTypes)[keyof typeof CollectionTypes];
export class Collection extends Domain {
id: string | undefined;
organizationId: string | undefined;
id: CollectionId | undefined;
organizationId: OrganizationId | undefined;
name: EncString | undefined;
externalId: string | undefined;
readOnly: boolean = false;

View File

@@ -2,6 +2,7 @@ import { Jsonify } from "type-fest";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { View } from "@bitwarden/common/models/view/view";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
import { Collection, CollectionType, CollectionTypes } from "./collection";
@@ -10,8 +11,8 @@ import { CollectionAccessDetailsResponse } from "./collection.response";
export const NestingDelimiter = "/";
export class CollectionView implements View, ITreeNodeObject {
id: string | undefined;
organizationId: string | undefined;
id: CollectionId | undefined;
organizationId: OrganizationId | undefined;
name: string = "";
externalId: string | undefined;
// readOnly applies to the items within a collection

View File

@@ -5,7 +5,7 @@ import { combineLatest, Observable, of, switchMap } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
@@ -42,7 +42,7 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
const orgIds = new Set(orgs.map((org) => org.id));
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
const hasManageCollections = collections.some(
(c) => c.manage && orgIds.has(c.organizationId!),
(c) => c.manage && orgIds.has(c.organizationId! as OrganizationId),
);
// When the user has dismissed the nudge or spotlight, return the nudge status directly

View File

@@ -5,7 +5,7 @@ import { combineLatest, Observable, of, switchMap } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
@@ -46,7 +46,7 @@ export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService {
const orgIds = new Set(orgs.map((org) => org.id));
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
const hasManageCollections = collections.some(
(c) => c.manage && orgIds.has(c.organizationId!),
(c) => c.manage && orgIds.has(c.organizationId! as OrganizationId),
);
// When the user has dismissed the nudge or spotlight, return the nudge status directly

View File

@@ -3,12 +3,13 @@
import { Jsonify } from "type-fest";
import { ProductTierType } from "../../../billing/enums";
import { OrganizationId } from "../../../types/guid";
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
import { PermissionsApi } from "../api/permissions.api";
import { OrganizationData } from "../data/organization.data";
export class Organization {
id: string;
id: OrganizationId;
name: string;
status: OrganizationUserStatusType;
@@ -99,7 +100,7 @@ export class Organization {
return;
}
this.id = obj.id;
this.id = obj.id as OrganizationId;
this.name = obj.name;
this.status = obj.status;
this.type = obj.type;

View File

@@ -2,14 +2,14 @@
// @ts-strict-ignore
import { ListResponse } from "../../../models/response/list.response";
import Domain from "../../../platform/models/domain/domain-base";
import { PolicyId } from "../../../types/guid";
import { OrganizationId, PolicyId } from "../../../types/guid";
import { PolicyType } from "../../enums";
import { PolicyData } from "../data/policy.data";
import { PolicyResponse } from "../response/policy.response";
export class Policy extends Domain {
id: PolicyId;
organizationId: string;
organizationId: OrganizationId;
type: PolicyType;
data: any;
@@ -26,7 +26,7 @@ export class Policy extends Domain {
}
this.id = obj.id;
this.organizationId = obj.organizationId;
this.organizationId = obj.organizationId as OrganizationId;
this.type = obj.type;
this.data = obj.data;
this.enabled = obj.enabled;

View File

@@ -1,9 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { PolicyType } from "../../enums";
export class PolicyRequest {
export type PolicyRequest = {
type: PolicyType;
enabled: boolean;
data: any;
}
};

View File

@@ -4,10 +4,12 @@
// eslint-disable-next-line no-restricted-imports
import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common";
import { CollectionId } from "../../types/guid";
import { CollectionExport } from "./collection.export";
export class CollectionWithIdExport extends CollectionExport {
id: string;
id: CollectionId;
static toView(req: CollectionWithIdExport, view = new CollectionView()) {
view.id = req.id;

View File

@@ -5,13 +5,14 @@
import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common";
import { EncString } from "../../key-management/crypto/models/enc-string";
import { emptyGuid, OrganizationId } from "../../types/guid";
import { safeGetString } from "./utils";
export class CollectionExport {
static template(): CollectionExport {
const req = new CollectionExport();
req.organizationId = "00000000-0000-0000-0000-000000000000";
req.organizationId = emptyGuid as OrganizationId;
req.name = "Collection name";
req.externalId = null;
return req;
@@ -35,7 +36,7 @@ export class CollectionExport {
return domain;
}
organizationId: string;
organizationId: OrganizationId;
name: string;
externalId: string;

View File

@@ -20,3 +20,8 @@ export type OrganizationIntegrationConfigurationId = Opaque<
string,
"OrganizationIntegrationConfigurationId"
>;
/**
* A string representation of an empty guid.
*/
export const emptyGuid = "00000000-0000-0000-0000-000000000000";

View File

@@ -2,14 +2,8 @@ import { Component, computed, HostBinding, input } from "@angular/core";
import { Utils } from "@bitwarden/common/platform/misc/utils";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
enum CharacterType {
Letter,
Emoji,
Special,
Number,
}
type CharacterType = "letter" | "emoji" | "special" | "number";
/**
* The color password is used primarily in the Generator pages and in the Login type form. It includes
* the logic for displaying letters as `text-main`, numbers as `primary`, and special symbols as
@@ -36,10 +30,10 @@ export class ColorPasswordComponent {
});
characterStyles: Record<CharacterType, string[]> = {
[CharacterType.Emoji]: [],
[CharacterType.Letter]: ["tw-text-main"],
[CharacterType.Special]: ["tw-text-danger"],
[CharacterType.Number]: ["tw-text-primary-600"],
emoji: [],
letter: ["tw-text-main"],
special: ["tw-text-danger"],
number: ["tw-text-primary-600"],
};
@HostBinding("class")
@@ -68,18 +62,18 @@ export class ColorPasswordComponent {
private getCharacterType(character: string): CharacterType {
if (character.match(Utils.regexpEmojiPresentation)) {
return CharacterType.Emoji;
return "emoji";
}
if (character.match(/\d/)) {
return CharacterType.Number;
return "number";
}
const specials = ["&", "<", ">", " "];
if (specials.includes(character) || character.match(/[^\w ]/)) {
return CharacterType.Special;
return "special";
}
return CharacterType.Letter;
return "letter";
}
}

View File

@@ -9,6 +9,7 @@ import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
@@ -20,7 +21,7 @@ import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.
import { ImportResult } from "../models/import-result";
export abstract class BaseImporter {
organizationId: string = null;
organizationId: OrganizationId = null;
// FIXME: This should be replaced by injecting the log service.
protected logService: LogService = new ConsoleLogService(false);
@@ -279,7 +280,7 @@ export abstract class BaseImporter {
result.collections = result.folders.map((f) => {
const collection = new CollectionView();
collection.name = f.name;
collection.id = f.id ?? undefined; // folder id may be null, which is not suitable for collections.
collection.id = (f.id as CollectionId) ?? undefined; // folder id may be null, which is not suitable for collections.
return collection;
});
result.folderRelationships = [];

View File

@@ -1,4 +1,5 @@
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrganizationId } from "@bitwarden/common/types/guid";
import {
testData as TestData,
@@ -103,7 +104,7 @@ describe("Keeper CSV Importer", () => {
});
it("should create collections, with subcollections and relationships", async () => {
importer.organizationId = Utils.newGuid();
importer.organizationId = Utils.newGuid() as OrganizationId;
const result = await importer.parse(TestData);
expect(result != null).toBe(true);
@@ -126,7 +127,7 @@ describe("Keeper CSV Importer", () => {
});
it("should create collections tree, with child collections and relationships", async () => {
importer.organizationId = Utils.newGuid();
importer.organizationId = Utils.newGuid() as OrganizationId;
const result = await importer.parse(testDataMultiCollection);
expect(result != null).toBe(true);

View File

@@ -1,4 +1,5 @@
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { testData as TestData } from "../spec-data/keeper-json/testdata.json";
@@ -93,7 +94,7 @@ describe("Keeper Json Importer", () => {
});
it("should create collections if part of an organization", async () => {
importer.organizationId = Utils.newGuid();
importer.organizationId = Utils.newGuid() as OrganizationId;
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);

View File

@@ -1,4 +1,5 @@
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrganizationId } from "@bitwarden/common/types/guid";
import {
credentialsData,
@@ -79,7 +80,7 @@ describe("Netwrix Password Secure CSV Importer", () => {
});
it("should parse an item and create a collection when importing into an organization", async () => {
importer.organizationId = Utils.newGuid();
importer.organizationId = Utils.newGuid() as OrganizationId;
const result = await importer.parse(credentialsData);
expect(result).not.toBeNull();
@@ -93,7 +94,7 @@ describe("Netwrix Password Secure CSV Importer", () => {
});
it("should parse multiple collections", async () => {
importer.organizationId = Utils.newGuid();
importer.organizationId = Utils.newGuid() as OrganizationId;
const result = await importer.parse(credentialsDataWithFolders);
expect(result).not.toBeNull();

View File

@@ -1,4 +1,5 @@
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { SecureNoteType, CipherType, FieldType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
@@ -224,7 +225,7 @@ describe("NordPass CSV Importer", () => {
});
it("should parse an item and create a collection if organizationId is set", async () => {
importer.organizationId = Utils.newGuid();
importer.organizationId = Utils.newGuid() as OrganizationId;
const result = await importer.parse(secureNoteData);
expect(result).not.toBeNull();

View File

@@ -1,4 +1,5 @@
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
@@ -691,7 +692,7 @@ describe("1Password 1Pux Importer", () => {
it("should create collections if part of an organization", async () => {
const importer = new OnePassword1PuxImporter();
importer.organizationId = Utils.newGuid();
importer.organizationId = Utils.newGuid() as OrganizationId;
const result = await importer.parse(SanitizedExportJson);
expect(result != null).toBe(true);

View File

@@ -1,3 +1,4 @@
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ImportResult } from "../../models/import-result";
@@ -146,7 +147,7 @@ describe("PasswordXPCsvImporter", () => {
});
it("should convert folders to collections when importing into an organization", async () => {
importer.organizationId = "someOrg";
importer.organizationId = "someOrg" as OrganizationId;
const result: ImportResult = await importer.parse(withFolders);
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(5);
@@ -172,7 +173,7 @@ describe("PasswordXPCsvImporter", () => {
});
it("should convert multi-level folders to collections when importing into an organization", async () => {
importer.organizationId = "someOrg";
importer.organizationId = "someOrg" as OrganizationId;
const result: ImportResult = await importer.parse(withMultipleFolders);
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(5);

View File

@@ -1,6 +1,7 @@
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { CollectionView } from "@bitwarden/admin-console/common";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { FieldType, SecureNoteType } from "@bitwarden/common/vault/enums";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CipherType } from "@bitwarden/sdk-internal";
@@ -485,7 +486,7 @@ describe("Password Depot 17 Xml Importer", () => {
it("should parse groups nodes into collections when importing into an organization", async () => {
const importer = new PasswordDepot17XmlImporter();
importer.organizationId = "someOrgId";
importer.organizationId = "someOrgId" as OrganizationId;
const collection = new CollectionView();
collection.name = "tempDB";
const actual = [collection];

View File

@@ -2,6 +2,7 @@ import { MockProxy } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
import { testData } from "../spec-data/protonpass-json/protonpass.json";
@@ -90,7 +91,7 @@ describe("Protonpass Json Importer", () => {
it("should create collections if part of an organization", async () => {
const testDataJson = JSON.stringify(testData);
importer.organizationId = Utils.newGuid();
importer.organizationId = Utils.newGuid() as OrganizationId;
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);

View File

@@ -1,3 +1,4 @@
import { OrganizationId } from "@bitwarden/common/types/guid";
import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
@@ -236,7 +237,7 @@ describe("PSONO JSON Importer", () => {
it("should create collections if part of an organization", async () => {
const importer = new PsonoJsonImporter();
importer.organizationId = "someOrg";
importer.organizationId = "someOrg" as OrganizationId;
const result = await importer.parse(FoldersTestDataJson);
expect(result != null).toBe(true);

View File

@@ -1,3 +1,4 @@
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
@@ -73,7 +74,7 @@ describe("Zoho Vault CSV Importer", () => {
it("should create collection and assign ciphers when importing into an organization", async () => {
const importer = new ZohoVaultCsvImporter();
importer.organizationId = "someOrgId";
importer.organizationId = "someOrgId" as OrganizationId;
const result = await importer.parse(samplezohovaultcsvdata);
expect(result != null).toBe(true);
expect(result.success).toBe(true);

View File

@@ -9,6 +9,7 @@ import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { MockSdkService } from "@bitwarden/common/platform/spec/mock-sdk.service";
import { CollectionId, OrganizationId } 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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -67,7 +68,7 @@ describe("ImportService", () => {
describe("getImporterInstance", () => {
describe("Get bitPasswordProtected importer", () => {
let importer: Importer;
const organizationId = Utils.newGuid();
const organizationId = Utils.newGuid() as OrganizationId;
const password = Utils.newGuid();
const promptForPassword_callback = async () => {
return password;
@@ -98,7 +99,7 @@ describe("ImportService", () => {
});
describe("setImportTarget", () => {
const organizationId = Utils.newGuid();
const organizationId = Utils.newGuid() as OrganizationId;
let importResult: ImportResult;
@@ -145,19 +146,19 @@ describe("ImportService", () => {
});
const mockImportTargetCollection = new CollectionView();
mockImportTargetCollection.id = "myImportTarget";
mockImportTargetCollection.id = "myImportTarget" as CollectionId;
mockImportTargetCollection.name = "myImportTarget";
mockImportTargetCollection.organizationId = organizationId;
const mockCollection1 = new CollectionView();
mockCollection1.id = "collection1";
mockCollection1.id = "collection1" as CollectionId;
mockCollection1.name = "collection1";
mockCollection1.organizationId = organizationId;
const mockCollection2 = new CollectionView();
mockCollection1.id = "collection2";
mockCollection1.name = "collection2";
mockCollection1.organizationId = organizationId;
mockCollection2.id = "collection2" as CollectionId;
mockCollection2.name = "collection2";
mockCollection2.organizationId = organizationId;
it("passing importTarget adds it to collections", async () => {
await importService["setImportTarget"](

View File

@@ -19,6 +19,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response"
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrganizationId } 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, toCipherTypeName } from "@bitwarden/common/vault/enums";
@@ -130,7 +131,7 @@ export class ImportService implements ImportServiceAbstraction {
async import(
importer: Importer,
fileContents: string,
organizationId: string = null,
organizationId: OrganizationId = null,
selectedImportTarget: FolderView | CollectionView = null,
canAccessImportExport: boolean,
): Promise<ImportResult> {
@@ -204,7 +205,7 @@ export class ImportService implements ImportServiceAbstraction {
getImporter(
format: ImportType | "bitwardenpasswordprotected",
promptForPassword_callback: () => Promise<string>,
organizationId: string = null,
organizationId: OrganizationId = null,
): Importer {
if (promptForPassword_callback == null) {
return null;
@@ -393,7 +394,10 @@ export class ImportService implements ImportServiceAbstraction {
return await this.importApiService.postImportCiphers(request);
}
private async handleOrganizationalImport(importResult: ImportResult, organizationId: string) {
private async handleOrganizationalImport(
importResult: ImportResult,
organizationId: OrganizationId,
) {
const request = new ImportOrganizationCiphersRequest();
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),

View File

@@ -14,6 +14,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { SelectComponent } from "@bitwarden/components";
@@ -33,9 +34,9 @@ const createMockCollection = (
canEdit = true,
): CollectionView => {
return {
id,
id: id as CollectionId,
name,
organizationId,
organizationId: organizationId as OrganizationId,
externalId: "",
readOnly,
hidePasswords: false,