diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts
index d1cf89eb087..7b73ad83053 100644
--- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts
+++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts
@@ -1,6 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
+import { of } from "rxjs";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@@ -17,9 +18,12 @@ describe("ExposedPasswordsReportComponent", () => {
let component: ExposedPasswordsReportComponent;
let fixture: ComponentFixture;
let auditService: MockProxy;
+ let organizationService: MockProxy;
beforeEach(() => {
auditService = mock();
+ organizationService = mock();
+ organizationService.organizations$ = of([]);
// 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
TestBed.configureTestingModule({
@@ -35,7 +39,7 @@ describe("ExposedPasswordsReportComponent", () => {
},
{
provide: OrganizationService,
- useValue: mock(),
+ useValue: organizationService,
},
{
provide: ModalService,
diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts
index 631e9ef8a85..39414487d7b 100644
--- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts
+++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts
@@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
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";
@@ -24,8 +25,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
protected organizationService: OrganizationService,
modalService: ModalService,
passwordRepromptService: PasswordRepromptService,
+ i18nService: I18nService,
) {
- super(modalService, passwordRepromptService, organizationService);
+ super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
}
async ngOnInit() {
@@ -36,7 +38,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
const allCiphers = await this.getAllCiphers();
const exposedPasswordCiphers: CipherView[] = [];
const promises: Promise[] = [];
- allCiphers.forEach((ciph) => {
+ this.filterStatus = [0];
+
+ allCiphers.forEach((ciph: any) => {
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
if (
type !== CipherType.Login ||
@@ -48,6 +52,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
) {
return;
}
+
const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => {
if (exposedCount > 0) {
exposedPasswordCiphers.push(ciph);
@@ -57,11 +62,8 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
promises.push(promise);
});
await Promise.all(promises);
- this.ciphers = [...exposedPasswordCiphers];
- }
- protected getAllCiphers(): Promise {
- return this.cipherService.getAllDecrypted();
+ this.filterCiphersByOrg(exposedPasswordCiphers);
}
protected canManageCipher(c: CipherView): boolean {
diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html
index d81fc2d4136..ae03a3bcb80 100644
--- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html
+++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html
@@ -16,9 +16,32 @@
- {{ "inactive2faFoundDesc" | i18n: (ciphers.length | number) }}
+ {{ "inactive2faFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
+
+
+
+ {{ getName(status) }}
+ {{ getCount(status) }}
+
+
+
+
+
+ |
+ {{ "name" | i18n }} |
+ {{ "owner" | i18n }} |
+
+
diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts
index 97321480fae..528f6306e0c 100644
--- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts
+++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts
@@ -1,6 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { mock } from "jest-mock-extended";
+import { MockProxy, mock } from "jest-mock-extended";
+import { of } from "rxjs";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@@ -16,8 +17,11 @@ import { cipherData } from "./reports-ciphers.mock";
describe("InactiveTwoFactorReportComponent", () => {
let component: InactiveTwoFactorReportComponent;
let fixture: ComponentFixture;
+ let organizationService: MockProxy;
beforeEach(() => {
+ organizationService = mock();
+ organizationService.organizations$ = of([]);
// 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
TestBed.configureTestingModule({
@@ -29,7 +33,7 @@ describe("InactiveTwoFactorReportComponent", () => {
},
{
provide: OrganizationService,
- useValue: mock(),
+ useValue: organizationService,
},
{
provide: ModalService,
diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts
index 15b79981b68..956607c8fb2 100644
--- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts
+++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts
@@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -26,8 +27,9 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
modalService: ModalService,
private logService: LogService,
passwordRepromptService: PasswordRepromptService,
+ i18nService: I18nService,
) {
- super(modalService, passwordRepromptService, organizationService);
+ super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
}
async ngOnInit() {
@@ -45,6 +47,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
const allCiphers = await this.getAllCiphers();
const inactive2faCiphers: CipherView[] = [];
const docs = new Map();
+ this.filterStatus = [0];
allCiphers.forEach((ciph) => {
const { type, login, isDeleted, edit, id, viewPassword } = ciph;
@@ -58,6 +61,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
) {
return;
}
+
for (let i = 0; i < login.uris.length; i++) {
const u = login.uris[i];
if (u.uri != null && u.uri !== "") {
@@ -75,15 +79,12 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
}
}
});
- this.ciphers = [...inactive2faCiphers];
+
+ this.filterCiphersByOrg(inactive2faCiphers);
this.cipherDocs = docs;
}
}
- protected getAllCiphers(): Promise {
- return this.cipherService.getAllDecrypted();
- }
-
private async load2fa() {
if (this.services.size > 0) {
return;
diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html
index cde2e59ea84..549773ba8ce 100644
--- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html
+++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html
@@ -16,9 +16,34 @@
- {{ "reusedPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
+ {{ "reusedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
+
+
+
+
+ {{ getName(status) }}
+ {{ getCount(status) }}
+
+
+
+
+
+
+ |
+ {{ "name" | i18n }} |
+ {{ "owner" | i18n }} |
+
+
diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts
index 450e42805a9..29e20c11af1 100644
--- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts
+++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts
@@ -1,6 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { mock } from "jest-mock-extended";
+import { MockProxy, mock } from "jest-mock-extended";
+import { of } from "rxjs";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@@ -15,8 +16,11 @@ import { ReusedPasswordsReportComponent } from "./reused-passwords-report.compon
describe("ReusedPasswordsReportComponent", () => {
let component: ReusedPasswordsReportComponent;
let fixture: ComponentFixture;
+ let organizationService: MockProxy;
beforeEach(() => {
+ organizationService = mock();
+ organizationService.organizations$ = of([]);
// 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
TestBed.configureTestingModule({
@@ -28,7 +32,7 @@ describe("ReusedPasswordsReportComponent", () => {
},
{
provide: OrganizationService,
- useValue: mock(),
+ useValue: organizationService,
},
{
provide: ModalService,
diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts
index f785186c158..cbc2ea11b5b 100644
--- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts
+++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts
@@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
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";
@@ -22,8 +23,9 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
protected organizationService: OrganizationService,
modalService: ModalService,
passwordRepromptService: PasswordRepromptService,
+ i18nService: I18nService,
) {
- super(modalService, passwordRepromptService, organizationService);
+ super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
}
async ngOnInit() {
@@ -34,6 +36,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
const allCiphers = await this.getAllCiphers();
const ciphersWithPasswords: CipherView[] = [];
this.passwordUseMap = new Map();
+ this.filterStatus = [0];
+
allCiphers.forEach((ciph) => {
const { type, login, isDeleted, edit, viewPassword } = ciph;
if (
@@ -46,6 +50,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
) {
return;
}
+
ciphersWithPasswords.push(ciph);
if (this.passwordUseMap.has(login.password)) {
this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) + 1);
@@ -57,11 +62,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem
(c) =>
this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password) > 1,
);
- this.ciphers = reusedPasswordCiphers;
- }
- protected getAllCiphers(): Promise {
- return this.cipherService.getAllDecrypted();
+ this.filterCiphersByOrg(reusedPasswordCiphers);
}
protected canManageCipher(c: CipherView): boolean {
diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html
index 616bdbba0b2..ced0ff9731d 100644
--- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html
+++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html
@@ -16,9 +16,33 @@
- {{ "unsecuredWebsitesFoundDesc" | i18n: (ciphers.length | number) }}
+ {{ "unsecuredWebsitesFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
+
+
+
+
+ {{ getName(status) }}
+ {{ getCount(status) }}
+
+
+
+
+
+ |
+ {{ "name" | i18n }} |
+ {{ "owner" | i18n }} |
+
+
diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts
index 5cdf640c554..3b7c6d350f7 100644
--- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts
+++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts
@@ -1,6 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { mock } from "jest-mock-extended";
+import { MockProxy, mock } from "jest-mock-extended";
+import { of } from "rxjs";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@@ -15,8 +16,11 @@ import { UnsecuredWebsitesReportComponent } from "./unsecured-websites-report.co
describe("UnsecuredWebsitesReportComponent", () => {
let component: UnsecuredWebsitesReportComponent;
let fixture: ComponentFixture;
+ let organizationService: MockProxy;
beforeEach(() => {
+ organizationService = mock();
+ organizationService.organizations$ = of([]);
// 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
TestBed.configureTestingModule({
@@ -28,7 +32,7 @@ describe("UnsecuredWebsitesReportComponent", () => {
},
{
provide: OrganizationService,
- useValue: mock(),
+ useValue: organizationService,
},
{
provide: ModalService,
diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts
index 2de70e928bb..769eb058cd6 100644
--- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts
+++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts
@@ -2,9 +2,9 @@ import { Component, OnInit } from "@angular/core";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
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";
import { PasswordRepromptService } from "@bitwarden/vault";
import { CipherReportComponent } from "./cipher-report.component";
@@ -21,8 +21,9 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl
protected organizationService: OrganizationService,
modalService: ModalService,
passwordRepromptService: PasswordRepromptService,
+ i18nService: I18nService,
) {
- super(modalService, passwordRepromptService, organizationService);
+ super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
}
async ngOnInit() {
@@ -31,18 +32,15 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl
async setCiphers() {
const allCiphers = await this.getAllCiphers();
+ this.filterStatus = [0];
const unsecuredCiphers = allCiphers.filter((c) => {
if (c.type !== CipherType.Login || !c.login.hasUris || c.isDeleted) {
return false;
}
- return c.login.uris.some((u) => u.uri != null && u.uri.indexOf("http://") === 0);
- });
- this.ciphers = unsecuredCiphers.filter(
- (c) => (!this.organization && c.edit) || (this.organization && !c.edit),
- );
- }
- protected getAllCiphers(): Promise {
- return this.cipherService.getAllDecrypted();
+ return c.login.uris.some((u: any) => u.uri != null && u.uri.indexOf("http://") === 0);
+ });
+
+ this.filterCiphersByOrg(unsecuredCiphers);
}
}
diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html
index b4c77b2fa15..a943c8c29ec 100644
--- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html
+++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html
@@ -16,9 +16,32 @@
- {{ "weakPasswordsFoundDesc" | i18n: (ciphers.length | number) }}
+ {{ "weakPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }}
+
+
+
+ {{ getName(status) }}
+ {{ getCount(status) }}
+
+
+
+
+
+ |
+ {{ "name" | i18n }} |
+ {{ "owner" | i18n }} |
+
+
diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts
index f1446c4209f..dbc367b108b 100644
--- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts
+++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts
@@ -1,6 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
+import { of } from "rxjs";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@@ -17,9 +18,12 @@ describe("WeakPasswordsReportComponent", () => {
let component: WeakPasswordsReportComponent;
let fixture: ComponentFixture;
let passwordStrengthService: MockProxy;
+ let organizationService: MockProxy;
beforeEach(() => {
passwordStrengthService = mock();
+ organizationService = mock();
+ organizationService.organizations$ = of([]);
// 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
TestBed.configureTestingModule({
@@ -35,7 +39,7 @@ describe("WeakPasswordsReportComponent", () => {
},
{
provide: OrganizationService,
- useValue: mock(),
+ useValue: organizationService,
},
{
provide: ModalService,
diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts
index a7ed119e19b..4d179b58f3d 100644
--- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts
+++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts
@@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -29,8 +30,9 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
protected organizationService: OrganizationService,
modalService: ModalService,
passwordRepromptService: PasswordRepromptService,
+ i18nService: I18nService,
) {
- super(modalService, passwordRepromptService, organizationService);
+ super(cipherService, modalService, passwordRepromptService, organizationService, i18nService);
}
async ngOnInit() {
@@ -38,7 +40,10 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
}
async setCiphers() {
- const allCiphers = await this.getAllCiphers();
+ const allCiphers: any = await this.getAllCiphers();
+ this.passwordStrengthCache = new Map();
+ this.weakPasswordCiphers = [];
+ this.filterStatus = [0];
this.findWeakPasswords(allCiphers);
}
@@ -55,6 +60,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
) {
return;
}
+
const hasUserName = this.isUserNameNotEmpty(ciph);
const cacheKey = this.getCacheKey(ciph);
if (!this.passwordStrengthCache.has(cacheKey)) {
@@ -87,6 +93,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
this.passwordStrengthCache.set(cacheKey, result.score);
}
const score = this.passwordStrengthCache.get(cacheKey);
+
if (score != null && score <= 2) {
this.passwordStrengthMap.set(id, this.scoreKey(score));
this.weakPasswordCiphers.push(ciph);
@@ -98,11 +105,8 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
this.passwordStrengthCache.get(this.getCacheKey(b))
);
});
- this.ciphers = [...this.weakPasswordCiphers];
- }
- protected getAllCiphers(): Promise {
- return this.cipherService.getAllDecrypted();
+ this.filterCiphersByOrg(this.weakPasswordCiphers);
}
protected canManageCipher(c: CipherView): boolean {
diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts
index 8b6ead33bee..dd18ca0879a 100644
--- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts
+++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts
@@ -32,7 +32,6 @@ export class VaultItemsComponent {
@Input() showCollections: boolean;
@Input() showGroups: boolean;
@Input() useEvents: boolean;
- @Input() cloneableOrganizationCiphers: boolean;
@Input() showPremiumFeatures: boolean;
@Input() showBulkMove: boolean;
@Input() showBulkTrashOptions: boolean;
@@ -160,10 +159,27 @@ export class VaultItemsComponent {
}
protected canClone(vaultItem: VaultItem) {
- return (
- (vaultItem.cipher.organizationId && this.cloneableOrganizationCiphers) ||
- vaultItem.cipher.organizationId == null
- );
+ if (vaultItem.cipher.organizationId == null) {
+ return true;
+ }
+
+ const org = this.allOrganizations.find((o) => o.id === vaultItem.cipher.organizationId);
+
+ // Admins and custom users can always clone in the Org Vault
+ if (this.viewingOrgVault && (org.isAdmin || org.permissions.editAnyCollection)) {
+ return true;
+ }
+
+ // Check if the cipher belongs to a collection with canManage permission
+ const orgCollections = this.allCollections.filter((c) => c.organizationId === org.id);
+
+ for (const collection of orgCollections) {
+ if (vaultItem.cipher.collectionIds.includes(collection.id) && collection.manage) {
+ return true;
+ }
+ }
+
+ return false;
}
private refreshItems() {
diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/apps/web/src/app/vault/core/views/collection-admin.view.ts
index 369c78636c0..2be84b0d246 100644
--- a/apps/web/src/app/vault/core/views/collection-admin.view.ts
+++ b/apps/web/src/app/vault/core/views/collection-admin.view.ts
@@ -51,6 +51,13 @@ export class CollectionAdminView extends CollectionView {
* Whether the user can modify user access to this collection
*/
canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
- return this.canEdit(org, flexibleCollectionsV1Enabled) || org.canManageUsers;
+ return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageUsers;
+ }
+
+ /**
+ * Whether the user can modify group access to this collection
+ */
+ canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
+ return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageGroups;
}
}
diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html
index 003066dadd6..3f95665f37a 100644
--- a/apps/web/src/app/vault/individual-vault/vault.component.html
+++ b/apps/web/src/app/vault/individual-vault/vault.component.html
@@ -47,7 +47,6 @@
[showBulkMove]="showBulkMove"
[showBulkTrashOptions]="filter.type === 'trash'"
[useEvents]="false"
- [cloneableOrganizationCiphers]="false"
[showAdminActions]="false"
(onEvent)="onVaultItemsEvent($event)"
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async"
diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts
index c4213989c6b..01e4dbaadfd 100644
--- a/apps/web/src/app/vault/org-vault/add-edit.component.ts
+++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts
@@ -81,22 +81,6 @@ export class AddEditComponent extends BaseAddEditComponent {
);
}
- protected allowOwnershipAssignment() {
- if (
- this.ownershipOptions != null &&
- (this.ownershipOptions.length > 1 || !this.allowPersonal)
- ) {
- if (this.organization != null) {
- return (
- this.cloneMode && this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)
- );
- } else {
- return !this.editMode || this.cloneMode;
- }
- }
- return false;
- }
-
protected loadCollections() {
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
return super.loadCollections();
diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html
index bcbd56630c0..f815fccb213 100644
--- a/apps/web/src/app/vault/org-vault/vault.component.html
+++ b/apps/web/src/app/vault/org-vault/vault.component.html
@@ -48,7 +48,6 @@
[showBulkMove]="false"
[showBulkTrashOptions]="filter.type === 'trash'"
[useEvents]="organization?.useEvents"
- [cloneableOrganizationCiphers]="true"
[showAdminActions]="true"
(onEvent)="onVaultItemsEvent($event)"
[showBulkEditCollectionAccess]="organization?.flexibleCollections"
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 3a590622b81..f5fff9a81dd 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -1809,12 +1809,16 @@
"unsecuredWebsitesFound": {
"message": "Unsecured websites found"
},
- "unsecuredWebsitesFoundDesc": {
- "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.",
+ "unsecuredWebsitesFoundReportDesc": {
+ "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.",
"placeholders": {
"count": {
"content": "$1",
"example": "8"
+ },
+ "vault": {
+ "content": "$2",
+ "example": "this will be 'vault' or 'vaults'"
}
}
},
@@ -1830,12 +1834,16 @@
"inactive2faFound": {
"message": "Logins without two-step login found"
},
- "inactive2faFoundDesc": {
- "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.",
+ "inactive2faFoundReportDesc": {
+ "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.",
"placeholders": {
"count": {
"content": "$1",
"example": "8"
+ },
+ "vault": {
+ "content": "$2",
+ "example": "this will be 'vault' or 'vaults'"
}
}
},
@@ -1854,12 +1862,16 @@
"exposedPasswordsFound": {
"message": "Exposed passwords found"
},
- "exposedPasswordsFoundDesc": {
- "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.",
+ "exposedPasswordsFoundReportDesc": {
+ "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.",
"placeholders": {
"count": {
"content": "$1",
"example": "8"
+ },
+ "vault": {
+ "content": "$2",
+ "example": "this will be 'vault' or 'vaults'"
}
}
},
@@ -1887,12 +1899,16 @@
"weakPasswordsFound": {
"message": "Weak passwords found"
},
- "weakPasswordsFoundDesc": {
- "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.",
+ "weakPasswordsFoundReportDesc": {
+ "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.",
"placeholders": {
"count": {
"content": "$1",
"example": "8"
+ },
+ "vault": {
+ "content": "$2",
+ "example": "this will be 'vault' or 'vaults'"
}
}
},
@@ -1908,12 +1924,16 @@
"reusedPasswordsFound": {
"message": "Reused passwords found"
},
- "reusedPasswordsFoundDesc": {
- "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.",
+ "reusedPasswordsFoundReportDesc": {
+ "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.",
"placeholders": {
"count": {
"content": "$1",
"example": "8"
+ },
+ "vault": {
+ "content": "$2",
+ "example": "this will be 'vault' or 'vaults'"
}
}
},
@@ -6460,6 +6480,9 @@
"editGroupCollectionsDesc": {
"message": "Grant access to collections by adding them to this group."
},
+ "editGroupCollectionsRestrictionsDesc": {
+ "message": "You can only assign collections you manage."
+ },
"accessAllCollectionsDesc": {
"message": "Grant access to all current and future collections."
},
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts
index 70eb8af7ba3..0dace2945e0 100644
--- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts
+++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts
@@ -1,5 +1,5 @@
import { Component } from "@angular/core";
-import { ActivatedRoute } from "@angular/router";
+import { ActivatedRoute, Router } from "@angular/router";
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -42,6 +42,7 @@ export class AccountComponent {
private dialogService: DialogService,
private configService: ConfigService,
private providerApiService: ProviderApiServiceAbstraction,
+ private router: Router,
) {}
async ngOnInit() {
@@ -93,9 +94,8 @@ export class AccountComponent {
return;
}
- this.formPromise = this.providerApiService.deleteProvider(this.providerId);
try {
- await this.formPromise;
+ await this.providerApiService.deleteProvider(this.providerId);
this.platformUtilsService.showToast(
"success",
this.i18nService.t("providerDeleted"),
@@ -104,7 +104,8 @@ export class AccountComponent {
} catch (e) {
this.logService.error(e);
}
- this.formPromise = null;
+
+ await this.router.navigate(["/"]);
}
private async verifyUser(): Promise {
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts
index 958fe0d48e9..6c005a1225d 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts
@@ -58,3 +58,16 @@ export class ServiceAccountPeopleAccessPoliciesView {
userAccessPolicies: UserServiceAccountAccessPolicyView[];
groupAccessPolicies: GroupServiceAccountAccessPolicyView[];
}
+
+export class ServiceAccountProjectPolicyPermissionDetailsView {
+ accessPolicy: ServiceAccountProjectAccessPolicyView;
+ hasPermission: boolean;
+}
+
+export class ServiceAccountGrantedPoliciesView {
+ grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[];
+}
+
+export class ProjectServiceAccountsAccessPoliciesView {
+ serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[];
+}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html
index 443711fd366..5d22358277f 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html
@@ -1,17 +1,27 @@
-
-
- {{ "projectMachineAccountsDescription" | i18n }}
-
-
-
-
+
+
+
+
+
+
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts
index 1521bb742db..668bdbae43f 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts
@@ -1,93 +1,69 @@
-import { Component, OnDestroy, OnInit } from "@angular/core";
+import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
+import { FormControl, FormGroup } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
-import { map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs";
+import { combineLatest, Subject, switchMap, takeUntil } from "rxjs";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
-import { SelectItemView } from "@bitwarden/components";
+import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policy.view";
import {
- ProjectAccessPoliciesView,
- ServiceAccountProjectAccessPolicyView,
-} from "../../models/view/access-policy.view";
+ ApItemValueType,
+ convertToProjectServiceAccountsAccessPoliciesView,
+} from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
+import {
+ ApItemViewType,
+ convertPotentialGranteesToApItemViewType,
+ convertProjectServiceAccountsViewToApItemViews,
+} from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type";
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
-import {
- AccessSelectorComponent,
- AccessSelectorRowView,
-} from "../../shared/access-policies/access-selector.component";
@Component({
selector: "sm-project-service-accounts",
templateUrl: "./project-service-accounts.component.html",
})
export class ProjectServiceAccountsComponent implements OnInit, OnDestroy {
+ private currentAccessPolicies: ApItemViewType[];
private destroy$ = new Subject();
private organizationId: string;
private projectId: string;
- protected rows$: Observable =
- this.accessPolicyService.projectAccessPolicyChanges$.pipe(
- startWith(null),
- switchMap(() =>
- this.accessPolicyService.getProjectAccessPolicies(this.organizationId, this.projectId),
- ),
- map((policies) =>
- policies.serviceAccountAccessPolicies.map((policy) => ({
- type: "serviceAccount",
- name: policy.serviceAccountName,
- id: policy.serviceAccountId,
- accessPolicyId: policy.id,
- read: policy.read,
- write: policy.write,
- icon: AccessSelectorComponent.serviceAccountIcon,
- static: false,
- })),
- ),
- );
+ private currentAccessPolicies$ = combineLatest([this.route.params]).pipe(
+ switchMap(([params]) =>
+ this.accessPolicyService
+ .getProjectServiceAccountsAccessPolicies(params.organizationId, params.projectId)
+ .then((policies) => {
+ return convertProjectServiceAccountsViewToApItemViews(policies);
+ }),
+ ),
+ );
- protected async handleUpdateAccessPolicy(policy: AccessSelectorRowView) {
- try {
- return await this.accessPolicyService.updateAccessPolicy(
- AccessSelectorComponent.getBaseAccessPolicyView(policy),
- );
- } catch (e) {
- this.validationService.showError(e);
- }
- }
+ private potentialGrantees$ = combineLatest([this.route.params]).pipe(
+ switchMap(([params]) =>
+ this.accessPolicyService
+ .getServiceAccountsPotentialGrantees(params.organizationId)
+ .then((grantees) => {
+ return convertPotentialGranteesToApItemViewType(grantees);
+ }),
+ ),
+ );
- protected handleCreateAccessPolicies(selected: SelectItemView[]) {
- const projectAccessPoliciesView = new ProjectAccessPoliciesView();
- projectAccessPoliciesView.serviceAccountAccessPolicies = selected
- .filter(
- (selection) => AccessSelectorComponent.getAccessItemType(selection) === "serviceAccount",
- )
- .map((filtered) => {
- const view = new ServiceAccountProjectAccessPolicyView();
- view.grantedProjectId = this.projectId;
- view.serviceAccountId = filtered.id;
- view.read = true;
- view.write = false;
- return view;
- });
+ protected formGroup = new FormGroup({
+ accessPolicies: new FormControl([] as ApItemValueType[]),
+ });
- return this.accessPolicyService.createProjectAccessPolicies(
- this.organizationId,
- this.projectId,
- projectAccessPoliciesView,
- );
- }
-
- protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) {
- try {
- await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId);
- } catch (e) {
- this.validationService.showError(e);
- }
- }
+ protected loading = true;
+ protected potentialGrantees: ApItemViewType[];
+ protected items: ApItemViewType[];
constructor(
private route: ActivatedRoute,
+ private changeDetectorRef: ChangeDetectorRef,
private validationService: ValidationService,
private accessPolicyService: AccessPolicyService,
+ private platformUtilsService: PlatformUtilsService,
+ private i18nService: I18nService,
) {}
ngOnInit(): void {
@@ -95,10 +71,97 @@ export class ProjectServiceAccountsComponent implements OnInit, OnDestroy {
this.organizationId = params.organizationId;
this.projectId = params.projectId;
});
+
+ combineLatest([this.potentialGrantees$, this.currentAccessPolicies$])
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(([potentialGrantees, currentAccessPolicies]) => {
+ this.potentialGrantees = potentialGrantees;
+ this.items = this.getItems(potentialGrantees, currentAccessPolicies);
+ this.setSelected(currentAccessPolicies);
+ });
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
+
+ submit = async () => {
+ if (this.isFormInvalid()) {
+ return;
+ }
+ const formValues = this.formGroup.value.accessPolicies;
+ this.formGroup.disable();
+
+ try {
+ const accessPoliciesView = await this.updateProjectServiceAccountsAccessPolicies(
+ this.organizationId,
+ this.projectId,
+ formValues,
+ );
+
+ const updatedView = convertProjectServiceAccountsViewToApItemViews(accessPoliciesView);
+ this.items = this.getItems(this.potentialGrantees, updatedView);
+ this.setSelected(updatedView);
+
+ this.platformUtilsService.showToast(
+ "success",
+ null,
+ this.i18nService.t("projectAccessUpdated"),
+ );
+ } catch (e) {
+ this.validationService.showError(e);
+ this.setSelected(this.currentAccessPolicies);
+ }
+ this.formGroup.enable();
+ };
+
+ private setSelected(policiesToSelect: ApItemViewType[]) {
+ this.loading = true;
+ this.currentAccessPolicies = policiesToSelect;
+ if (policiesToSelect != undefined) {
+ // Must detect changes so that AccessSelector @Inputs() are aware of the latest
+ // potentialGrantees, otherwise no selected values will be patched below
+ this.changeDetectorRef.detectChanges();
+ this.formGroup.patchValue({
+ accessPolicies: policiesToSelect.map((m) => ({
+ type: m.type,
+ id: m.id,
+ permission: m.permission,
+ })),
+ });
+ }
+ this.loading = false;
+ }
+
+ private isFormInvalid(): boolean {
+ this.formGroup.markAllAsTouched();
+ return this.formGroup.invalid;
+ }
+
+ private async updateProjectServiceAccountsAccessPolicies(
+ organizationId: string,
+ projectId: string,
+ selectedPolicies: ApItemValueType[],
+ ): Promise {
+ const view = convertToProjectServiceAccountsAccessPoliciesView(projectId, selectedPolicies);
+ return await this.accessPolicyService.putProjectServiceAccountsAccessPolicies(
+ organizationId,
+ projectId,
+ view,
+ );
+ }
+
+ private getItems(potentialGrantees: ApItemViewType[], currentAccessPolicies: ApItemViewType[]) {
+ // If the user doesn't have access to the service account, they won't be in the potentialGrantees list.
+ // Add them to the potentialGrantees list if they are selected.
+ const items = [...potentialGrantees];
+ for (const policy of currentAccessPolicies) {
+ const exists = potentialGrantees.some((grantee) => grantee.id === policy.id);
+ if (!exists) {
+ items.push(policy);
+ }
+ }
+ return items;
+ }
}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html
index b97c5ef1141..623542bd33d 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html
@@ -1,17 +1,27 @@
-
-
- {{ "machineAccountProjectsDescription" | i18n }}
-
-
-
-
+
+
+
+
+
+
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts
index 2fcc10988df..a6f3d720b74 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts
@@ -1,90 +1,68 @@
-import { Component, OnDestroy, OnInit } from "@angular/core";
+import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
+import { FormControl, FormGroup } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
-import { combineLatestWith, map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs";
+import { combineLatest, Subject, switchMap, takeUntil } from "rxjs";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
-import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
-import { ServiceAccountProjectAccessPolicyView } from "../../models/view/access-policy.view";
-import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
+import { ServiceAccountGrantedPoliciesView } from "../../models/view/access-policy.view";
import {
- AccessSelectorComponent,
- AccessSelectorRowView,
-} from "../../shared/access-policies/access-selector.component";
+ ApItemValueType,
+ convertToServiceAccountGrantedPoliciesView,
+} from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
+import {
+ ApItemViewType,
+ convertPotentialGranteesToApItemViewType,
+ convertGrantedPoliciesToAccessPolicyItemViews,
+} from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type";
+import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
@Component({
selector: "sm-service-account-projects",
templateUrl: "./service-account-projects.component.html",
})
export class ServiceAccountProjectsComponent implements OnInit, OnDestroy {
+ private currentAccessPolicies: ApItemViewType[];
private destroy$ = new Subject();
- private serviceAccountId: string;
private organizationId: string;
+ private serviceAccountId: string;
- protected rows$: Observable =
- this.accessPolicyService.serviceAccountGrantedPolicyChanges$.pipe(
- startWith(null),
- combineLatestWith(this.route.params),
- switchMap(([_, params]) =>
- this.accessPolicyService.getGrantedPolicies(params.serviceAccountId, params.organizationId),
- ),
- map((policies) => {
- return policies.map((policy) => {
- return {
- type: "project",
- name: policy.grantedProjectName,
- id: policy.grantedProjectId,
- accessPolicyId: policy.id,
- read: policy.read,
- write: policy.write,
- icon: AccessSelectorComponent.projectIcon,
- static: false,
- } as AccessSelectorRowView;
- });
- }),
- );
+ private currentAccessPolicies$ = combineLatest([this.route.params]).pipe(
+ switchMap(([params]) =>
+ this.accessPolicyService
+ .getServiceAccountGrantedPolicies(params.organizationId, params.serviceAccountId)
+ .then((policies) => {
+ return convertGrantedPoliciesToAccessPolicyItemViews(policies);
+ }),
+ ),
+ );
- protected handleCreateAccessPolicies(selected: SelectItemView[]) {
- const serviceAccountProjectAccessPolicyView = selected
- .filter((selection) => AccessSelectorComponent.getAccessItemType(selection) === "project")
- .map((filtered) => {
- const view = new ServiceAccountProjectAccessPolicyView();
- view.serviceAccountId = this.serviceAccountId;
- view.grantedProjectId = filtered.id;
- view.read = true;
- view.write = false;
- return view;
- });
+ private potentialGrantees$ = combineLatest([this.route.params]).pipe(
+ switchMap(([params]) =>
+ this.accessPolicyService
+ .getProjectsPotentialGrantees(params.organizationId)
+ .then((grantees) => {
+ return convertPotentialGranteesToApItemViewType(grantees);
+ }),
+ ),
+ );
- return this.accessPolicyService.createGrantedPolicies(
- this.organizationId,
- this.serviceAccountId,
- serviceAccountProjectAccessPolicyView,
- );
- }
+ protected formGroup = new FormGroup({
+ accessPolicies: new FormControl([] as ApItemValueType[]),
+ });
- protected async handleUpdateAccessPolicy(policy: AccessSelectorRowView) {
- try {
- return await this.accessPolicyService.updateAccessPolicy(
- AccessSelectorComponent.getBaseAccessPolicyView(policy),
- );
- } catch (e) {
- this.validationService.showError(e);
- }
- }
-
- protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) {
- try {
- await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId);
- } catch (e) {
- this.validationService.showError(e);
- }
- }
+ protected loading = true;
+ protected potentialGrantees: ApItemViewType[];
constructor(
private route: ActivatedRoute,
+ private changeDetectorRef: ChangeDetectorRef,
private validationService: ValidationService,
private accessPolicyService: AccessPolicyService,
+ private platformUtilsService: PlatformUtilsService,
+ private i18nService: I18nService,
) {}
ngOnInit(): void {
@@ -92,10 +70,119 @@ export class ServiceAccountProjectsComponent implements OnInit, OnDestroy {
this.organizationId = params.organizationId;
this.serviceAccountId = params.serviceAccountId;
});
+
+ combineLatest([this.potentialGrantees$, this.currentAccessPolicies$])
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(([potentialGrantees, currentAccessPolicies]) => {
+ this.potentialGrantees = this.getPotentialGrantees(
+ potentialGrantees,
+ currentAccessPolicies,
+ );
+ this.setSelected(currentAccessPolicies);
+ });
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
+
+ submit = async () => {
+ if (this.isFormInvalid()) {
+ return;
+ }
+ const formValues = this.getFormValues();
+ this.formGroup.disable();
+
+ try {
+ const grantedViews = await this.updateServiceAccountGrantedPolicies(
+ this.organizationId,
+ this.serviceAccountId,
+ formValues,
+ );
+
+ this.currentAccessPolicies = convertGrantedPoliciesToAccessPolicyItemViews(grantedViews);
+
+ this.platformUtilsService.showToast(
+ "success",
+ null,
+ this.i18nService.t("serviceAccountAccessUpdated"),
+ );
+ } catch (e) {
+ this.validationService.showError(e);
+ this.setSelected(this.currentAccessPolicies);
+ }
+ this.formGroup.enable();
+ };
+
+ private setSelected(policiesToSelect: ApItemViewType[]) {
+ this.loading = true;
+ this.currentAccessPolicies = policiesToSelect;
+ if (policiesToSelect != undefined) {
+ // Must detect changes so that AccessSelector @Inputs() are aware of the latest
+ // potentialGrantees, otherwise no selected values will be patched below
+ this.changeDetectorRef.detectChanges();
+ this.formGroup.patchValue({
+ accessPolicies: policiesToSelect.map((m) => ({
+ type: m.type,
+ id: m.id,
+ permission: m.permission,
+ readOnly: m.readOnly,
+ })),
+ });
+ }
+ this.loading = false;
+ }
+
+ private isFormInvalid(): boolean {
+ this.formGroup.markAllAsTouched();
+ return this.formGroup.invalid;
+ }
+
+ private async updateServiceAccountGrantedPolicies(
+ organizationId: string,
+ serviceAccountId: string,
+ selectedPolicies: ApItemValueType[],
+ ): Promise {
+ const grantedViews = convertToServiceAccountGrantedPoliciesView(
+ serviceAccountId,
+ selectedPolicies,
+ );
+ return await this.accessPolicyService.putServiceAccountGrantedPolicies(
+ organizationId,
+ serviceAccountId,
+ grantedViews,
+ );
+ }
+
+ private getPotentialGrantees(
+ potentialGrantees: ApItemViewType[],
+ currentAccessPolicies: ApItemViewType[],
+ ) {
+ // If the user doesn't have access to the project, they won't be in the potentialGrantees list.
+ // Add them to the potentialGrantees list so they can be selected as read-only.
+ for (const policy of currentAccessPolicies) {
+ const exists = potentialGrantees.some((grantee) => grantee.id === policy.id);
+ if (!exists) {
+ potentialGrantees.push(policy);
+ }
+ }
+ return potentialGrantees;
+ }
+
+ private getFormValues(): ApItemValueType[] {
+ // The read-only disabled form values are not included in the formGroup value.
+ // Manually add them to the returned result to ensure they are included in the form submission.
+ let formValues = this.formGroup.value.accessPolicies;
+ formValues = formValues.concat(
+ this.currentAccessPolicies
+ .filter((m) => m.readOnly)
+ .map((m) => ({
+ id: m.id,
+ type: m.type,
+ permission: m.permission,
+ })),
+ );
+ return formValues;
+ }
}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html
index e1faf2a1859..e926ba6a13d 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html
@@ -29,14 +29,17 @@
bitRow
*ngFor="let item of selectionList.selectedItems; let i = index"
[formGroupName]="i"
+ [ngClass]="{ 'tw-text-muted': item.readOnly }"
>
-
+
+ |
+
+ {{ item.labelName }}
|
- {{ item.labelName }} |
+
+
+
+ {{ item.permission | i18n }}
+
+
+
{{ staticPermission | i18n }}
|
| | | | | |