1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 14:53:33 +00:00

Limit collection actions presented to permitted (#1247)

* Limit collection actions presented to permitted

* Revert useless move

* Limit vault view to editable ciphers and collections

* Update jslib

* PR review
This commit is contained in:
Matt Gibson
2021-10-20 16:17:27 -05:00
committed by GitHub
parent 044ac513ae
commit 9dd859af7a
10 changed files with 88 additions and 29 deletions

View File

@@ -24,8 +24,11 @@ const routes: Routes = [
canActivate: [OrganizationTypeGuardService], canActivate: [OrganizationTypeGuardService],
data: { data: {
permissions: [ permissions: [
Permissions.ManageAssignedCollections, Permissions.CreateNewCollections,
Permissions.ManageAllCollections, Permissions.EditAnyCollection,
Permissions.DeleteAnyCollection,
Permissions.EditAssignedCollections,
Permissions.DeleteAssignedCollections,
Permissions.AccessEventLogs, Permissions.AccessEventLogs,
Permissions.ManageGroups, Permissions.ManageGroups,
Permissions.ManageUsers, Permissions.ManageUsers,

2
jslib

Submodule jslib updated: f09fb69882...815b436f7c

View File

@@ -15,17 +15,18 @@
<div class="form-group"> <div class="form-group">
<label for="name">{{'name' | i18n}}</label> <label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required <input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required
appAutofocus> appAutofocus [disabled]="!this.canSave">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="externalId">{{'externalId' | i18n}}</label> <label for="externalId">{{'externalId' | i18n}}</label>
<input id="externalId" class="form-control" type="text" name="ExternalId" [(ngModel)]="externalId"> <input id="externalId" class="form-control" type="text" name="ExternalId" [(ngModel)]="externalId"
[disabled]="!this.canSave">
<small class="form-text text-muted">{{'externalIdDesc' | i18n}}</small> <small class="form-text text-muted">{{'externalIdDesc' | i18n}}</small>
</div> </div>
<ng-container *ngIf="accessGroups"> <ng-container *ngIf="accessGroups">
<h3 class="mt-4 d-flex mb-0"> <h3 class="mt-4 d-flex mb-0">
{{'groupAccess' | i18n}} {{'groupAccess' | i18n}}
<div class="ml-auto" *ngIf="groups && groups.length"> <div class="ml-auto" *ngIf="groups && groups.length && this.canSave">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0"> <button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{'selectAll' | i18n}} {{'selectAll' | i18n}}
</button> </button>
@@ -50,7 +51,7 @@
<tr *ngFor="let g of groups; let i = index"> <tr *ngFor="let g of groups; let i = index">
<td class="table-list-checkbox" (click)="check(g)"> <td class="table-list-checkbox" (click)="check(g)">
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked" <input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked"
[disabled]="g.accessAll" appStopProp> [disabled]="g.accessAll || !this.canSave" appStopProp>
</td> </td>
<td (click)="check(g)"> <td (click)="check(g)">
{{g.name}} {{g.name}}
@@ -62,11 +63,11 @@
</td> </td>
<td class="text-center"> <td class="text-center">
<input type="checkbox" [(ngModel)]="g.hidePasswords" <input type="checkbox" [(ngModel)]="g.hidePasswords"
name="Groups[{{i}}].HidePasswords" [disabled]="!g.checked || g.accessAll"> name="Groups[{{i}}].HidePasswords" [disabled]="!g.checked || g.accessAll || !this.canSave">
</td> </td>
<td class="text-center"> <td class="text-center">
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly" <input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly"
[disabled]="!g.checked || g.accessAll"> [disabled]="!g.checked || g.accessAll || !this.canSave">
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -74,22 +75,23 @@
</ng-container> </ng-container>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading" *ngIf="this.canSave">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span> <span>{{'save' | i18n}}</span>
</button> </button>
<button type="button" class="btn btn-outline-secondary" <button type="button" class="btn btn-outline-secondary"
data-dismiss="modal">{{'cancel' | i18n}}</button> data-dismiss="modal">{{'cancel' | i18n}}</button>
<div class="ml-auto"> <div class="ml-auto" *ngIf="this.canDelete">
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger" <button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading" appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode"
[appApiAction]="deletePromise"> [disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i> <i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" <i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
title="{{'loading' | i18n}}" aria-hidden="true"></i> title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button> </button>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -30,6 +30,8 @@ import { Utils } from 'jslib-common/misc/utils';
export class CollectionAddEditComponent implements OnInit { export class CollectionAddEditComponent implements OnInit {
@Input() collectionId: string; @Input() collectionId: string;
@Input() organizationId: string; @Input() organizationId: string;
@Input() canSave: boolean;
@Input() canDelete: boolean;
@Output() onSavedCollection = new EventEmitter(); @Output() onSavedCollection = new EventEmitter();
@Output() onDeletedCollection = new EventEmitter(); @Output() onDeletedCollection = new EventEmitter();

View File

@@ -6,7 +6,7 @@
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}" <input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
[(ngModel)]="searchText"> [(ngModel)]="searchText">
</div> </div>
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="add()"> <button type="button" *ngIf="this.canCreate" class="btn btn-sm btn-outline-primary ml-3" (click)="add()">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i> <i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'newCollection' | i18n}} {{'newCollection' | i18n}}
</button> </button>
@@ -27,17 +27,17 @@
<a href="#" appStopClick (click)="edit(c)">{{c.name}}</a> <a href="#" appStopClick (click)="edit(c)">{{c.name}}</a>
</td> </td>
<td class="table-list-options"> <td class="table-list-options">
<div class="dropdown" appListDropdown> <div class="dropdown" appListDropdown *ngIf="this.canEdit(c) || this.canDelete(c)">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}"> aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog fa-lg" aria-hidden="true"></i> <i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#" appStopClick (click)="users(c)"> <a class="dropdown-item" href="#" appStopClick *ngIf="this.canEdit(c)" (click)="users(c)">
<i class="fa fa-fw fa-users" aria-hidden="true"></i> <i class="fa fa-fw fa-users" aria-hidden="true"></i>
{{'users' | i18n}} {{'users' | i18n}}
</a> </a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(c)"> <a class="dropdown-item text-danger" href="#" appStopClick *ngIf="this.canDelete(c)" (click)="delete(c)">
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i> <i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
{{'delete' | i18n}} {{'delete' | i18n}}
</a> </a>

View File

@@ -21,6 +21,7 @@ import { ModalService } from 'jslib-angular/services/modal.service';
import { CollectionData } from 'jslib-common/models/data/collectionData'; import { CollectionData } from 'jslib-common/models/data/collectionData';
import { Collection } from 'jslib-common/models/domain/collection'; import { Collection } from 'jslib-common/models/domain/collection';
import { Organization } from 'jslib-common/models/domain/organization';
import { import {
CollectionDetailsResponse, CollectionDetailsResponse,
CollectionResponse, CollectionResponse,
@@ -40,8 +41,11 @@ export class CollectionsComponent implements OnInit {
@ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef; @ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef;
loading = true; loading = true;
organization: Organization;
canCreate: boolean = false;
organizationId: string; organizationId: string;
collections: CollectionView[]; collections: CollectionView[];
assignedCollections: CollectionView[];
pagedCollections: CollectionView[]; pagedCollections: CollectionView[];
searchText: string; searchText: string;
@@ -67,16 +71,27 @@ export class CollectionsComponent implements OnInit {
} }
async load() { async load() {
const organization = await this.userService.getOrganization(this.organizationId); this.organization = await this.userService.getOrganization(this.organizationId);
let response: ListResponse<CollectionResponse>; this.canCreate = this.organization.canCreateNewCollections;
if (organization.canViewAllCollections) {
response = await this.apiService.getCollections(this.organizationId); const decryptCollections = async (r: ListResponse<CollectionResponse>) => {
} else { const collections = r.data.filter(c => c.organizationId === this.organizationId).map(d =>
response = await this.apiService.getUserCollections(); new Collection(new CollectionData(d as CollectionDetailsResponse)));
return await this.collectionService.decryptMany(collections);
};
if (this.organization.canViewAssignedCollections) {
const response = await this.apiService.getUserCollections();
this.assignedCollections = await decryptCollections(response);
} }
const collections = response.data.filter(c => c.organizationId === this.organizationId).map(r =>
new Collection(new CollectionData(r as CollectionDetailsResponse))); if (this.organization.canViewAllCollections) {
this.collections = await this.collectionService.decryptMany(collections); const response = await this.apiService.getCollections(this.organizationId);
this.collections = await decryptCollections(response);
} else {
this.collections = this.assignedCollections;
}
this.resetPaging(); this.resetPaging();
this.loading = false; this.loading = false;
} }
@@ -99,9 +114,20 @@ export class CollectionsComponent implements OnInit {
} }
async edit(collection: CollectionView) { async edit(collection: CollectionView) {
const canCreate = collection == null && this.canCreate;
const canEdit = collection != null && this.canEdit(collection);
const canDelete = collection != null && this.canDelete(collection);
if (!(canCreate || canEdit || canDelete)) {
this.toasterService.popAsync('error', null, this.i18nService.t('missingPermissions'));
return;
}
const [modal] = await this.modalService.openViewRef(CollectionAddEditComponent, this.addEditModalRef, comp => { const [modal] = await this.modalService.openViewRef(CollectionAddEditComponent, this.addEditModalRef, comp => {
comp.organizationId = this.organizationId; comp.organizationId = this.organizationId;
comp.collectionId = collection != null ? collection.id : null; comp.collectionId = collection != null ? collection.id : null;
comp.canSave = canCreate || canEdit;
comp.canDelete = canDelete;
comp.onSavedCollection.subscribe(() => { comp.onSavedCollection.subscribe(() => {
modal.close(); modal.close();
this.load(); this.load();
@@ -131,6 +157,7 @@ export class CollectionsComponent implements OnInit {
this.removeCollection(collection); this.removeCollection(collection);
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
this.toasterService.popAsync('error', null, this.i18nService.t('missingPermissions'));
} }
} }
@@ -165,6 +192,28 @@ export class CollectionsComponent implements OnInit {
return !searching && this.collections && this.collections.length > this.pageSize; return !searching && this.collections && this.collections.length > this.pageSize;
} }
canEdit(collection: CollectionView) {
if (this.organization.canEditAnyCollection) {
return true;
}
if (this.organization.canEditAssignedCollections && this.assignedCollections.some(c => c.id === collection.id)) {
return true;
}
return false;
}
canDelete(collection: CollectionView) {
if (this.organization.canDeleteAnyCollection) {
return true;
}
if (this.organization.canDeleteAssignedCollections && this.assignedCollections.some(c => c.id === collection.id)) {
return true;
}
return false;
}
private removeCollection(collection: CollectionView) { private removeCollection(collection: CollectionView) {
const index = this.collections.indexOf(collection); const index = this.collections.indexOf(collection);
if (index > -1) { if (index > -1) {

View File

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

View File

@@ -44,7 +44,7 @@ export class CiphersComponent extends BaseCiphersComponent {
} }
async load(filter: (cipher: CipherView) => boolean = null) { async load(filter: (cipher: CipherView) => boolean = null) {
if (this.organization.canViewAllCollections) { if (this.organization.canEditAnyCollection) {
this.accessEvents = this.organization.useEvents; this.accessEvents = this.organization.useEvents;
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id); this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
} else { } else {

View File

@@ -29,7 +29,7 @@ export class GroupingsComponent extends BaseGroupingsComponent {
} }
async loadCollections() { async loadCollections() {
if (!this.organization.canViewAllCollections) { if (!this.organization.canEditAnyCollection) {
await super.loadCollections(this.organization.id); await super.loadCollections(this.organization.id);
return; return;
} }

View File

@@ -3817,6 +3817,9 @@
"accessReports": { "accessReports": {
"message": "Access Reports" "message": "Access Reports"
}, },
"missingPermissions": {
"message": "You lack the necessary permissions to perform this action."
},
"manageAllCollections": { "manageAllCollections": {
"message": "Manage All Collections" "message": "Manage All Collections"
}, },