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
|
@ -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;
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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,17 +129,22 @@ async function _rebuildPermissions(tx, restriction) {
|
|||
}
|
||||
|
||||
|
||||
// Change user 1 role to global role that has admin===true
|
||||
let adminRole;
|
||||
for (const role in config.roles.global) {
|
||||
if (config.roles.global[role].admin) {
|
||||
adminRole = role;
|
||||
break;
|
||||
// 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) {
|
||||
adminRole = role;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (adminRole) {
|
||||
await tx('users').update('role', adminRole).where('id', 1 /* Admin user id */);
|
||||
if (adminRole) {
|
||||
await tx('users').update('role', adminRole).where('id', 1 /* Admin user id */);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue