Added permission check on documentlist action menu (#1832)

* #ADF-166 - add permissin check on folder creation

* #ADF-166 - removed wrong pushed change

* #ADF-166 - improved permission check

* #ADF-166 - added test for menu action permission check

* #ADF-166 upgraded disabled attribute to match all the browsers

* #ADF-166 added peer review changes

* #ADF-166 added some little code improvements
This commit is contained in:
Vito
2017-04-24 04:15:54 -07:00
committed by Mario Romano
parent dad7a575f7
commit 93b8d3742f
8 changed files with 266 additions and 47 deletions

View File

@@ -20,7 +20,8 @@
[allowDropFiles]="true" [allowDropFiles]="true"
(error)="onNavigationError($event)" (error)="onNavigationError($event)"
(success)="resetError()" (success)="resetError()"
(preview)="showFile($event)"> (preview)="showFile($event)"
(permissionError)="onPermissionsFailed($event)">
<data-columns> <data-columns>
<data-column key="$thumbnail" type="image" [sortable]="false"></data-column> <data-column key="$thumbnail" type="image" [sortable]="false"></data-column>
<data-column <data-column

View File

@@ -27,7 +27,7 @@
</div> </div>
<nav class="mdl-navigation"> <nav class="mdl-navigation">
<i class="icon material-icons icon-margin">link</i> <i class="icon material-icons icon-margin">link</i>
<div class="mdl-textfield mdl-js-textfield adf-setting-input-paddingg"> <div class="mdl-textfield mdl-js-textfield adf-setting-input-padding">
<input class="mdl-textfield__input" <input class="mdl-textfield__input"
type="text" type="text"
(change)="onChangeBPMHost($event)" (change)="onChangeBPMHost($event)"

View File

@@ -783,6 +783,7 @@ DocumentList emits the following events:
| `nodeDblClick` | emitted when user double-clicks list node | | `nodeDblClick` | emitted when user double-clicks list node |
| `folderChange` | emitted once current display folder has changed | | `folderChange` | emitted once current display folder has changed |
| `preview` | emitted when user acts upon files with either single or double click (depends on `navigation-mode`), recommended for Viewer components integration | | `preview` | emitted when user acts upon files with either single or double click (depends on `navigation-mode`), recommended for Viewer components integration |
| `permissionError` | emitted when user is attempting to create a folder via action menu but it doesn't have the permission to do it |
## Advanced usage and customization ## Advanced usage and customization

View File

@@ -2,7 +2,8 @@
*ngIf="creationMenuActions" *ngIf="creationMenuActions"
[folderId]="currentFolderId" [folderId]="currentFolderId"
(success)="onActionMenuSuccess($event)" (success)="onActionMenuSuccess($event)"
(error)="onActionMenuError($event)"> (error)="onActionMenuError($event)"
(permissionErrorEvent)="onPermissionError($event)">
</alfresco-document-menu-action> </alfresco-document-menu-action>
<alfresco-datatable <alfresco-datatable
[data]="data" [data]="data"

View File

@@ -22,7 +22,14 @@ import {
import { Subject } from 'rxjs/Rx'; import { Subject } from 'rxjs/Rx';
import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, Pagination } from 'alfresco-js-api'; import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging, Pagination } from 'alfresco-js-api';
import { AlfrescoTranslationService, DataColumnListComponent } from 'ng2-alfresco-core'; import { AlfrescoTranslationService, DataColumnListComponent } from 'ng2-alfresco-core';
import { DataRowEvent, DataTableComponent, ObjectDataColumn, DataCellEvent, DataRowActionEvent, DataColumn } from 'ng2-alfresco-datatable'; import {
DataRowEvent,
DataTableComponent,
ObjectDataColumn,
DataCellEvent,
DataRowActionEvent,
DataColumn
} from 'ng2-alfresco-datatable';
import { DocumentListService } from './../services/document-list.service'; import { DocumentListService } from './../services/document-list.service';
import { ContentActionModel } from './../models/content-action.model'; import { ContentActionModel } from './../models/content-action.model';
import { ShareDataTableAdapter, ShareDataRow, RowFilter, ImageResolver } from './../data/share-datatable-adapter'; import { ShareDataTableAdapter, ShareDataRow, RowFilter, ImageResolver } from './../data/share-datatable-adapter';
@@ -132,6 +139,9 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
@Output() @Output()
error: EventEmitter<any> = new EventEmitter(); error: EventEmitter<any> = new EventEmitter();
@Output()
permissionError: EventEmitter<any> = new EventEmitter();
@ViewChild(DataTableComponent) @ViewChild(DataTableComponent)
dataTable: DataTableComponent; dataTable: DataTableComponent;
@@ -328,11 +338,11 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
// gets folder node and its content // gets folder node and its content
loadFolderByNodeId(nodeId: string) { loadFolderByNodeId(nodeId: string) {
this.documentListService.getFolderNode(nodeId).then(node => { this.documentListService.getFolderNode(nodeId).then(node => {
this.folderNode = node; this.folderNode = node;
this.currentFolderId = node.id; this.currentFolderId = node.id;
this.skipCount = 0; this.skipCount = 0;
this.loadFolderNodesByFolderNodeId(node.id, this.pageSize, this.skipCount).catch(err => this.error.emit(err)); this.loadFolderNodesByFolderNodeId(node.id, this.pageSize, this.skipCount).catch(err => this.error.emit(err));
}) })
.catch(err => this.error.emit(err)); .catch(err => this.error.emit(err));
} }
@@ -488,4 +498,8 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni
this.skipCount = event.skipCount; this.skipCount = event.skipCount;
this.reload(); this.reload();
} }
onPermissionError(event) {
this.permissionError.emit(event);
}
} }

View File

@@ -1,5 +1,5 @@
<div class="container"> <div class="container">
<button md-button [mdMenuTriggerFor]="menu"> <button id="folder-create-button" md-button [mdMenuTriggerFor]="menu" [disabled]="isButtonDisabled()">
<md-icon>add</md-icon> <md-icon>add</md-icon>
<span>{{ 'ALFRESCO_DOCUMENT_LIST.BUTTON.ACTION_CREATE' | translate }}</span> <span>{{ 'ALFRESCO_DOCUMENT_LIST.BUTTON.ACTION_CREATE' | translate }}</span>
</button> </button>

View File

@@ -16,9 +16,11 @@
*/ */
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleChange } from '@angular/core';
import { import {
AlfrescoAuthenticationService, AlfrescoAuthenticationService,
AlfrescoSettingsService, AlfrescoSettingsService,
AlfrescoTranslationService,
AlfrescoApiService, AlfrescoApiService,
CoreModule, CoreModule,
LogService LogService
@@ -28,6 +30,56 @@ import { DocumentMenuActionComponent } from './document-menu-action.component';
declare let jasmine: any; declare let jasmine: any;
let exampleFolderWithCreate = {
'entry': {
'aspectNames': ['cm:auditable'],
'allowableOperations': ['create'],
'createdAt': '2017-04-03T11:34:35.708+0000',
'isFolder': true,
'isFile': false,
'createdByUser': { 'id': 'admin', 'displayName': 'Administrator' },
'modifiedAt': '2017-04-03T11:34:35.708+0000',
'modifiedByUser': { 'id': 'admin', 'displayName': 'Administrator' },
'name': 'test-folder2',
'id': 'c0284dc3-841d-48b2-955c-bcb2218e2b03',
'nodeType': 'cm:folder',
'parentId': '1ee81bf8-52d6-4cfc-a924-1efbc79306bf'
}
};
let exampleFolderWithPermissions = {
'entry': {
'aspectNames': ['cm:auditable'],
'allowableOperations': ['check'],
'createdAt': '2017-04-03T11:34:35.708+0000',
'isFolder': true,
'isFile': false,
'createdByUser': { 'id': 'admin', 'displayName': 'Administrator' },
'modifiedAt': '2017-04-03T11:34:35.708+0000',
'modifiedByUser': { 'id': 'admin', 'displayName': 'Administrator' },
'name': 'test-folder2',
'id': 'c0284dc3-841d-48b2-955c-bcb2218e2b03',
'nodeType': 'cm:folder',
'parentId': '1ee81bf8-52d6-4cfc-a924-1efbc79306bf'
}
};
let exampleFolderWithNoOperations = {
'entry': {
'aspectNames': ['cm:auditable'],
'createdAt': '2017-04-03T11:34:35.708+0000',
'isFolder': true,
'isFile': false,
'createdByUser': { 'id': 'admin', 'displayName': 'Administrator' },
'modifiedAt': '2017-04-03T11:34:35.708+0000',
'modifiedByUser': { 'id': 'admin', 'displayName': 'Administrator' },
'name': 'test-folder2',
'id': 'c0284dc3-841d-48b2-955c-bcb2218e2b03',
'nodeType': 'cm:folder',
'parentId': '1ee81bf8-52d6-4cfc-a924-1efbc79306bf'
}
};
describe('Document menu action', () => { describe('Document menu action', () => {
let component: DocumentMenuActionComponent; let component: DocumentMenuActionComponent;
@@ -50,6 +102,9 @@ describe('Document menu action', () => {
}); });
TestBed.compileComponents(); TestBed.compileComponents();
let translateService = TestBed.get(AlfrescoTranslationService);
spyOn(translateService, 'get').and.returnValue({ value: 'fake translated message' });
})); }));
beforeEach(() => { beforeEach(() => {
@@ -69,7 +124,7 @@ describe('Document menu action', () => {
describe('Folder creation', () => { describe('Folder creation', () => {
it('should createFolder fire a success event if the folder has been created', (done) => { it('should createFolder fire a success event if the folder has been created', (done) => {
component.allowableOperations = ['create'];
component.showDialog(); component.showDialog();
component.createFolder('test-folder'); component.createFolder('test-folder');
@@ -81,26 +136,12 @@ describe('Document menu action', () => {
jasmine.Ajax.requests.mostRecent().respondWith({ jasmine.Ajax.requests.mostRecent().respondWith({
status: 200, status: 200,
contentType: 'application/json', contentType: 'application/json',
responseText: JSON.stringify({ responseText: JSON.stringify(exampleFolderWithCreate)
'entry': {
'aspectNames': ['cm:auditable'],
'createdAt': '2017-04-03T11:34:35.708+0000',
'isFolder': true,
'isFile': false,
'createdByUser': {'id': 'admin', 'displayName': 'Administrator'},
'modifiedAt': '2017-04-03T11:34:35.708+0000',
'modifiedByUser': {'id': 'admin', 'displayName': 'Administrator'},
'name': 'test-folder2',
'id': 'c0284dc3-841d-48b2-955c-bcb2218e2b03',
'nodeType': 'cm:folder',
'parentId': '1ee81bf8-52d6-4cfc-a924-1efbc79306bf'
}
})
}); });
}); });
it('should createFolder fire an error event if the folder has not been created', (done) => { it('should createFolder fire an error event if the folder has not been created', (done) => {
component.allowableOperations = ['create'];
component.showDialog(); component.showDialog();
component.createFolder('test-folder'); component.createFolder('test-folder');
@@ -113,5 +154,118 @@ describe('Document menu action', () => {
status: 403 status: 403
}); });
}); });
it('should createFolder fire an error when folder already exists', (done) => {
component.allowableOperations = ['create'];
component.showDialog();
component.createFolder('test-folder');
component.error.subscribe((err) => {
expect(err.message).toEqual('fake translated message');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 403,
responseText: JSON.stringify({ message: 'Fake folder exists', error: { statusCode: 409 } })
});
});
});
describe('Check Permissions', () => {
it('should get the folder permission when folderId is changed', async(() => {
let change = new SimpleChange('folder-id', 'new-folder-id');
component.ngOnChanges({ 'folderId': change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(exampleFolderWithCreate)
});
fixture.whenStable().then(() => {
fixture.detectChanges();
let createButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#folder-create-button');
expect(createButton).toBeDefined();
expect(component.allowableOperations).toBeDefined();
expect(component.allowableOperations).not.toBeNull();
expect(createButton.disabled).toBeFalsy();
});
}));
it('should disable the create button if folder does not have any allowable operations', async(() => {
let change = new SimpleChange('folder-id', 'new-folder-id');
component.ngOnChanges({ 'folderId': change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(exampleFolderWithNoOperations)
});
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let createButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#folder-create-button');
expect(createButton).toBeDefined();
expect(createButton.disabled).toBeTruthy();
});
}));
it('should disable the create button if folder does not have create permission', async(() => {
let change = new SimpleChange('folder-id', 'new-folder-id');
component.ngOnChanges({ 'folderId': change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(exampleFolderWithPermissions)
});
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let createButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#folder-create-button');
expect(createButton).toBeDefined();
expect(createButton.disabled).toBeTruthy();
});
}));
it('should not disable the option when disableWithNoPermission is false', async(() => {
component.disableWithNoPermission = false;
let change = new SimpleChange('folder-id', 'new-folder-id');
component.ngOnChanges({ 'folderId': change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(exampleFolderWithNoOperations)
});
fixture.whenStable().then(() => {
fixture.detectChanges();
let createButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#folder-create-button');
expect(createButton).toBeDefined();
expect(createButton.disabled).toBeFalsy();
});
}));
it('should emit permission event error when user does not have create permission', async(() => {
let change = new SimpleChange('folder-id', 'new-folder-id');
component.ngOnChanges({ 'folderId': change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(exampleFolderWithNoOperations)
});
component.permissionErrorEvent.subscribe((error) => {
expect(error.type).toEqual('folder');
expect(error.action).toEqual('create');
});
component.showDialog();
component.createFolder('not-allowed');
}));
}); });
}); });

View File

@@ -15,12 +15,13 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core'; import { Component, Input, Output, EventEmitter, ViewChild, OnChanges, SimpleChanges } from '@angular/core';
import { AlfrescoTranslationService, LogService } from 'ng2-alfresco-core'; import { AlfrescoTranslationService, LogService } from 'ng2-alfresco-core';
import { MinimalNodeEntity } from 'alfresco-js-api'; import { MinimalNodeEntity } from 'alfresco-js-api';
import { DocumentListService } from './../services/document-list.service'; import { DocumentListService } from './../services/document-list.service';
import { ContentActionModel } from './../models/content-action.model'; import { ContentActionModel } from './../models/content-action.model';
import { PermissionModel } from '../models/permissions.model';
declare let dialogPolyfill: any; declare let dialogPolyfill: any;
@@ -32,17 +33,23 @@ const ERROR_FOLDER_ALREADY_EXIST = 409;
styleUrls: ['./document-menu-action.component.css'], styleUrls: ['./document-menu-action.component.css'],
templateUrl: './document-menu-action.component.html' templateUrl: './document-menu-action.component.html'
}) })
export class DocumentMenuActionComponent { export class DocumentMenuActionComponent implements OnChanges {
@Input() @Input()
folderId: string; folderId: string;
@Input()
disableWithNoPermission: boolean = true;
@Output() @Output()
success = new EventEmitter(); success = new EventEmitter();
@Output() @Output()
error = new EventEmitter(); error = new EventEmitter();
@Output()
permissionErrorEvent = new EventEmitter();
@ViewChild('dialog') @ViewChild('dialog')
dialog: any; dialog: any;
@@ -52,6 +59,8 @@ export class DocumentMenuActionComponent {
folderName: string = ''; folderName: string = '';
allowableOperations: string[];
constructor(private documentListService: DocumentListService, constructor(private documentListService: DocumentListService,
private translateService: AlfrescoTranslationService, private translateService: AlfrescoTranslationService,
private logService: LogService) { private logService: LogService) {
@@ -61,27 +70,43 @@ export class DocumentMenuActionComponent {
} }
} }
ngOnChanges(changes: SimpleChanges) {
if (changes && changes['folderId']) {
if (changes['folderId'].currentValue !== changes['folderId'].previousValue) {
this.loadCurrentNodePermissions(changes['folderId'].currentValue);
}
}
}
public createFolder(name: string) { public createFolder(name: string) {
this.cancel(); this.cancel();
this.documentListService.createFolder(name, this.folderId) if (this.hasCreatePermission()) {
.subscribe( this.documentListService.createFolder(name, this.folderId)
(res: MinimalNodeEntity) => { .subscribe(
this.folderName = ''; (res: MinimalNodeEntity) => {
this.logService.info(res.entry); this.folderName = '';
this.success.emit({node: res.entry}); this.logService.info(res.entry);
}, this.success.emit({ node: res.entry });
error => { },
if (error.response) { error => {
let errorMessagePlaceholder = this.getErrorMessage(error.response); if (error.response) {
this.message = this.formatString(errorMessagePlaceholder, [name]); let errorMessagePlaceholder = this.getErrorMessage(error.response);
this.error.emit({message: this.message}); this.message = this.formatString(errorMessagePlaceholder, [name]);
this.logService.error(this.message); this.error.emit({ message: this.message });
} else { this.logService.error(this.message);
this.error.emit(error); } else {
this.logService.error(error); this.error.emit(error);
this.logService.error(error);
}
} }
} );
); } else {
this.permissionErrorEvent.emit(new PermissionModel({
type: 'folder',
action: 'create',
permission: 'create'
}));
}
} }
public showDialog() { public showDialog() {
@@ -127,4 +152,27 @@ export class DocumentMenuActionComponent {
isFolderNameEmpty() { isFolderNameEmpty() {
return this.folderName === '' ? true : false; return this.folderName === '' ? true : false;
} }
isButtonDisabled(): boolean {
return !this.hasCreatePermission() && this.disableWithNoPermission ? true : undefined;
}
hasPermission(permission: string): boolean {
let hasPermission: boolean = false;
if (this.allowableOperations) {
let permFound = this.allowableOperations.find(element => element === permission);
hasPermission = permFound ? true : false;
}
return hasPermission;
}
hasCreatePermission() {
return this.hasPermission('create');
}
loadCurrentNodePermissions(nodeId: string) {
this.documentListService.getFolderNode(nodeId).then(node => {
this.allowableOperations = node ? node['allowableOperations'] : null;
}).catch(err => this.error.emit(err));
}
} }