Skip to content

Commit

Permalink
Add read permissions (#2849) (#2879)
Browse files Browse the repository at this point in the history
Adding the ability to see read permissions alongside of write permissions
Added flag enablePermissionManagement to control UI element to assign permissions.
Removed pureComputed from non-observable values.

Co-authored-by: Richard D Boyce, PhD <boycer@u.washington.edu>
  • Loading branch information
chrisknoll and rkboyce committed Aug 14, 2023
1 parent 8f54f70 commit 92061d0
Show file tree
Hide file tree
Showing 23 changed files with 192 additions and 80 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*~
.idea
/web.config
/node_modules/
Expand Down
64 changes: 49 additions & 15 deletions js/components/security/access/configure-access-modal.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,80 @@
data: {
classes: classes,
isLoading: isLoading,
roleName: roleName,
columns: columns,
accessList: accessList,
readRoleName: readRoleName,
writeRoleName: writeRoleName,
writeAccessColumns: writeAccessColumns,
readAccessColumns: readAccessColumns,
writeAccessList: writeAccessList,
readAccessList: readAccessList,
grantAccess: grantAccess,
revokeRoleAccess: revokeRoleAccess,
roleOptions: roleOptions,
roleSearch: roleSearch
readRoleOptions: readRoleOptions,
readRoleSearch: readRoleSearch,
writeRoleOptions: writeRoleOptions,
writeRoleSearch: writeRoleSearch
}">
<loading data-bind="css: classes('loading-panel'), visible: isLoading()" params="status: ko.i18n('common.configureAccessModal.loadingAccessList', 'Loading access list...')"></loading>
<div data-bind="css: classes()">
<div data-bind="if: !isLoading()">
<div data-bind="css: classes('new-access')">
<label data-bind="css: classes('new-access-label'), text: ko.i18n('common.configureAccessModal.addWriteAccessToRole', 'Add write access to role:')"></label>
<label data-bind="css: classes('new-access-label'), text: ko.i18n('common.configureAccessModal.addWriteAccessToRole', 'Add WRITE access to role:')"></label>
<div class="input-group"
data-bind="css: classes({ element: 'new-access-btn-group', extra: ['new-access-btn-group'] })">
<input
class="form-control"
data-bind="
textInput: roleSearch,
value: roleName,
textInput: writeRoleSearch,
value: writeRoleName,
eventType: 'blur',
ko_autocomplete: { source: roleOptions(), minLength: 0, maxShowItems: 10, scroll: true }"
ko_autocomplete: { source: writeRoleOptions(), minLength: 0, maxShowItems: 10, scroll: true }"
>
<span class="input-group-btn">
<button class="btn btn-primary" type="button" data-bind="click: grantAccess, attr: { disabled: !(roleName() && roleName().length) }, text: ko.i18n('common.add', 'Add')"></button>
<button class="btn btn-primary" type="button" data-bind="click: grantAccess.bind($data,'WRITE'), attr: { disabled: !(writeRoleName() && writeRoleName().length) }, text: ko.i18n('common.add', 'Add')"></button>
</span>
</div>
<div data-bind="css: classes('access-list')">
<label data-bind="css: classes('access-list-label'), text: ko.i18n('common.configureAccessModal.rolesWithWriteAccess', 'Roles with write access:')"></label>
<label data-bind="css: classes('access-list-label'), text: ko.i18n('common.configureAccessModal.rolesWithWriteAccess', 'Roles with WRITE access:')"></label>
<div>
<table data-bind="
css: classes({ element: 'access-table', extra: ['table', 'table-bordered', 'table-hover'] }),
dataTable:{
data: accessList,
options: {columns: columns, language: ko.i18n('datatable.language')},
data: writeAccessList,
options: {columns: writeAccessColumns, language: ko.i18n('datatable.language')},
}
"/>
</div>
</div>
</div>
</div>
<div data-bind="css: classes('new-access')">
<label data-bind="css: classes('new-access-label'), text: ko.i18n('common.configureAccessModal.addReadAccessToRole', 'Add READ access to role:')"></label>
<div class="input-group"
data-bind="css: classes({ element: 'new-access-btn-group', extra: ['new-access-btn-group'] })">
<input
class="form-control"
data-bind="
textInput: readRoleSearch,
value: readRoleName,
eventType: 'blur',
ko_autocomplete: { source: readRoleOptions(), minLength: 0, maxShowItems: 10, scroll: true }"
>
<span class="input-group-btn">
<button class="btn btn-primary" type="button" data-bind="click: grantAccess.bind($data,'READ'), attr: { disabled: !(readRoleName() && readRoleName().length) }, text: ko.i18n('common.add', 'Add')"></button>
</span>
</div>
<div data-bind="css: classes('access-list')">
<label data-bind="css: classes('access-list-label'), text: ko.i18n('common.configureAccessModal.rolesWithReadAccess', 'Roles with READ access:')"></label>
<div>
<table data-bind="
css: classes({ element: 'access-table', extra: ['table', 'table-bordered', 'table-hover'] }),
dataTable:{
data: readAccessList,
options: {columns: readAccessColumns, language: ko.i18n('datatable.language')},
}
"/>
</div>
</div>
</div>
</div>
</div>
</atlas-modal>
</atlas-modal>
100 changes: 72 additions & 28 deletions js/components/security/access/configure-access-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,80 +19,124 @@ define([

this.isModalShown = params.isModalShown;
this.isLoading = ko.observable(false);
this.accessList = ko.observable([]);
this.roleName = ko.observable();

this.roleSuggestions = ko.observable([]);
this.roleOptions = ko.computed(() => this.roleSuggestions().map(r => r.name));
this.roleSearch = ko.observable();
this.roleSearch.subscribe(str => this.loadRoleSuggestions(str));
this.writeRoleName = ko.observable();
this.writeAccessList = ko.observable([]);
this.writeRoleSuggestions = ko.observable([]);
this.writeRoleOptions = ko.computed(() => this.writeRoleSuggestions().map(r => r.name));
this.writeRoleSearch = ko.observable();
this.writeRoleSearch.subscribe(str => this.loadWriteRoleSuggestions(str));

this.readAccessList = ko.observable([]);
this.readRoleName = ko.observable();
this.readRoleSuggestions = ko.observable([]);
this.readRoleOptions = ko.computed(() => this.readRoleSuggestions().map(r => r.name));
this.readRoleSearch = ko.observable();
this.readRoleSearch.subscribe(str => this.loadReadRoleSuggestions(str));

this.isOwnerFn = params.isOwnerFn;
this.grantAccessFn = params.grantAccessFn;
this.loadAccessListFn = params.loadAccessListFn;
this.revokeAccessFn = params.revokeAccessFn;
this.loadRoleSuggestionsFn = params.loadRoleSuggestionsFn;

this.columns = [
this.readAccessColumns = [
{
class: this.classes('access-tbl-col-id'),
title: ko.i18n('readAccessColumns.id', 'ID'),
data: 'id'
},
{
class: this.classes('access-tbl-col-name'),
title: ko.i18n('readAccessColumns.name', 'Name'),
data: 'name'
},
{
class: this.classes('access-tbl-col-action'),
title: ko.i18n('readAccessColumns.action', 'Action'),
render: (s, p, d) => !this.isOwnerFn(d.name) ? `<a data-bind="css: '${this.classes('revoke-link')}', click: revoke, text: ko.i18n('common.configureAccessModal.revoke', 'Revoke')"></a>` : '-'
}
];

this.writeAccessColumns = [
{
class: this.classes('access-tbl-col-id'),
title: ko.i18n('columns.id', 'ID'),
title: ko.i18n('writeAccessColumns.id', 'ID'),
data: 'id'
},
{
class: this.classes('access-tbl-col-name'),
title: ko.i18n('columns.name', 'Name'),
title: ko.i18n('writeAccessColumns.name', 'Name'),
data: 'name'
},
{
class: this.classes('access-tbl-col-action'),
title: ko.i18n('columns.action', 'Action'),
title: ko.i18n('writeAccessColumns.action', 'Action'),
render: (s, p, d) => !this.isOwnerFn(d.name) ? `<a data-bind="css: '${this.classes('revoke-link')}', click: revoke, text: ko.i18n('common.configureAccessModal.revoke', 'Revoke')"></a>` : '-'
}
];

this.isModalShown.subscribe(open => !!open && this.loadAccessList());
}

async _loadAccessList() {
let accessList = await this.loadAccessListFn();
accessList = accessList.map(a => ({ ...a, revoke: () => this.revokeRoleAccess(a.id) }));
this.accessList(accessList);
async _loadReadAccessList() {
let accessList = await this.loadAccessListFn('READ');
accessList = accessList.map(a => ({ ...a, revoke: () => this.revokeRoleAccess(a.id, 'READ') }));
this.readAccessList(accessList);
}

async _loadWriteAccessList() {
let accessList = await this.loadAccessListFn('WRITE');
accessList = accessList.map(a => ({ ...a, revoke: () => this.revokeRoleAccess(a.id, 'WRITE') }));
this.writeAccessList(accessList);
}

async loadRoleSuggestions() {
const res = await this.loadRoleSuggestionsFn(this.roleSearch());
this.roleSuggestions(res);
async loadReadRoleSuggestions() {
const res = await this.loadRoleSuggestionsFn(this.readRoleSearch());
this.readRoleSuggestions(res);
}

async loadWriteRoleSuggestions() {
const res = await this.loadRoleSuggestionsFn(this.writeRoleSearch());
this.writeRoleSuggestions(res);
}

async loadAccessList() {
this.isLoading(false);
this.isLoading(true);
try {
await this._loadAccessList();
await this._loadReadAccessList();
await this._loadWriteAccessList();
} catch (ex) {
console.log(ex);
}
this.isLoading(false);
}

async grantAccess() {
async grantAccess(perm_type) {
this.isLoading(true);
try {
const role = this.roleSuggestions().find(r => r.name === this.roleName());
await this.grantAccessFn(role.id);
await this._loadAccessList();
this.roleName('');
if (perm_type == 'WRITE'){
const role = this.writeRoleSuggestions().find(r => r.name === this.writeRoleName());
await this.grantAccessFn(role.id,'WRITE');
await this._loadWriteAccessList();
this.writeRoleName('');
} else {
const role = this.readRoleSuggestions().find(r => r.name === this.readRoleName());
await this.grantAccessFn(role.id,'READ');
await this._loadReadAccessList();
this.readRoleName('');
}
} catch (ex) {
console.log(ex);
}
this.isLoading(false);
}

async revokeRoleAccess(roleId) {
async revokeRoleAccess(roleId, perm_type) {
this.isLoading(true);
try {
await this.revokeAccessFn(roleId);
await this._loadAccessList();
try {
await this.revokeAccessFn(roleId, perm_type);
await this.loadAccessList();
} catch (ex) {
console.log(ex);
}
Expand Down
1 change: 1 addition & 0 deletions js/config/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ define(function () {
url: 'http://localhost:8080/WebAPI/'
};
appConfig.disableBrowserCheck = false; // browser check will happen by default
appConfig.enablePermissionManagement = true; // allow UI to assign read/write permissions to entities
appConfig.cacheSources = false;
appConfig.pollInterval = 60000;
appConfig.cohortComparisonResultsEnabled = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@
<!-- ko ifnot: isNewEntity() -->
<button type="button" class="btn btn-primary" data-bind="click: () => isTagsModalShown(!isTagsModalShown()), visible: isEditPermitted() && !previewVersion(), css: { disabled: isProcessing() }, title: ko.i18n('common.tags', 'Tags')"><i class="fa fa-tags"></i></button>
<button type="button" class="btn btn-primary" data-bind="visible: !previewVersion(), click: copyCc, css: {disabled: !canCopy() || isProcessing() }, title: , title: ko.i18n('common.createACopy', 'Create a copy')"><i class="fa fa-copy"></i></button>

<!-- ko if: enablePermissionManagement -->
<button class="btn btn-primary" data-bind="visible: isOwner() && !previewVersion(), click: () => isAccessModalShown(!isAccessModalShown()), title: ko.i18n('common.configureAccess', 'Configure access')">
<i class="fa fa-lock"></i>
</button>
<button type="button" class="btn btn-danger" data-bind="visible: !previewVersion(), click: deleteCc, css: {disabled: !$component.isDeletePermitted() || isProcessing() }"><i class="fa fa-trash-alt"></i></button>
<!-- /ko -->

<button type="button" class="btn btn-danger" data-bind="visible: !previewVersion(), click: deleteCc, css: {disabled: !$component.isDeletePermitted() || isProcessing() }"><i class="fa fa-trash-alt"></i></button>
<!-- /ko -->
</div>
</div>
Expand Down Expand Up @@ -103,4 +107,4 @@
loadAvailableTagsFn: $component.loadAvailableTags,
checkAssignPermissionFn: $component.checkAssignPermission,
checkUnassignPermissionFn: $component.checkUnassignPermission
"></tags-modal>
"></tags-modal>
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ define([
this.selectedSourceId = ko.observable(params.router.routerParams().sourceId);
this.areStratasNamesEmpty = ko.observable();
this.duplicatedStrataNames = ko.observable([]);

this.enablePermissionManagement = config.enablePermissionManagement;
this.designDirtyFlag = sharedState.CohortCharacterization.dirtyFlag;
this.loading = ko.observable(false);
this.defaultName = ko.unwrap(constants.newEntityNames.characterization);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@
<button class="btn btn-primary" data-bind="click: closeAnalysis, enable: !isProcessing(), title: ko.i18n('common.close', 'Close')"><i class="fa fa-times"></i></button>
<!-- ko ifnot: isNewEntity -->
<button type="button" class="btn btn-primary" data-bind="click: copyFeatureAnalysis, css: {disabled: !canCopy() || isProcessing() }, title: ko.i18n('common.createACopy', 'Create a copy')"><i class="fa fa-copy"></i></button>

<!-- ko if: enablePermissionManagement -->
<button class="btn btn-primary" data-bind="visible: isOwner, enable: !isProcessing(), click: () => isAccessModalShown(!isAccessModalShown()), title: ko.i18n('common.configureAccess', 'Configure access')">
<i class="fa fa-lock"></i>
</button>
<!-- /ko -->

<button class="btn btn-danger" data-bind="click: deleteFeature, enable: canDelete() && !isProcessing(), title: ko.i18n('common.delete', 'Delete')"><i class="fa fa-trash-alt"></i></button>
<!-- /ko -->
</div>
Expand Down Expand Up @@ -53,4 +57,4 @@
grantAccessFn: $component.grantAccess,
revokeAccessFn: $component.revokeAccess,
loadRoleSuggestionsFn: $component.loadAccessRoleSuggestions
"></configure-access-modal>
"></configure-access-modal>
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ define([
return !this.isNewEntity() && this.initialFeatureType() === featureTypes.PRESET;
});
this.editorClasses = ko.computed(() => this.classes({ element: 'content', modifiers: this.canEdit() ? '' : 'disabled' }))

this.enablePermissionManagement = config.enablePermissionManagement;
this.selectedTabKey = ko.observable();
this.componentParams = ko.observable({
...params,
Expand Down
9 changes: 6 additions & 3 deletions js/pages/cohort-definitions/cohort-definition-manager.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@
data-bind="visible: !previewVersion(), title: ko.i18n('cohortDefinitions.cohortDefinitionManager.createCopyCohortTitle', 'Create a copy of this cohort definition'), click: copy, enable: canCopy() && !isProcessing()"><i
class="fa fa-copy"></i></button>
<button class="btn btn-primary"
data-bind="visible: !previewVersion(), title: ko.i18n('cohortDefinitions.cohortDefinitionManager.getLinkCohortTitle', 'Get a link to this cohort definition'), enable: !dirtyFlag().isDirty() && !isProcessing(), click: function () { $component.cohortLinkModalOpened(true) }"><i
class="fa fa-link"></i></button>
data-bind="visible: !previewVersion(), title: ko.i18n('cohortDefinitions.cohortDefinitionManager.getLinkCohortTitle', 'Get a link to this cohort definition'), enable: !dirtyFlag().isDirty() && !isProcessing(), click: function () { $component.cohortLinkModalOpened(true) }"><i class="fa fa-link"></i></button>

<!-- ko if: enablePermissionManagement -->
<button class="btn btn-primary"
data-bind="title: ko.i18n('common.configureAccess', 'Configure access'), visible: isOwner() && !previewVersion(), click: () => isAccessModalShown(!isAccessModalShown())">
<i class="fa fa-lock"></i>
</button>
<!-- /ko -->

<!-- ko if: !isRunning() -->
<button class="btn btn-danger"
data-bind="visible: !previewVersion(), title: ko.i18n('common.delete', 'Delete'), click: $component.delete, enable: canDelete() && !isProcessing()"><i
Expand Down Expand Up @@ -790,4 +793,4 @@ <h3 data-bind="text: ko.i18n('cohortDefinitions.cohortDefinitionManager.panels.a
</div>
</script>

<!-- /ko -->
<!-- /ko -->
3 changes: 2 additions & 1 deletion js/pages/cohort-definitions/cohort-definition-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,11 @@ define(['jquery', 'knockout', 'text!./cohort-definition-manager.html',
super(params);

this.previewVersion = sharedState.CohortDefinition.previewVersion;

this.pollTimeoutId = null;
this.authApi = authApi;
this.config = config;
this.enablePermissionManagement = config.enablePermissionManagement;
this.relatedSourcecodesOptions = globalConstants.relatedSourcecodesOptions;
this.commonUtils = commonUtils;
this.isLoading = ko.observable(false);
Expand Down
4 changes: 4 additions & 0 deletions js/pages/concept-sets/conceptset-manager.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@
<!-- ko if: $component.currentConceptSet().id != null && $component.currentConceptSet().id != 0 -->
<button type="button" class="btn btn-primary" data-bind="click: () => isTagsModalShown(!isTagsModalShown()), visible: canEdit() && !previewVersion(), css: { disabled: isProcessing() }, title: ko.i18n('common.tags', 'Tags')"><i class="fa fa-tags"></i></button>
<button type="button" class="btn btn-primary" data-bind="visible: !previewVersion(), click: optimize, css: { disabled: !canOptimize() || isProcessing() }, text: ko.i18n('cs.manager.optimize', 'Optimize')"></button>

<!-- ko if: enablePermissionManagement -->
<button class="btn btn-primary" data-bind="visible: !previewVersion() && isOwner(), click: () => isAccessModalShown(!isAccessModalShown()), title: ko.i18n('common.configureAccess', 'Configure access')">
<i class="fa fa-lock"></i>
</button>
<!-- /ko -->

<button type="button" class="btn btn-danger" data-bind="visible: !previewVersion(), click: $component.delete, css: { disabled: !canDelete() || isProcessing() }, title: ko.i18n('common.delete', 'Delete')"><i class="fa fa-trash-alt"></i></button>
<!-- /ko -->
</div>
Expand Down

0 comments on commit 92061d0

Please sign in to comment.