Harmonization with IVIS

This commit is contained in:
Tomas Bures 2019-01-04 21:31:01 +01:00
parent 428fb9db7b
commit 397f85dac4
41 changed files with 8587 additions and 10940 deletions

View file

@ -234,7 +234,6 @@ function createApp(appType) {
useWith404Fallback('/static', express.static(path.join(__dirname, '..', 'client', 'static')));
useWith404Fallback('/mailtrain', express.static(path.join(__dirname, '..', 'client', 'dist')));
useWith404Fallback('/locales', express.static(path.join(__dirname, '..', 'client', 'locales')));
useWith404Fallback('/static-npm/fontawesome', express.static(path.join(__dirname, '..', 'client', 'node_modules', '@fortawesome', 'fontawesome-free', 'webfonts')));
useWith404Fallback('/static-npm/popper.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'popper.js', 'dist', 'umd', 'popper.min.js')));

View file

@ -2,6 +2,8 @@
const knex = require('./knex');
const entitySettings = require('./entity-settings');
const { enforce } = require('./helpers');
const shares = require('../models/shares');
async function ajaxListTx(tx, params, queryFun, columns, options) {
options = options || {};
@ -109,7 +111,8 @@ async function ajaxListTx(tx, params, queryFun, columns, options) {
}
async function ajaxListWithPermissionsTx(tx, context, fetchSpecs, params, queryFun, columns, options) {
// Note that this function is not intended to be used with the synthetic admin context obtained by contextHelpers.getAdminContext()
enforce(!context.user.admin, 'ajaxListWithPermissionsTx is not supposed to be called by assumed admin');
options = options || {};
const permCols = [];
@ -138,13 +141,18 @@ async function ajaxListWithPermissionsTx(tx, context, fetchSpecs, params, queryF
const entityType = entitySettings.getEntityType(fetchSpec.entityTypeId);
if (fetchSpec.requiredOperations) {
const requiredOperations = shares.filterPermissionsByRestrictedAccessHandler(context, fetchSpec.entityTypeId, null, fetchSpec.requiredOperations, 'ajaxListWithPermissionsTx');
const entityIdColumn = fetchSpec.column ? fetchSpec.column : entityType.entitiesTable + '.id';
query = query.innerJoin(
function () {
return this.from(entityType.permissionsTable).distinct('entity').where('user', context.user.id).whereIn('operation', fetchSpec.requiredOperations).as(`permitted__${fetchSpec.entityTypeId}`);
},
`permitted__${fetchSpec.entityTypeId}.entity`, entityIdColumn)
if (requiredOperations.length > 0) {
query = query.innerJoin(
function () {
return this.from(entityType.permissionsTable).distinct('entity').where('user', context.user.id).whereIn('operation', requiredOperations).as(`permitted__${fetchSpec.entityTypeId}`);
},
`permitted__${fetchSpec.entityTypeId}.entity`, entityIdColumn)
} else {
query = query.whereRaw('FALSE');
}
}
}
@ -186,9 +194,7 @@ async function ajaxListWithPermissions(context, fetchSpecs, params, queryFun, co
});
}
module.exports = {
ajaxListTx,
ajaxList,
ajaxListWithPermissionsTx,
ajaxListWithPermissions
};
module.exports.ajaxListTx = ajaxListTx;
module.exports.ajaxList = ajaxList;
module.exports.ajaxListWithPermissionsTx = ajaxListWithPermissionsTx;
module.exports.ajaxListWithPermissions = ajaxListWithPermissions;

View file

@ -1,7 +1,9 @@
'use strict';
const config = require('config');
const moment = require('moment');
const path = require('path');
const knexConstructor = require('knex');
const knex = require('knex')({
client: 'mysql',
@ -16,9 +18,9 @@ const knex = require('knex')({
]
},
migrations: {
directory: __dirname + '/../setup/knex/migrations'
directory: path.join(__dirname, '..', 'setup', 'knex', 'migrations')
}
//, debug: true
//, debug: true
});

View file

@ -1,8 +1,8 @@
'use strict';
const { enforce } = require('./helpers');
const shares = require('../models/shares');
const interoperableErrors = require('../../shared/interoperable-errors');
const shares = require('../models/shares');
async function validateEntity(tx, entity) {
enforce(entity.namespace, 'Entity namespace not set');

View file

@ -222,7 +222,7 @@ if (LdapStrategy) {
module.exports.isAuthMethodLocal = true;
passport.use(new LocalStrategy(nodeifyFunction(async (username, password) => {
return await users.getByUsernameIfPasswordMatch(username, password);
return await users.getByUsernameIfPasswordMatch(contextHelpers.getAdminContext(), username, password);
})));
passport.serializeUser((user, done) => done(null, user.id));

View file

@ -13,7 +13,7 @@ const dependencyHelpers = require('../lib/dependency-helpers');
const allowedKeys = new Set(['name', 'description', 'namespace']);
async function listTree(context) {
// FIXME - process permissions
enforce(!context.user.admin, 'listTree is not supposed to be called by assumed admin');
const entityType = entitySettings.getEntityType('namespace');
@ -64,6 +64,7 @@ async function listTree(context) {
entry.title = row.name;
entry.description = row.description;
entry.permissions = row.permissions ? row.permissions.split(';') : [];
entry.permissions = shares.filterPermissionsByRestrictedAccessHandler(context, 'namespace', row.id, entry.permissions, 'namespaces.listTree')
}
// Prune out the inaccessible namespaces
@ -117,19 +118,66 @@ async function getById(context, id) {
});
}
async function create(context, entity) {
async function getChildrenTx(tx, context, id) {
await shares.enforceEntityPermissionTx(tx, context, 'namespace', id, 'view');
const entityType = entitySettings.getEntityType('namespace');
const extraKeys = em.get('models.namespaces.extraKeys', []);
let children;
if (context.user.admin) {
children = await knex('namespaces')
.where('namespaces.namespace', id)
.select([
'namespaces.id', 'namespaces.name', 'namespaces.description', 'namespaces.namespace',
...extraKeys.map(key => 'namespaces.' + key)
]);
} else {
const rows = await knex('namespaces')
.leftJoin(entityType.permissionsTable, {
[entityType.permissionsTable + '.entity']: 'namespaces.id',
[entityType.permissionsTable + '.user']: context.user.id
})
.where('namespaces.namespace', id)
.groupBy('namespaces.id')
.select([
'namespaces.id', 'namespaces.name', 'namespaces.description', 'namespaces.namespace',
...extraKeys.map(key => 'namespaces.' + key),
knex.raw(`GROUP_CONCAT(${entityType.permissionsTable + '.operation'} SEPARATOR \';\') as permissions`)
]);
children = [];
for (const row of rows) {
row.permissions = row.permissions ? row.permissions.split(';') : [];
row.permissions = shares.filterPermissionsByRestrictedAccessHandler(context, 'namespace', row.id, row.permissions, 'namespaces.getChildrenTx');
if (row.permissions.includes('view')) {
children.push(row);
}
}
}
return children;
}
async function createTx(tx, context, entity) {
enforce(entity.namespace, 'Parent namespace must be set');
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createNamespace');
const ids = await tx('namespaces').insert(filterObject(entity, allowedKeys));
const 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.rebuildPermissionsTx(tx, { entityTypeId: 'namespace', entityId: id });
return id;
}
async function create(context, entity) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createNamespace');
const ids = await tx('namespaces').insert(filterObject(entity, allowedKeys));
const 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.rebuildPermissionsTx(tx, { entityTypeId: 'namespace', entityId: id });
return id;
return await createTx(tx, context, entity);
});
}
@ -187,6 +235,8 @@ async function remove(context, id) {
module.exports.hash = hash;
module.exports.listTree = listTree;
module.exports.getById = getById;
module.exports.getChildrenTx = getChildrenTx;
module.exports.create = create;
module.exports.createTx = createTx;
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
module.exports.remove = remove;

View file

@ -332,8 +332,6 @@ async function rebuildPermissionsTx(tx, restriction) {
}
const entities = await entitiesQuery;
// TODO - process restriction.parentId
const parentEntities = new Map();
let nonChildEntities;
if (entityType.dependentPermissions) {
@ -541,30 +539,7 @@ async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredO
requiredOperations = [ requiredOperations ];
}
if (context.user.restrictedAccessHandler) {
const originalRequiredOperations = requiredOperations;
if (context.user.restrictedAccessHandler.permissions) {
const entityPerms = context.user.restrictedAccessHandler.permissions[entityTypeId];
if (!entityPerms) {
requiredOperations = [];
} else if (entityPerms === true) {
// no change to require operations
} else if (entityPerms instanceof Set) {
requiredOperations = requiredOperations.filter(perm => entityPerms.has(perm));
} else {
const allowedPerms = entityPerms[entityId];
if (allowedPerms) {
requiredOperations = requiredOperations.filter(perm => allowedPerms.has(perm));
} else {
requiredOperations = [];
}
}
} else {
requiredOperations = [];
}
log.verbose('check permissions with restrictedAccessHandler -- entityTypeId: ' + entityTypeId + ' entityId: ' + entityId + ' requiredOperations: [' + originalRequiredOperations + '] -> [' + requiredOperations + ']');
}
requiredOperations = filterPermissionsByRestrictedAccessHandler(context, entityTypeId, entityId, requiredOperations, 'checkPermissions');
if (requiredOperations.length === 0) {
return false;
@ -686,9 +661,59 @@ async function getPermissionsTx(tx, context, entityTypeId, entityId) {
.where('entity', entityId)
.where('user', context.user.id);
return rows.map(x => x.operation);
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;
@ -710,3 +735,5 @@ module.exports.throwPermissionDenied = throwPermissionDenied;
module.exports.regenerateRoleNamesTable = regenerateRoleNamesTable;
module.exports.getGlobalPermissions = getGlobalPermissions;
module.exports.getPermissionsTx = getPermissionsTx;
module.exports.filterPermissionsByRestrictedAccessHandler = filterPermissionsByRestrictedAccessHandler;
module.exports.isAccessibleByRestrictedAccessHandler = isAccessibleByRestrictedAccessHandler;

View file

@ -36,20 +36,28 @@ function hash(entity) {
return hasher.hash(filterObject(entity, hashKeys));
}
async function _getBy(context, key, value, extraColumns = []) {
async function _getByTx(tx, context, key, value, extraColumns = []) {
const columns = ['id', 'username', 'name', 'email', 'namespace', 'role', ...extraColumns];
const user = await knex('users').select(columns).where(key, value).first();
const user = await tx('users').select(columns).where(key, value).first();
if (!user) {
shares.throwPermissionDenied();
}
// Note that getRestrictedAccessToken relies to this check to see whether a user may impersonate another. If "manageUsers" here were to be changed to something like "viewUsers", then
// a corresponding check has to be added to getRestrictedAccessToken
await shares.enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers');
return user;
}
async function _getBy(context, key, value, extraColumns = []) {
return await knex.transaction(async tx => {
return await _getByTx(tx, context, key, value, extraColumns);
});
}
async function getById(context, id) {
return await _getBy(context, 'id', id);
}
@ -131,7 +139,7 @@ async function _validateAndPreprocess(tx, entity, isCreate, isOwnAccount) {
if (!isOwnAccount) {
const otherUserWithSameUsernameQuery = tx('users').where('username', entity.username);
if (entity.id) {
if (!isCreate) {
otherUserWithSameUsernameQuery.andWhereNot('id', entity.id);
}
@ -254,9 +262,9 @@ async function getByUsername(username) {
return await _getBy(contextHelpers.getAdminContext(), 'username', username);
}
async function getByUsernameIfPasswordMatch(username, password) {
async function getByUsernameIfPasswordMatch(context, username, password) {
try {
const user = await _getBy(contextHelpers.getAdminContext(), 'username', username, ['password']);
const user = await _getBy('username', username, ['password']);
if (!await bcryptCompare(password, user.password)) {
throw new interoperableErrors.IncorrectPasswordError();
@ -405,8 +413,10 @@ async function getByRestrictedAccessToken(token) {
if (tokenEntry) {
const user = await getById(contextHelpers.getAdminContext(), tokenEntry.userId);
user.restrictedAccessMethod = tokenEntry.method;
user.restrictedAccessHandler = tokenEntry.handler;
user.restrictedAccessToken = tokenEntry.token;
user.restrictedAccessParams = tokenEntry.params;
return user;

5179
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -35,28 +35,28 @@
"lodash": "^4.17.11",
"mocha": "^5.2.0",
"phantomjs-prebuilt": "^2.1.16",
"selenium-webdriver": "^3.5.0",
"selenium-webdriver": "^3.6.0",
"url-pattern": "^1.0.3"
},
"optionalDependencies": {
"posix": "^4.1.2"
},
"dependencies": {
"aws-sdk": "^2.358.0",
"aws-sdk": "^2.383.0",
"bcrypt-nodejs": "0.0.3",
"bluebird": "^3.5.3",
"body-parser": "^1.18.3",
"bounce-handler": "7.3.2-fork.3",
"compression": "^1.7.3",
"config": "^2.0.1",
"config": "^3.0.1",
"connect-flash": "^0.1.1",
"connect-redis": "^3.4.0",
"cookie-parser": "^1.4.3",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"csurf": "^1.9.0",
"csv-parse": "^3.2.0",
"csv-stringify": "^4.3.1",
"csv-parse": "^4.3.0",
"csv-stringify": "^5.1.2",
"device": "^0.3.9",
"dompurify": "^1.0.8",
"escape-html": "^1.0.3",
@ -75,16 +75,16 @@
"he": "^1.2.0",
"html-to-text": "^4.0.0",
"humanize": "0.0.9",
"i18next": "^12.0.0",
"i18next": "^13.1.0",
"isemail": "^3.2.0",
"jsdom": "^13.0.0",
"juice": "^5.0.1",
"knex": "^0.15.2",
"jsdom": "^13.1.0",
"juice": "^5.1.0",
"knex": "^0.16.3",
"libmime": "^4.0.1",
"mailparser": "^2.4.3",
"memory-cache": "^0.2.0",
"mjml": "^4.2.0",
"moment": "^2.22.2",
"mjml": "^4.3.0",
"moment": "^2.23.0",
"moment-timezone": "^0.5.23",
"morgan": "^1.9.1",
"multer": "^1.4.1",
@ -93,12 +93,10 @@
"node-mocks-http": "^1.7.3",
"node-object-hash": "^1.4.1",
"nodeify": "^1.0.1",
"nodemailer": "^4.6.8",
"nodemailer": "^5.0.0",
"nodemailer-openpgp": "^1.2.0",
"npm": "^6.4.1",
"npmlog": "^4.1.2",
"nyc": "^13.1.0",
"openpgp": "^4.2.1",
"openpgp": "^4.4.3",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"premailer-api": "^1.0.4",
@ -106,7 +104,7 @@
"request-promise": "^4.2.2",
"serve-favicon": "^2.5.0",
"shortid": "^2.2.14",
"slugify": "^1.3.3",
"slugify": "^1.3.4",
"smtp-server": "^3.4.7",
"toml": "^2.3.3",
"try-require": "^1.2.1",

View file

@ -1,9 +1,7 @@
'use strict';
const config = require('config');
const passport = require('../../lib/passport');
const users = require('../../models/users');
const shares = require('../../models/shares');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');

View file

@ -3,6 +3,6 @@
const config = require('./config');
module.exports = {
client: 'mysql2',
client: 'mysql',
connection: config.mysql
};

View file

@ -22,7 +22,7 @@
{{#if mailtrainConfig}}
<script>
{{#if reactCsrfToken}}window.csfrToken = '{{reactCsrfToken}}';{{/if}}
{{#if reactCsrfToken}}window.csrfToken = '{{reactCsrfToken}}';{{/if}}
window.mailtrainConfig = {{{mailtrainConfig}}};
</script>

View file

@ -22,7 +22,7 @@
{{#if mailtrainConfig}}
<script>
{{#if reactCsrfToken}}window.csfrToken = '{{reactCsrfToken}}';{{/if}}
{{#if reactCsrfToken}}window.csrfToken = '{{reactCsrfToken}}';{{/if}}
window.mailtrainConfig = {{{mailtrainConfig}}};
</script>

View file

@ -16,7 +16,7 @@
{{#if mailtrainConfig}}
<script>
{{#if reactCsrfToken}}window.csfrToken = '{{reactCsrfToken}}';{{/if}}
{{#if reactCsrfToken}}window.csrfToken = '{{reactCsrfToken}}';{{/if}}
window.mailtrainConfig = {{{mailtrainConfig}}};
</script>

View file

@ -30,7 +30,7 @@
{{#if mailtrainConfig}}
<script>
{{#if reactCsrfToken}}window.csfrToken = '{{reactCsrfToken}}';{{/if}}
{{#if reactCsrfToken}}window.csrfToken = '{{reactCsrfToken}}';{{/if}}
window.mailtrainConfig = {{{mailtrainConfig}}};
</script>

View file

@ -40,7 +40,7 @@
{{/if}}
<script>
window.csfrToken = '{{reactCsrfToken}}';
window.csrfToken = '{{reactCsrfToken}}';
window.mailtrainConfig = {{{mailtrainConfig}}};
</script>