'use strict'; let _ = require('../lib/translate')._; const knex = require('../lib/knex'); const config = require('config'); const { enforce } = require('../lib/helpers'); const dtHelpers = require('../lib/dt-helpers'); const permissions = require('../lib/permissions'); const interoperableErrors = require('../shared/interoperable-errors'); async function listByEntityDTAjax(context, entityTypeId, entityId, params) { const entityType = permissions.getEntityType(entityTypeId); await enforceEntityPermission(context, entityTypeId, entityId, 'share'); return await dtHelpers.ajaxList( params, builder => builder .from(entityType.sharesTable) .innerJoin('users', entityType.sharesTable + '.user', 'users.id') .innerJoin('generated_role_names', 'generated_role_names.role', 'users.role') .where('generated_role_names.entity_type', entityTypeId) .where(`${entityType.sharesTable}.entity`, entityId), [ 'users.username', 'users.name', 'generated_role_names.name', 'users.id' ] ); } async function listByUserDTAjax(context, entityTypeId, userId, params) { const user = await knex('users').where('id', userId).first(); if (!user) { shares.throwPermissionDenied(); } await enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers'); const entityType = permissions.getEntityType(entityTypeId); return await dtHelpers.ajaxListWithPermissions( context, [{ entityTypeId }], params, builder => builder .from(entityType.sharesTable) .innerJoin(entityType.entitiesTable, entityType.sharesTable + '.entity', entityType.entitiesTable + '.id') .innerJoin('generated_role_names', 'generated_role_names.role', entityType.sharesTable + '.role') .where('generated_role_names.entity_type', entityTypeId) .where(entityType.sharesTable + '.user', userId), [ entityType.entitiesTable + '.name', 'generated_role_names.name', entityType.entitiesTable + '.id' ] ); } async function listUnassignedUsersDTAjax(context, entityTypeId, entityId, params) { const entityType = permissions.getEntityType(entityTypeId); await enforceEntityPermission(context, entityTypeId, entityId, 'share'); return await dtHelpers.ajaxList( params, builder => builder .from('users') .whereNotExists(function() { return this .select('*') .from(entityType.sharesTable) .whereRaw(`users.id = ${entityType.sharesTable}.user`) .andWhere(`${entityType.sharesTable}.entity`, entityId); }), ['users.id', 'users.username', 'users.name'] ); } async function listRolesDTAjax(context, entityTypeId, params) { return await dtHelpers.ajaxList( params, builder => builder .from('generated_role_names') .where({entity_type: entityTypeId}), ['role', 'name', 'description'] ); } async function assign(context, entityTypeId, entityId, userId, role) { const entityType = permissions.getEntityType(entityTypeId); await enforceEntityPermission(context, entityTypeId, entityId, 'share'); await knex.transaction(async tx => { enforce(await tx('users').where('id', userId).select('id').first(), 'Invalid user id'); enforce(await tx(entityType.entitiesTable).where('id', entityId).select('id').first(), 'Invalid entity id'); const entry = await tx(entityType.sharesTable).where({user: userId, entity: entityId}).select('role').first(); if (entry) { if (!role) { await tx(entityType.sharesTable).where({user: userId, entity: entityId}).del(); } else if (entry.role !== role) { await tx(entityType.sharesTable).where({user: userId, entity: entityId}).update('role', role); } } else { await tx(entityType.sharesTable).insert({ user: userId, entity: entityId, role }); } await tx(entityType.permissionsTable).where({user: userId, entity: entityId}).del(); if (entityTypeId === 'namespace') { await rebuildPermissions(tx, {userId}); } else if (role) { await rebuildPermissions(tx, { entityTypeId, entityId, userId }); } }); } async function _rebuildPermissions(tx, restriction) { const namespaceEntityType = permissions.getEntityType('namespace'); // Collect entity types we care about let restrictedEntityTypes; if (restriction.entityTypeId) { const entityType = permissions.getEntityType(restriction.entityTypeId); restrictedEntityTypes = { [restriction.entityTypeId]: entityType }; } else { restrictedEntityTypes = permissions.getEntityTypes(); } // 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 */); } } // Reset root and own namespace shares as per the user roles const usersWithRoleInOwnNamespaceQuery = tx('users') .leftJoin(namespaceEntityType.sharesTable, { 'users.id': `${namespaceEntityType.sharesTable}.user`, 'users.namespace': `${namespaceEntityType.sharesTable}.entity` }) .select(['users.id', 'users.namespace', 'users.role as userRole', `${namespaceEntityType.sharesTable}.role`]); if (restriction.userId) { usersWithRoleInOwnNamespaceQuery.where('users.id', restriction.userId); } const usersWithRoleInOwnNamespace = await usersWithRoleInOwnNamespaceQuery; for (const user of usersWithRoleInOwnNamespace) { const roleConf = config.roles.global[user.userRole]; if (roleConf) { const desiredRole = roleConf.ownNamespaceRole; if (desiredRole && user.role !== desiredRole) { await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: user.namespace }).del(); await tx(namespaceEntityType.sharesTable).insert({ user: user.id, entity: user.namespace, role: desiredRole }); } } } const usersWithRoleInRootNamespaceQuery = tx('users') .leftJoin(namespaceEntityType.sharesTable, { 'users.id': `${namespaceEntityType.sharesTable}.user`, [`${namespaceEntityType.sharesTable}.entity`]: 1 /* Global namespace id */ }) .select(['users.id', 'users.role as userRole', `${namespaceEntityType.sharesTable}.role`]); if (restriction.userId) { usersWithRoleInRootNamespaceQuery.andWhere('users.id', restriction.userId); } const usersWithRoleInRootNamespace = await usersWithRoleInRootNamespaceQuery; for (const user of usersWithRoleInRootNamespace) { const roleConf = config.roles.global[user.userRole]; if (roleConf) { const desiredRole = roleConf.rootNamespaceRole; if (desiredRole && user.role !== desiredRole) { await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: 1 /* Global namespace id */ }).del(); await tx(namespaceEntityType.sharesTable).insert({ user: user.id, entity: 1 /* Global namespace id */, role: desiredRole }); } } } // Build the map of all namespaces // nsMap is a map of namespaces - each of the following shape: // .id - id of the namespace // .namespace - id of the parent or null if no parent // .userPermissions - Map userId -> [entityTypeId] -> array of permissions // .transitiveUserPermissions - the same as above, but taking into account transitive permission obtained from namespace parents const namespaces = await tx('namespaces').select(['id', 'namespace']); const nsMap = new Map(); for (const namespace of namespaces) { namespace.userPermissions = new Map(); nsMap.set(namespace.id, namespace); } // This populates .userPermissions const nsSharesQuery = tx(namespaceEntityType.sharesTable).select(['entity', 'user', 'role']); if (restriction.userId) { nsSharesQuery.where('user', restriction.userId); } const nsShares = await nsSharesQuery; for (const nsShare of nsShares) { const ns = nsMap.get(nsShare.entity); const userPerms = {}; ns.userPermissions.set(nsShare.user, userPerms); for (const entityTypeId in restrictedEntityTypes) { if (config.roles.namespace[nsShare.role] && config.roles.namespace[nsShare.role].children && config.roles.namespace[nsShare.role].children[entityTypeId]) { userPerms[entityTypeId] = new Set(config.roles.namespace[nsShare.role].children[entityTypeId]); } else { userPerms[entityTypeId] = new Set(); } } } // This computes .transitiveUserPermissions for (const ns of nsMap.values()) { ns.transitiveUserPermissions = new Map(); for (const userPermsPair of ns.userPermissions) { const userPerms = {}; ns.transitiveUserPermissions.set(userPermsPair[0], userPerms); for (const entityTypeId in restrictedEntityTypes) { userPerms[entityTypeId] = new Set(userPermsPair[1][entityTypeId]); } } let parentId = ns.namespace; while (parentId) { const parent = nsMap.get(parentId); for (const userPermsPair of parent.userPermissions) { const user = userPermsPair[0]; if (ns.transitiveUserPermissions.has(user)) { const userPerms = ns.transitiveUserPermissions.get(user); for (const entityTypeId in restrictedEntityTypes) { for (const perm of userPermsPair[1][entityTypeId]) { userPerms[entityTypeId].add(perm); } } } else { const userPerms = {} ns.transitiveUserPermissions.set(user, userPerms); for (const entityTypeId in restrictedEntityTypes) { userPerms[entityTypeId] = new Set(userPermsPair[1][entityTypeId]); } } } parentId = parent.namespace; } } // This reads direct shares from DB, joins each with the permissions from namespaces and stores the permissions into DB for (const entityTypeId in restrictedEntityTypes) { const entityType = restrictedEntityTypes[entityTypeId]; const expungeQuery = tx(entityType.permissionsTable).del(); if (restriction.entityId) { expungeQuery.where('entity', restriction.entityId); } if (restriction.userId) { expungeQuery.where('user', restriction.userId); } await expungeQuery; const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace']); if (restriction.entityId) { entitiesQuery.where('id', restriction.entityId); } const entities = await entitiesQuery; for (const entity of entities) { const permsPerUser = new Map(); if (entity.namespace) { // The root namespace has not parent namespace, thus the test const transitiveUserPermissions = nsMap.get(entity.namespace).transitiveUserPermissions; for (const transitivePermsPair of transitiveUserPermissions.entries()) { permsPerUser.set(transitivePermsPair[0], new Set(transitivePermsPair[1][entityTypeId])); } } const directSharesQuery = tx(entityType.sharesTable).select(['user', 'role']).where('entity', entity.id); if (restriction.userId) { directSharesQuery.andWhere('user', restriction.userId); } const directShares = await directSharesQuery; for (const share of directShares) { let userPerms; if (permsPerUser.has(share.user)) { userPerms = permsPerUser.get(share.user); } else { userPerms = new Set(); permsPerUser.set(share.user, userPerms); } if (config.roles[entityTypeId][share.role] && config.roles[entityTypeId][share.role].permissions) { for (const perm of config.roles[entityTypeId][share.role].permissions) { userPerms.add(perm); } } } for (const userPermsPair of permsPerUser.entries()) { const data = []; for (const operation of userPermsPair[1]) { data.push({user: userPermsPair[0], entity: entity.id, operation}); } if (data.length > 0) { await tx(entityType.permissionsTable).insert(data); } } } } } async function rebuildPermissions(tx, restriction) { restriction = restriction || {}; if (tx) { await _rebuildPermissions(tx, restriction); } else { await knex.transaction(async tx => { await _rebuildPermissions(tx, restriction); }); } } async function regenerateRoleNamesTable() { await knex.transaction(async tx => { await tx('generated_role_names').del(); const entityTypeIds = ['global', ...Object.keys(permissions.getEntityTypes())]; for (const entityTypeId of entityTypeIds) { const roles = config.roles[entityTypeId]; for (const role in roles) { await tx('generated_role_names').insert({ entity_type: entityTypeId, role, name: roles[role].name, description: roles[role].description, }); } } }); } function throwPermissionDenied() { throw new interoperableErrors.PermissionDeniedError(_('Permission denied')); } async function removeDefaultShares(tx, user) { const namespaceEntityType = permissions.getEntityType('namespace'); const roleConf = config.roles.global[user.role]; if (roleConf) { const desiredRole = roleConf.rootNamespaceRole; if (roleConf.ownNamespaceRole) { await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: user.namespace }).del(); } if (roleConf.rootNamespaceRole) { await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: 1 /* Global namespace id */ }).del(); } } } function enforceGlobalPermission(context, requiredOperations) { if (typeof requiredOperations === 'string') { requiredOperations = [ requiredOperations ]; } const roleSpec = config.roles.global[context.user.role]; if (roleSpec) { for (const requiredOperation of requiredOperations) { if (roleSpec.permissions.includes(requiredOperation)) { return; } } } throwPermissionDenied(); } async function _checkPermission(context, entityTypeId, entityId, requiredOperations) { const entityType = permissions.getEntityType(entityTypeId); if (typeof requiredOperations === 'string') { requiredOperations = [ requiredOperations ]; } const permsQuery = knex(entityType.permissionsTable) .where('user', context.user.id) .whereIn('operation', requiredOperations); if (entityId) { permsQuery.andWhere('entity', entityId); } const perms = await permsQuery.first(); return !!perms; } async function checkEntityPermission(context, entityTypeId, entityId, requiredOperations) { if (!entityId) { return false; } return await _checkPermission(context, entityTypeId, entityId, requiredOperations); } async function checkTypePermission(context, entityTypeId, requiredOperations) { return await _checkPermission(context, entityTypeId, null, requiredOperations); } async function enforceEntityPermission(context, entityTypeId, entityId, requiredOperations) { const perms = await checkEntityPermission(context, entityTypeId, entityId, requiredOperations); if (!perms) { throwPermissionDenied(); } } async function enforceTypePermission(context, entityTypeId, requiredOperations) { const perms = await checkTypePermission(context, entityTypeId, requiredOperations); if (!perms) { throwPermissionDenied(); } } module.exports = { listByEntityDTAjax, listByUserDTAjax, listUnassignedUsersDTAjax, listRolesDTAjax, assign, rebuildPermissions, removeDefaultShares, enforceEntityPermission, enforceTypePermission, checkEntityPermission, checkTypePermission, enforceGlobalPermission, throwPermissionDenied, regenerateRoleNamesTable };