diff --git a/apps/web/src/app/organizations/core/core-organization.module.ts b/apps/web/src/app/organizations/core/core-organization.module.ts new file mode 100644 index 0000000000..57362e01d7 --- /dev/null +++ b/apps/web/src/app/organizations/core/core-organization.module.ts @@ -0,0 +1,4 @@ +import { NgModule } from "@angular/core"; + +@NgModule({}) +export class CoreOrganizationModule {} diff --git a/apps/web/src/app/organizations/core/index.ts b/apps/web/src/app/organizations/core/index.ts new file mode 100644 index 0000000000..e68991103c --- /dev/null +++ b/apps/web/src/app/organizations/core/index.ts @@ -0,0 +1,4 @@ +export * from "./core-organization.module"; +export * from "./services/collection-admin.service"; +export * from "./views/collection-access-selection-view"; +export * from "./views/collection-admin-view"; diff --git a/apps/web/src/app/organizations/core/services/collection-admin.service.ts b/apps/web/src/app/organizations/core/services/collection-admin.service.ts new file mode 100644 index 0000000000..039751eb75 --- /dev/null +++ b/apps/web/src/app/organizations/core/services/collection-admin.service.ts @@ -0,0 +1,123 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { EncString } from "@bitwarden/common/models/domain/enc-string"; +import { CollectionRequest } from "@bitwarden/common/models/request/collection.request"; +import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request"; +import { + CollectionAccessDetailsResponse, + CollectionResponse, +} from "@bitwarden/common/models/response/collection.response"; +import { CollectionView } from "@bitwarden/common/models/view/collection.view"; + +import { CoreOrganizationModule } from "../core-organization.module"; +import { CollectionAdminView } from "../views/collection-admin-view"; + +@Injectable({ providedIn: CoreOrganizationModule }) +export class CollectionAdminService { + constructor(private apiService: ApiService, private cryptoService: CryptoService) {} + + async getAll(organizationId: string): Promise { + const collectionResponse = await this.apiService.getCollections(organizationId); + if (collectionResponse?.data == null || collectionResponse.data.length === 0) { + return []; + } + + return await this.decryptMany(organizationId, collectionResponse.data); + } + + async get( + organizationId: string, + collectionId: string + ): Promise { + const collectionResponse = await this.apiService.getCollectionDetails( + organizationId, + collectionId + ); + + if (collectionResponse == null) { + return undefined; + } + + const [view] = await this.decryptMany(organizationId, [collectionResponse]); + + return view; + } + + async save(collection: CollectionAdminView): Promise { + const request = await this.encrypt(collection); + + let response: CollectionResponse; + if (collection.id == null) { + response = await this.apiService.postCollection(collection.organizationId, request); + collection.id = response.id; + } else { + response = await this.apiService.putCollection( + collection.organizationId, + collection.id, + request + ); + } + + // TODO: Implement upsert when in PS-1083: Collection Service refactors + // await this.collectionService.upsert(data); + return; + } + + async delete(organizationId: string, collectionId: string): Promise { + await this.apiService.deleteCollection(organizationId, collectionId); + } + + private async decryptMany( + organizationId: string, + collections: CollectionResponse[] | CollectionAccessDetailsResponse[] + ): Promise { + const orgKey = await this.cryptoService.getOrgKey(organizationId); + + const promises = collections.map(async (c) => { + const view = new CollectionAdminView(); + view.id = c.id; + view.name = await this.cryptoService.decryptToUtf8(new EncString(c.name), orgKey); + view.externalId = c.externalId; + view.organizationId = c.organizationId; + + if (isCollectionAccessDetailsResponse(c)) { + view.groups = c.groups; + view.users = c.users; + } + + return view; + }); + + return await Promise.all(promises); + } + + private async encrypt(model: CollectionAdminView): Promise { + if (model.organizationId == null) { + throw new Error("Collection has no organization id."); + } + const key = await this.cryptoService.getOrgKey(model.organizationId); + if (key == null) { + throw new Error("No key for this collection's organization."); + } + const collection = new CollectionRequest(); + collection.externalId = model.externalId; + collection.name = (await this.cryptoService.encrypt(model.name, key)).encryptedString; + collection.groups = model.groups.map( + (group) => new SelectionReadOnlyRequest(group.id, group.readOnly, group.hidePasswords) + ); + collection.users = model.users.map( + (user) => new SelectionReadOnlyRequest(user.id, user.readOnly, user.hidePasswords) + ); + return collection; + } +} + +function isCollectionAccessDetailsResponse( + response: CollectionResponse | CollectionAccessDetailsResponse +): response is CollectionAccessDetailsResponse { + const anyResponse = response as any; + + return anyResponse?.groups instanceof Array && anyResponse?.users instanceof Array; +} diff --git a/apps/web/src/app/organizations/core/views/collection-access-selection-view.ts b/apps/web/src/app/organizations/core/views/collection-access-selection-view.ts new file mode 100644 index 0000000000..38191605fd --- /dev/null +++ b/apps/web/src/app/organizations/core/views/collection-access-selection-view.ts @@ -0,0 +1,25 @@ +import { View } from "@bitwarden/common/models/view/view"; + +interface SelectionResponseLike { + id: string; + readOnly: boolean; + hidePasswords: boolean; +} + +export class CollectionAccessSelectionView extends View { + readonly id: string; + readonly readOnly: boolean; + readonly hidePasswords: boolean; + + constructor(response?: SelectionResponseLike) { + super(); + + if (!response) { + return; + } + + this.id = response.id; + this.readOnly = response.readOnly; + this.hidePasswords = response.hidePasswords; + } +} diff --git a/apps/web/src/app/organizations/core/views/collection-admin-view.ts b/apps/web/src/app/organizations/core/views/collection-admin-view.ts new file mode 100644 index 0000000000..e0ad35bf43 --- /dev/null +++ b/apps/web/src/app/organizations/core/views/collection-admin-view.ts @@ -0,0 +1,25 @@ +import { CollectionView } from "@bitwarden/common/models/view/collection.view"; +import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/models/response/collection.response"; + +import { CollectionAccessSelectionView } from "./collection-access-selection-view"; + +export class CollectionAdminView extends CollectionView { + groups: CollectionAccessSelectionView[] = []; + users: CollectionAccessSelectionView[] = []; + + constructor(response?: CollectionAccessDetailsResponse) { + super(response); + + if (!response) { + return; + } + + this.groups = response.groups + ? response.groups.map((g) => new CollectionAccessSelectionView(g)) + : []; + + this.users = response.users + ? response.users.map((g) => new CollectionAccessSelectionView(g)) + : []; + } +} diff --git a/apps/web/src/app/organizations/manage/collection-add-edit.component.html b/apps/web/src/app/organizations/manage/collection-add-edit.component.html deleted file mode 100644 index 97c973a419..0000000000 --- a/apps/web/src/app/organizations/manage/collection-add-edit.component.html +++ /dev/null @@ -1,162 +0,0 @@ - diff --git a/apps/web/src/app/organizations/manage/collection-add-edit.component.ts b/apps/web/src/app/organizations/manage/collection-add-edit.component.ts deleted file mode 100644 index 6f0ad08292..0000000000 --- a/apps/web/src/app/organizations/manage/collection-add-edit.component.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; -import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; -import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; -import { Utils } from "@bitwarden/common/misc/utils"; -import { EncString } from "@bitwarden/common/models/domain/enc-string"; -import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key"; -import { CollectionRequest } from "@bitwarden/common/models/request/collection.request"; -import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request"; - -import { GroupServiceAbstraction } from "../services/abstractions/group"; -import { GroupView } from "../views/group.view"; - -@Component({ - selector: "app-collection-add-edit", - templateUrl: "collection-add-edit.component.html", -}) -export class CollectionAddEditComponent implements OnInit { - @Input() collectionId: string; - @Input() organizationId: string; - @Input() canSave: boolean; - @Input() canDelete: boolean; - @Output() onSavedCollection = new EventEmitter(); - @Output() onDeletedCollection = new EventEmitter(); - - loading = true; - editMode = false; - accessGroups = false; - title: string; - name: string; - externalId: string; - groups: GroupView[] = []; - formPromise: Promise; - deletePromise: Promise; - - private orgKey: SymmetricCryptoKey; - - constructor( - private apiService: ApiService, - private groupApiService: GroupServiceAbstraction, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private cryptoService: CryptoService, - private logService: LogService, - private organizationService: OrganizationService - ) {} - - async ngOnInit() { - const organization = await this.organizationService.get(this.organizationId); - this.accessGroups = organization.useGroups; - this.editMode = this.loading = this.collectionId != null; - if (this.accessGroups) { - const groupsResponse = await this.groupApiService.getAll(this.organizationId); - this.groups = groupsResponse.sort(Utils.getSortFunction(this.i18nService, "name")); - } - this.orgKey = await this.cryptoService.getOrgKey(this.organizationId); - - if (this.editMode) { - this.editMode = true; - this.title = this.i18nService.t("editCollection"); - try { - const collection = await this.apiService.getCollectionDetails( - this.organizationId, - this.collectionId - ); - this.name = await this.cryptoService.decryptToUtf8( - new EncString(collection.name), - this.orgKey - ); - this.externalId = collection.externalId; - if (collection.groups != null && this.groups.length > 0) { - collection.groups.forEach((s) => { - const group = this.groups.filter((g) => !g.accessAll && g.id === s.id); - if (group != null && group.length > 0) { - (group[0] as any).checked = true; - (group[0] as any).readOnly = s.readOnly; - (group[0] as any).hidePasswords = s.hidePasswords; - } - }); - } - } catch (e) { - this.logService.error(e); - } - } else { - this.title = this.i18nService.t("addCollection"); - } - - this.groups.forEach((g) => { - if (g.accessAll) { - (g as any).checked = true; - } - }); - - this.loading = false; - } - - check(g: GroupView, select?: boolean) { - if (g.accessAll) { - return; - } - (g as any).checked = select == null ? !(g as any).checked : select; - if (!(g as any).checked) { - (g as any).readOnly = false; - (g as any).hidePasswords = false; - } - } - - selectAll(select: boolean) { - this.groups.forEach((g) => this.check(g, select)); - } - - async submit() { - if (this.orgKey == null) { - throw new Error("No encryption key for this organization."); - } - - const request = new CollectionRequest(); - request.name = (await this.cryptoService.encrypt(this.name, this.orgKey)).encryptedString; - request.externalId = this.externalId; - request.groups = this.groups - .filter((g) => (g as any).checked && !g.accessAll) - .map( - (g) => new SelectionReadOnlyRequest(g.id, !!(g as any).readOnly, !!(g as any).hidePasswords) - ); - - try { - if (this.editMode) { - this.formPromise = this.apiService.putCollection( - this.organizationId, - this.collectionId, - request - ); - } else { - this.formPromise = this.apiService.postCollection(this.organizationId, request); - } - await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode ? "editedCollectionId" : "createdCollectionId", this.name) - ); - this.onSavedCollection.emit(); - } catch (e) { - this.logService.error(e); - } - } - - async delete() { - if (!this.editMode) { - return; - } - - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t("deleteCollectionConfirmation"), - this.name, - this.i18nService.t("yes"), - this.i18nService.t("no"), - "warning" - ); - if (!confirmed) { - return false; - } - - try { - this.deletePromise = this.apiService.deleteCollection(this.organizationId, this.collectionId); - await this.deletePromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("deletedCollectionId", this.name) - ); - this.onDeletedCollection.emit(); - } catch (e) { - this.logService.error(e); - } - } -} diff --git a/apps/web/src/app/organizations/manage/collections.component.html b/apps/web/src/app/organizations/manage/collections.component.html index 976a63fe6d..69fc9e9896 100644 --- a/apps/web/src/app/organizations/manage/collections.component.html +++ b/apps/web/src/app/organizations/manage/collections.component.html @@ -65,6 +65,16 @@