Seeming working (though not very thoroughly tested) granular access control for reports, report templates and namespaces.
Should work both in local auth case and LDAP auth case.
This commit is contained in:
parent
89256d62bd
commit
34823cf0cf
17 changed files with 352 additions and 146 deletions
104
access-control.md
Normal file
104
access-control.md
Normal file
|
@ -0,0 +1,104 @@
|
|||
## Access Control
|
||||
|
||||
This document describes the key features and concepts of the current state of
|
||||
access control in Mailtrain.
|
||||
|
||||
The current state provides user management and granular access control to reports
|
||||
and report templates. The user management supports both local authentication and
|
||||
LDAP-based authentication.
|
||||
|
||||
The access control has two abstractions levels: a high-level intended to be used through web UI,
|
||||
and low-level, intended to be configured once through the Mailtrain config file. The high-level
|
||||
layer serves for providing access to variuous resources, while the low-level layer is meant
|
||||
to define the access roles in Mailtrain to reflect an organisational or process hierarchy.
|
||||
|
||||
### High-level access management (through web UI)
|
||||
|
||||
On the high abstraction level, which is accessible to users via the web-based UI, Mailtrain
|
||||
recognizes different entities (reports, report templates, etc.) and user roles that regulate
|
||||
access to these entities (e.g. role "reporter" that allows viewing a report but prevents editing
|
||||
or deleting it). Access to entities is provided through so called "shares". A share is essentially
|
||||
a triple: entity - role - user.
|
||||
|
||||
Mailtrain further features hierarchical namespaces. Every entity has to reside in a namespace
|
||||
(in reality, the namespace itself is an entity to which access can be given).
|
||||
|
||||
While sharing an entity with a user gives the user access to the particular entity (in the
|
||||
scope of the role), sharing a namespace amounts to giving access to all entities within
|
||||
the namespace and transitively in all child namespaces. The role that regulates the access to the
|
||||
particular namespaces further determines the access to all different entity types that can
|
||||
reside in the namespace.
|
||||
|
||||
To simplify the management of permissions, every user is associated with one global role and
|
||||
a namespace. The global role regulates access to global resources and operations (i.e. those
|
||||
things that are not associated with any namespace). An example of such a global operation is
|
||||
rebuilding the permission cache. Further, the global role determines a default share of the
|
||||
root namespace and the namespace of the user. For example, an administrator's global role may
|
||||
specify that a user get administrator's role in the root namespace, which effectively gives
|
||||
him/her access to everything.
|
||||
|
||||
Mailtrain resets these default shares at start and also whenever permission cache is rebuilt
|
||||
(essentially every time user, namespace or some entity is created or when share or user's
|
||||
role is assigned). This effectively prevents deleting or overriding the default shares that
|
||||
the user has through the global role.
|
||||
|
||||
|
||||
### Low-level access management (through config file)
|
||||
|
||||
Internally, Mailtrain relies on fine-grained permissions, which are triplets:
|
||||
user - operation - entity (e.g. user id 1 - view - report id 2). These permissions are stored
|
||||
in a permission cache (in DB) and automatically generated at startup and whenever the permissions
|
||||
could have changed.
|
||||
|
||||
Mailtrain's config file defines the roles (available in the high-level access management) and
|
||||
specifies the mapping of roles to operations.
|
||||
|
||||
The roles are potentially different for each entity type/scope (currently global, namespace, report,
|
||||
report template). Each role defines the permitted operations for the given entity type/scope.
|
||||
A namespace role further defines allowed operations for entity types within and under the
|
||||
namespace.
|
||||
|
||||
The following defines the role master for scope "global". This effectively means that in
|
||||
"Create/Edit User" form, the user can be given role "Master".
|
||||
The role gives the permission to rebuild the permission cache. Further, it specifies that the
|
||||
holder of the role will automatically be given access (share) to the root namespace in the
|
||||
namespace role "master". This role is also an admin role, which means that user id 1 will always be reset to this role.
|
||||
This serves as a kind of bootstrap that makes sure that there is always a user that can be
|
||||
used to give access to other users.
|
||||
```
|
||||
[roles.global.master]
|
||||
name="Master"
|
||||
admin=true
|
||||
description="All permissions"
|
||||
permissions=["rebuildPermissions"]
|
||||
rootNamespaceRole="master"
|
||||
```
|
||||
|
||||
This defines the role "master" for "report" entities. It lists the operations that a user
|
||||
that has "master" access to a particular report can do with the report. Note that to get the
|
||||
"master" access to a particular report through this role, the report would either have to be shared with the user
|
||||
with role "master".
|
||||
|
||||
```
|
||||
[roles.report.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||
```
|
||||
|
||||
The following defines the role "master" for "namespace" entities. Similarly to the example above,
|
||||
it lists operations that relate to a namespace. In particular all "create" operations pertain
|
||||
to a namespace rathen than to an entity, which at the time of creation does not exist yet.
|
||||
Additionally, the namespace roles define permissions to all entity types under the namespace
|
||||
(including child namespaces).
|
||||
```
|
||||
[roles.namespace.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
|
||||
|
||||
[roles.namespace.master.children]
|
||||
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
||||
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||
namespace=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
|
||||
```
|
1
app.js
1
app.js
|
@ -329,7 +329,6 @@ if (app.get('env') === 'development') {
|
|||
return next();
|
||||
}
|
||||
|
||||
console.log(err);
|
||||
if (req.needsJSONResponse) {
|
||||
const resp = {
|
||||
message: err.message,
|
||||
|
|
|
@ -585,13 +585,14 @@ function withForm(target) {
|
|||
scheduleValidateForm(self);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Ignoring unhandled error in "validateFormState": ' + error);
|
||||
console.log('Error in "validateFormState": ' + error);
|
||||
|
||||
self.setState(previousState => ({
|
||||
formState: previousState.formState.set('isServerValidationRunning', false)
|
||||
}));
|
||||
|
||||
scheduleValidateForm(self);
|
||||
// TODO: It might be good not to give up immediatelly, but retry a couple of times
|
||||
// scheduleValidateForm(self);
|
||||
});
|
||||
} else {
|
||||
if (formValidateResolve) {
|
||||
|
|
|
@ -24,6 +24,9 @@
|
|||
border-top: 0px none;
|
||||
}
|
||||
|
||||
.mt-treetable-container .mt-treetable-title {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.form-group .mt-treetable-container {
|
||||
border: 1px solid #cccccc;
|
||||
|
|
|
@ -49,13 +49,15 @@ class TreeTable extends Component {
|
|||
const response = await axios.get(dataUrl);
|
||||
const treeData = response.data;
|
||||
|
||||
treeData.expanded = true;
|
||||
for (const child of treeData.children) {
|
||||
for (const root of treeData) {
|
||||
root.expanded = true;
|
||||
for (const child of root.children) {
|
||||
child.expanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
treeData: [ response.data ]
|
||||
treeData
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -66,7 +68,8 @@ class TreeTable extends Component {
|
|||
selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
|
||||
onSelectionChangedAsync: PropTypes.func,
|
||||
actions: PropTypes.func,
|
||||
withHeader: PropTypes.bool
|
||||
withHeader: PropTypes.bool,
|
||||
withDescription: PropTypes.bool
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
@ -100,26 +103,35 @@ class TreeTable extends Component {
|
|||
};
|
||||
|
||||
let createNodeFn;
|
||||
if (this.props.actions) {
|
||||
createNodeFn = (event, data) => {
|
||||
const node = data.node;
|
||||
const tdList = jQuery(node.tr).find(">td");
|
||||
|
||||
let tdIdx = 1;
|
||||
|
||||
if (this.props.withDescription) {
|
||||
const descHtml = ReactDOMServer.renderToStaticMarkup(<div>{node.data.description}</div>);
|
||||
tdList.eq(tdIdx).html(descHtml);
|
||||
tdIdx += 1;
|
||||
}
|
||||
|
||||
if (this.props.actions) {
|
||||
const linksContainer = jQuery('<span class="mt-action-links"/>');
|
||||
|
||||
const actions = this.props.actions(node.key);
|
||||
const actions = this.props.actions(node);
|
||||
for (const {label, link} of actions) {
|
||||
const lnkHtml = ReactDOMServer.renderToStaticMarkup(<a href={link}>{label}</a>);
|
||||
const lnk = jQuery(lnkHtml);
|
||||
lnk.click((evt) => { evt.preventDefault(); this.navigateTo(link) });
|
||||
lnk.click((evt) => {
|
||||
evt.preventDefault();
|
||||
this.navigateTo(link)
|
||||
});
|
||||
linksContainer.append(lnk);
|
||||
}
|
||||
|
||||
tdList.eq(1).html(linksContainer);
|
||||
};
|
||||
} else {
|
||||
createNodeFn = (event, data) => {};
|
||||
tdList.eq(tdIdx).html(linksContainer);
|
||||
tdIdx += 1;
|
||||
}
|
||||
};
|
||||
|
||||
this.tree = jQuery(this.domTable).fancytree({
|
||||
extensions: ['glyph', 'table'],
|
||||
|
@ -220,6 +232,7 @@ class TreeTable extends Component {
|
|||
const props = this.props;
|
||||
const actions = props.actions;
|
||||
const withHeader = props.withHeader;
|
||||
const withDescription = props.withDescription;
|
||||
|
||||
let containerClass = 'mt-treetable-container';
|
||||
if (this.selectMode === TreeSelectMode.NONE) {
|
||||
|
@ -238,7 +251,8 @@ class TreeTable extends Component {
|
|||
{props.withHeader &&
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('Name')}</th>
|
||||
<th className="mt-treetable-title">{t('Name')}</th>
|
||||
{withDescription && <th>{t('Description')}</th>}
|
||||
{actions && <th></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -246,6 +260,7 @@ class TreeTable extends Component {
|
|||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
{withDescription && <td></td>}
|
||||
{actions && <td></td>}
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -66,8 +66,10 @@ export default class CUD extends Component {
|
|||
axios.get('/rest/namespaces-tree')
|
||||
.then(response => {
|
||||
|
||||
response.data.expanded = true;
|
||||
const data = [response.data];
|
||||
const data = response.data;
|
||||
for (const root of data) {
|
||||
root.expanded = true;
|
||||
}
|
||||
|
||||
if (this.props.edit && !this.isEditGlobal()) {
|
||||
this.removeNsIdSubtree(data);
|
||||
|
|
|
@ -16,16 +16,25 @@ export default class List extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const actions = key => [
|
||||
{
|
||||
const actions = node => {
|
||||
const actions = [];
|
||||
|
||||
if (node.data.permissions.includes('edit')) {
|
||||
actions.push({
|
||||
label: 'Edit',
|
||||
link: '/namespaces/edit/' + key
|
||||
},
|
||||
{
|
||||
label: 'Share',
|
||||
link: '/namespaces/share/' + key
|
||||
link: '/namespaces/edit/' + node.key
|
||||
});
|
||||
}
|
||||
];
|
||||
|
||||
if (node.data.permissions.includes('share')) {
|
||||
actions.push({
|
||||
label: 'Share',
|
||||
link: '/namespaces/share/' + node.key
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -35,7 +44,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Namespaces')}</Title>
|
||||
|
||||
<TreeTable withHeader dataUrl="/rest/namespaces-tree" actions={actions} />
|
||||
<TreeTable withHeader withDescription dataUrl="/rest/namespaces-tree" actions={actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -31,6 +31,10 @@ export default class List extends Component {
|
|||
entityTypeId: 'reportTemplate',
|
||||
requiredOperations: ['execute']
|
||||
},
|
||||
createReportTemplate: {
|
||||
entityTypeId: 'namespace',
|
||||
requiredOperations: ['createReportTemplate']
|
||||
},
|
||||
viewReportTemplate: {
|
||||
entityTypeId: 'reportTemplate',
|
||||
requiredOperations: ['view']
|
||||
|
@ -41,7 +45,7 @@ export default class List extends Component {
|
|||
|
||||
this.setState({
|
||||
createPermitted: result.data.createReport && result.data.executeReportTemplate,
|
||||
templatesPermitted: result.data.viewReportTemplate
|
||||
templatesPermitted: result.data.createReportTemplate || result.data.viewReportTemplate
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -118,11 +118,12 @@ export default class Share extends Component {
|
|||
}
|
||||
];
|
||||
|
||||
const sharesColumns = [
|
||||
{ data: 0, title: t('Username') },
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Role') }
|
||||
];
|
||||
const sharesColumns = [];
|
||||
sharesColumns.push({ data: 0, title: t('Username') });
|
||||
if (mailtrainConfig.isAuthMethodLocal) {
|
||||
sharesColumns.push({ data: 1, title: t('Name') });
|
||||
}
|
||||
sharesColumns.push({ data: 2, title: t('Role') });
|
||||
|
||||
|
||||
let usersLabelIndex = 1;
|
||||
|
|
|
@ -33,7 +33,7 @@ export default class CUD extends Component {
|
|||
this.initForm({
|
||||
serverValidation: {
|
||||
url: '/rest/users-validate',
|
||||
changed: ['username', 'email'],
|
||||
changed: mailtrainConfig.isAuthMethodLocal ? ['username', 'email'] : ['username'],
|
||||
extra: ['id']
|
||||
}
|
||||
});
|
||||
|
@ -86,6 +86,7 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
|
||||
if (mailtrainConfig.isAuthMethodLocal) {
|
||||
const email = state.getIn(['email', 'value']);
|
||||
const emailServerValidation = state.getIn(['email', 'serverValidation']);
|
||||
|
||||
|
@ -128,6 +129,7 @@ export default class CUD extends Component {
|
|||
|
||||
state.setIn(['password', 'error'], passwordMsgs.length > 0 ? passwordMsgs : null);
|
||||
state.setIn(['password2', 'error'], password !== password2 ? t('Passwords must match') : null);
|
||||
}
|
||||
|
||||
validateNamespace(t, state);
|
||||
}
|
||||
|
|
|
@ -5,11 +5,100 @@ const hasher = require('node-object-hash')();
|
|||
const { enforce, filterObject } = require('../lib/helpers');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
const shares = require('./shares');
|
||||
const permissions = require('../lib/permissions');
|
||||
|
||||
const allowedKeys = new Set(['name', 'description', 'namespace']);
|
||||
|
||||
async function list() {
|
||||
return await knex('namespaces');
|
||||
async function listTree(context) {
|
||||
// FIXME - process permissions
|
||||
|
||||
const entityType = permissions.getEntityType('namespace');
|
||||
|
||||
// This builds a forest of namespaces that contains only those namespace that the user has access to
|
||||
// This goes in three steps: 1) tree with all namespaces is built with parent-children links, 2) the namespaces that are not accessible
|
||||
// by the user are pruned out, which potentially transforms the tree to a forest, 3) unneeded attributes (i.e. parent links)
|
||||
// are removed and children are turned to an array are sorted alphabetically by name
|
||||
|
||||
// Build a tree
|
||||
const rows = await knex('namespaces')
|
||||
.innerJoin(entityType.permissionsTable, {
|
||||
[entityType.permissionsTable + '.entity']: 'namespaces.id',
|
||||
[entityType.permissionsTable + '.user']: context.user.id
|
||||
})
|
||||
.groupBy('namespaces.id')
|
||||
.select([
|
||||
'namespaces.id', 'namespaces.name', 'namespaces.description', 'namespaces.namespace',
|
||||
knex.raw(`GROUP_CONCAT(${entityType.permissionsTable + '.operation'} SEPARATOR \';\') as permissions`)
|
||||
]);
|
||||
|
||||
const entries = {};
|
||||
|
||||
for (let row of rows) {
|
||||
let entry;
|
||||
if (!entries[row.id]) {
|
||||
entry = {
|
||||
children: {}
|
||||
};
|
||||
entries[row.id] = entry;
|
||||
} else {
|
||||
entry = entries[row.id];
|
||||
}
|
||||
|
||||
if (row.namespace) {
|
||||
if (!entries[row.namespace]) {
|
||||
entries[row.namespace] = {
|
||||
children: {}
|
||||
};
|
||||
}
|
||||
|
||||
entries[row.namespace].children[row.id] = entry;
|
||||
entry.parent = entries[row.namespace];
|
||||
} else {
|
||||
entry.parent = null;
|
||||
}
|
||||
|
||||
entry.key = row.id;
|
||||
entry.title = row.name;
|
||||
entry.description = row.description;
|
||||
entry.permissions = row.permissions.split(';');
|
||||
}
|
||||
|
||||
// Prune out the inaccessible namespaces
|
||||
for (const entryId in entries) {
|
||||
const entry = entries[entryId];
|
||||
|
||||
if (!entry.permissions.includes('view')) {
|
||||
for (const childId in entry.children) {
|
||||
const child = entry.children[childId];
|
||||
child.parent = entry.parent;
|
||||
|
||||
if (entry.parent) {
|
||||
entry.parent.children[childId] = child;
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.parent) {
|
||||
delete entry.parent.children[entryId];
|
||||
}
|
||||
|
||||
delete entries[entryId];
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the roots before we discard the parent link
|
||||
const roots = Object.values(entries).filter(x => x.parent === null);
|
||||
|
||||
// Remove parent link, transform children to an array and sort it
|
||||
for (const entryId in entries) {
|
||||
const entry = entries[entryId];
|
||||
|
||||
entry.children = Object.values(entry.children);
|
||||
entry.children.sort((x, y) => x.title.localeCompare(y.title));
|
||||
|
||||
delete entry.parent;
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
function hash(entity) {
|
||||
|
@ -31,18 +120,20 @@ async function create(context, entity) {
|
|||
enforce(entity.namespace, 'Parent namespace must be set');
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createNamespace');
|
||||
|
||||
let id;
|
||||
await knex.transaction(async tx => {
|
||||
if (!await tx('namespaces').select(['id']).where('id', entity.namespace).first()) {
|
||||
throw new interoperableErrors.DependencyNotFoundError();
|
||||
}
|
||||
|
||||
const id = await tx('namespaces').insert(filterObject(entity, allowedKeys));
|
||||
const ids = await tx('namespaces').insert(filterObject(entity, allowedKeys));
|
||||
id = ids[0];
|
||||
|
||||
// We don't have to rebuild all entity types, because no entity can be a child of the namespace at this moment.
|
||||
await shares.rebuildPermissions(tx, { entityTypeId: 'namespace', entityId: id });
|
||||
});
|
||||
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, entity) {
|
||||
|
@ -95,7 +186,7 @@ async function remove(context, id) {
|
|||
|
||||
module.exports = {
|
||||
hash,
|
||||
list,
|
||||
listTree,
|
||||
getById,
|
||||
create,
|
||||
updateWithConsistencyCheck,
|
||||
|
|
|
@ -38,15 +38,17 @@ async function listDTAjax(context, params) {
|
|||
async function create(context, entity) {
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createReportTemplate');
|
||||
|
||||
let id;
|
||||
await knex.transaction(async tx => {
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
|
||||
const id = await tx('report_templates').insert(filterObject(entity, allowedKeys));
|
||||
const ids = await tx('report_templates').insert(filterObject(entity, allowedKeys));
|
||||
id = ids[0];
|
||||
|
||||
await shares.rebuildPermissions(tx, { entityTypeId: 'reportTemplate', entityId: id });
|
||||
});
|
||||
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, entity) {
|
||||
|
|
|
@ -71,7 +71,8 @@ async function create(context, entity) {
|
|||
|
||||
entity.params = JSON.stringify(entity.params);
|
||||
|
||||
id = await tx('reports').insert(filterObject(entity, allowedKeys));
|
||||
const ids = await tx('reports').insert(filterObject(entity, allowedKeys));
|
||||
id = ids[0];
|
||||
|
||||
await shares.rebuildPermissions(tx, { entityTypeId: 'report', entityId: id });
|
||||
});
|
||||
|
|
|
@ -129,7 +129,11 @@ async function _rebuildPermissions(tx, restriction) {
|
|||
}
|
||||
|
||||
|
||||
// Change user 1 role to global role that has admin===true
|
||||
// To prevent users locking out themselves, we consider user with id 1 to be the admin and always assign it
|
||||
// the admin role. The admin role is a global role that has admin===true
|
||||
// If this behavior is not desired, it is enough to delete the user with id 1.
|
||||
const adminUser = await tx('users').where('id', 1 /* Admin user id */).first();
|
||||
if (adminUser) {
|
||||
let adminRole;
|
||||
for (const role in config.roles.global) {
|
||||
if (config.roles.global[role].admin) {
|
||||
|
@ -141,6 +145,7 @@ async function _rebuildPermissions(tx, restriction) {
|
|||
if (adminRole) {
|
||||
await tx('users').update('role', adminRole).where('id', 1 /* Admin user id */);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Reset root and own namespace shares as per the user roles
|
||||
|
|
|
@ -173,15 +173,13 @@ async function create(context, user) {
|
|||
await shares.enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers');
|
||||
}
|
||||
|
||||
let id;
|
||||
await knex.transaction(async tx => {
|
||||
if (passport.isAuthMethodLocal) {
|
||||
await _validateAndPreprocess(tx, user, true);
|
||||
|
||||
const userId = await tx('users').insert(filterObject(user, allowedKeys));
|
||||
|
||||
await shares.rebuildPermissions(tx, { userId });
|
||||
|
||||
return userId;
|
||||
const ids = await tx('users').insert(filterObject(user, allowedKeys));
|
||||
id = ids[0];
|
||||
|
||||
} else {
|
||||
const filteredUser = filterObject(user, allowedKeysExternal);
|
||||
|
@ -189,18 +187,19 @@ async function create(context, user) {
|
|||
|
||||
await namespaceHelpers.validateEntity(tx, user);
|
||||
|
||||
const userId = await tx('users').insert(filteredUser);
|
||||
|
||||
await shares.rebuildPermissions(tx, { userId });
|
||||
|
||||
return userId;
|
||||
const ids = await tx('users').insert(filteredUser);
|
||||
id = ids[0];
|
||||
}
|
||||
|
||||
await shares.rebuildPermissions(tx, { userId: id });
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, user, isOwnAccount) {
|
||||
await knex.transaction(async tx => {
|
||||
const existing = await tx('users').where(['id', 'namespace', 'role'], user.id).first();
|
||||
const existing = await tx('users').where('id', user.id).first();
|
||||
if (!existing) {
|
||||
shares.throwPermissionDenied();
|
||||
}
|
||||
|
@ -226,7 +225,7 @@ async function updateWithConsistencyCheck(context, user, isOwnAccount) {
|
|||
|
||||
await tx('users').where('id', user.id).update(filterObject(user, isOwnAccount ? ownAccountAllowedKeys : allowedKeys));
|
||||
} else {
|
||||
enforce(isOwnAccount, 'Local user management is required');
|
||||
enforce(!isOwnAccount, 'Local user management is required');
|
||||
enforce(user.role in config.roles.global, 'Unknown role');
|
||||
await namespaceHelpers.validateEntity(tx, user);
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ router.postAsync('/account-validate', passport.loggedIn, passport.csrfProtection
|
|||
const data = req.body;
|
||||
data.id = req.user.id;
|
||||
|
||||
return res.json(await users.serverValidate(data, true));
|
||||
return res.json(await users.serverValidate(req.context, data, true));
|
||||
});
|
||||
|
||||
router.getAsync('/access-token', passport.loggedIn, async (req, res) => {
|
||||
|
|
|
@ -35,41 +35,9 @@ router.deleteAsync('/namespaces/:nsId', passport.loggedIn, passport.csrfProtecti
|
|||
|
||||
router.getAsync('/namespaces-tree', passport.loggedIn, async (req, res) => {
|
||||
|
||||
// FIXME - process permissions
|
||||
const tree = await namespaces.listTree(req.context);
|
||||
|
||||
const entries = {};
|
||||
let root; // Only the Root namespace is without a parent
|
||||
const rows = await namespaces.list();
|
||||
|
||||
for (let row of rows) {
|
||||
let entry;
|
||||
if (!entries[row.id]) {
|
||||
entry = {
|
||||
children: []
|
||||
};
|
||||
entries[row.id] = entry;
|
||||
} else {
|
||||
entry = entries[row.id];
|
||||
}
|
||||
|
||||
if (row.namespace) {
|
||||
if (!entries[row.namespace]) {
|
||||
entries[row.namespace] = {
|
||||
children: []
|
||||
};
|
||||
}
|
||||
|
||||
entries[row.namespace].children.push(entry);
|
||||
|
||||
} else {
|
||||
root = entry;
|
||||
}
|
||||
|
||||
entry.title = row.name;
|
||||
entry.key = row.id;
|
||||
}
|
||||
|
||||
return res.json(root);
|
||||
return res.json(tree);
|
||||
});
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue