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:
parent
044ac513ae
commit
9dd859af7a
@ -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
2
jslib
@ -1 +1 @@
|
||||
Subproject commit f09fb69882525b3be7b2e257e7723eeb79b343d1
|
||||
Subproject commit 815b436f7ce9f8825575f288b1ae98c1dc54f1d2
|
@ -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>
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -3817,6 +3817,9 @@
|
||||
"accessReports": {
|
||||
"message": "Access Reports"
|
||||
},
|
||||
"missingPermissions": {
|
||||
"message": "You lack the necessary permissions to perform this action."
|
||||
},
|
||||
"manageAllCollections": {
|
||||
"message": "Manage All Collections"
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user