mirror of
https://github.com/bitwarden/browser
synced 2026-03-02 03:21:19 +00:00
Merge branch 'main' into km/pm-27297
This commit is contained in:
@@ -514,7 +514,7 @@ export class vNextMembersComponent {
|
||||
if (result.error != null) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
message: this.i18nService.t(result.error),
|
||||
message: result.error,
|
||||
});
|
||||
this.logService.error(result.error);
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { DIALOG_DATA, DialogRef, ToastService } from "@bitwarden/components";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
AutoConfirmPolicyDialogComponent,
|
||||
AutoConfirmPolicyDialogData,
|
||||
} from "./auto-confirm-edit-policy-dialog.component";
|
||||
|
||||
describe("AutoConfirmPolicyDialogComponent", () => {
|
||||
let component: AutoConfirmPolicyDialogComponent;
|
||||
let fixture: ComponentFixture<AutoConfirmPolicyDialogComponent>;
|
||||
|
||||
let mockPolicyApiService: MockProxy<PolicyApiServiceAbstraction>;
|
||||
let mockAccountService: FakeAccountService;
|
||||
let mockOrganizationService: MockProxy<OrganizationService>;
|
||||
let mockPolicyService: MockProxy<PolicyService>;
|
||||
let mockRouter: MockProxy<Router>;
|
||||
let mockAutoConfirmService: MockProxy<AutomaticUserConfirmationService>;
|
||||
let mockDialogRef: MockProxy<DialogRef>;
|
||||
let mockToastService: MockProxy<ToastService>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
let mockKeyService: MockProxy<KeyService>;
|
||||
|
||||
const mockUserId = newGuid() as UserId;
|
||||
const mockOrgId = newGuid() as OrganizationId;
|
||||
|
||||
const mockDialogData: AutoConfirmPolicyDialogData = {
|
||||
organizationId: mockOrgId,
|
||||
policy: {
|
||||
name: "autoConfirm",
|
||||
description: "Auto Confirm Policy",
|
||||
type: PolicyType.AutoConfirm,
|
||||
component: {} as any,
|
||||
showDescription: true,
|
||||
display$: () => of(true),
|
||||
},
|
||||
firstTimeDialog: false,
|
||||
};
|
||||
|
||||
const mockOrg = {
|
||||
id: mockOrgId,
|
||||
name: "Test Organization",
|
||||
enabled: true,
|
||||
isAdmin: true,
|
||||
canManagePolicies: true,
|
||||
} as Organization;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockPolicyApiService = mock<PolicyApiServiceAbstraction>();
|
||||
mockAccountService = mockAccountServiceWith(mockUserId);
|
||||
mockOrganizationService = mock<OrganizationService>();
|
||||
mockPolicyService = mock<PolicyService>();
|
||||
mockRouter = mock<Router>();
|
||||
mockAutoConfirmService = mock<AutomaticUserConfirmationService>();
|
||||
mockDialogRef = mock<DialogRef>();
|
||||
mockToastService = mock<ToastService>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
mockKeyService = mock<KeyService>();
|
||||
|
||||
mockPolicyService.policies$.mockReturnValue(of([]));
|
||||
mockOrganizationService.organizations$.mockReturnValue(of([mockOrg]));
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AutoConfirmPolicyDialogComponent],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
{ provide: DIALOG_DATA, useValue: mockDialogData },
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: PolicyApiServiceAbstraction, useValue: mockPolicyApiService },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: ToastService, useValue: mockToastService },
|
||||
{ provide: KeyService, useValue: mockKeyService },
|
||||
{ provide: OrganizationService, useValue: mockOrganizationService },
|
||||
{ provide: PolicyService, useValue: mockPolicyService },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: AutomaticUserConfirmationService, useValue: mockAutoConfirmService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
})
|
||||
.overrideComponent(AutoConfirmPolicyDialogComponent, {
|
||||
set: { template: "<div></div>" },
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AutoConfirmPolicyDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("handleSubmit", () => {
|
||||
beforeEach(() => {
|
||||
// Mock the policyComponent
|
||||
component.policyComponent = {
|
||||
buildRequest: jest.fn().mockResolvedValue({ enabled: true, data: null }),
|
||||
enabled: { value: true },
|
||||
setSingleOrgEnabled: jest.fn(),
|
||||
} as any;
|
||||
|
||||
mockAutoConfirmService.configuration$.mockReturnValue(
|
||||
of({ enabled: false, showSetupDialog: true, showBrowserNotification: undefined }),
|
||||
);
|
||||
mockAutoConfirmService.upsert.mockResolvedValue(undefined);
|
||||
mockI18nService.t.mockReturnValue("Policy updated");
|
||||
});
|
||||
|
||||
it("should enable SingleOrg policy when it was not already enabled", async () => {
|
||||
mockPolicyApiService.putPolicyVNext.mockResolvedValue({} as any);
|
||||
|
||||
// Call handleSubmit with singleOrgEnabled = false (meaning it needs to be enabled)
|
||||
await component["handleSubmit"](false);
|
||||
|
||||
// First call should be SingleOrg enable
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
mockOrgId,
|
||||
PolicyType.SingleOrg,
|
||||
{ policy: { enabled: true, data: null } },
|
||||
);
|
||||
});
|
||||
|
||||
it("should not enable SingleOrg policy when it was already enabled", async () => {
|
||||
mockPolicyApiService.putPolicyVNext.mockResolvedValue({} as any);
|
||||
|
||||
// Call handleSubmit with singleOrgEnabled = true (meaning it's already enabled)
|
||||
await component["handleSubmit"](true);
|
||||
|
||||
// Should only call putPolicyVNext once (for AutoConfirm, not SingleOrg)
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledTimes(1);
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledWith(
|
||||
mockOrgId,
|
||||
PolicyType.AutoConfirm,
|
||||
{ policy: { enabled: true, data: null } },
|
||||
);
|
||||
});
|
||||
|
||||
it("should rollback SingleOrg policy when AutoConfirm fails and SingleOrg was enabled during action", async () => {
|
||||
const autoConfirmError = new Error("AutoConfirm failed");
|
||||
|
||||
// First call (SingleOrg enable) succeeds, second call (AutoConfirm) fails, third call (SingleOrg rollback) succeeds
|
||||
mockPolicyApiService.putPolicyVNext
|
||||
.mockResolvedValueOnce({} as any) // SingleOrg enable
|
||||
.mockRejectedValueOnce(autoConfirmError) // AutoConfirm fails
|
||||
.mockResolvedValueOnce({} as any); // SingleOrg rollback
|
||||
|
||||
await expect(component["handleSubmit"](false)).rejects.toThrow("AutoConfirm failed");
|
||||
|
||||
// Verify: SingleOrg enabled, AutoConfirm attempted, SingleOrg rolled back
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledTimes(3);
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
mockOrgId,
|
||||
PolicyType.SingleOrg,
|
||||
{ policy: { enabled: true, data: null } },
|
||||
);
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
mockOrgId,
|
||||
PolicyType.AutoConfirm,
|
||||
{ policy: { enabled: true, data: null } },
|
||||
);
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
mockOrgId,
|
||||
PolicyType.SingleOrg,
|
||||
{ policy: { enabled: false, data: null } },
|
||||
);
|
||||
});
|
||||
|
||||
it("should not rollback SingleOrg policy when AutoConfirm fails but SingleOrg was already enabled", async () => {
|
||||
const autoConfirmError = new Error("AutoConfirm failed");
|
||||
|
||||
// AutoConfirm call fails (SingleOrg was already enabled, so no SingleOrg calls)
|
||||
mockPolicyApiService.putPolicyVNext.mockRejectedValue(autoConfirmError);
|
||||
|
||||
await expect(component["handleSubmit"](true)).rejects.toThrow("AutoConfirm failed");
|
||||
|
||||
// Verify only AutoConfirm was called (no SingleOrg enable/rollback)
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledTimes(1);
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledWith(
|
||||
mockOrgId,
|
||||
PolicyType.AutoConfirm,
|
||||
{ policy: { enabled: true, data: null } },
|
||||
);
|
||||
});
|
||||
|
||||
it("should keep both policies enabled when both submissions succeed", async () => {
|
||||
mockPolicyApiService.putPolicyVNext.mockResolvedValue({} as any);
|
||||
|
||||
await component["handleSubmit"](false);
|
||||
|
||||
// Verify two calls: SingleOrg enable and AutoConfirm enable
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledTimes(2);
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
mockOrgId,
|
||||
PolicyType.SingleOrg,
|
||||
{ policy: { enabled: true, data: null } },
|
||||
);
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
mockOrgId,
|
||||
PolicyType.AutoConfirm,
|
||||
{ policy: { enabled: true, data: null } },
|
||||
);
|
||||
});
|
||||
|
||||
it("should re-throw the error after rollback", async () => {
|
||||
const autoConfirmError = new Error("Network error");
|
||||
|
||||
mockPolicyApiService.putPolicyVNext
|
||||
.mockResolvedValueOnce({} as any) // SingleOrg enable
|
||||
.mockRejectedValueOnce(autoConfirmError) // AutoConfirm fails
|
||||
.mockResolvedValueOnce({} as any); // SingleOrg rollback
|
||||
|
||||
await expect(component["handleSubmit"](false)).rejects.toThrow("Network error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setSingleOrgPolicy", () => {
|
||||
it("should call putPolicyVNext with enabled: true when enabling", async () => {
|
||||
mockPolicyApiService.putPolicyVNext.mockResolvedValue({} as any);
|
||||
|
||||
await component["setSingleOrgPolicy"](true);
|
||||
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledWith(
|
||||
mockOrgId,
|
||||
PolicyType.SingleOrg,
|
||||
{ policy: { enabled: true, data: null } },
|
||||
);
|
||||
});
|
||||
|
||||
it("should call putPolicyVNext with enabled: false when disabling", async () => {
|
||||
mockPolicyApiService.putPolicyVNext.mockResolvedValue({} as any);
|
||||
|
||||
await component["setSingleOrgPolicy"](false);
|
||||
|
||||
expect(mockPolicyApiService.putPolicyVNext).toHaveBeenCalledWith(
|
||||
mockOrgId,
|
||||
PolicyType.SingleOrg,
|
||||
{ policy: { enabled: false, data: null } },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -181,10 +181,21 @@ export class AutoConfirmPolicyDialogComponent
|
||||
}
|
||||
|
||||
private async handleSubmit(singleOrgEnabled: boolean) {
|
||||
if (!singleOrgEnabled) {
|
||||
await this.submitSingleOrg();
|
||||
const enabledSingleOrgDuringAction = !singleOrgEnabled;
|
||||
|
||||
if (enabledSingleOrgDuringAction) {
|
||||
await this.setSingleOrgPolicy(true);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.submitAutoConfirm();
|
||||
} catch (error) {
|
||||
// Roll back SingleOrg if we enabled it during this action
|
||||
if (enabledSingleOrgDuringAction) {
|
||||
await this.setSingleOrgPolicy(false);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await this.submitAutoConfirm();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,11 +209,9 @@ export class AutoConfirmPolicyDialogComponent
|
||||
|
||||
const autoConfirmRequest = await this.policyComponent.buildRequest();
|
||||
|
||||
await this.policyApiService.putPolicy(
|
||||
this.data.organizationId,
|
||||
this.data.policy.type,
|
||||
autoConfirmRequest,
|
||||
);
|
||||
await this.policyApiService.putPolicyVNext(this.data.organizationId, this.data.policy.type, {
|
||||
policy: autoConfirmRequest,
|
||||
});
|
||||
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
@@ -225,17 +234,15 @@ export class AutoConfirmPolicyDialogComponent
|
||||
}
|
||||
}
|
||||
|
||||
private async submitSingleOrg(): Promise<void> {
|
||||
private async setSingleOrgPolicy(enabled: boolean): Promise<void> {
|
||||
const singleOrgRequest: PolicyRequest = {
|
||||
enabled: true,
|
||||
enabled,
|
||||
data: null,
|
||||
};
|
||||
|
||||
await this.policyApiService.putPolicyVNext(
|
||||
this.data.organizationId,
|
||||
PolicyType.SingleOrg,
|
||||
singleOrgRequest,
|
||||
);
|
||||
await this.policyApiService.putPolicyVNext(this.data.organizationId, PolicyType.SingleOrg, {
|
||||
policy: singleOrgRequest,
|
||||
});
|
||||
}
|
||||
|
||||
private async openBrowserExtension() {
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</bit-tab>
|
||||
<bit-tab label="{{ 'access' | i18n }}">
|
||||
<bit-tab [label]="accessTabLabel">
|
||||
<div class="tw-mb-3">
|
||||
<ng-container *ngIf="dialogReadonly">
|
||||
<span>{{ "readOnlyCollectionAccess" | i18n }}</span>
|
||||
|
||||
@@ -361,6 +361,12 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
return this.params.readonly === true;
|
||||
}
|
||||
|
||||
protected get accessTabLabel(): string {
|
||||
return this.dialogReadonly
|
||||
? this.i18nService.t("viewAccess")
|
||||
: this.i18nService.t("editAccess");
|
||||
}
|
||||
|
||||
protected async cancel() {
|
||||
this.close(CollectionDialogAction.Canceled);
|
||||
}
|
||||
|
||||
@@ -57,12 +57,8 @@
|
||||
<ng-container *ngIf="subscription">
|
||||
<ng-container *ngIf="enableDiscountDisplay$ | async as enableDiscount; else noDiscount">
|
||||
<div class="tw-flex tw-items-center tw-gap-2 tw-flex-wrap tw-justify-end">
|
||||
<span [attr.aria-label]="'nextChargeDateAndAmount' | i18n">
|
||||
{{
|
||||
(sub.subscription.periodEndDate | date: "MMM d, y") +
|
||||
", " +
|
||||
(discountedSubscriptionAmount | currency: "$")
|
||||
}}
|
||||
<span [attr.aria-label]="'nextChargeDate' | i18n">
|
||||
{{ sub.subscription.periodEndDate | date: "MMM d, y" }}
|
||||
</span>
|
||||
<billing-discount-badge
|
||||
[discount]="getDiscount(sub?.customerDiscount)"
|
||||
@@ -71,12 +67,8 @@
|
||||
</ng-container>
|
||||
<ng-template #noDiscount>
|
||||
<div class="tw-flex tw-items-center tw-gap-2 tw-flex-wrap tw-justify-end">
|
||||
<span [attr.aria-label]="'nextChargeDateAndAmount' | i18n">
|
||||
{{
|
||||
(sub.subscription.periodEndDate | date: "MMM d, y") +
|
||||
", " +
|
||||
(subscriptionAmount | currency: "$")
|
||||
}}
|
||||
<span [attr.aria-label]="'nextChargeDate' | i18n">
|
||||
{{ sub.subscription.periodEndDate | date: "MMM d, y" }}
|
||||
</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -722,6 +722,8 @@ export class EventService {
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Edge"];
|
||||
case DeviceType.IEBrowser:
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - IE"];
|
||||
case DeviceType.DuckDuckGoBrowser:
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - DuckDuckGo"];
|
||||
case DeviceType.Server:
|
||||
return ["bwi-user-monitor", this.i18nService.t("server")];
|
||||
case DeviceType.WindowsCLI:
|
||||
|
||||
@@ -12,7 +12,6 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherBulkDeleteRequest } from "@bitwarden/common/vault/models/request/cipher-bulk-delete.request";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
import {
|
||||
CenterPositionStrategy,
|
||||
@@ -148,11 +147,16 @@ export class BulkDeleteDialogComponent {
|
||||
}
|
||||
|
||||
private async deleteCiphersAdmin(ciphers: string[]): Promise<any> {
|
||||
const deleteRequest = new CipherBulkDeleteRequest(ciphers, this.organization.id);
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
if (this.permanent) {
|
||||
return await this.apiService.deleteManyCiphersAdmin(deleteRequest);
|
||||
await this.cipherService.deleteManyWithServer(ciphers, userId, true, this.organization.id);
|
||||
} else {
|
||||
return await this.apiService.putDeleteManyCiphersAdmin(deleteRequest);
|
||||
await this.cipherService.softDeleteManyWithServer(
|
||||
ciphers,
|
||||
userId,
|
||||
true,
|
||||
this.organization.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3281,6 +3281,9 @@
|
||||
"nextChargeHeader": {
|
||||
"message": "Next Charge"
|
||||
},
|
||||
"nextChargeDate": {
|
||||
"message": "Next charge date"
|
||||
},
|
||||
"plan": {
|
||||
"message": "Plan"
|
||||
},
|
||||
@@ -6928,8 +6931,8 @@
|
||||
"activateAutofill": {
|
||||
"message": "Activate auto-fill"
|
||||
},
|
||||
"activateAutofillPolicyDesc": {
|
||||
"message": "Activate the auto-fill on page load setting on the browser extension for all existing and new members."
|
||||
"activateAutofillPolicyDescription": {
|
||||
"message": "Activate the autofill on page load setting on the browser extension for all existing and new members."
|
||||
},
|
||||
"experimentalFeature": {
|
||||
"message": "Compromised or untrusted websites can exploit auto-fill on page load."
|
||||
@@ -11366,6 +11369,18 @@
|
||||
"automaticDomainClaimProcess": {
|
||||
"message": "Bitwarden will attempt to claim the domain 3 times during the first 72 hours. If the domain can’t be claimed, check the DNS record in your host and manually claim. The domain will be removed from your organization in 7 days if it is not claimed."
|
||||
},
|
||||
"automaticDomainClaimProcess1": {
|
||||
"message": "Bitwarden will attempt to claim the domain within 72 hours. If the domain can't be claimed, verify your DNS record and claim manually. Unclaimed domains are removed after 7 days."
|
||||
},
|
||||
"automaticDomainClaimProcess2": {
|
||||
"message": "Once claimed, existing members with claimed domains will be emailed about the "
|
||||
},
|
||||
"accountOwnershipChange": {
|
||||
"message": "account ownership change"
|
||||
},
|
||||
"automaticDomainClaimProcessEnd": {
|
||||
"message": "."
|
||||
},
|
||||
"domainNotClaimed": {
|
||||
"message": "$DOMAIN$ not claimed. Check your DNS records.",
|
||||
"placeholders": {
|
||||
@@ -11378,8 +11393,8 @@
|
||||
"domainStatusClaimed": {
|
||||
"message": "Claimed"
|
||||
},
|
||||
"domainStatusUnderVerification": {
|
||||
"message": "Under verification"
|
||||
"domainStatusPending": {
|
||||
"message": "Pending"
|
||||
},
|
||||
"claimedDomainsDescription": {
|
||||
"message": "Claim a domain to own member accounts. The SSO identifier page will be skipped during login for members with claimed domains and administrators will be able to delete claimed accounts."
|
||||
|
||||
@@ -319,7 +319,7 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
https://*.paypal.com
|
||||
https://www.paypalobjects.com
|
||||
https://q.stripe.com
|
||||
https://haveibeenpwned.com
|
||||
https://logos.haveibeenpwned.com
|
||||
;media-src
|
||||
'self'
|
||||
https://assets.bitwarden.com
|
||||
|
||||
Reference in New Issue
Block a user