1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-21 11:35:34 +01: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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 88 additions and 29 deletions

View File

@ -24,8 +24,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,

2
jslib

@ -1 +1 @@
Subproject commit f09fb69882525b3be7b2e257e7723eeb79b343d1
Subproject commit 815b436f7ce9f8825575f288b1ae98c1dc54f1d2

View File

@ -15,17 +15,18 @@
<div class="form-group">
<label for="name">{{'name' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required
appAutofocus>
appAutofocus [disabled]="!this.canSave">
</div>
<div class="form-group">
<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>
</div>
<ng-container *ngIf="accessGroups">
<h3 class="mt-4 d-flex mb-0">
{{'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">
{{'selectAll' | i18n}}
</button>
@ -50,7 +51,7 @@
<tr *ngFor="let g of groups; let i = index">
<td class="table-list-checkbox" (click)="check(g)">
<input type="checkbox" [(ngModel)]="g.checked" name="Groups[{{i}}].Checked"
[disabled]="g.accessAll" appStopProp>
[disabled]="g.accessAll || !this.canSave" appStopProp>
</td>
<td (click)="check(g)">
{{g.name}}
@ -62,11 +63,11 @@
</td>
<td class="text-center">
<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 class="text-center">
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly"
[disabled]="!g.checked || g.accessAll">
[disabled]="!g.checked || g.accessAll || !this.canSave">
</td>
</tr>
</tbody>
@ -74,22 +75,23 @@
</ng-container>
</div>
<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>
<span>{{'save' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
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"
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
[appApiAction]="deletePromise">
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode"
[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-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@ -30,6 +30,8 @@ import { Utils } from 'jslib-common/misc/utils';
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();

View File

@ -6,7 +6,7 @@
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
[(ngModel)]="searchText">
</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>
{{'newCollection' | i18n}}
</button>
@ -27,17 +27,17 @@
<a href="#" appStopClick (click)="edit(c)">{{c.name}}</a>
</td>
<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"
aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<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>
{{'users' | i18n}}
</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>
{{'delete' | i18n}}
</a>

View File

@ -21,6 +21,7 @@ import { ModalService } from 'jslib-angular/services/modal.service';
import { CollectionData } from 'jslib-common/models/data/collectionData';
import { Collection } from 'jslib-common/models/domain/collection';
import { Organization } from 'jslib-common/models/domain/organization';
import {
CollectionDetailsResponse,
CollectionResponse,
@ -40,8 +41,11 @@ export class CollectionsComponent implements OnInit {
@ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef;
loading = true;
organization: Organization;
canCreate: boolean = false;
organizationId: string;
collections: CollectionView[];
assignedCollections: CollectionView[];
pagedCollections: CollectionView[];
searchText: string;
@ -67,16 +71,27 @@ export class CollectionsComponent implements OnInit {
}
async load() {
const organization = await this.userService.getOrganization(this.organizationId);
let response: ListResponse<CollectionResponse>;
if (organization.canViewAllCollections) {
response = await this.apiService.getCollections(this.organizationId);
} else {
response = await this.apiService.getUserCollections();
this.organization = await this.userService.getOrganization(this.organizationId);
this.canCreate = this.organization.canCreateNewCollections;
const decryptCollections = async (r: ListResponse<CollectionResponse>) => {
const collections = r.data.filter(c => c.organizationId === this.organizationId).map(d =>
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)));
this.collections = await this.collectionService.decryptMany(collections);
if (this.organization.canViewAllCollections) {
const response = await this.apiService.getCollections(this.organizationId);
this.collections = await decryptCollections(response);
} else {
this.collections = this.assignedCollections;
}
this.resetPaging();
this.loading = false;
}
@ -99,9 +114,20 @@ export class CollectionsComponent implements OnInit {
}
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 => {
comp.organizationId = this.organizationId;
comp.collectionId = collection != null ? collection.id : null;
comp.canSave = canCreate || canEdit;
comp.canDelete = canDelete;
comp.onSavedCollection.subscribe(() => {
modal.close();
this.load();
@ -131,6 +157,7 @@ export class CollectionsComponent implements OnInit {
this.removeCollection(collection);
} catch (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;
}
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) {
const index = this.collections.indexOf(collection);
if (index > -1) {

View File

@ -9,7 +9,7 @@
{{'people' | i18n}}
</a>
<a routerLink="collections" class="list-group-item" routerLinkActive="active"
*ngIf="organization.canViewAssignedCollections || organization.canViewAllCollections">
*ngIf="organization.canViewAllCollections || organization.canViewAssignedCollections">
{{'collections' | i18n}}
</a>
<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) {
if (this.organization.canViewAllCollections) {
if (this.organization.canEditAnyCollection) {
this.accessEvents = this.organization.useEvents;
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
} else {

View File

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

View File

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