DocumentList - Check permissions on delete folder/file (#1808)

* Add check permission on delete folder/file

* Provide a way to disable the action when if there is no permission

* Improve the code using the external permission inside the folder/document action service

* Add basic documentation.
 - How disable the button when the permission is missing
 - How to show a notification message when the permission is missing

* Resize the image

* Change the value to true for demo purpose

* Update folder-actions.service.ts

* Update document-actions.service.ts
This commit is contained in:
Maurizio Vitale 2017-04-11 13:43:33 +01:00 committed by Mario Romano
parent ab3d18e5c1
commit f6102dfc07
20 changed files with 460 additions and 32 deletions

View File

@ -70,7 +70,10 @@
</content-action>
<content-action
target="folder"
permission="delete"
[disableWithNoPermission]="true"
title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.DELETE' | translate}}"
(permissionEvent)="onPermissionsFailed($event)"
handler="delete">
</content-action>
<!-- document actions -->
@ -91,6 +94,9 @@
</content-action>
<content-action
target="document"
permission="delete"
[disableWithNoPermission]="true"
(permissionEvent)="onPermissionsFailed($event)"
title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DELETE' | translate}}"
handler="delete">
</content-action>

View File

@ -17,7 +17,7 @@
import { Component, OnInit, AfterViewInit, Optional, ViewChild, ViewChildren, ChangeDetectorRef } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { AlfrescoAuthenticationService, LogService } from 'ng2-alfresco-core';
import { AlfrescoAuthenticationService, LogService, NotificationService } from 'ng2-alfresco-core';
import { DocumentActionsService, DocumentListComponent, ContentActionHandler, DocumentActionModel, FolderActionModel } from 'ng2-alfresco-documentlist';
import { FormService } from 'ng2-activiti-form';
import { UploadButtonComponent, UploadDragAreaComponent } from 'ng2-alfresco-upload';
@ -55,6 +55,7 @@ export class FilesComponent implements OnInit, AfterViewInit {
private logService: LogService,
private changeDetector: ChangeDetectorRef,
private router: Router,
private notificationService: NotificationService,
@Optional() private route: ActivatedRoute) {
documentActions.setHandler('my-handler', this.myDocumentActionHandler.bind(this));
}
@ -168,6 +169,10 @@ export class FilesComponent implements OnInit, AfterViewInit {
}.bind(this);
}
onPermissionsFailed(event: any) {
this.notificationService.openSnackMessage(`you don't have the ${event.permission} permission to ${event.action} the ${event.type} `, 4000);
}
reload(event: any) {
if (event && event.value && event.value.entry && event.value.entry.parentId) {
if (this.documentList.currentFolderId === event.value.entry.parentId) {

View File

@ -34,14 +34,14 @@
[class.alfresco-datatable__row--selected]="selectedRow === row"
[adf-upload]="allowDropFiles" [adf-upload-data]="row">
<!-- Actions (right) -->
<!-- Actions (left) -->
<td *ngIf="actions && actionsPosition === 'left'" class="alfresco-datatable__actions-cell">
<button [id]="'action_menu_' + idx" alfresco-mdl-button class="mdl-button--icon" [attr.data-automation-id]="actions_menu">
<i class="material-icons">more_vert</i>
</button>
<ul alfresco-mdl-menu class="mdl-menu--bottom-left"
[attr.for]="'action_menu_' + idx">
<li class="mdl-menu__item"
<li class="mdl-menu__item" [attr.disabled]="action.disabled"
[attr.data-automation-id]="action.title"
*ngFor="let action of getRowActions(row)"
(click)="onExecuteRowAction(row, action)">
@ -90,7 +90,7 @@
</button>
<ul alfresco-mdl-menu class="mdl-menu--bottom-right"
[attr.for]="'action_menu_' + idx">
<li class="mdl-menu__item"
<li class="mdl-menu__item" [attr.disabled]="action.disabled"
[attr.data-automation-id]="action.title"
*ngFor="let action of getRowActions(row)"
(click)="onExecuteRowAction(row, action)">

View File

@ -414,4 +414,87 @@ describe('DataTable', () => {
dataTable.onImageLoadingError(event);
expect(event.target.src).toBe(originalSrc);
});
it('should disable the action if there is no permission and disableWithNoPermission true', () => {
dataTable.data = new ObjectDataTableAdapter(
[{id: 1, name: 'xyz', allowableOperations: ['create', 'update']}],
[]
);
let row = dataTable.data.getRows();
let actions = [
{
disableWithNoPermission: true,
permission: 'delete',
target: 'folder',
title: 'action2'
}
];
let updateActions = dataTable.checkPermissions(row[0], actions);
expect(updateActions[0].disabled).toBe(true);
});
it('should not disable the action if there is no permission and disableWithNoPermission false', () => {
dataTable.data = new ObjectDataTableAdapter(
[{id: 1, name: 'xyz', allowableOperations: ['create', 'update']}],
[]
);
let row = dataTable.data.getRows();
let actions = [
{
disableWithNoPermission: false,
permission: 'delete',
target: 'folder',
title: 'action2'
}
];
let updateActions = dataTable.checkPermissions(row[0], actions);
expect(updateActions[0].disabled).toBeUndefined();
});
it('should not disable the action if there is the right permission', () => {
dataTable.data = new ObjectDataTableAdapter(
[{ id: 1, name: 'xyz', allowableOperations: ['create', 'update', 'delete'] }],
[]
);
let row = dataTable.data.getRows();
let actions = [
{
permission: 'delete',
target: 'folder',
title: 'action2'
}
];
let updateActions = dataTable.checkPermissions(row[0], actions);
expect(updateActions[0].disabled).toBeUndefined();
});
it('should not disable the action if there are no permissions', () => {
dataTable.data = new ObjectDataTableAdapter(
[{id: 1, name: 'xyz', allowableOperations: null}],
[]
);
let row = dataTable.data.getRows();
let actions = [
{
permission: 'delete',
target: 'folder',
title: 'action2'
}
];
let updateActions = dataTable.checkPermissions(row[0], actions);
expect(updateActions[0].disabled).toBeUndefined();
});
});

View File

@ -236,10 +236,40 @@ export class DataTableComponent implements AfterContentInit, OnChanges {
getRowActions(row: DataRow, col: DataColumn): any[] {
let event = new DataCellEvent(row, col, []);
this.showRowActionsMenu.emit(event);
return event.value.actions;
return this.checkPermissions(row, event.value.actions);
}
checkPermissions(row: DataRow, actions: any[]) {
let actionsPermission = [];
actions.forEach((action) => {
actionsPermission.push(this.checkPermission(row, action));
});
return actionsPermission;
}
checkPermission(row: DataRow, action) {
if (action.permission) {
if (this.hasPermissions(row)) {
let permissions = row.getValue('allowableOperations');
let findPermission = permissions.find(permission => permission === action.permission);
if (!findPermission && action.disableWithNoPermission === true) {
action.disabled = true;
}
}
}
return action;
}
private hasPermissions(row: DataRow): boolean {
return row.getValue('allowableOperations') ? true : false;
}
onExecuteRowAction(row: DataRow, action: any) {
if (action.disabled) {
event.stopPropagation();
} else {
this.executeRowAction.emit(new DataRowActionEvent(row, action));
}
}
}

View File

@ -612,6 +612,61 @@ The following action handlers are provided out-of-box:
All system handler names are case-insensitive, `handler="download"` and `handler="DOWNLOAD"`
will trigger the same `download` action.
##### Delete - Show notification message with no permission
You can show a notification error when the user don't have the right permission to perform the action.
The ContentActionComponent provides the event permissionEvent that is raised when the permission specified in the permission property is missing
You can subscribe to this event from your component and use the NotificationService to show a message.
```html
<alfresco-document-list ...>
<content-actions>
<content-action
target="document"
title="Delete"
permission="delete"
(permissionEvent)="onPermissionsFailed($event)"
handler="delete">
</content-action>
</content-actions>
</alfresco-document-list>
export class MyComponent {
onPermissionsFailed(event: any) {
this.notificationService.openSnackMessage(`you don't have the ${event.permission} permission to ${event.action} the ${event.type} `, 4000);
}
}
```
![Delete show notification message](docs/assets/content-action-notification-message.png)
##### Delete - Disable button checking the permission
You can easily disable a button when the user doesn't own the permission to perform the action related to the button.
The ContentActionComponent provides the property permission that must contain the permission to check and a property disableWithNoPermission that can be true if
you want see the button disabled.
```html
<alfresco-document-list ...>
<content-actions>
<content-action
target="document"
title="Delete"
permission="delete"
disableWithNoPermission="true"
handler="delete">
</content-action>
</content-actions>
</alfresco-document-list>
```
![Delete disable action button](docs/assets/content-action-disable-delete-button.png)
##### Download
Initiates download of the corresponding document file.
@ -632,7 +687,6 @@ Initiates download of the corresponding document file.
![Download document action](docs/assets/document-action-download.png)
#### Folder actions
Folder actions have the same declaration as document actions except ```taget="folder"``` attribute value.

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

View File

@ -53,6 +53,7 @@ export * from './src/services/document-list.service';
// models
export * from './src/models/content-action.model';
export * from './src/models/document-library.model';
export * from './src/models/permissions.model';
export const DOCUMENT_LIST_DIRECTIVES: any[] = [
DocumentListComponent,

View File

@ -41,9 +41,18 @@ export class ContentActionComponent implements OnInit, OnChanges {
@Input()
target: string;
@Input()
permission: string;
@Input()
disableWithNoPermission: boolean;
@Output()
execute = new EventEmitter();
@Output()
permissionEvent = new EventEmitter();
model: ContentActionModel;
constructor(
@ -57,6 +66,8 @@ export class ContentActionComponent implements OnInit, OnChanges {
this.model = new ContentActionModel({
title: this.title,
icon: this.icon,
permission: this.permission,
disableWithNoPermission: this.disableWithNoPermission,
target: this.target
});
@ -98,6 +109,9 @@ export class ContentActionComponent implements OnInit, OnChanges {
if (ltarget === 'folder') {
if (this.folderActions) {
this.folderActions.permissionEvent.subscribe((permision) => {
this.permissionEvent.emit(permision);
});
return this.folderActions.getHandler(name);
}
return null;

View File

@ -86,7 +86,22 @@ describe('DocumentList', () => {
spyOn(action, 'handler').and.stub();
documentList.executeContentAction(node, action);
expect(action.handler).toHaveBeenCalledWith(node, documentList);
expect(action.handler).toHaveBeenCalledWith(node, documentList, undefined);
});
it('should execute action with node and permission', () => {
let node = new FileNode();
let action = new ContentActionModel();
action.handler = function () {
console.log('mock handler');
};
action.permission = 'fake-permission';
spyOn(action, 'handler').and.stub();
documentList.executeContentAction(node, action);
expect(action.handler).toHaveBeenCalledWith(node, documentList, 'fake-permission');
});

View File

@ -306,7 +306,7 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
*/
executeContentAction(node: MinimalNodeEntity, action: ContentActionModel) {
if (node && node.entry && action) {
action.handler(node, this);
action.handler(node, this, action.permission);
}
}

View File

@ -0,0 +1,28 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PermissionModel } from './../models/permissions.model';
export class PermissionErrorEvent {
readonly error: PermissionModel;
constructor(error: PermissionModel) {
this.error = error;
}
}

View File

@ -20,6 +20,8 @@ export class ContentActionModel {
title: string;
handler: ContentActionHandler;
target: string;
permission: string;
disableWithNoPermission: boolean;
constructor(obj?: any) {
if (obj) {
@ -27,12 +29,14 @@ export class ContentActionModel {
this.title = obj.title;
this.handler = obj.handler;
this.target = obj.target;
this.permission = obj.permission;
this.disableWithNoPermission = obj.disableWithNoPermission;
}
}
}
export interface ContentActionHandler {
(obj: any, target?: any): any;
(obj: any, target?: any, permission?: string): any;
}
export class DocumentActionModel extends ContentActionModel {

View File

@ -0,0 +1,30 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class PermissionModel {
type: string;
action: string;
permission: string;
constructor(obj?: any) {
if (obj) {
this.type = obj.type || null;
this.action = obj.action || null;
this.permission = obj.permission || null;
}
}
}

View File

@ -101,6 +101,62 @@ describe('DocumentActionsService', () => {
expect(service.getHandler('delete')).toBeDefined();
});
it('should not delete the file node if there are no permissions', (done) => {
spyOn(documentListService, 'deleteNode').and.callThrough();
service.permissionEvent.subscribe((permission) => {
expect(permission).toBeDefined();
expect(permission.type).toEqual('content');
expect(permission.action).toEqual('delete');
done();
});
let file = new FileNode();
service.getHandler('delete')(file);
});
it('should delete the file node if there is the delete permission', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let permission = 'delete';
let file = new FileNode();
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(fileWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalledWith(file.entry.id);
});
it('should not delete the file node if there is no delete permission', (done) => {
spyOn(documentListService, 'deleteNode').and.callThrough();
service.permissionEvent.subscribe((permission) => {
expect(permission).toBeDefined();
expect(permission.type).toEqual('content');
expect(permission.action).toEqual('delete');
done();
});
let permission = 'delete';
let file = new FileNode();
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = ['create', 'update'];
service.getHandler('delete')(fileWithPermission, null, permission);
});
it('should delete the file node if there is the delete and others permission ', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let permission = 'delete';
let file = new FileNode();
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = ['create', 'update', permission];
service.getHandler('delete')(fileWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalledWith(file.entry.id);
});
it('should register download action', () => {
expect(service.getHandler('download')).toBeDefined();
});
@ -152,8 +208,11 @@ describe('DocumentActionsService', () => {
it('should delete file node', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let permission = 'delete';
let file = new FileNode();
service.getHandler('delete')(file);
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(fileWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalledWith(file.entry.id);
});
@ -165,8 +224,11 @@ describe('DocumentActionsService', () => {
service.getHandler('delete')(folder);
expect(documentListService.deleteNode).not.toHaveBeenCalled();
let permission = 'delete';
let file = new FileNode();
service.getHandler('delete')(file);
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(fileWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalled();
});
@ -184,8 +246,11 @@ describe('DocumentActionsService', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let target = jasmine.createSpyObj('obj', ['reload']);
let permission = 'delete';
let file = new FileNode();
service.getHandler('delete')(file, target);
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(fileWithPermission, target, permission);
expect(documentListService.deleteNode).toHaveBeenCalled();
expect(target.reload).toHaveBeenCalled();

View File

@ -19,9 +19,14 @@ import { Injectable } from '@angular/core';
import { ContentActionHandler } from '../models/content-action.model';
import { DocumentListService } from './document-list.service';
import { AlfrescoContentService } from 'ng2-alfresco-core';
import { PermissionModel } from '../models/permissions.model';
import { Subject } from 'rxjs/Rx';
@Injectable()
export class DocumentActionsService {
permissionEvent: Subject<PermissionModel> = new Subject<PermissionModel>();
private handlers: { [id: string]: ContentActionHandler; } = {};
constructor(private documentListService?: DocumentListService,
@ -82,13 +87,28 @@ export class DocumentActionsService {
return false;
}
private deleteNode(obj: any, target?: any) {
if (this.canExecuteAction(obj) && obj.entry && obj.entry.id) {
private deleteNode(obj: any, target?: any, permission?: string) {
if (this.canExecuteAction(obj)) {
if (this.hasPermission(obj.entry, permission)) {
this.documentListService.deleteNode(obj.entry.id).subscribe(() => {
if (target && typeof target.reload === 'function') {
target.reload();
}
});
} else {
this.permissionEvent.next(new PermissionModel({type: 'content', action: 'delete', permission: permission}));
}
}
}
private hasPermission(node: any, permission: string): boolean {
if (this.hasPermissions(node)) {
return node.allowableOperations.find(permision => permision === permission) ? true : false;
}
return false;
}
private hasPermissions(node: any): boolean {
return node && node.allowableOperations ? true : false;
}
}

View File

@ -72,7 +72,7 @@ export class DocumentListService {
let params: any = {
includeSource: true,
include: ['path', 'properties']
include: ['path', 'properties', 'allowableOperations']
};
if (folder) {
@ -121,7 +121,7 @@ export class DocumentListService {
getFolderNode(nodeId: string): Promise<MinimalNodeEntryEntity> {
let opts: any = {
includeSource: true,
include: ['path', 'properties']
include: ['path', 'properties', 'allowableOperations']
};
let nodes: any = this.apiService.getInstance().nodes;

View File

@ -97,24 +97,73 @@ describe('FolderActionsService', () => {
expect(service.getHandler('delete')).toBeDefined();
});
it('should delete folder node', () => {
it('should not delete the folder node if there are no permissions', (done) => {
spyOn(documentListService, 'deleteNode').and.callThrough();
service.permissionEvent.subscribe((permission) => {
expect(permission).toBeDefined();
expect(permission.type).toEqual('folder');
expect(permission.action).toEqual('delete');
done();
});
let folder = new FolderNode();
service.getHandler('delete')(folder);
});
it('should delete the folder node if there is the delete permission', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let permission = 'delete';
let folder = new FolderNode();
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = [ permission ];
service.getHandler('delete')(folderWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalledWith(folder.entry.id);
});
it('should not delete the folder node if there is no delete permission', (done) => {
spyOn(documentListService, 'deleteNode').and.callThrough();
service.permissionEvent.subscribe((permission) => {
expect(permission).toBeDefined();
expect(permission.type).toEqual('folder');
expect(permission.action).toEqual('delete');
done();
});
let folder = new FolderNode();
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = ['create', 'update'];
service.getHandler('delete')(folderWithPermission);
});
it('should delete the folder node if there is the delete and others permission ', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let permission = 'delete';
let folder = new FolderNode();
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = ['create', 'update', permission];
service.getHandler('delete')(folderWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalledWith(folder.entry.id);
});
it('should support deletion only folder node', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let permission = 'delete';
let file = new FileNode();
service.getHandler('delete')(file);
expect(documentListService.deleteNode).not.toHaveBeenCalled();
let folder = new FolderNode();
service.getHandler('delete')(folder);
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(folderWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalled();
});
@ -131,9 +180,13 @@ describe('FolderActionsService', () => {
it('should reload target upon node deletion', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let permission = 'delete';
let target = jasmine.createSpyObj('obj', ['reload']);
let folder = new FolderNode();
service.getHandler('delete')(folder, target);
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(folderWithPermission, target, permission);
expect(documentListService.deleteNode).toHaveBeenCalled();
expect(target.reload).toHaveBeenCalled();

View File

@ -17,10 +17,15 @@
import { Injectable } from '@angular/core';
import { ContentActionHandler } from '../models/content-action.model';
import { PermissionModel } from '../models/permissions.model';
import { DocumentListService } from './document-list.service';
import { Subject } from 'rxjs/Rx';
@Injectable()
export class FolderActionsService {
permissionEvent: Subject<PermissionModel> = new Subject<PermissionModel>();
private handlers: { [id: string]: ContentActionHandler; } = {};
constructor(private documentListService?: DocumentListService) {
@ -66,13 +71,28 @@ export class FolderActionsService {
window.alert('standard folder action 2');
}
private deleteNode(obj: any, target?: any) {
if (this.canExecuteAction(obj) && obj.entry && obj.entry.id) {
private deleteNode(obj: any, target?: any, permission?: string) {
if (this.canExecuteAction(obj)) {
if (this.hasPermission(obj.entry, permission)) {
this.documentListService.deleteNode(obj.entry.id).subscribe(() => {
if (target && typeof target.reload === 'function') {
target.reload();
}
});
} else {
this.permissionEvent.next(new PermissionModel({type: 'folder', action: 'delete', permission: permission}));
}
}
}
private hasPermission(node: any, permissionToCheck: string): boolean {
if (this.hasPermissions(node)) {
return node.allowableOperations.find(permision => permision === permissionToCheck) ? true : false;
}
return false;
}
private hasPermissions(node: any): boolean {
return node && node.allowableOperations ? true : false;
}
}