mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-16 01:21:48 +01:00
SM-1146: Display total counts of projects, machine accounts, secrets in Secrets Manager (#9791)
* SM-1146: Secrets Manager total counts * SM-1146: Tab link component simplifications * SM-1146: Total counts update on CRUD * SM-1146: Total counts API paths update * SM-1146: Unit test coverage for services * SM-1146: Fix incorrect types returned * SM-1146: Storybook example for tab-link with child counter * SM-1146: Tailwind states with groups * SM-1146: Moving counts view types in one file * SM-1146: Moving counts methods, responses to one shared service * SM-1146: Removing redundant services imports * SM-1146: Removing redundant observables * SM-1337: Total counts hidden for suspended organizations * SM-1336: Total counts updated on import * SM-1336: Import error handling refactor * SM-1336: Import error handling improvements * SM-1336: Import error not working with project errors, Unit Test coverage * Update bitwarden_license/bit-web/src/app/secrets-manager/settings/porting/sm-import.component.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * SM-1336: UT deprecation removal * SM-1336: Better UT * SM-1336: Lint fix * SM-1146: Linter fix --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
parent
dfb69f8130
commit
a3bf74ae1b
@ -6,19 +6,31 @@
|
|||||||
[text]="'projects' | i18n"
|
[text]="'projects' | i18n"
|
||||||
route="projects"
|
route="projects"
|
||||||
[relativeTo]="route.parent"
|
[relativeTo]="route.parent"
|
||||||
></bit-nav-item>
|
>
|
||||||
|
<div slot="end" *ngIf="isOrgEnabled$ | async">
|
||||||
|
{{ organizationCounts?.projects }}
|
||||||
|
</div>
|
||||||
|
</bit-nav-item>
|
||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
icon="bwi-key"
|
icon="bwi-key"
|
||||||
[text]="'secrets' | i18n"
|
[text]="'secrets' | i18n"
|
||||||
route="secrets"
|
route="secrets"
|
||||||
[relativeTo]="route.parent"
|
[relativeTo]="route.parent"
|
||||||
></bit-nav-item>
|
>
|
||||||
|
<div slot="end" *ngIf="isOrgEnabled$ | async">
|
||||||
|
{{ organizationCounts?.secrets }}
|
||||||
|
</div>
|
||||||
|
</bit-nav-item>
|
||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
icon="bwi-wrench"
|
icon="bwi-wrench"
|
||||||
[text]="'machineAccounts' | i18n"
|
[text]="'machineAccounts' | i18n"
|
||||||
route="machine-accounts"
|
route="machine-accounts"
|
||||||
[relativeTo]="route.parent"
|
[relativeTo]="route.parent"
|
||||||
></bit-nav-item>
|
>
|
||||||
|
<div slot="end" *ngIf="isOrgEnabled$ | async">
|
||||||
|
{{ organizationCounts?.serviceAccounts }}
|
||||||
|
</div>
|
||||||
|
</bit-nav-item>
|
||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
icon="bwi-providers"
|
icon="bwi-providers"
|
||||||
[text]="'integrations' | i18n"
|
[text]="'integrations' | i18n"
|
||||||
|
@ -1,26 +1,91 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { concatMap } from "rxjs";
|
import {
|
||||||
|
combineLatest,
|
||||||
|
concatMap,
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
startWith,
|
||||||
|
Subject,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { SecretsManagerLogo } from "@bitwarden/web-vault/app/layouts/secrets-manager-logo";
|
import { SecretsManagerLogo } from "@bitwarden/web-vault/app/layouts/secrets-manager-logo";
|
||||||
|
|
||||||
|
import { OrganizationCounts } from "../models/view/counts.view";
|
||||||
|
import { ProjectService } from "../projects/project.service";
|
||||||
|
import { SecretService } from "../secrets/secret.service";
|
||||||
|
import { ServiceAccountService } from "../service-accounts/service-account.service";
|
||||||
|
import { SecretsManagerPortingApiService } from "../settings/services/sm-porting-api.service";
|
||||||
|
import { CountService } from "../shared/counts/count.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "sm-navigation",
|
selector: "sm-navigation",
|
||||||
templateUrl: "./navigation.component.html",
|
templateUrl: "./navigation.component.html",
|
||||||
})
|
})
|
||||||
export class NavigationComponent {
|
export class NavigationComponent implements OnInit, OnDestroy {
|
||||||
protected readonly logo = SecretsManagerLogo;
|
protected readonly logo = SecretsManagerLogo;
|
||||||
protected orgFilter = (org: Organization) => org.canAccessSecretsManager;
|
protected orgFilter = (org: Organization) => org.canAccessSecretsManager;
|
||||||
protected isAdmin$ = this.route.params.pipe(
|
protected isAdmin$: Observable<boolean>;
|
||||||
concatMap(
|
protected isOrgEnabled$: Observable<boolean>;
|
||||||
async (params) => (await this.organizationService.get(params.organizationId))?.isAdmin,
|
protected organizationCounts: OrganizationCounts;
|
||||||
),
|
private destroy$: Subject<void> = new Subject<void>();
|
||||||
);
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
|
private countService: CountService,
|
||||||
|
private projectService: ProjectService,
|
||||||
|
private secretService: SecretService,
|
||||||
|
private serviceAccountService: ServiceAccountService,
|
||||||
|
private portingApiService: SecretsManagerPortingApiService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
const org$ = this.route.params.pipe(
|
||||||
|
concatMap((params) => this.organizationService.get(params.organizationId)),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.isAdmin$ = org$.pipe(
|
||||||
|
map((org) => org?.isAdmin),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.isOrgEnabled$ = org$.pipe(
|
||||||
|
map((org) => org?.enabled),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
);
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
org$,
|
||||||
|
this.projectService.project$.pipe(startWith(null)),
|
||||||
|
this.secretService.secret$.pipe(startWith(null)),
|
||||||
|
this.serviceAccountService.serviceAccount$.pipe(startWith(null)),
|
||||||
|
this.portingApiService.imports$.pipe(startWith(null)),
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
filter(([org]) => org?.enabled),
|
||||||
|
switchMap(([org]) => this.countService.getOrganizationCounts(org.id)),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe((organizationCounts) => {
|
||||||
|
this.organizationCounts = {
|
||||||
|
projects: organizationCounts.projects,
|
||||||
|
secrets: organizationCounts.secrets,
|
||||||
|
serviceAccounts: organizationCounts.serviceAccounts,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
export type OrganizationCounts = {
|
||||||
|
projects: number;
|
||||||
|
secrets: number;
|
||||||
|
serviceAccounts: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProjectCounts = {
|
||||||
|
secrets: number;
|
||||||
|
people: number;
|
||||||
|
serviceAccounts: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceAccountCounts = {
|
||||||
|
projects: number;
|
||||||
|
people: number;
|
||||||
|
accessTokens: number;
|
||||||
|
};
|
@ -54,7 +54,7 @@
|
|||||||
[projects]="view.latestProjects"
|
[projects]="view.latestProjects"
|
||||||
></sm-projects-list>
|
></sm-projects-list>
|
||||||
<div *ngIf="view.allProjects.length > 0" class="tw-ml-auto tw-mt-4 tw-max-w-max">
|
<div *ngIf="view.allProjects.length > 0" class="tw-ml-auto tw-mt-4 tw-max-w-max">
|
||||||
{{ "showingPortionOfTotal" | i18n: view.latestProjects.length : view.allProjects.length }}
|
{{ "showingPortionOfTotal" | i18n: view.latestProjects.length : view.counts.projects }}
|
||||||
<a bitLink routerLink="projects" class="tw-ml-2">{{ "viewAll" | i18n }}</a>
|
<a bitLink routerLink="projects" class="tw-ml-2">{{ "viewAll" | i18n }}</a>
|
||||||
</div>
|
</div>
|
||||||
</sm-section>
|
</sm-section>
|
||||||
@ -72,7 +72,7 @@
|
|||||||
[secrets]="view.latestSecrets"
|
[secrets]="view.latestSecrets"
|
||||||
></sm-secrets-list>
|
></sm-secrets-list>
|
||||||
<div *ngIf="view.allSecrets.length > 0" class="tw-ml-auto tw-mt-4 tw-max-w-max">
|
<div *ngIf="view.allSecrets.length > 0" class="tw-ml-auto tw-mt-4 tw-max-w-max">
|
||||||
{{ "showingPortionOfTotal" | i18n: view.latestSecrets.length : view.allSecrets.length }}
|
{{ "showingPortionOfTotal" | i18n: view.latestSecrets.length : view.counts.secrets }}
|
||||||
<a bitLink routerLink="secrets" class="tw-ml-2">{{ "viewAll" | i18n }}</a>
|
<a bitLink routerLink="secrets" class="tw-ml-2">{{ "viewAll" | i18n }}</a>
|
||||||
</div>
|
</div>
|
||||||
</sm-section>
|
</sm-section>
|
||||||
|
@ -21,6 +21,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { OrganizationCounts } from "../models/view/counts.view";
|
||||||
import { ProjectListView } from "../models/view/project-list.view";
|
import { ProjectListView } from "../models/view/project-list.view";
|
||||||
import { SecretListView } from "../models/view/secret-list.view";
|
import { SecretListView } from "../models/view/secret-list.view";
|
||||||
import {
|
import {
|
||||||
@ -51,6 +52,7 @@ import {
|
|||||||
ServiceAccountOperation,
|
ServiceAccountOperation,
|
||||||
} from "../service-accounts/dialog/service-account-dialog.component";
|
} from "../service-accounts/dialog/service-account-dialog.component";
|
||||||
import { ServiceAccountService } from "../service-accounts/service-account.service";
|
import { ServiceAccountService } from "../service-accounts/service-account.service";
|
||||||
|
import { CountService } from "../shared/counts/count.service";
|
||||||
import { SecretsListComponent } from "../shared/secrets-list.component";
|
import { SecretsListComponent } from "../shared/secrets-list.component";
|
||||||
|
|
||||||
import { SMOnboardingTasks, SMOnboardingTasksService } from "./sm-onboarding-tasks.service";
|
import { SMOnboardingTasks, SMOnboardingTasksService } from "./sm-onboarding-tasks.service";
|
||||||
@ -87,11 +89,13 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
latestProjects: ProjectListView[];
|
latestProjects: ProjectListView[];
|
||||||
latestSecrets: SecretListView[];
|
latestSecrets: SecretListView[];
|
||||||
tasks: OrganizationTasks;
|
tasks: OrganizationTasks;
|
||||||
|
counts: OrganizationCounts;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private projectService: ProjectService,
|
private projectService: ProjectService,
|
||||||
|
private countService: CountService,
|
||||||
private secretService: SecretService,
|
private secretService: SecretService,
|
||||||
private serviceAccountService: ServiceAccountService,
|
private serviceAccountService: ServiceAccountService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
@ -148,10 +152,19 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
share(),
|
share(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const counts$ = combineLatest([
|
||||||
|
orgId$,
|
||||||
|
this.secretService.secret$.pipe(startWith(null)),
|
||||||
|
this.projectService.project$.pipe(startWith(null)),
|
||||||
|
]).pipe(
|
||||||
|
switchMap(([orgId]) => this.countService.getOrganizationCounts(orgId)),
|
||||||
|
share(),
|
||||||
|
);
|
||||||
|
|
||||||
this.view$ = orgId$.pipe(
|
this.view$ = orgId$.pipe(
|
||||||
switchMap((orgId) =>
|
switchMap((orgId) =>
|
||||||
combineLatest([projects$, secrets$, serviceAccounts$]).pipe(
|
combineLatest([projects$, secrets$, serviceAccounts$, counts$]).pipe(
|
||||||
switchMap(async ([projects, secrets, serviceAccounts]) => ({
|
switchMap(async ([projects, secrets, serviceAccounts, counts]) => ({
|
||||||
latestProjects: this.getRecentItems(projects, this.tableSize),
|
latestProjects: this.getRecentItems(projects, this.tableSize),
|
||||||
latestSecrets: this.getRecentItems(secrets, this.tableSize),
|
latestSecrets: this.getRecentItems(secrets, this.tableSize),
|
||||||
allProjects: projects,
|
allProjects: projects,
|
||||||
@ -162,6 +175,11 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
createProject: projects.length > 0,
|
createProject: projects.length > 0,
|
||||||
createServiceAccount: serviceAccounts.length > 0,
|
createServiceAccount: serviceAccounts.length > 0,
|
||||||
}),
|
}),
|
||||||
|
counts: {
|
||||||
|
projects: counts.projects,
|
||||||
|
secrets: counts.secrets,
|
||||||
|
serviceAccounts: counts.serviceAccounts,
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -3,10 +3,25 @@
|
|||||||
<bit-breadcrumb [route]="['..']" icon="bwi-angle-left">{{ "projects" | i18n }}</bit-breadcrumb>
|
<bit-breadcrumb [route]="['..']" icon="bwi-angle-left">{{ "projects" | i18n }}</bit-breadcrumb>
|
||||||
</bit-breadcrumbs>
|
</bit-breadcrumbs>
|
||||||
<bit-tab-nav-bar label="Main" slot="tabs">
|
<bit-tab-nav-bar label="Main" slot="tabs">
|
||||||
<bit-tab-link [route]="['secrets']">{{ "secrets" | i18n }}</bit-tab-link>
|
<bit-tab-link [route]="['secrets']">
|
||||||
|
{{ "secrets" | i18n }}
|
||||||
|
<div slot="end" class="tw-text-muted">
|
||||||
|
{{ projectCounts?.secrets }}
|
||||||
|
</div>
|
||||||
|
</bit-tab-link>
|
||||||
<ng-container *ngIf="project.write">
|
<ng-container *ngIf="project.write">
|
||||||
<bit-tab-link [route]="['people']">{{ "people" | i18n }}</bit-tab-link>
|
<bit-tab-link [route]="['people']">
|
||||||
<bit-tab-link [route]="['machine-accounts']">{{ "machineAccounts" | i18n }}</bit-tab-link>
|
{{ "people" | i18n }}
|
||||||
|
<div slot="end" class="tw-text-muted">
|
||||||
|
{{ projectCounts?.people }}
|
||||||
|
</div>
|
||||||
|
</bit-tab-link>
|
||||||
|
<bit-tab-link [route]="['machine-accounts']">
|
||||||
|
{{ "machineAccounts" | i18n }}
|
||||||
|
<div slot="end" class="tw-text-muted">
|
||||||
|
{{ projectCounts?.serviceAccounts }}
|
||||||
|
</div>
|
||||||
|
</bit-tab-link>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</bit-tab-nav-bar>
|
</bit-tab-nav-bar>
|
||||||
<sm-new-menu></sm-new-menu>
|
<sm-new-menu></sm-new-menu>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
combineLatest,
|
||||||
filter,
|
filter,
|
||||||
@ -13,11 +13,13 @@ import {
|
|||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { ProjectCounts } from "../../models/view/counts.view";
|
||||||
import { ProjectView } from "../../models/view/project.view";
|
import { ProjectView } from "../../models/view/project.view";
|
||||||
|
import { SecretService } from "../../secrets/secret.service";
|
||||||
|
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
||||||
|
import { CountService } from "../../shared/counts/count.service";
|
||||||
import {
|
import {
|
||||||
OperationType,
|
OperationType,
|
||||||
ProjectDialogComponent,
|
ProjectDialogComponent,
|
||||||
@ -31,6 +33,7 @@ import { ProjectService } from "../project.service";
|
|||||||
})
|
})
|
||||||
export class ProjectComponent implements OnInit, OnDestroy {
|
export class ProjectComponent implements OnInit, OnDestroy {
|
||||||
protected project$: Observable<ProjectView>;
|
protected project$: Observable<ProjectView>;
|
||||||
|
protected projectCounts: ProjectCounts;
|
||||||
|
|
||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
private projectId: string;
|
private projectId: string;
|
||||||
@ -40,11 +43,11 @@ export class ProjectComponent implements OnInit, OnDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private projectService: ProjectService,
|
private projectService: ProjectService,
|
||||||
private router: Router,
|
private secretService: SecretService,
|
||||||
|
private accessPolicyService: AccessPolicyService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
|
private countService: CountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -62,13 +65,23 @@ export class ProjectComponent implements OnInit, OnDestroy {
|
|||||||
const organization$ = this.route.params.pipe(
|
const organization$ = this.route.params.pipe(
|
||||||
concatMap((params) => this.organizationService.get$(params.organizationId)),
|
concatMap((params) => this.organizationService.get$(params.organizationId)),
|
||||||
);
|
);
|
||||||
|
const projectCounts$ = combineLatest([
|
||||||
|
this.route.params,
|
||||||
|
this.secretService.secret$.pipe(startWith(null)),
|
||||||
|
this.accessPolicyService.accessPolicy$.pipe(startWith(null)),
|
||||||
|
]).pipe(switchMap(([params]) => this.countService.getProjectCounts(params.projectId)));
|
||||||
|
|
||||||
combineLatest([projectId$, organization$])
|
combineLatest([projectId$, organization$, projectCounts$])
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe(([projectId, organization]) => {
|
.subscribe(([projectId, organization, projectCounts]) => {
|
||||||
this.organizationId = organization.id;
|
this.organizationId = organization.id;
|
||||||
this.projectId = projectId;
|
this.projectId = projectId;
|
||||||
this.organizationEnabled = organization.enabled;
|
this.organizationEnabled = organization.enabled;
|
||||||
|
this.projectCounts = {
|
||||||
|
secrets: projectCounts.secrets,
|
||||||
|
people: projectCounts.people,
|
||||||
|
serviceAccounts: projectCounts.serviceAccounts,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { ServiceAccountPeopleAccessPoliciesView } from "../../models/view/access-policies/service-account-people-access-policies.view";
|
||||||
import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service";
|
import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service";
|
||||||
import {
|
import {
|
||||||
ApItemValueType,
|
ApItemValueType,
|
||||||
@ -179,7 +180,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy {
|
|||||||
private async updateServiceAccountPeopleAccessPolicies(
|
private async updateServiceAccountPeopleAccessPolicies(
|
||||||
serviceAccountId: string,
|
serviceAccountId: string,
|
||||||
selectedPolicies: ApItemValueType[],
|
selectedPolicies: ApItemValueType[],
|
||||||
) {
|
): Promise<ServiceAccountPeopleAccessPoliciesView> {
|
||||||
const serviceAccountPeopleView = convertToPeopleAccessPoliciesView(selectedPolicies);
|
const serviceAccountPeopleView = convertToPeopleAccessPoliciesView(selectedPolicies);
|
||||||
return await this.accessPolicyService.putServiceAccountPeopleAccessPolicies(
|
return await this.accessPolicyService.putServiceAccountPeopleAccessPolicies(
|
||||||
serviceAccountId,
|
serviceAccountId,
|
||||||
|
@ -10,9 +10,24 @@
|
|||||||
</bit-breadcrumbs>
|
</bit-breadcrumbs>
|
||||||
<sm-new-menu></sm-new-menu>
|
<sm-new-menu></sm-new-menu>
|
||||||
<bit-tab-nav-bar label="Main" slot="tabs">
|
<bit-tab-nav-bar label="Main" slot="tabs">
|
||||||
<bit-tab-link [route]="['projects']">{{ "projects" | i18n }}</bit-tab-link>
|
<bit-tab-link [route]="['projects']">
|
||||||
<bit-tab-link [route]="['people']">{{ "people" | i18n }}</bit-tab-link>
|
{{ "projects" | i18n }}
|
||||||
<bit-tab-link [route]="['access']">{{ "accessTokens" | i18n }}</bit-tab-link>
|
<div slot="end" class="tw-text-muted">
|
||||||
|
{{ serviceAccountCounts?.projects }}
|
||||||
|
</div>
|
||||||
|
</bit-tab-link>
|
||||||
|
<bit-tab-link [route]="['people']">
|
||||||
|
{{ "people" | i18n }}
|
||||||
|
<div slot="end" class="tw-text-muted">
|
||||||
|
{{ serviceAccountCounts?.people }}
|
||||||
|
</div>
|
||||||
|
</bit-tab-link>
|
||||||
|
<bit-tab-link [route]="['access']">
|
||||||
|
{{ "accessTokens" | i18n }}
|
||||||
|
<div slot="end" class="tw-text-muted">
|
||||||
|
{{ serviceAccountCounts?.accessTokens }}
|
||||||
|
</div>
|
||||||
|
</bit-tab-link>
|
||||||
<bit-tab-link [route]="['events']">{{ "eventLogs" | i18n }}</bit-tab-link>
|
<bit-tab-link [route]="['events']">{{ "eventLogs" | i18n }}</bit-tab-link>
|
||||||
</bit-tab-nav-bar>
|
</bit-tab-nav-bar>
|
||||||
<button
|
<button
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { Subject, combineLatest, filter, startWith, switchMap, takeUntil } from "rxjs";
|
import { Subject, combineLatest, filter, startWith, switchMap, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { ServiceAccountCounts } from "../models/view/counts.view";
|
||||||
import { ServiceAccountView } from "../models/view/service-account.view";
|
import { ServiceAccountView } from "../models/view/service-account.view";
|
||||||
|
import { AccessPolicyService } from "../shared/access-policies/access-policy.service";
|
||||||
|
import { CountService } from "../shared/counts/count.service";
|
||||||
|
|
||||||
|
import { AccessService } from "./access/access.service";
|
||||||
import { AccessTokenCreateDialogComponent } from "./access/dialogs/access-token-create-dialog.component";
|
import { AccessTokenCreateDialogComponent } from "./access/dialogs/access-token-create-dialog.component";
|
||||||
import { ServiceAccountService } from "./service-account.service";
|
import { ServiceAccountService } from "./service-account.service";
|
||||||
|
|
||||||
@ -17,7 +19,6 @@ import { ServiceAccountService } from "./service-account.service";
|
|||||||
})
|
})
|
||||||
export class ServiceAccountComponent implements OnInit, OnDestroy {
|
export class ServiceAccountComponent implements OnInit, OnDestroy {
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private organizationId: string;
|
|
||||||
private serviceAccountId: string;
|
private serviceAccountId: string;
|
||||||
|
|
||||||
private onChange$ = this.serviceAccountService.serviceAccount$.pipe(
|
private onChange$ = this.serviceAccountService.serviceAccount$.pipe(
|
||||||
@ -34,20 +35,39 @@ export class ServiceAccountComponent implements OnInit, OnDestroy {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
protected serviceAccountCounts: ServiceAccountCounts;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private serviceAccountService: ServiceAccountService,
|
private serviceAccountService: ServiceAccountService,
|
||||||
|
private accessPolicyService: AccessPolicyService,
|
||||||
|
private accessService: AccessService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private router: Router,
|
private countService: CountService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.serviceAccount$.pipe(takeUntil(this.destroy$)).subscribe((serviceAccountView) => {
|
const serviceAccountCounts$ = combineLatest([
|
||||||
this.serviceAccountView = serviceAccountView;
|
this.route.params,
|
||||||
});
|
this.accessPolicyService.accessPolicy$.pipe(startWith(null)),
|
||||||
|
this.accessService.accessToken$.pipe(startWith(null)),
|
||||||
|
this.onChange$,
|
||||||
|
]).pipe(
|
||||||
|
switchMap(([params, _]) =>
|
||||||
|
this.countService.getServiceAccountCounts(params.serviceAccountId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
combineLatest([this.serviceAccount$, serviceAccountCounts$])
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe(([serviceAccountView, serviceAccountCounts]) => {
|
||||||
|
this.serviceAccountView = serviceAccountView;
|
||||||
|
this.serviceAccountCounts = {
|
||||||
|
projects: serviceAccountCounts.projects,
|
||||||
|
people: serviceAccountCounts.people,
|
||||||
|
accessTokens: serviceAccountCounts.accessTokens,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export class SecretsManagerImportErrorLine {
|
export class SecretsManagerImportErrorLine {
|
||||||
id: number;
|
id: number;
|
||||||
type: "Project" | "Secret";
|
type: "Project" | "Secret";
|
||||||
key: "string";
|
key: string;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,11 @@ import { FormControl, FormGroup } from "@angular/forms";
|
|||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SecretsManagerImportErrorDialogComponent,
|
SecretsManagerImportErrorDialogComponent,
|
||||||
@ -33,8 +31,7 @@ export class SecretsManagerImportComponent implements OnInit, OnDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private organizationService: OrganizationService,
|
private toastService: ToastService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
protected fileDownloadService: FileDownloadService,
|
protected fileDownloadService: FileDownloadService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private secretsManagerPortingApiService: SecretsManagerPortingApiService,
|
private secretsManagerPortingApiService: SecretsManagerPortingApiService,
|
||||||
@ -60,45 +57,42 @@ export class SecretsManagerImportComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (importContents == null) {
|
if (importContents == null) {
|
||||||
this.platformUtilsService.showToast(
|
this.toastService.showToast({
|
||||||
"error",
|
variant: "error",
|
||||||
this.i18nService.t("errorOccurred"),
|
title: this.i18nService.t("errorOccurred"),
|
||||||
this.i18nService.t("selectFile"),
|
message: this.i18nService.t("selectFile"),
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const error = await this.secretsManagerPortingApiService.import(this.orgId, importContents);
|
await this.secretsManagerPortingApiService.import(this.orgId, importContents);
|
||||||
|
|
||||||
if (error?.lines?.length > 0) {
|
this.toastService.showToast({
|
||||||
this.openImportErrorDialog(error);
|
variant: "success",
|
||||||
return;
|
title: null,
|
||||||
} else if (!Utils.isNullOrWhitespace(error?.message)) {
|
message: this.i18nService.t("importSuccess"),
|
||||||
this.platformUtilsService.showToast(
|
});
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
error.message,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
} else if (error != null) {
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("errorReadingImportFile"),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("importSuccess"));
|
|
||||||
this.clearForm();
|
this.clearForm();
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
this.platformUtilsService.showToast(
|
if (error instanceof SecretsManagerImportError && error?.lines?.length > 0) {
|
||||||
"error",
|
this.openImportErrorDialog(error);
|
||||||
this.i18nService.t("errorOccurred"),
|
} else {
|
||||||
this.i18nService.t("errorReadingImportFile"),
|
let message;
|
||||||
);
|
if (error instanceof Error && !Utils.isNullOrWhitespace(error?.message)) {
|
||||||
this.logService.error(error);
|
message = error.message;
|
||||||
|
} else {
|
||||||
|
message = this.i18nService.t("errorReadingImportFile");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: this.i18nService.t("errorOccurred"),
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logService.error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,286 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||||
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
|
import { SecretsManagerImportError } from "../models/error/sm-import-error";
|
||||||
|
import { SecretsManagerImportRequest } from "../models/requests/sm-import.request";
|
||||||
|
import { SecretsManagerImportedProjectRequest } from "../models/requests/sm-imported-project.request";
|
||||||
|
import { SecretsManagerImportedSecretRequest } from "../models/requests/sm-imported-secret.request";
|
||||||
|
|
||||||
|
import { SecretsManagerPortingApiService } from "./sm-porting-api.service";
|
||||||
|
|
||||||
|
describe("SecretsManagerPortingApiService", () => {
|
||||||
|
let sut: SecretsManagerPortingApiService;
|
||||||
|
|
||||||
|
const apiService = mock<ApiService>();
|
||||||
|
const encryptService = mock<EncryptService>();
|
||||||
|
const cryptoService = mock<CryptoService>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
sut = new SecretsManagerPortingApiService(apiService, encryptService, cryptoService);
|
||||||
|
|
||||||
|
encryptService.encrypt.mockResolvedValue(mockEncryptedString);
|
||||||
|
encryptService.decryptToUtf8.mockResolvedValue(mockUnencryptedString);
|
||||||
|
|
||||||
|
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||||
|
const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey;
|
||||||
|
cryptoService.getOrgKey.mockResolvedValue(mockOrgKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("instantiates", () => {
|
||||||
|
expect(sut).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("import", () => {
|
||||||
|
const organizationId = Utils.newGuid();
|
||||||
|
const project1 = createProject();
|
||||||
|
const project2 = createProject();
|
||||||
|
const secret1 = createSecret(project1.id);
|
||||||
|
const secret2 = createSecret(project1.id);
|
||||||
|
|
||||||
|
const importData = createImportData([project1, project2], [secret1, secret2]);
|
||||||
|
|
||||||
|
it("emits the import successful", async () => {
|
||||||
|
const expectedRequest = toRequest([project1, project2], [secret1, secret2]);
|
||||||
|
|
||||||
|
let subscriptionCount = 0;
|
||||||
|
sut.imports$.subscribe((request) => {
|
||||||
|
expect(request).toBeDefined();
|
||||||
|
expect(request.projects.length).toEqual(2);
|
||||||
|
expect(request.secrets.length).toEqual(2);
|
||||||
|
expect(request.projects[0]).toEqual(expectedRequest.projects[0]);
|
||||||
|
expect(request.projects[1]).toEqual(expectedRequest.projects[1]);
|
||||||
|
expect(request.secrets[0]).toEqual(expectedRequest.secrets[0]);
|
||||||
|
expect(request.secrets[1]).toEqual(expectedRequest.secrets[1]);
|
||||||
|
subscriptionCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
await sut.import(organizationId, importData);
|
||||||
|
|
||||||
|
expect(subscriptionCount).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correct api service send parameters", async () => {
|
||||||
|
const expectedRequest = toRequest([project1, project2], [secret1, secret2]);
|
||||||
|
|
||||||
|
await sut.import(organizationId, importData);
|
||||||
|
|
||||||
|
expect(apiService.send).toHaveBeenCalledWith(
|
||||||
|
"POST",
|
||||||
|
`/sm/${organizationId}/import`,
|
||||||
|
expectedRequest,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("throws SecretsManagerImportError", () => {
|
||||||
|
it("server error", async () => {
|
||||||
|
apiService.send.mockRejectedValue(new Error("server error"));
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
await sut.import(organizationId, importData);
|
||||||
|
}).rejects.toThrow(new SecretsManagerImportError("server error"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validation error project invalid field in list", async () => {
|
||||||
|
apiService.send.mockRejectedValue({
|
||||||
|
message: "invalid field",
|
||||||
|
validationErrors: {
|
||||||
|
"$.Projects[1].id": ["invalid id"],
|
||||||
|
},
|
||||||
|
} as ValidationError);
|
||||||
|
|
||||||
|
const expectedError = new SecretsManagerImportError();
|
||||||
|
expectedError.lines = [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: "Project",
|
||||||
|
key: project2.name,
|
||||||
|
errorMessage: "invalid id",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sut.import(organizationId, importData);
|
||||||
|
// Expected to throw error before this line
|
||||||
|
expect(true).toBe(false);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
expect(err).toBeInstanceOf(SecretsManagerImportError);
|
||||||
|
const importError = err as SecretsManagerImportError;
|
||||||
|
expect(importError.lines).toEqual(expectedError.lines);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validation error project invalid field in field", async () => {
|
||||||
|
apiService.send.mockRejectedValue({
|
||||||
|
message: "invalid field",
|
||||||
|
validationErrors: {
|
||||||
|
"Projects[1].id": ["invalid id"],
|
||||||
|
},
|
||||||
|
} as ValidationError);
|
||||||
|
|
||||||
|
const expectedError = new SecretsManagerImportError();
|
||||||
|
expectedError.lines = [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: "Project",
|
||||||
|
key: project2.name,
|
||||||
|
errorMessage: "invalid id",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sut.import(organizationId, importData);
|
||||||
|
// Expected to throw error before this line
|
||||||
|
expect(true).toBe(false);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
expect(err).toBeInstanceOf(SecretsManagerImportError);
|
||||||
|
const importError = err as SecretsManagerImportError;
|
||||||
|
expect(importError.lines).toEqual(expectedError.lines);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validation error secret invalid field in list", async () => {
|
||||||
|
apiService.send.mockRejectedValue({
|
||||||
|
message: "invalid field",
|
||||||
|
validationErrors: {
|
||||||
|
"$.Secrets[1].id": ["invalid id"],
|
||||||
|
},
|
||||||
|
} as ValidationError);
|
||||||
|
|
||||||
|
const expectedError = new SecretsManagerImportError();
|
||||||
|
expectedError.lines = [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: "Secret",
|
||||||
|
key: secret2.key,
|
||||||
|
errorMessage: "invalid id",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sut.import(organizationId, importData);
|
||||||
|
// Expected to throw error before this line
|
||||||
|
expect(true).toBe(false);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
expect(err).toBeInstanceOf(SecretsManagerImportError);
|
||||||
|
const importError = err as SecretsManagerImportError;
|
||||||
|
expect(importError.lines).toEqual(expectedError.lines);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validation error secret invalid field in field", async () => {
|
||||||
|
apiService.send.mockRejectedValue({
|
||||||
|
message: "invalid field",
|
||||||
|
validationErrors: {
|
||||||
|
"Secrets[1].id": ["invalid id"],
|
||||||
|
},
|
||||||
|
} as ValidationError);
|
||||||
|
|
||||||
|
const expectedError = new SecretsManagerImportError();
|
||||||
|
expectedError.lines = [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: "Secret",
|
||||||
|
key: secret2.key,
|
||||||
|
errorMessage: "invalid id",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sut.import(organizationId, importData);
|
||||||
|
// Expected to throw error before this line
|
||||||
|
expect(true).toBe(false);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
expect(err).toBeInstanceOf(SecretsManagerImportError);
|
||||||
|
const importError = err as SecretsManagerImportError;
|
||||||
|
expect(importError.lines).toEqual(expectedError.lines);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type ValidationError = {
|
||||||
|
message: string;
|
||||||
|
validationErrors: Record<string, string[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImportProject = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImportSecret = {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
note: string;
|
||||||
|
projectIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function createProject(): ImportProject {
|
||||||
|
const id = Utils.newGuid();
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
name: "project " + id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSecret(projectId: string): ImportSecret {
|
||||||
|
const id = Utils.newGuid();
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
key: "key " + id,
|
||||||
|
value: "value " + id,
|
||||||
|
note: "note " + id,
|
||||||
|
projectIds: [projectId],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createImportData(projects: ImportProject[], secrets: ImportSecret[]): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
projects: projects,
|
||||||
|
secrets: secrets,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRequest(
|
||||||
|
projects: ImportProject[],
|
||||||
|
secrets: ImportSecret[],
|
||||||
|
): SecretsManagerImportRequest {
|
||||||
|
return {
|
||||||
|
projects: projects.map(
|
||||||
|
(project) =>
|
||||||
|
({
|
||||||
|
id: project.id,
|
||||||
|
name: mockEncryptedString,
|
||||||
|
}) as SecretsManagerImportedProjectRequest,
|
||||||
|
),
|
||||||
|
secrets: secrets.map(
|
||||||
|
(secret) =>
|
||||||
|
({
|
||||||
|
id: secret.id,
|
||||||
|
key: mockEncryptedString,
|
||||||
|
value: mockEncryptedString,
|
||||||
|
note: mockEncryptedString,
|
||||||
|
projectIds: secret.projectIds,
|
||||||
|
}) as SecretsManagerImportedSecretRequest,
|
||||||
|
),
|
||||||
|
} as SecretsManagerImportRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockEncryptedString = {
|
||||||
|
encryptedString: "mockEncryptedString",
|
||||||
|
} as EncString;
|
||||||
|
const mockUnencryptedString = "mockUnEncryptedString";
|
@ -1,10 +1,10 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
|
||||||
import { SecretsManagerImportError } from "../models/error/sm-import-error";
|
import { SecretsManagerImportError } from "../models/error/sm-import-error";
|
||||||
@ -22,11 +22,13 @@ import {
|
|||||||
providedIn: "root",
|
providedIn: "root",
|
||||||
})
|
})
|
||||||
export class SecretsManagerPortingApiService {
|
export class SecretsManagerPortingApiService {
|
||||||
|
protected _imports = new Subject<SecretsManagerImportRequest>();
|
||||||
|
imports$ = this._imports.asObservable();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private i18nService: I18nService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async export(organizationId: string): Promise<string> {
|
async export(organizationId: string): Promise<string> {
|
||||||
@ -45,7 +47,7 @@ export class SecretsManagerPortingApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async import(organizationId: string, fileContents: string): Promise<SecretsManagerImportError> {
|
async import(organizationId: string, fileContents: string): Promise<void> {
|
||||||
let requestObject = {};
|
let requestObject = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -59,9 +61,11 @@ export class SecretsManagerPortingApiService {
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this._imports.next(requestBody);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorResponse = new ErrorResponse(error, 400);
|
const errorResponse = new ErrorResponse(error, 400);
|
||||||
return this.handleServerError(errorResponse, requestObject);
|
throw this.handleServerError(errorResponse, requestObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,12 +166,12 @@ export class SecretsManagerPortingApiService {
|
|||||||
let itemType;
|
let itemType;
|
||||||
const id = Number(key.match(/[0-9]+/)[0]);
|
const id = Number(key.match(/[0-9]+/)[0]);
|
||||||
|
|
||||||
switch (key.match(/^\w+/)[0]) {
|
switch (key.match(/^[$\\.]*(\w+)/)[1].toLowerCase()) {
|
||||||
case "Projects":
|
case "projects":
|
||||||
item = importResult.projects[id];
|
item = importResult.projects[id];
|
||||||
itemType = "Project";
|
itemType = "Project";
|
||||||
break;
|
break;
|
||||||
case "Secrets":
|
case "secrets":
|
||||||
item = importResult.secrets[id];
|
item = importResult.secrets[id];
|
||||||
itemType = "Secret";
|
itemType = "Secret";
|
||||||
break;
|
break;
|
||||||
@ -177,8 +181,8 @@ export class SecretsManagerPortingApiService {
|
|||||||
|
|
||||||
result.lines.push({
|
result.lines.push({
|
||||||
id: id + 1,
|
id: id + 1,
|
||||||
type: itemType == "Project" ? "Project" : "Secret",
|
type: itemType === "Project" ? "Project" : "Secret",
|
||||||
key: item.key,
|
key: itemType === "Project" ? item.name : item.key,
|
||||||
errorMessage: value.length > 0 ? value[0] : "",
|
errorMessage: value.length > 0 ? value[0] : "",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,6 @@ import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
|
|||||||
import { SecretsManagerImportErrorDialogComponent } from "./dialog/sm-import-error-dialog.component";
|
import { SecretsManagerImportErrorDialogComponent } from "./dialog/sm-import-error-dialog.component";
|
||||||
import { SecretsManagerExportComponent } from "./porting/sm-export.component";
|
import { SecretsManagerExportComponent } from "./porting/sm-export.component";
|
||||||
import { SecretsManagerImportComponent } from "./porting/sm-import.component";
|
import { SecretsManagerImportComponent } from "./porting/sm-import.component";
|
||||||
import { SecretsManagerPortingApiService } from "./services/sm-porting-api.service";
|
|
||||||
import { SecretsManagerPortingService } from "./services/sm-porting.service";
|
import { SecretsManagerPortingService } from "./services/sm-porting.service";
|
||||||
import { SettingsRoutingModule } from "./settings-routing.module";
|
import { SettingsRoutingModule } from "./settings-routing.module";
|
||||||
|
|
||||||
@ -16,6 +15,6 @@ import { SettingsRoutingModule } from "./settings-routing.module";
|
|||||||
SecretsManagerExportComponent,
|
SecretsManagerExportComponent,
|
||||||
SecretsManagerImportErrorDialogComponent,
|
SecretsManagerImportErrorDialogComponent,
|
||||||
],
|
],
|
||||||
providers: [SecretsManagerPortingService, SecretsManagerPortingApiService],
|
providers: [SecretsManagerPortingService],
|
||||||
})
|
})
|
||||||
export class SettingsModule {}
|
export class SettingsModule {}
|
||||||
|
@ -0,0 +1,359 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||||
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
|
import {
|
||||||
|
GroupAccessPolicyView,
|
||||||
|
ServiceAccountAccessPolicyView,
|
||||||
|
UserAccessPolicyView,
|
||||||
|
} from "../../models/view/access-policies/access-policy.view";
|
||||||
|
import { ProjectPeopleAccessPoliciesView } from "../../models/view/access-policies/project-people-access-policies.view";
|
||||||
|
import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policies/project-service-accounts-access-policies.view";
|
||||||
|
import {
|
||||||
|
GrantedProjectPolicyPermissionDetailsView,
|
||||||
|
ServiceAccountGrantedPoliciesView,
|
||||||
|
} from "../../models/view/access-policies/service-account-granted-policies.view";
|
||||||
|
import { ServiceAccountPeopleAccessPoliciesView } from "../../models/view/access-policies/service-account-people-access-policies.view";
|
||||||
|
|
||||||
|
import { AccessPolicyService } from "./access-policy.service";
|
||||||
|
import { PeopleAccessPoliciesRequest } from "./models/requests/people-access-policies.request";
|
||||||
|
import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request";
|
||||||
|
import { ServiceAccountGrantedPoliciesRequest } from "./models/requests/service-account-granted-policies.request";
|
||||||
|
|
||||||
|
import { trackEmissions } from "@bitwarden/common/../spec";
|
||||||
|
|
||||||
|
describe("AccessPolicyService", () => {
|
||||||
|
let sut: AccessPolicyService;
|
||||||
|
|
||||||
|
const cryptoService = mock<CryptoService>();
|
||||||
|
const apiService = mock<ApiService>();
|
||||||
|
const encryptService = mock<EncryptService>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
sut = new AccessPolicyService(cryptoService, apiService, encryptService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("instantiates", () => {
|
||||||
|
expect(sut).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("putProjectPeopleAccessPolicies", () => {
|
||||||
|
it("emits the updated policies", async () => {
|
||||||
|
const userAccessPolicyView1 = createUserAccessPolicyView(false, true);
|
||||||
|
const userAccessPolicyView2 = createUserAccessPolicyView(true, false);
|
||||||
|
const groupAccessPolicyView1 = createGroupAccessPolicyView(false, true);
|
||||||
|
const groupAccessPolicyView2 = createGroupAccessPolicyView(true, false);
|
||||||
|
|
||||||
|
const view = {
|
||||||
|
userAccessPolicies: [userAccessPolicyView1, userAccessPolicyView2],
|
||||||
|
groupAccessPolicies: [groupAccessPolicyView1, groupAccessPolicyView2],
|
||||||
|
} as ProjectPeopleAccessPoliciesView;
|
||||||
|
|
||||||
|
apiService.send.mockResolvedValue(toProjectPeopleAccessPoliciesResponseRaw(view));
|
||||||
|
const emissions = trackEmissions(sut.accessPolicy$);
|
||||||
|
const expectedRequest = toPeopleAccessPoliciesRequest(view);
|
||||||
|
const projectId = Utils.newGuid();
|
||||||
|
|
||||||
|
const result = await sut.putProjectPeopleAccessPolicies(projectId, view);
|
||||||
|
|
||||||
|
expect(result).toEqual(view);
|
||||||
|
expect(apiService.send).toHaveBeenCalledWith(
|
||||||
|
"PUT",
|
||||||
|
"/projects/" + projectId + "/access-policies/people",
|
||||||
|
expectedRequest,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(emissions).toEqual([view]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("putServiceAccountPeopleAccessPolicies", () => {
|
||||||
|
it("emits the updated policies", async () => {
|
||||||
|
const userAccessPolicyView1 = createUserAccessPolicyView(false, true);
|
||||||
|
const userAccessPolicyView2 = createUserAccessPolicyView(true, false);
|
||||||
|
const groupAccessPolicyView1 = createGroupAccessPolicyView(false, true);
|
||||||
|
const groupAccessPolicyView2 = createGroupAccessPolicyView(true, false);
|
||||||
|
|
||||||
|
const view = {
|
||||||
|
userAccessPolicies: [userAccessPolicyView1, userAccessPolicyView2],
|
||||||
|
groupAccessPolicies: [groupAccessPolicyView1, groupAccessPolicyView2],
|
||||||
|
} as ServiceAccountPeopleAccessPoliciesView;
|
||||||
|
|
||||||
|
apiService.send.mockResolvedValue(toServiceAccountPeopleAccessPoliciesResponseRaw(view));
|
||||||
|
const emissions = trackEmissions(sut.accessPolicy$);
|
||||||
|
const expectedRequest = toPeopleAccessPoliciesRequest(view);
|
||||||
|
const projectId = Utils.newGuid();
|
||||||
|
|
||||||
|
const result = await sut.putServiceAccountPeopleAccessPolicies(projectId, view);
|
||||||
|
|
||||||
|
expect(result).toEqual(view);
|
||||||
|
expect(apiService.send).toHaveBeenCalledWith(
|
||||||
|
"PUT",
|
||||||
|
"/service-accounts/" + projectId + "/access-policies/people",
|
||||||
|
expectedRequest,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(emissions).toEqual([view]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("putServiceAccountGrantedPolicies", () => {
|
||||||
|
it("emits the updated policies", async () => {
|
||||||
|
const policyPermissionDetailsView1 = createGrantedProjectPolicyPermissionDetailsView(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const policyPermissionDetailsView2 = createGrantedProjectPolicyPermissionDetailsView(
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const policyPermissionDetailsView3 = createGrantedProjectPolicyPermissionDetailsView(
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const view = {
|
||||||
|
grantedProjectPolicies: [
|
||||||
|
policyPermissionDetailsView1,
|
||||||
|
policyPermissionDetailsView2,
|
||||||
|
policyPermissionDetailsView3,
|
||||||
|
],
|
||||||
|
} as ServiceAccountGrantedPoliciesView;
|
||||||
|
|
||||||
|
apiService.send.mockResolvedValue(toServiceAccountGrantedPoliciesResponseRaw(view));
|
||||||
|
const emissions = trackEmissions(sut.accessPolicy$);
|
||||||
|
const expectedRequest = toServiceAccountGrantedPoliciesRequest(view);
|
||||||
|
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||||
|
const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey;
|
||||||
|
cryptoService.getOrgKey.mockResolvedValue(mockOrgKey);
|
||||||
|
encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c.encryptedString));
|
||||||
|
const organizationId = Utils.newGuid();
|
||||||
|
const serviceAccountId = Utils.newGuid();
|
||||||
|
|
||||||
|
const result = await sut.putServiceAccountGrantedPolicies(
|
||||||
|
organizationId,
|
||||||
|
serviceAccountId,
|
||||||
|
view,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(view);
|
||||||
|
expect(apiService.send).toHaveBeenCalledWith(
|
||||||
|
"PUT",
|
||||||
|
"/service-accounts/" + serviceAccountId + "/granted-policies",
|
||||||
|
expectedRequest,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(emissions).toEqual([view]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("putProjectServiceAccountsAccessPolicies", () => {
|
||||||
|
it("emits the updated policies", async () => {
|
||||||
|
const accessPolicyView1 = createServiceAccountAccessPolicyView(false);
|
||||||
|
const accessPolicyView2 = createServiceAccountAccessPolicyView(true);
|
||||||
|
|
||||||
|
const view = {
|
||||||
|
serviceAccountAccessPolicies: [accessPolicyView1, accessPolicyView2],
|
||||||
|
} as ProjectServiceAccountsAccessPoliciesView;
|
||||||
|
|
||||||
|
apiService.send.mockResolvedValue(toProjectServiceAccountsAccessPoliciesResponseRaw(view));
|
||||||
|
const emissions = trackEmissions(sut.accessPolicy$);
|
||||||
|
const expectedRequest = toProjectServiceAccountsAccessPoliciesRequest(view);
|
||||||
|
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||||
|
const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey;
|
||||||
|
cryptoService.getOrgKey.mockResolvedValue(mockOrgKey);
|
||||||
|
encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c.encryptedString));
|
||||||
|
const organizationId = Utils.newGuid();
|
||||||
|
const projectId = Utils.newGuid();
|
||||||
|
|
||||||
|
const result = await sut.putProjectServiceAccountsAccessPolicies(
|
||||||
|
organizationId,
|
||||||
|
projectId,
|
||||||
|
view,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(view);
|
||||||
|
expect(apiService.send).toHaveBeenCalledWith(
|
||||||
|
"PUT",
|
||||||
|
"/projects/" + projectId + "/access-policies/service-accounts",
|
||||||
|
expectedRequest,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(emissions).toEqual([view]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createUserAccessPolicyView(isWrite: boolean, currentUser: boolean): UserAccessPolicyView {
|
||||||
|
const id = Utils.newGuid();
|
||||||
|
return {
|
||||||
|
organizationUserId: id,
|
||||||
|
organizationUserName: "Example organization user name " + id,
|
||||||
|
read: true,
|
||||||
|
write: isWrite,
|
||||||
|
currentUser: currentUser,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGroupAccessPolicyView(
|
||||||
|
isWrite: boolean,
|
||||||
|
currentUserInGroup: boolean,
|
||||||
|
): GroupAccessPolicyView {
|
||||||
|
const id = Utils.newGuid();
|
||||||
|
return {
|
||||||
|
groupId: id,
|
||||||
|
groupName: "Example group name " + id,
|
||||||
|
currentUserInGroup: currentUserInGroup,
|
||||||
|
read: true,
|
||||||
|
write: isWrite,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createServiceAccountAccessPolicyView(isWrite: boolean): ServiceAccountAccessPolicyView {
|
||||||
|
const id = Utils.newGuid();
|
||||||
|
return {
|
||||||
|
serviceAccountId: id,
|
||||||
|
serviceAccountName: "Example service account name " + id,
|
||||||
|
read: true,
|
||||||
|
write: isWrite,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGrantedProjectPolicyPermissionDetailsView(
|
||||||
|
isWrite: boolean,
|
||||||
|
hasPermissions: boolean,
|
||||||
|
): GrantedProjectPolicyPermissionDetailsView {
|
||||||
|
const id = Utils.newGuid();
|
||||||
|
return {
|
||||||
|
accessPolicy: {
|
||||||
|
grantedProjectId: id,
|
||||||
|
grantedProjectName: "Example project name " + id,
|
||||||
|
read: true,
|
||||||
|
write: isWrite,
|
||||||
|
},
|
||||||
|
hasPermission: hasPermissions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPeopleAccessPoliciesRequest(
|
||||||
|
view: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView,
|
||||||
|
): PeopleAccessPoliciesRequest {
|
||||||
|
return {
|
||||||
|
userAccessPolicyRequests: view.userAccessPolicies.map((ap) => ({
|
||||||
|
granteeId: ap.organizationUserId,
|
||||||
|
read: ap.read,
|
||||||
|
write: ap.write,
|
||||||
|
})),
|
||||||
|
groupAccessPolicyRequests: view.groupAccessPolicies.map((ap) => ({
|
||||||
|
granteeId: ap.groupId,
|
||||||
|
read: ap.read,
|
||||||
|
write: ap.write,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toServiceAccountGrantedPoliciesRequest(
|
||||||
|
view: ServiceAccountGrantedPoliciesView,
|
||||||
|
): ServiceAccountGrantedPoliciesRequest {
|
||||||
|
return {
|
||||||
|
projectGrantedPolicyRequests: view.grantedProjectPolicies.map((ap) => ({
|
||||||
|
grantedId: ap.accessPolicy.grantedProjectId,
|
||||||
|
read: ap.accessPolicy.read,
|
||||||
|
write: ap.accessPolicy.write,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toProjectServiceAccountsAccessPoliciesRequest(
|
||||||
|
view: ProjectServiceAccountsAccessPoliciesView,
|
||||||
|
): ProjectServiceAccountsAccessPoliciesRequest {
|
||||||
|
return {
|
||||||
|
serviceAccountAccessPolicyRequests: view.serviceAccountAccessPolicies.map((ap) => {
|
||||||
|
return {
|
||||||
|
granteeId: ap.serviceAccountId,
|
||||||
|
read: ap.read,
|
||||||
|
write: ap.write,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toProjectPeopleAccessPoliciesResponseRaw(view: ProjectPeopleAccessPoliciesView) {
|
||||||
|
return {
|
||||||
|
userAccessPolicies: view.userAccessPolicies.map((ap) => ({
|
||||||
|
organizationUserId: ap.organizationUserId,
|
||||||
|
organizationUserName: ap.organizationUserName,
|
||||||
|
currentUser: ap.currentUser,
|
||||||
|
read: ap.read,
|
||||||
|
write: ap.write,
|
||||||
|
})),
|
||||||
|
groupAccessPolicies: view.groupAccessPolicies.map((ap) => ({
|
||||||
|
groupId: ap.groupId,
|
||||||
|
groupName: ap.groupName,
|
||||||
|
currentUserInGroup: ap.currentUserInGroup,
|
||||||
|
read: ap.read,
|
||||||
|
write: ap.write,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toServiceAccountPeopleAccessPoliciesResponseRaw(
|
||||||
|
view: ServiceAccountPeopleAccessPoliciesView,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
userAccessPolicies: view.userAccessPolicies.map((ap) => ({
|
||||||
|
organizationUserId: ap.organizationUserId,
|
||||||
|
organizationUserName: ap.organizationUserName,
|
||||||
|
currentUser: ap.currentUser,
|
||||||
|
read: ap.read,
|
||||||
|
write: ap.write,
|
||||||
|
})),
|
||||||
|
groupAccessPolicies: view.groupAccessPolicies.map((ap) => ({
|
||||||
|
groupId: ap.groupId,
|
||||||
|
groupName: ap.groupName,
|
||||||
|
currentUserInGroup: ap.currentUserInGroup,
|
||||||
|
read: ap.read,
|
||||||
|
write: ap.write,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toServiceAccountGrantedPoliciesResponseRaw(view: ServiceAccountGrantedPoliciesView) {
|
||||||
|
return {
|
||||||
|
grantedProjectPolicies: view.grantedProjectPolicies.map((ap) => ({
|
||||||
|
accessPolicy: {
|
||||||
|
grantedProjectId: ap.accessPolicy.grantedProjectId,
|
||||||
|
grantedProjectName: ap.accessPolicy.grantedProjectName,
|
||||||
|
read: ap.accessPolicy.read,
|
||||||
|
write: ap.accessPolicy.write,
|
||||||
|
},
|
||||||
|
hasPermission: ap.hasPermission,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toProjectServiceAccountsAccessPoliciesResponseRaw(
|
||||||
|
view: ProjectServiceAccountsAccessPoliciesView,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
serviceAccountAccessPolicies: view.serviceAccountAccessPolicies.map((ap) => ({
|
||||||
|
serviceAccountId: ap.serviceAccountId,
|
||||||
|
serviceAccountName: ap.serviceAccountName,
|
||||||
|
currentUser: true,
|
||||||
|
read: ap.read,
|
||||||
|
write: ap.write,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
@ -46,6 +47,15 @@ import { GrantedProjectAccessPolicyPermissionDetailsResponse } from "./models/re
|
|||||||
providedIn: "root",
|
providedIn: "root",
|
||||||
})
|
})
|
||||||
export class AccessPolicyService {
|
export class AccessPolicyService {
|
||||||
|
protected _accessPolicy: Subject<
|
||||||
|
| ProjectPeopleAccessPoliciesView
|
||||||
|
| ProjectServiceAccountsAccessPoliciesView
|
||||||
|
| ServiceAccountPeopleAccessPoliciesView
|
||||||
|
| ServiceAccountGrantedPoliciesView
|
||||||
|
> = new Subject();
|
||||||
|
|
||||||
|
accessPolicy$ = this._accessPolicy.asObservable();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
protected apiService: ApiService,
|
protected apiService: ApiService,
|
||||||
@ -70,7 +80,7 @@ export class AccessPolicyService {
|
|||||||
async putProjectPeopleAccessPolicies(
|
async putProjectPeopleAccessPolicies(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
peoplePoliciesView: ProjectPeopleAccessPoliciesView,
|
peoplePoliciesView: ProjectPeopleAccessPoliciesView,
|
||||||
) {
|
): Promise<ProjectPeopleAccessPoliciesView> {
|
||||||
const request = this.getPeopleAccessPoliciesRequest(peoplePoliciesView);
|
const request = this.getPeopleAccessPoliciesRequest(peoplePoliciesView);
|
||||||
const r = await this.apiService.send(
|
const r = await this.apiService.send(
|
||||||
"PUT",
|
"PUT",
|
||||||
@ -80,7 +90,9 @@ export class AccessPolicyService {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const results = new ProjectPeopleAccessPoliciesResponse(r);
|
const results = new ProjectPeopleAccessPoliciesResponse(r);
|
||||||
return this.createPeopleAccessPoliciesView(results);
|
const view = this.createPeopleAccessPoliciesView(results);
|
||||||
|
this._accessPolicy.next(view);
|
||||||
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getServiceAccountPeopleAccessPolicies(
|
async getServiceAccountPeopleAccessPolicies(
|
||||||
@ -101,7 +113,7 @@ export class AccessPolicyService {
|
|||||||
async putServiceAccountPeopleAccessPolicies(
|
async putServiceAccountPeopleAccessPolicies(
|
||||||
serviceAccountId: string,
|
serviceAccountId: string,
|
||||||
peoplePoliciesView: ServiceAccountPeopleAccessPoliciesView,
|
peoplePoliciesView: ServiceAccountPeopleAccessPoliciesView,
|
||||||
) {
|
): Promise<ServiceAccountPeopleAccessPoliciesView> {
|
||||||
const request = this.getPeopleAccessPoliciesRequest(peoplePoliciesView);
|
const request = this.getPeopleAccessPoliciesRequest(peoplePoliciesView);
|
||||||
const r = await this.apiService.send(
|
const r = await this.apiService.send(
|
||||||
"PUT",
|
"PUT",
|
||||||
@ -111,7 +123,9 @@ export class AccessPolicyService {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const results = new ServiceAccountPeopleAccessPoliciesResponse(r);
|
const results = new ServiceAccountPeopleAccessPoliciesResponse(r);
|
||||||
return this.createPeopleAccessPoliciesView(results);
|
const view = this.createPeopleAccessPoliciesView(results);
|
||||||
|
this._accessPolicy.next(view);
|
||||||
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getServiceAccountGrantedPolicies(
|
async getServiceAccountGrantedPolicies(
|
||||||
@ -145,7 +159,9 @@ export class AccessPolicyService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const result = new ServiceAccountGrantedPoliciesPermissionDetailsResponse(r);
|
const result = new ServiceAccountGrantedPoliciesPermissionDetailsResponse(r);
|
||||||
return await this.createServiceAccountGrantedPoliciesView(result, organizationId);
|
const view = await this.createServiceAccountGrantedPoliciesView(result, organizationId);
|
||||||
|
this._accessPolicy.next(view);
|
||||||
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectServiceAccountsAccessPolicies(
|
async getProjectServiceAccountsAccessPolicies(
|
||||||
@ -179,7 +195,9 @@ export class AccessPolicyService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const result = new ProjectServiceAccountsAccessPoliciesResponse(r);
|
const result = new ProjectServiceAccountsAccessPoliciesResponse(r);
|
||||||
return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId);
|
const view = await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId);
|
||||||
|
this._accessPolicy.next(view);
|
||||||
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSecretAccessPolicies(
|
async getSecretAccessPolicies(
|
||||||
@ -450,7 +468,7 @@ export class AccessPolicyService {
|
|||||||
|
|
||||||
private createPeopleAccessPoliciesView(
|
private createPeopleAccessPoliciesView(
|
||||||
response: ProjectPeopleAccessPoliciesResponse | ServiceAccountPeopleAccessPoliciesResponse,
|
response: ProjectPeopleAccessPoliciesResponse | ServiceAccountPeopleAccessPoliciesResponse,
|
||||||
) {
|
): ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView {
|
||||||
return {
|
return {
|
||||||
userAccessPolicies: this.createUserAccessPolicyViews(response.userAccessPolicies),
|
userAccessPolicies: this.createUserAccessPolicyViews(response.userAccessPolicies),
|
||||||
groupAccessPolicies: this.createGroupAccessPolicyViews(response.groupAccessPolicies),
|
groupAccessPolicies: this.createGroupAccessPolicyViews(response.groupAccessPolicies),
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
|
import { CountService } from "./count.service";
|
||||||
|
|
||||||
|
describe("SecretsManagerService", () => {
|
||||||
|
let sut: CountService;
|
||||||
|
|
||||||
|
const apiService = mock<ApiService>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
sut = new CountService(apiService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("instantiates", () => {
|
||||||
|
expect(sut).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getOrganizationCounts", () => {
|
||||||
|
it("returns counts", async () => {
|
||||||
|
apiService.send.mockResolvedValue({
|
||||||
|
projects: 1,
|
||||||
|
secrets: 2,
|
||||||
|
serviceAccounts: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const organizationId = Utils.newGuid();
|
||||||
|
|
||||||
|
const result = await sut.getOrganizationCounts(organizationId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.projects).toEqual(1);
|
||||||
|
expect(result.secrets).toEqual(2);
|
||||||
|
expect(result.serviceAccounts).toEqual(3);
|
||||||
|
expect(apiService.send).toHaveBeenCalledWith(
|
||||||
|
"GET",
|
||||||
|
"/organizations/" + organizationId + "/sm-counts",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getProjectCounts", () => {
|
||||||
|
it("returns counts", async () => {
|
||||||
|
apiService.send.mockResolvedValue({
|
||||||
|
people: 1,
|
||||||
|
secrets: 2,
|
||||||
|
serviceAccounts: 3,
|
||||||
|
});
|
||||||
|
const projectId = Utils.newGuid();
|
||||||
|
|
||||||
|
const result = await sut.getProjectCounts(projectId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.people).toEqual(1);
|
||||||
|
expect(result.secrets).toEqual(2);
|
||||||
|
expect(result.serviceAccounts).toEqual(3);
|
||||||
|
expect(apiService.send).toHaveBeenCalledWith(
|
||||||
|
"GET",
|
||||||
|
"/projects/" + projectId + "/sm-counts",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getServiceAccountCounts", () => {
|
||||||
|
it("returns counts", async () => {
|
||||||
|
const serviceAccountId = Utils.newGuid();
|
||||||
|
apiService.send.mockResolvedValue({
|
||||||
|
projects: 1,
|
||||||
|
people: 2,
|
||||||
|
accessTokens: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sut.getServiceAccountCounts(serviceAccountId);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result.projects).toEqual(1);
|
||||||
|
expect(result.people).toEqual(2);
|
||||||
|
expect(result.accessTokens).toEqual(3);
|
||||||
|
expect(apiService.send).toHaveBeenCalledWith(
|
||||||
|
"GET",
|
||||||
|
"/service-accounts/" + serviceAccountId + "/sm-counts",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,47 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
|
||||||
|
import { OrganizationCountsResponse } from "./models/responses/organization-counts.response";
|
||||||
|
import { ProjectCountsResponse } from "./models/responses/project-counts.response";
|
||||||
|
import { ServiceAccountCountsResponse } from "./models/responses/service-account-counts.response";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class CountService {
|
||||||
|
constructor(private apiService: ApiService) {}
|
||||||
|
|
||||||
|
async getOrganizationCounts(organizationId: string): Promise<OrganizationCountsResponse> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/organizations/" + organizationId + "/sm-counts",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return new OrganizationCountsResponse(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectCounts(projectId: string): Promise<ProjectCountsResponse> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/projects/" + projectId + "/sm-counts",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return new ProjectCountsResponse(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getServiceAccountCounts(serviceAccountId: string): Promise<ServiceAccountCountsResponse> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/service-accounts/" + serviceAccountId + "/sm-counts",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return new ServiceAccountCountsResponse(r);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||||
|
|
||||||
|
export class OrganizationCountsResponse extends BaseResponse {
|
||||||
|
projects: number;
|
||||||
|
secrets: number;
|
||||||
|
serviceAccounts: number;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.projects = this.getResponseProperty("Projects");
|
||||||
|
this.secrets = this.getResponseProperty("Secrets");
|
||||||
|
this.serviceAccounts = this.getResponseProperty("ServiceAccounts");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||||
|
|
||||||
|
export class ProjectCountsResponse extends BaseResponse {
|
||||||
|
people: number;
|
||||||
|
secrets: number;
|
||||||
|
serviceAccounts: number;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.people = this.getResponseProperty("People");
|
||||||
|
this.secrets = this.getResponseProperty("Secrets");
|
||||||
|
this.serviceAccounts = this.getResponseProperty("ServiceAccounts");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||||
|
|
||||||
|
export class ServiceAccountCountsResponse extends BaseResponse {
|
||||||
|
projects: number;
|
||||||
|
people: number;
|
||||||
|
accessTokens: number;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
this.projects = this.getResponseProperty("Projects");
|
||||||
|
this.people = this.getResponseProperty("People");
|
||||||
|
this.accessTokens = this.getResponseProperty("AccessTokens");
|
||||||
|
}
|
||||||
|
}
|
@ -100,13 +100,9 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
#endSlot
|
|
||||||
*ngIf="data.open"
|
*ngIf="data.open"
|
||||||
class="tw-flex -tw-ml-3 tw-pr-4 tw-gap-1 [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2"
|
class="tw-flex -tw-ml-3 tw-pr-4 tw-gap-1 [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2 empty:tw-hidden"
|
||||||
[ngClass]="[
|
[ngClass]="[variant === 'tree' ? 'tw-py-1' : 'tw-py-2']"
|
||||||
variant === 'tree' ? 'tw-py-1' : 'tw-py-2',
|
|
||||||
endSlot.childElementCount === 0 ? 'tw-hidden' : '',
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
<ng-content select="[slot=end]"></ng-content>
|
<ng-content select="[slot=end]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,6 +9,12 @@
|
|||||||
[attr.aria-disabled]="disabled"
|
[attr.aria-disabled]="disabled"
|
||||||
ariaCurrentWhenActive="page"
|
ariaCurrentWhenActive="page"
|
||||||
role="link"
|
role="link"
|
||||||
|
class="tw-flex tw-group/tab hover:tw-no-underline"
|
||||||
>
|
>
|
||||||
<ng-content></ng-content>
|
<div class="group-hover/tab:tw-underline">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
<div class="tw-font-normal tw-ml-2 empty:tw-ml-0">
|
||||||
|
<ng-content select="[slot=end]"></ng-content>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -33,6 +33,12 @@ class ItemThreeDummyComponent {}
|
|||||||
})
|
})
|
||||||
class DisabledDummyComponent {}
|
class DisabledDummyComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "bit-tab-item-with-child-counter-dummy",
|
||||||
|
template: "Router - Item With Child Counter selected",
|
||||||
|
})
|
||||||
|
class ItemWithChildCounterDummyComponent {}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Component Library/Tabs",
|
title: "Component Library/Tabs",
|
||||||
component: TabGroupComponent,
|
component: TabGroupComponent,
|
||||||
@ -42,6 +48,7 @@ export default {
|
|||||||
ActiveDummyComponent,
|
ActiveDummyComponent,
|
||||||
ItemTwoDummyComponent,
|
ItemTwoDummyComponent,
|
||||||
ItemThreeDummyComponent,
|
ItemThreeDummyComponent,
|
||||||
|
ItemWithChildCounterDummyComponent,
|
||||||
DisabledDummyComponent,
|
DisabledDummyComponent,
|
||||||
],
|
],
|
||||||
imports: [CommonModule, TabsModule, ButtonModule, FormFieldModule, RouterModule],
|
imports: [CommonModule, TabsModule, ButtonModule, FormFieldModule, RouterModule],
|
||||||
@ -55,6 +62,7 @@ export default {
|
|||||||
{ path: "active", component: ActiveDummyComponent },
|
{ path: "active", component: ActiveDummyComponent },
|
||||||
{ path: "item-2", component: ItemTwoDummyComponent },
|
{ path: "item-2", component: ItemTwoDummyComponent },
|
||||||
{ path: "item-3", component: ItemThreeDummyComponent },
|
{ path: "item-3", component: ItemThreeDummyComponent },
|
||||||
|
{ path: "item-with-child-counter", component: ItemWithChildCounterDummyComponent },
|
||||||
{ path: "disabled", component: DisabledDummyComponent },
|
{ path: "disabled", component: DisabledDummyComponent },
|
||||||
],
|
],
|
||||||
{ useHash: true },
|
{ useHash: true },
|
||||||
@ -102,6 +110,12 @@ export const NavigationTabs: Story = {
|
|||||||
<bit-tab-link [route]="['active']">Active</bit-tab-link>
|
<bit-tab-link [route]="['active']">Active</bit-tab-link>
|
||||||
<bit-tab-link [route]="['item-2']">Item 2</bit-tab-link>
|
<bit-tab-link [route]="['item-2']">Item 2</bit-tab-link>
|
||||||
<bit-tab-link [route]="['item-3']">Item 3</bit-tab-link>
|
<bit-tab-link [route]="['item-3']">Item 3</bit-tab-link>
|
||||||
|
<bit-tab-link [route]="['item-with-child-counter']">
|
||||||
|
Item With Counter
|
||||||
|
<div slot="end" class="tw-pl-2 tw-text-muted">
|
||||||
|
42
|
||||||
|
</div>
|
||||||
|
</bit-tab-link>
|
||||||
<bit-tab-link [route]="['disable']" [disabled]="true">Disabled</bit-tab-link>
|
<bit-tab-link [route]="['disable']" [disabled]="true">Disabled</bit-tab-link>
|
||||||
</bit-tab-nav-bar>
|
</bit-tab-nav-bar>
|
||||||
<div class="tw-bg-transparent tw-text-semibold tw-text-center tw-text-main tw-py-10">
|
<div class="tw-bg-transparent tw-text-semibold tw-text-center tw-text-main tw-py-10">
|
||||||
|
Loading…
Reference in New Issue
Block a user