mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 06:43:35 +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:
@@ -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
2
jslib
Submodule jslib updated: f09fb69882...815b436f7c
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user