Feature/split manage collections permission (#1211)

* Update guard services and routing

* Add depenent checkbox to handle sub permissions

* Present new collections premissions

* Use new split permissions

* Rename to nested-checkbox.component

* Clarify css class name

* update jslib
This commit is contained in:
Matt Gibson 2021-10-05 11:12:44 -05:00 committed by GitHub
parent 7a43510cf5
commit 998d36a5d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 181 additions and 72 deletions

2
jslib

@ -1 +1 @@
Subproject commit ce71c0c0bd6667573e0e611222dc415770ba3909
Subproject commit 91c5393ae7a84e9f4d90391d072cae56e7a3ff41

View File

@ -0,0 +1,18 @@
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" [name]="pascalize(parentId)" [id]="parentId"
[(ngModel)]="parentChecked" [indeterminate]="parentIndeterminate">
<label class="form-check-label font-weight-normal" [for]="parentId">
{{parentId | i18n}}
</label>
</div>
<div class="form-group form-group-child-check mb-0">
<div class="form-check mt-1" *ngFor="let c of checkboxes">
<input class="form-check-input" type="checkbox" [name]="pascalize(c.id)" [id]="c.id" [ngModel]="c.get()"
(ngModelChange)="c.set($event)">
<label class="form-check-label font-weight-normal" [for]="c.id">
{{c.id | i18n}}
</label>
</div>
</div>
</div>

View File

@ -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);
}
}

View File

@ -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:

View File

@ -69,7 +69,7 @@ export class CollectionsComponent implements OnInit {
async load() {
const organization = await this.userService.getOrganization(this.organizationId);
let response: ListResponse<CollectionResponse>;
if (organization.canManageAllCollections) {
if (organization.canViewAllCollections) {
response = await this.apiService.getCollections(this.organizationId);
} else {
response = await this.apiService.getUserCollections();

View File

@ -9,7 +9,7 @@
{{'people' | i18n}}
</a>
<a routerLink="collections" class="list-group-item" routerLinkActive="active"
*ngIf="organization.canManageAssignedCollections || organization.canManageAllCollections">
*ngIf="organization.canViewAssignedCollections || organization.canViewAllCollections">
{{'collections' | i18n}}
</a>
<a routerLink="groups" class="list-group-item" routerLinkActive="active"

View File

@ -80,7 +80,8 @@
<div class="mb-3">
<label class="font-weight-bold mb-0">Manager Permissions</label>
<hr class="my-0 mr-2" />
<div class="form-group mb-0">
<!-- Deprecated Sep 29 2021 -->
<div class="form-group mb-0" *ngIf="fallbackToManageAssignedCollections">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageAssignedCollections"
id="manageAssignedCollections"
@ -91,6 +92,10 @@
</label>
</div>
</div>
<!-- -->
<app-nested-checkbox *ngIf="!fallbackToManageAssignedCollections" parentId="manageAssignedCollections"
[checkboxes]="manageAssignedCollectionsCheckboxes">
</app-nested-checkbox>
</div>
</div>
<div class="col-6">
@ -133,7 +138,8 @@
</label>
</div>
</div>
<div class="form-group mb-0">
<!-- Deprecated 29 Sep 2021 -->
<div class="form-group mb-0" *ngIf="fallbackToManageAllCollections">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageAllCollections"
id="manageAllCollections" [(ngModel)]="permissions.manageAllCollections">
@ -142,6 +148,10 @@
</label>
</div>
</div>
<!-- -->
<app-nested-checkbox *ngIf="!fallbackToManageAllCollections" parentId="manageAllCollections"
[checkboxes]="manageAllCollectionsCheckboxes">
</app-nested-checkbox>
<div class="form-group mb-0">
<div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" name="manageGroups"

View File

@ -48,6 +48,48 @@ export class UserAddEditComponent implements OnInit {
deletePromise: Promise<any>;
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 { }
}
}

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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

View File

@ -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,
],
},
},
{

View File

@ -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,

View File

@ -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) ||

View File

@ -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();

View File

@ -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"
},

View File

@ -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;