diff --git a/.github/whitelist-capital-letters.txt b/.github/whitelist-capital-letters.txt index a6d2f96079f..2339c1756bd 100644 --- a/.github/whitelist-capital-letters.txt +++ b/.github/whitelist-capital-letters.txt @@ -60,6 +60,7 @@ ./libs/common/src/misc/nodeUtils.ts ./libs/common/src/misc/linkedFieldOption.decorator.ts ./libs/common/src/misc/serviceUtils.ts +./libs/common/src/misc/serviceUtils.spec.ts ./libs/common/src/types/twoFactorResponse.ts ./libs/common/src/types/authResponse.ts ./libs/common/src/types/syncEventArgs.ts diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 6cd57fe7cc1..92d7e31258e 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -416,7 +416,7 @@ export class GetCommand extends DownloadCommand { throw new Error("No encryption key for this organization."); } - const response = await this.apiService.getCollectionDetails(options.organizationId, id); + const response = await this.apiService.getCollectionAccessDetails(options.organizationId, id); const decCollection = new CollectionView(response); decCollection.name = await this.cryptoService.decryptToUtf8( new EncString(response.name), diff --git a/apps/desktop/src/app/vault/vault-filter/vault-filter.module.ts b/apps/desktop/src/app/vault/vault-filter/vault-filter.module.ts index 6442a2b7b8b..996bdf807ab 100644 --- a/apps/desktop/src/app/vault/vault-filter/vault-filter.module.ts +++ b/apps/desktop/src/app/vault/vault-filter/vault-filter.module.ts @@ -1,6 +1,7 @@ import { NgModule } from "@angular/core"; import { BrowserModule } from "@angular/platform-browser"; +import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "@bitwarden/angular/abstractions/deprecated-vault-filter.service"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { VaultFilterService } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service"; @@ -22,6 +23,11 @@ import { VaultFilterComponent } from "./vault-filter.component"; TypeFilterComponent, ], exports: [VaultFilterComponent], - providers: [VaultFilterService], + providers: [ + { + provide: DeprecatedVaultFilterServiceAbstraction, + useClass: VaultFilterService, + }, + ], }) export class VaultFilterModule {} diff --git a/apps/web/src/app/common/base.people.component.ts b/apps/web/src/app/common/base.people.component.ts index a76b5817c8e..7464a41877f 100644 --- a/apps/web/src/app/common/base.people.component.ts +++ b/apps/web/src/app/common/base.people.component.ts @@ -7,7 +7,6 @@ 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 { OrganizationUserUserDetailsResponse } from "@bitwarden/common/abstractions/organization-user/responses"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; @@ -20,6 +19,7 @@ import { Utils } from "@bitwarden/common/misc/utils"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ProviderUserUserDetailsResponse } from "@bitwarden/common/models/response/provider/provider-user.response"; +import { OrganizationUserView } from "../organizations/core/views/organization-user.view"; import { UserConfirmComponent } from "../organizations/manage/user-confirm.component"; type StatusType = OrganizationUserStatusType | ProviderUserStatusType; @@ -28,7 +28,7 @@ const MaxCheckedCount = 500; @Directive() export abstract class BasePeopleComponent< - UserType extends ProviderUserUserDetailsResponse | OrganizationUserUserDetailsResponse + UserType extends ProviderUserUserDetailsResponse | OrganizationUserView > { @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef; @@ -110,7 +110,7 @@ export abstract class BasePeopleComponent< ) {} abstract edit(user: UserType): void; - abstract getUsers(): Promise>; + abstract getUsers(): Promise | UserType[]>; abstract deleteUser(id: string): Promise; abstract revokeUser(id: string): Promise; abstract restoreUser(id: string): Promise; @@ -125,9 +125,14 @@ export abstract class BasePeopleComponent< this.statusMap.set(status, []); } - this.allUsers = response.data != null && response.data.length > 0 ? response.data : []; + if (response instanceof ListResponse) { + this.allUsers = response.data != null && response.data.length > 0 ? response.data : []; + } else if (Array.isArray(response)) { + this.allUsers = response; + } + this.allUsers.sort( - Utils.getSortFunction( + Utils.getSortFunction( this.i18nService, "email" ) @@ -176,7 +181,7 @@ export abstract class BasePeopleComponent< this.didScroll = this.pagedUsers.length > this.pageSize; } - checkUser(user: OrganizationUserUserDetailsResponse, select?: boolean) { + checkUser(user: UserType, select?: boolean) { (user as any).checked = select == null ? !(user as any).checked : select; } diff --git a/apps/web/src/app/components/nested-checkbox.component.html b/apps/web/src/app/components/nested-checkbox.component.html deleted file mode 100644 index 9f585e66427..00000000000 --- a/apps/web/src/app/components/nested-checkbox.component.html +++ /dev/null @@ -1,30 +0,0 @@ -
-
- - -
-
-
- - -
-
-
diff --git a/apps/web/src/app/components/nested-checkbox.component.ts b/apps/web/src/app/components/nested-checkbox.component.ts deleted file mode 100644 index eebd01e25d1..00000000000 --- a/apps/web/src/app/components/nested-checkbox.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; - -import { Utils } from "@bitwarden/common/misc/utils"; - -@Component({ - selector: "app-nested-checkbox", - templateUrl: "nested-checkbox.component.html", -}) -export class NestedCheckboxComponent { - @Input() parentId: string; - @Input() checkboxes: { id: string; get: () => boolean; set: (v: boolean) => void }[]; - @Output() onSavedUser = new EventEmitter(); - @Output() onDeletedUser = new EventEmitter(); - - get parentIndeterminate() { - return !this.parentChecked && this.checkboxes.some((c) => c.get()); - } - - get parentChecked() { - return this.checkboxes.every((c) => c.get()); - } - - set parentChecked(value: boolean) { - this.checkboxes.forEach((c) => { - c.set(value); - }); - } - - pascalize(s: string) { - return Utils.camelToPascalCase(s); - } -} 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 00000000000..57362e01d7c --- /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 00000000000..2f4f7e585bc --- /dev/null +++ b/apps/web/src/app/organizations/core/index.ts @@ -0,0 +1,3 @@ +export * from "./core-organization.module"; +export * from "./services"; +export * from "./views"; 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 00000000000..d89343ffd48 --- /dev/null +++ b/apps/web/src/app/organizations/core/services/collection-admin.service.ts @@ -0,0 +1,126 @@ +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 { 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.getManyCollectionsWithAccessDetails( + 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.getCollectionAccessDetails( + 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; + view.assigned = c.assigned; + } + + 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/services/group/group.service.ts b/apps/web/src/app/organizations/core/services/group/group.service.ts new file mode 100644 index 00000000000..ba7c56a75a9 --- /dev/null +++ b/apps/web/src/app/organizations/core/services/group/group.service.ts @@ -0,0 +1,106 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; + +import { CoreOrganizationModule } from "../../core-organization.module"; +import { GroupView } from "../../views/group.view"; + +import { GroupRequest } from "./requests/group.request"; +import { OrganizationGroupBulkRequest } from "./requests/organization-group-bulk.request"; +import { GroupDetailsResponse, GroupResponse } from "./responses/group.response"; + +@Injectable({ providedIn: CoreOrganizationModule }) +export class GroupService { + constructor(private apiService: ApiService) {} + + async delete(orgId: string, groupId: string): Promise { + await this.apiService.send( + "DELETE", + "/organizations/" + orgId + "/groups/" + groupId, + null, + true, + false + ); + } + + async deleteMany(orgId: string, groupIds: string[]): Promise { + await this.apiService.send( + "DELETE", + "/organizations/" + orgId + "/groups", + new OrganizationGroupBulkRequest(groupIds), + true, + true + ); + } + + async get(orgId: string, groupId: string): Promise { + const r = await this.apiService.send( + "GET", + "/organizations/" + orgId + "/groups/" + groupId + "/details", + null, + true, + true + ); + + return GroupView.fromResponse(new GroupDetailsResponse(r)); + } + + async getAll(orgId: string): Promise { + const r = await this.apiService.send( + "GET", + "/organizations/" + orgId + "/groups", + null, + true, + true + ); + + const listResponse = new ListResponse(r, GroupDetailsResponse); + + return listResponse.data?.map((gr) => GroupView.fromResponse(gr)) ?? []; + } + + async save(group: GroupView): Promise { + const request = new GroupRequest(); + request.name = group.name; + request.externalId = group.externalId; + request.accessAll = group.accessAll; + request.users = group.members; + request.collections = group.collections.map( + (c) => new SelectionReadOnlyRequest(c.id, c.readOnly, c.hidePasswords) + ); + + if (group.id == undefined) { + return await this.postGroup(group.organizationId, request); + } else { + return await this.putGroup(group.organizationId, group.id, request); + } + } + + private async postGroup(organizationId: string, request: GroupRequest): Promise { + const r = await this.apiService.send( + "POST", + "/organizations/" + organizationId + "/groups", + request, + true, + true + ); + return GroupView.fromResponse(new GroupResponse(r)); + } + + private async putGroup( + organizationId: string, + id: string, + request: GroupRequest + ): Promise { + const r = await this.apiService.send( + "PUT", + "/organizations/" + organizationId + "/groups/" + id, + request, + true, + true + ); + return GroupView.fromResponse(new GroupResponse(r)); + } +} diff --git a/libs/common/src/models/request/group.request.ts b/apps/web/src/app/organizations/core/services/group/requests/group.request.ts similarity index 51% rename from libs/common/src/models/request/group.request.ts rename to apps/web/src/app/organizations/core/services/group/requests/group.request.ts index f3cb6d85e6f..e49c7978d33 100644 --- a/libs/common/src/models/request/group.request.ts +++ b/apps/web/src/app/organizations/core/services/group/requests/group.request.ts @@ -1,8 +1,9 @@ -import { SelectionReadOnlyRequest } from "./selection-read-only.request"; +import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request"; export class GroupRequest { name: string; accessAll: boolean; externalId: string; collections: SelectionReadOnlyRequest[] = []; + users: string[] = []; } diff --git a/apps/web/src/app/organizations/core/services/group/requests/organization-group-bulk.request.ts b/apps/web/src/app/organizations/core/services/group/requests/organization-group-bulk.request.ts new file mode 100644 index 00000000000..d8f1b5876dc --- /dev/null +++ b/apps/web/src/app/organizations/core/services/group/requests/organization-group-bulk.request.ts @@ -0,0 +1,7 @@ +export class OrganizationGroupBulkRequest { + ids: string[]; + + constructor(ids: string[]) { + this.ids = ids == null ? [] : ids; + } +} diff --git a/libs/common/src/models/response/group.response.ts b/apps/web/src/app/organizations/core/services/group/responses/group.response.ts similarity index 81% rename from libs/common/src/models/response/group.response.ts rename to apps/web/src/app/organizations/core/services/group/responses/group.response.ts index bb87a4e29bb..a825b40fe13 100644 --- a/libs/common/src/models/response/group.response.ts +++ b/apps/web/src/app/organizations/core/services/group/responses/group.response.ts @@ -1,5 +1,5 @@ -import { BaseResponse } from "./base.response"; -import { SelectionReadOnlyResponse } from "./selection-read-only.response"; +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { SelectionReadOnlyResponse } from "@bitwarden/common/models/response/selection-read-only.response"; export class GroupResponse extends BaseResponse { id: string; diff --git a/apps/web/src/app/organizations/core/services/index.ts b/apps/web/src/app/organizations/core/services/index.ts new file mode 100644 index 00000000000..1e670faccd6 --- /dev/null +++ b/apps/web/src/app/organizations/core/services/index.ts @@ -0,0 +1,3 @@ +export * from "./group/group.service"; +export * from "./collection-admin.service"; +export * from "./user-admin.service"; diff --git a/apps/web/src/app/organizations/core/services/user-admin.service.ts b/apps/web/src/app/organizations/core/services/user-admin.service.ts new file mode 100644 index 00000000000..f94298d51dc --- /dev/null +++ b/apps/web/src/app/organizations/core/services/user-admin.service.ts @@ -0,0 +1,88 @@ +import { Injectable } from "@angular/core"; + +import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service"; +import { + OrganizationUserInviteRequest, + OrganizationUserUpdateRequest, +} from "@bitwarden/common/abstractions/organization-user/requests"; +import { OrganizationUserDetailsResponse } from "@bitwarden/common/abstractions/organization-user/responses"; + +import { CoreOrganizationModule } from "../core-organization.module"; +import { OrganizationUserAdminView } from "../views/organization-user-admin-view"; + +@Injectable({ providedIn: CoreOrganizationModule }) +export class UserAdminService { + constructor(private organizationUserService: OrganizationUserService) {} + + async get( + organizationId: string, + organizationUserId: string + ): Promise { + const userResponse = await this.organizationUserService.getOrganizationUser( + organizationId, + organizationUserId, + { + includeGroups: true, + } + ); + + if (userResponse == null) { + return undefined; + } + + const [view] = await this.decryptMany(organizationId, [userResponse]); + + return view; + } + + async save(user: OrganizationUserAdminView): Promise { + const request = new OrganizationUserUpdateRequest(); + request.accessAll = user.accessAll; + request.permissions = user.permissions; + request.type = user.type; + request.collections = user.collections; + request.groups = user.groups; + + await this.organizationUserService.putOrganizationUser(user.organizationId, user.id, request); + } + + async invite(emails: string[], user: OrganizationUserAdminView): Promise { + const request = new OrganizationUserInviteRequest(); + request.emails = emails; + request.accessAll = user.accessAll; + request.permissions = user.permissions; + request.type = user.type; + request.collections = user.collections; + request.groups = user.groups; + + await this.organizationUserService.postOrganizationUserInvite(user.organizationId, request); + } + + private async decryptMany( + organizationId: string, + users: OrganizationUserDetailsResponse[] + ): Promise { + const promises = users.map(async (u) => { + const view = new OrganizationUserAdminView(); + + view.id = u.id; + view.organizationId = organizationId; + view.userId = u.userId; + view.type = u.type; + view.status = u.status; + view.accessAll = u.accessAll; + view.permissions = u.permissions; + view.resetPasswordEnrolled = u.resetPasswordEnrolled; + view.collections = u.collections.map((c) => ({ + id: c.id, + hidePasswords: c.hidePasswords, + readOnly: c.readOnly, + })); + view.groups = u.groups; + + return view; + }); + + return await Promise.all(promises); + } +} 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 00000000000..38191605fd1 --- /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 00000000000..dd7be147a3f --- /dev/null +++ b/apps/web/src/app/organizations/core/views/collection-admin.view.ts @@ -0,0 +1,32 @@ +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[] = []; + + /** + * Flag indicating the user has been explicitly assigned to this Collection + */ + assigned: boolean; + + 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)) + : []; + + this.assigned = response.assigned; + } +} diff --git a/apps/web/src/app/organizations/core/views/group.view.ts b/apps/web/src/app/organizations/core/views/group.view.ts new file mode 100644 index 00000000000..f91c5854760 --- /dev/null +++ b/apps/web/src/app/organizations/core/views/group.view.ts @@ -0,0 +1,25 @@ +import { View } from "@bitwarden/common/src/models/view/view"; + +import { GroupDetailsResponse, GroupResponse } from "../services/group/responses/group.response"; + +import { CollectionAccessSelectionView } from "./collection-access-selection.view"; + +export class GroupView implements View { + id: string; + organizationId: string; + name: string; + accessAll: boolean; + externalId: string; + collections: CollectionAccessSelectionView[] = []; + members: string[] = []; + + static fromResponse(response: GroupResponse): GroupView { + const view: GroupView = Object.assign(new GroupView(), response) as GroupView; + + if (response instanceof GroupDetailsResponse && response.collections != undefined) { + view.collections = response.collections.map((c) => new CollectionAccessSelectionView(c)); + } + + return view; + } +} diff --git a/apps/web/src/app/organizations/core/views/index.ts b/apps/web/src/app/organizations/core/views/index.ts new file mode 100644 index 00000000000..e7ba6859901 --- /dev/null +++ b/apps/web/src/app/organizations/core/views/index.ts @@ -0,0 +1,5 @@ +export * from "./collection-access-selection.view"; +export * from "./collection-admin.view"; +export * from "./group.view"; +export * from "./organization-user.view"; +export * from "./organization-user-admin-view"; diff --git a/apps/web/src/app/organizations/core/views/organization-user-admin-view.ts b/apps/web/src/app/organizations/core/views/organization-user-admin-view.ts new file mode 100644 index 00000000000..df5c985f2d1 --- /dev/null +++ b/apps/web/src/app/organizations/core/views/organization-user-admin-view.ts @@ -0,0 +1,19 @@ +import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType"; +import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; +import { PermissionsApi } from "@bitwarden/common/models/api/permissions.api"; + +import { CollectionAccessSelectionView } from "./collection-access-selection.view"; + +export class OrganizationUserAdminView { + id: string; + userId: string; + organizationId: string; + type: OrganizationUserType; + status: OrganizationUserStatusType; + accessAll: boolean; + permissions: PermissionsApi; + resetPasswordEnrolled: boolean; + + collections: CollectionAccessSelectionView[] = []; + groups: string[] = []; +} diff --git a/apps/web/src/app/organizations/core/views/organization-user.view.ts b/apps/web/src/app/organizations/core/views/organization-user.view.ts new file mode 100644 index 00000000000..6c15c91af20 --- /dev/null +++ b/apps/web/src/app/organizations/core/views/organization-user.view.ts @@ -0,0 +1,40 @@ +import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/abstractions/organization-user/responses"; +import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType"; +import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; +import { PermissionsApi } from "@bitwarden/common/models/api/permissions.api"; + +import { CollectionAccessSelectionView } from "./collection-access-selection.view"; + +export class OrganizationUserView { + id: string; + userId: string; + type: OrganizationUserType; + status: OrganizationUserStatusType; + accessAll: boolean; + permissions: PermissionsApi; + resetPasswordEnrolled: boolean; + name: string; + email: string; + twoFactorEnabled: boolean; + usesKeyConnector: boolean; + + collections: CollectionAccessSelectionView[] = []; + groups: string[] = []; + + groupNames: string[] = []; + collectionNames: string[] = []; + + static fromResponse(response: OrganizationUserUserDetailsResponse): OrganizationUserView { + const view = Object.assign(new OrganizationUserView(), response) as OrganizationUserView; + + if (response.collections != undefined) { + view.collections = response.collections.map((c) => new CollectionAccessSelectionView(c)); + } + + if (response.groups != undefined) { + view.groups = response.groups; + } + + return view; + } +} diff --git a/apps/web/src/app/organizations/layouts/organization-layout.component.html b/apps/web/src/app/organizations/layouts/organization-layout.component.html index 8ed9f0e80fc..0d537a4b2a3 100644 --- a/apps/web/src/app/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/organizations/layouts/organization-layout.component.html @@ -7,10 +7,15 @@ [activeOrganization]="organization" > - {{ "vault" | i18n }} - - {{ "manage" | i18n }} - + {{ + "vault" | i18n + }} + {{ + "members" | i18n + }} + {{ + "groups" | i18n + }} {{ getReportTabLabel(organization) | i18n }} diff --git a/apps/web/src/app/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/organizations/layouts/organization-layout.component.ts index d8f2fdd8a33..69a04b68a59 100644 --- a/apps/web/src/app/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/organizations/layouts/organization-layout.component.ts @@ -5,10 +5,10 @@ import { map, mergeMap, Observable, Subject, takeUntil } from "rxjs"; import { canAccessBillingTab, canAccessGroupsTab, - canAccessManageTab, canAccessMembersTab, canAccessReportingTab, canAccessSettingsTab, + canAccessVaultTab, getOrganizationById, OrganizationService, } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; @@ -45,12 +45,12 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { this._destroy.complete(); } - canShowSettingsTab(organization: Organization): boolean { - return canAccessSettingsTab(organization); + canShowVaultTab(organization: Organization): boolean { + return canAccessVaultTab(organization); } - canShowManageTab(organization: Organization): boolean { - return canAccessManageTab(organization); + canShowSettingsTab(organization: Organization): boolean { + return canAccessSettingsTab(organization); } canShowMembersTab(organization: Organization): boolean { 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 41368d589a7..00000000000 --- 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 index 1a26d21fba2..e69de29bb2d 100644 --- a/apps/web/src/app/organizations/manage/collection-add-edit.component.ts +++ b/apps/web/src/app/organizations/manage/collection-add-edit.component.ts @@ -1,182 +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 { GroupResponse } from "@bitwarden/common/models/response/group.response"; - -@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: GroupResponse[] = []; - formPromise: Promise; - deletePromise: Promise; - - private orgKey: SymmetricCryptoKey; - - constructor( - private apiService: ApiService, - 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.apiService.getGroups(this.organizationId); - this.groups = groupsResponse.data - .map((r) => r) - .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: GroupResponse, 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", - false, - "app-collection-add-edit .modal-content" - ); - 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 976a63fe6d4..69fc9e9896e 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 @@