mailtrain/server/models/shares.js
Tomas Bures 0d7f962c86 Fix - subscriber custom data were not listed in correct order in the subcribers list
"Test user" field added to segment rules
Configuration option to automatically share arbitrary namespace based on user role.
2019-01-12 11:21:38 +01:00

728 lines
No EOL
27 KiB
JavaScript

'use strict';
const knex = require('../lib/knex');
const config = require('config');
const { enforce, castToInteger } = require('../lib/helpers');
const dtHelpers = require('../lib/dt-helpers');
const entitySettings = require('../lib/entity-settings');
const interoperableErrors = require('../../shared/interoperable-errors');
const log = require('../lib/log');
const {getGlobalNamespaceId} = require('../../shared/namespaces');
const {getAdminId} = require('../../shared/users');
// TODO: This would really benefit from some permission cache connected to rebuildPermissions
// A bit of the problem is that the cache would have to expunged as the result of other processes modifying entites/permissions
async function listByEntityDTAjax(context, entityTypeId, entityId, params) {
return await knex.transaction(async (tx) => {
const entityType = entitySettings.getEntityType(entityTypeId);
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
return await dtHelpers.ajaxListTx(
tx,
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', entityType.sharesTable + '.auto']
);
});
}
async function listByUserDTAjax(context, entityTypeId, userId, params) {
return await knex.transaction(async (tx) => {
const user = await tx('users').where('id', userId).first();
if (!user) {
shares.throwPermissionDenied();
}
await enforceEntityPermissionTx(tx, context, 'namespace', user.namespace, 'manageUsers');
const entityType = entitySettings.getEntityType(entityTypeId);
return await dtHelpers.ajaxListWithPermissionsTx(
tx,
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', entityType.sharesTable + '.auto']
);
});
}
async function listUnassignedUsersDTAjax(context, entityTypeId, entityId, params) {
return await knex.transaction(async (tx) => {
const entityType = entitySettings.getEntityType(entityTypeId);
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
return await dtHelpers.ajaxListTx(
tx,
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(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 = entitySettings.getEntityType(entityTypeId);
await knex.transaction(async tx => {
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
enforce(await tx('users').where('id', userId).select('id').first(), 'Invalid user id');
const extraColumns = entityType.dependentPermissions ? entityType.dependentPermissions.extraColumns : [];
const entity = await tx(entityType.entitiesTable).where('id', entityId).select(['id', ...extraColumns]).first();
enforce(entity, 'Invalid entity id');
if (entityType.dependentPermissions) {
enforce(!entityType.dependentPermissions.getParent(entity), 'Cannot share/unshare a dependent entity');
}
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 rebuildPermissionsTx(tx, {userId});
} else if (role) {
await rebuildPermissionsTx(tx, { entityTypeId, entityId, userId });
}
});
}
async function rebuildPermissionsTx(tx, restriction) {
restriction = restriction || {};
const namespaceEntityType = entitySettings.getEntityType('namespace');
// Collect entity types we care about
let restrictedEntityTypes;
if (restriction.entityTypeId) {
const entityType = entitySettings.getEntityType(restriction.entityTypeId);
restrictedEntityTypes = {
[restriction.entityTypeId]: entityType
};
} else {
restrictedEntityTypes = entitySettings.getEntityTypesWithPermissions();
}
// 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', getAdminId()).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', getAdminId());
}
}
// Reset root, own and shared namespaces shares as per the user roles
const usersAutoSharesQry = tx('users')
.select(['users.id', 'users.role', 'users.namespace']);
if (restriction.userId) {
usersAutoSharesQry.where('users.id', restriction.userId);
}
const usersAutoShares = await usersAutoSharesQry;
for (const user of usersAutoShares) {
const roleConf = config.roles.global[user.role];
if (roleConf) {
const desiredRoles = new Map();
if (roleConf.sharedNamespaces) {
for (const shrKey in roleConf.sharedNamespaces) {
const shrRole = roleConf.sharedNamespaces[shrKey];
const shrNsId = castToInteger(shrKey);
desiredRoles.set(shrNsId, shrRole);
}
}
if (roleConf.ownNamespaceRole) {
desiredRoles.set(user.namespace, roleConf.ownNamespaceRole);
}
if (roleConf.rootNamespaceRole) {
desiredRoles.set(getGlobalNamespaceId(), roleConf.rootNamespaceRole);
}
for (const [nsId, role] of desiredRoles.entries()) {
await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: nsId }).del();
await tx(namespaceEntityType.sharesTable).insert({ user: user.id, entity: nsId, role: role, auto: true });
}
}
}
// 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 extraColumns = entityType.dependentPermissions ? entityType.dependentPermissions.extraColumns : [];
const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace', ...extraColumns]);
const notToBeInserted = new Set();
if (restriction.entityId) {
if (restriction.parentId) {
notToBeInserted.add(restriction.parentId);
entitiesQuery.whereIn('id', [restriction.entityId, restriction.parentId]);
} else {
entitiesQuery.where('id', restriction.entityId);
}
}
const entities = await entitiesQuery;
const parentEntities = new Map();
let nonChildEntities;
if (entityType.dependentPermissions) {
nonChildEntities = [];
for (const entity of entities) {
const parent = entityType.dependentPermissions.getParent(entity);
if (parent) {
let childEntities;
if (parentEntities.has(parent)) {
childEntities = parentEntities.get(parent);
} else {
childEntities = [];
parentEntities.set(parent, childEntities);
}
childEntities.push(entity.id);
} else {
nonChildEntities.push(entity);
}
}
} else {
nonChildEntities = entities;
}
for (const entity of nonChildEntities) {
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);
}
}
}
if (!notToBeInserted.has(entity.id)) {
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);
}
}
}
if (parentEntities.has(entity.id)) {
const childEntities = parentEntities.get(entity.id);
for (const childId of childEntities) {
for (const userPermsPair of permsPerUser.entries()) {
const data = [];
for (const operation of userPermsPair[1]) {
if (operation !== 'share') {
data.push({user: userPermsPair[0], entity: childId, operation});
}
}
if (data.length > 0) {
await tx(entityType.permissionsTable).insert(data);
}
}
}
}
}
}
}
async function rebuildPermissions(restriction) {
await knex.transaction(async tx => {
await rebuildPermissionsTx(tx, restriction);
});
}
async function regenerateRoleNamesTable() {
await knex.transaction(async tx => {
await tx('generated_role_names').del();
const entityTypeIds = ['global', ...Object.keys(entitySettings.getEntityTypesWithPermissions())];
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 = entitySettings.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: getGlobalNamespaceId() }).del();
}
}
}
function checkGlobalPermission(context, requiredOperations) {
if (!context.user) {
return false;
}
if (typeof requiredOperations === 'string') {
requiredOperations = [ requiredOperations ];
}
if (context.user.restrictedAccessHandler) {
const originalRequiredOperations = requiredOperations;
const allowedPerms = context.user.restrictedAccessHandler.globalPermissions;
if (allowedPerms) {
requiredOperations = requiredOperations.filter(perm => allowedPerms.has(perm));
} else {
requiredOperations = [];
}
log.verbose('check global permissions with restrictedAccessHandler -- requiredOperations: [' + originalRequiredOperations + '] -> [' + requiredOperations + ']');
}
if (requiredOperations.length === 0) {
return false;
}
if (context.user.admin) { // This handles the getAdminContext() case
return true;
}
const roleSpec = config.roles.global[context.user.role];
let success = false;
if (roleSpec) {
for (const requiredOperation of requiredOperations) {
if (roleSpec.permissions.includes(requiredOperation)) {
success = true;
break;
}
}
}
return success;
}
function enforceGlobalPermission(context, requiredOperations) {
if (!checkGlobalPermission(context, requiredOperations)) {
throwPermissionDenied();
}
}
async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
if (!context.user) {
return false;
}
const entityType = entitySettings.getEntityType(entityTypeId);
if (typeof requiredOperations === 'string') {
requiredOperations = [ requiredOperations ];
}
requiredOperations = filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, requiredOperations, 'checkPermissions');
if (requiredOperations.length === 0) {
return false;
}
if (context.user.admin) { // This handles the getAdminContext() case. In this case we don't check the permission, but just the existence.
const existsQuery = tx(entityType.entitiesTable);
if (entityId) {
existsQuery.where('id', entityId);
}
const exists = await existsQuery.first();
return !!exists;
} else {
const permsQuery = tx(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 knex.transaction(async tx => {
return await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
});
}
async function checkEntityPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
if (!entityId) {
return false;
}
return await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
}
async function checkTypePermission(context, entityTypeId, requiredOperations) {
return await knex.transaction(async tx => {
return await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
});
}
async function enforceEntityPermission(context, entityTypeId, entityId, requiredOperations) {
if (!entityId) {
throwPermissionDenied();
}
await knex.transaction(async tx => {
const result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
if (!result) {
log.info(`Denying permission ${entityTypeId}.${entityId} ${requiredOperations}`);
throwPermissionDenied();
}
});
}
async function enforceEntityPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
if (!entityId) {
throwPermissionDenied();
}
const result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
if (!result) {
log.info(`Denying permission ${entityTypeId}.${entityId} ${requiredOperations}`);
throwPermissionDenied();
}
}
async function enforceTypePermission(context, entityTypeId, requiredOperations) {
await knex.transaction(async tx => {
const result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
if (!result) {
log.info(`Denying permission ${entityTypeId} ${requiredOperations}`);
throwPermissionDenied();
}
});
}
async function enforceTypePermissionTx(tx, context, entityTypeId, requiredOperations) {
const result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
if (!result) {
log.info(`Denying permission ${entityTypeId} ${requiredOperations}`);
throwPermissionDenied();
}
}
function getGlobalPermissions(context) {
if (!context.user) {
return [];
}
enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');
return (config.roles.global[context.user.role] || {}).permissions || [];
}
async function getPermissionsTx(tx, context, entityTypeId, entityId) {
if (!context.user) {
return [];
}
enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');
const entityType = entitySettings.getEntityType(entityTypeId);
const rows = await tx(entityType.permissionsTable)
.select('operation')
.where('entity', entityId)
.where('user', context.user.id);
const operations = rows.map(x => x.operation);
return filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, operations, 'getPermissions');
}
// If entityId is null, it means that we require that restrictedAccessHandler does not differentiate based on entityId. This is used in ajaxListWithPermissionsTx.
function filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, permissions, operationMsg) {
if (context.user.restrictedAccessHandler) {
const originalOperations = permissions;
if (context.user.restrictedAccessHandler.permissions) {
const entityPerms = context.user.restrictedAccessHandler.permissions[entityTypeId];
if (!entityPerms) {
permissions = [];
} else if (entityPerms === true) {
// no change to operations
} else if (entityPerms instanceof Set) {
permissions = permissions.filter(perm => entityPerms.has(perm));
} else {
if (entityId) {
const allowedPerms = entityPerms[entityId];
if (allowedPerms) {
permissions = permissions.filter(perm => allowedPerms.has(perm));
} else {
const allowedPerms = entityPerms['default'];
if (allowedPerms) {
permissions = permissions.filter(perm => allowedPerms.has(perm));
} else {
permissions = [];
}
}
} else {
const allowedPerms = entityPerms['default'];
if (allowedPerms) {
permissions = permissions.filter(perm => allowedPerms.has(perm));
} else {
permissions = [];
}
}
}
} else {
permissions = [];
}
log.verbose(operationMsg + ' with restrictedAccessHandler -- entityTypeId: ' + entityTypeId + ' entityId: ' + entityId + ' operations: [' + originalOperations + '] -> [' + permissions + ']');
}
return permissions;
}
function isAccessibleByRestrictedAccessHandler(context, entityTypeId, entityId, permissions, operationMsg) {
return filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, permissions, operationMsg).length > 0;
}
module.exports.listByEntityDTAjax = listByEntityDTAjax;
module.exports.listByUserDTAjax = listByUserDTAjax;
module.exports.listUnassignedUsersDTAjax = listUnassignedUsersDTAjax;
module.exports.listRolesDTAjax = listRolesDTAjax;
module.exports.assign = assign;
module.exports.rebuildPermissionsTx = rebuildPermissionsTx;
module.exports.rebuildPermissions = rebuildPermissions;
module.exports.removeDefaultShares = removeDefaultShares;
module.exports.enforceEntityPermission = enforceEntityPermission;
module.exports.enforceEntityPermissionTx = enforceEntityPermissionTx;
module.exports.enforceTypePermission = enforceTypePermission;
module.exports.enforceTypePermissionTx = enforceTypePermissionTx;
module.exports.checkEntityPermissionTx = checkEntityPermissionTx;
module.exports.checkEntityPermission = checkEntityPermission;
module.exports.checkTypePermission = checkTypePermission;
module.exports.enforceGlobalPermission = enforceGlobalPermission;
module.exports.checkGlobalPermission = checkGlobalPermission;
module.exports.throwPermissionDenied = throwPermissionDenied;
module.exports.regenerateRoleNamesTable = regenerateRoleNamesTable;
module.exports.getGlobalPermissions = getGlobalPermissions;
module.exports.getPermissionsTx = getPermissionsTx;
module.exports.filterPermissionsByRestrictedAccessHandler = filterPermissionsByRestrictedAccessHandler;
module.exports.isAccessibleByRestrictedAccessHandler = isAccessibleByRestrictedAccessHandler;