diff --git a/jslib b/jslib index ce71c0c0bd6..91c5393ae7a 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit ce71c0c0bd6667573e0e611222dc415770ba3909 +Subproject commit 91c5393ae7a84e9f4d90391d072cae56e7a3ff41 diff --git a/src/app/components/nested-checkbox.component.html b/src/app/components/nested-checkbox.component.html new file mode 100644 index 00000000000..9dd21108127 --- /dev/null +++ b/src/app/components/nested-checkbox.component.html @@ -0,0 +1,18 @@ +
+
+ + +
+
+
+ + +
+
+
diff --git a/src/app/components/nested-checkbox.component.ts b/src/app/components/nested-checkbox.component.ts new file mode 100644 index 00000000000..3358e482f15 --- /dev/null +++ b/src/app/components/nested-checkbox.component.ts @@ -0,0 +1,37 @@ +import { + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; +import { Utils } from 'jslib-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/src/app/layouts/organization-layout.component.ts b/src/app/layouts/organization-layout.component.ts index 73cc5e8f0b0..c6b4b4bd8cb 100644 --- a/src/app/layouts/organization-layout.component.ts +++ b/src/app/layouts/organization-layout.component.ts @@ -82,8 +82,8 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { get showManageTab(): boolean { return this.organization.canManageUsers || - this.organization.canManageAssignedCollections || - this.organization.canManageAllCollections || + this.organization.canViewAllCollections || + this.organization.canViewAssignedCollections || this.organization.canManageGroups || this.organization.canManagePolicies || this.organization.canAccessEventLogs; @@ -109,7 +109,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { case this.organization.canManageUsers: route = 'manage/people'; break; - case this.organization.canManageAssignedCollections || this.organization.canManageAllCollections: + case this.organization.canViewAssignedCollections || this.organization.canViewAllCollections: route = 'manage/collections'; break; case this.organization.canManageGroups: diff --git a/src/app/organizations/manage/collections.component.ts b/src/app/organizations/manage/collections.component.ts index 0f148c6db39..e92474ab5bb 100644 --- a/src/app/organizations/manage/collections.component.ts +++ b/src/app/organizations/manage/collections.component.ts @@ -69,7 +69,7 @@ export class CollectionsComponent implements OnInit { async load() { const organization = await this.userService.getOrganization(this.organizationId); let response: ListResponse; - if (organization.canManageAllCollections) { + if (organization.canViewAllCollections) { response = await this.apiService.getCollections(this.organizationId); } else { response = await this.apiService.getUserCollections(); diff --git a/src/app/organizations/manage/manage.component.html b/src/app/organizations/manage/manage.component.html index da0052a3946..daa82b0fd21 100644 --- a/src/app/organizations/manage/manage.component.html +++ b/src/app/organizations/manage/manage.component.html @@ -9,7 +9,7 @@ {{'people' | i18n}} + *ngIf="organization.canViewAssignedCollections || organization.canViewAllCollections"> {{'collections' | i18n}}
-
+ +
+ + +
@@ -133,7 +138,8 @@
-
+ +
@@ -142,6 +148,10 @@
+ + +
; organizationUserType = OrganizationUserType; + manageAllCollectionsCheckboxes = [ + { + id: 'createNewCollections', + get: () => this.permissions.createNewCollections, + set: (v: boolean) => this.permissions.createNewCollections = v, + }, + { + id: 'editAnyCollection', + get: () => this.permissions.editAnyCollection, + set: (v: boolean) => this.permissions.editAnyCollection = v, + }, + { + id: 'deleteAnyCollection', + get: () => this.permissions.deleteAnyCollection, + set: (v: boolean) => this.permissions.deleteAnyCollection = v, + }, + ]; + + manageAssignedCollectionsCheckboxes = [ + { + id: 'editAssignedCollections', + get: () => this.permissions.editAssignedCollections, + set: (v: boolean) => this.permissions.editAssignedCollections = v, + }, + { + id: 'deleteAssignedCollections', + get: () => this.permissions.deleteAssignedCollections, + set: (v: boolean) => this.permissions.deleteAssignedCollections = v, + }, + ]; + + get fallbackToManageAllCollections() { + return this.permissions.createNewCollections == null && + this.permissions.editAnyCollection == null && + this.permissions.deleteAnyCollection == null; + } + + get fallbackToManageAssignedCollections() { + return this.permissions.editAssignedCollections == null && + this.permissions.deleteAssignedCollections == null; + } + get customUserTypeSelected(): boolean { return this.type === OrganizationUserType.Custom; } @@ -107,39 +149,7 @@ export class UserAddEditComponent implements OnInit { } setRequestPermissions(p: PermissionsApi, clearPermissions: boolean) { - p.accessBusinessPortal = clearPermissions ? - false : - this.permissions.accessBusinessPortal; - p.accessEventLogs = this.permissions.accessEventLogs = clearPermissions ? - false : - this.permissions.accessEventLogs; - p.accessImportExport = clearPermissions ? - false : - this.permissions.accessImportExport; - p.accessReports = clearPermissions ? - false : - this.permissions.accessReports; - p.manageAllCollections = clearPermissions ? - false : - this.permissions.manageAllCollections; - p.manageAssignedCollections = clearPermissions ? - false : - this.permissions.manageAssignedCollections; - p.manageGroups = clearPermissions ? - false : - this.permissions.manageGroups; - p.manageSso = clearPermissions ? - false : - this.permissions.manageSso; - p.managePolicies = clearPermissions ? - false : - this.permissions.managePolicies; - p.manageUsers = clearPermissions ? - false : - this.permissions.manageUsers; - p.manageResetPassword = clearPermissions ? - false : - this.permissions.manageResetPassword; + Object.assign(p, clearPermissions ? new PermissionsApi() : this.permissions); return p; } @@ -203,5 +213,4 @@ export class UserAddEditComponent implements OnInit { this.onDeletedUser.emit(); } catch { } } - } diff --git a/src/app/organizations/vault/add-edit.component.ts b/src/app/organizations/vault/add-edit.component.ts index 4dce9e2b057..aa392255d58 100644 --- a/src/app/organizations/vault/add-edit.component.ts +++ b/src/app/organizations/vault/add-edit.component.ts @@ -46,7 +46,7 @@ export class AddEditComponent extends BaseAddEditComponent { protected allowOwnershipAssignment() { if (this.ownershipOptions != null && (this.ownershipOptions.length > 1 || !this.allowPersonal)) { if (this.organization != null) { - return this.cloneMode && this.organization.canManageAllCollections; + return this.cloneMode && this.organization.canEditAnyCollection; } else { return !this.editMode || this.cloneMode; } @@ -55,14 +55,14 @@ export class AddEditComponent extends BaseAddEditComponent { } protected loadCollections() { - if (!this.organization.canManageAllCollections) { + if (!this.organization.canEditAnyCollection) { return super.loadCollections(); } return Promise.resolve(this.collections); } protected async loadCipher() { - if (!this.organization.canManageAllCollections) { + if (!this.organization.canEditAnyCollection) { return await super.loadCipher(); } const response = await this.apiService.getCipherAdmin(this.cipherId); @@ -72,14 +72,14 @@ export class AddEditComponent extends BaseAddEditComponent { } protected encryptCipher() { - if (!this.organization.canManageAllCollections) { + if (!this.organization.canEditAnyCollection) { return super.encryptCipher(); } return this.cipherService.encrypt(this.cipher, null, this.originalCipher); } protected async saveCipher(cipher: Cipher) { - if (!this.organization.canManageAllCollections || cipher.organizationId == null) { + if (!this.organization.canEditAnyCollection || cipher.organizationId == null) { return super.saveCipher(cipher); } if (this.editMode && !this.cloneMode) { @@ -92,7 +92,7 @@ export class AddEditComponent extends BaseAddEditComponent { } protected async deleteCipher() { - if (!this.organization.canManageAllCollections) { + if (!this.organization.canEditAnyCollection) { return super.deleteCipher(); } return this.cipher.isDeleted ? this.apiService.deleteCipherAdmin(this.cipherId) diff --git a/src/app/organizations/vault/attachments.component.ts b/src/app/organizations/vault/attachments.component.ts index 9c11b247679..66cca981e10 100644 --- a/src/app/organizations/vault/attachments.component.ts +++ b/src/app/organizations/vault/attachments.component.ts @@ -30,13 +30,13 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { } protected async reupload(attachment: AttachmentView) { - if (this.organization.canManageAllCollections && this.showFixOldAttachments(attachment)) { + if (this.organization.canEditAnyCollection && this.showFixOldAttachments(attachment)) { await super.reuploadCipherAttachment(attachment, true); } } protected async loadCipher() { - if (!this.organization.canManageAllCollections) { + if (!this.organization.canEditAnyCollection) { return await super.loadCipher(); } const response = await this.apiService.getCipherAdmin(this.cipherId); @@ -44,17 +44,17 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { } protected saveCipherAttachment(file: File) { - return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, this.organization.canManageAllCollections); + return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, this.organization.canEditAnyCollection); } protected deleteCipherAttachment(attachmentId: string) { - if (!this.organization.canManageAllCollections) { + if (!this.organization.canEditAnyCollection) { return super.deleteCipherAttachment(attachmentId); } return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId); } protected showFixOldAttachments(attachment: AttachmentView) { - return attachment.key == null && this.organization.canManageAllCollections; + return attachment.key == null && this.organization.canEditAnyCollection; } } diff --git a/src/app/organizations/vault/ciphers.component.ts b/src/app/organizations/vault/ciphers.component.ts index e83e6ba135f..0096743ff29 100644 --- a/src/app/organizations/vault/ciphers.component.ts +++ b/src/app/organizations/vault/ciphers.component.ts @@ -42,7 +42,7 @@ export class CiphersComponent extends BaseCiphersComponent { } async load(filter: (cipher: CipherView) => boolean = null) { - if (this.organization.canManageAllCollections) { + if (this.organization.canViewAllCollections) { this.accessEvents = this.organization.useEvents; this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id); } else { @@ -54,7 +54,7 @@ export class CiphersComponent extends BaseCiphersComponent { } async applyFilter(filter: (cipher: CipherView) => boolean = null) { - if (this.organization.canManageAllCollections) { + if (this.organization.canViewAllCollections) { await super.applyFilter(filter); } else { const f = (c: CipherView) => c.organizationId === this.organization.id && (filter == null || filter(c)); @@ -70,13 +70,13 @@ export class CiphersComponent extends BaseCiphersComponent { } protected deleteCipher(id: string) { - if (!this.organization.canManageAllCollections) { + if (!this.organization.canEditAnyCollection) { return super.deleteCipher(id, this.deleted); } return this.deleted ? this.apiService.deleteCipherAdmin(id) : this.apiService.putDeleteCipherAdmin(id); } protected showFixOldAttachments(c: CipherView) { - return this.organization.canManageAllCollections && c.hasOldAttachments; + return this.organization.canEditAnyCollection && c.hasOldAttachments; } } diff --git a/src/app/organizations/vault/collections.component.ts b/src/app/organizations/vault/collections.component.ts index 5bdcedc9051..85b10378143 100644 --- a/src/app/organizations/vault/collections.component.ts +++ b/src/app/organizations/vault/collections.component.ts @@ -28,7 +28,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { } protected async loadCipher() { - if (!this.organization.canManageAllCollections) { + if (!this.organization.canViewAllCollections) { return await super.loadCipher(); } const response = await this.apiService.getCipherAdmin(this.cipherId); @@ -36,21 +36,21 @@ export class CollectionsComponent extends BaseCollectionsComponent { } protected loadCipherCollections() { - if (!this.organization.canManageAllCollections) { + if (!this.organization.canViewAllCollections) { return super.loadCipherCollections(); } return this.collectionIds; } protected loadCollections() { - if (!this.organization.canManageAllCollections) { + if (!this.organization.canViewAllCollections) { return super.loadCollections(); } return Promise.resolve(this.collections); } protected saveCollections() { - if (this.organization.canManageAllCollections) { + if (this.organization.canEditAnyCollection) { const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds); return this.apiService.putCipherCollectionsAdmin(this.cipherId, request); } else { diff --git a/src/app/organizations/vault/groupings.component.ts b/src/app/organizations/vault/groupings.component.ts index 34a0543791e..845b007df60 100644 --- a/src/app/organizations/vault/groupings.component.ts +++ b/src/app/organizations/vault/groupings.component.ts @@ -29,7 +29,7 @@ export class GroupingsComponent extends BaseGroupingsComponent { } async loadCollections() { - if (!this.organization.canManageAllCollections) { + if (!this.organization.canViewAllCollections) { await super.loadCollections(this.organization.id); return; } diff --git a/src/app/organizations/vault/vault.component.ts b/src/app/organizations/vault/vault.component.ts index 4d98e38ae9c..288bb28b165 100644 --- a/src/app/organizations/vault/vault.component.ts +++ b/src/app/organizations/vault/vault.component.ts @@ -72,7 +72,7 @@ export class VaultComponent implements OnInit, OnDestroy { const queryParamsSub = this.route.queryParams.subscribe(async qParams => { this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search; - if (!this.organization.canManageAllCollections) { + if (!this.organization.canViewAllCollections) { await this.syncService.fullSync(false); this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { this.ngZone.run(async () => { @@ -223,7 +223,7 @@ export class VaultComponent implements OnInit, OnDestroy { async editCipherCollections(cipher: CipherView) { const [modal] = await this.modalService.openViewRef(CollectionsComponent, this.collectionsModalRef, comp => { - if (this.organization.canManageAllCollections) { + if (this.organization.canEditAnyCollection) { comp.collectionIds = cipher.collectionIds; comp.collections = this.groupingsComponent.collections.filter(c => !c.readOnly); } @@ -240,7 +240,7 @@ export class VaultComponent implements OnInit, OnDestroy { const component = await this.editCipher(null); component.organizationId = this.organization.id; component.type = this.type; - if (this.organization.canManageAllCollections) { + if (this.organization.canEditAnyCollection) { component.collections = this.groupingsComponent.collections.filter(c => !c.readOnly); } if (this.collectionId != null) { @@ -273,7 +273,7 @@ export class VaultComponent implements OnInit, OnDestroy { const component = await this.editCipher(cipher); component.cloneMode = true; component.organizationId = this.organization.id; - if (this.organization.canManageAllCollections) { + if (this.organization.canEditAnyCollection) { component.collections = this.groupingsComponent.collections.filter(c => !c.readOnly); } // Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value diff --git a/src/app/oss-routing.module.ts b/src/app/oss-routing.module.ts index 85e47a41e18..487e772bac4 100644 --- a/src/app/oss-routing.module.ts +++ b/src/app/oss-routing.module.ts @@ -350,8 +350,11 @@ const routes: Routes = [ canActivate: [OrganizationTypeGuardService], data: { permissions: [ - Permissions.ManageAssignedCollections, - Permissions.ManageAllCollections, + Permissions.CreateNewCollections, + Permissions.EditAnyCollection, + Permissions.DeleteAnyCollection, + Permissions.EditAssignedCollections, + Permissions.DeleteAssignedCollections, Permissions.AccessEventLogs, Permissions.ManageGroups, Permissions.ManageUsers, @@ -370,7 +373,13 @@ const routes: Routes = [ canActivate: [OrganizationTypeGuardService], data: { titleId: 'collections', - permissions: [Permissions.ManageAssignedCollections, Permissions.ManageAllCollections], + permissions: [ + Permissions.CreateNewCollections, + Permissions.EditAnyCollection, + Permissions.DeleteAnyCollection, + Permissions.EditAssignedCollections, + Permissions.DeleteAssignedCollections, + ], }, }, { diff --git a/src/app/oss.module.ts b/src/app/oss.module.ts index a4cf7f3337a..79f13863d61 100644 --- a/src/app/oss.module.ts +++ b/src/app/oss.module.ts @@ -12,6 +12,7 @@ import { ToasterModule } from 'angular2-toaster'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { AvatarComponent } from './components/avatar.component'; +import { NestedCheckboxComponent } from './components/nested-checkbox.component'; import { PasswordRepromptComponent } from './components/password-reprompt.component'; import { PasswordStrengthComponent } from './components/password-strength.component'; @@ -356,6 +357,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); LockComponent, LoginComponent, NavbarComponent, + NestedCheckboxComponent, OptionsComponent, OrgAccountComponent, OrgAddEditComponent, diff --git a/src/app/services/organization-type-guard.service.ts b/src/app/services/organization-type-guard.service.ts index 8a2ffbeff8c..eeaf23a45dd 100644 --- a/src/app/services/organization-type-guard.service.ts +++ b/src/app/services/organization-type-guard.service.ts @@ -22,8 +22,11 @@ export class OrganizationTypeGuardService implements CanActivate { (permissions.indexOf(Permissions.AccessEventLogs) !== -1 && org.canAccessEventLogs) || (permissions.indexOf(Permissions.AccessImportExport) !== -1 && org.canAccessImportExport) || (permissions.indexOf(Permissions.AccessReports) !== -1 && org.canAccessReports) || - (permissions.indexOf(Permissions.ManageAllCollections) !== -1 && org.canManageAllCollections) || - (permissions.indexOf(Permissions.ManageAssignedCollections) !== -1 && org.canManageAssignedCollections) || + (permissions.indexOf(Permissions.CreateNewCollections) !== -1 && org.canCreateNewCollections) || + (permissions.indexOf(Permissions.EditAnyCollection) !== -1 && org.canEditAnyCollection) || + (permissions.indexOf(Permissions.DeleteAnyCollection) !== -1 && org.canDeleteAnyCollection) || + (permissions.indexOf(Permissions.EditAssignedCollections) !== -1 && org.canEditAssignedCollections) || + (permissions.indexOf(Permissions.DeleteAssignedCollections) !== -1 && org.canDeleteAssignedCollections) || (permissions.indexOf(Permissions.ManageGroups) !== -1 && org.canManageGroups) || (permissions.indexOf(Permissions.ManageOrganization) !== -1 && org.isOwner) || (permissions.indexOf(Permissions.ManagePolicies) !== -1 && org.canManagePolicies) || diff --git a/src/app/vault/bulk-delete.component.ts b/src/app/vault/bulk-delete.component.ts index 1e11f58e28b..403e3144bf3 100644 --- a/src/app/vault/bulk-delete.component.ts +++ b/src/app/vault/bulk-delete.component.ts @@ -29,7 +29,7 @@ export class BulkDeleteComponent { private i18nService: I18nService, private apiService: ApiService) { } async submit() { - if (!this.organization || !this.organization.canManageAllCollections) { + if (!this.organization || !this.organization.canEditAnyCollection) { await this.deleteCiphers(); } else { await this.deleteCiphersAdmin(); diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 2b4a9ae4d44..93fe809e20b 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -3824,9 +3824,24 @@ "manageAllCollections": { "message": "Manage All Collections" }, + "createNewCollections": { + "message": "Create New Collections" + }, + "editAnyCollection": { + "message": "Edit Any Collection" + }, + "deleteAnyCollection": { + "message": "Delete Any Collection" + }, "manageAssignedCollections": { "message": "Manage Assigned Collections" }, + "editAssignedCollections": { + "message": "Edit Assigned Collections" + }, + "deleteAssignedCollections": { + "message": "Delete Assigned Collections" + }, "manageGroups": { "message": "Manage Groups" }, diff --git a/src/scss/forms.scss b/src/scss/forms.scss index 379efc67c5e..1be32529f48 100644 --- a/src/scss/forms.scss +++ b/src/scss/forms.scss @@ -58,6 +58,12 @@ label.form-check-label, .form-control-file { } } +.form-group { + .form-group-child-check { + @extend .ml-4 + } +} + .form-inline { input[type='datetime-local'] { width: 200px;