WiP on permissions

Doesn't run. This commit is just to backup the changes.
This commit is contained in:
Tomas Bures 2017-07-26 22:42:05 +03:00
parent 5df444f641
commit 89c9615592
37 changed files with 913 additions and 366 deletions

6
app.js
View file

@ -19,6 +19,7 @@ const handlebarsHelpers = require('./lib/handlebars-helpers');
const compression = require('compression'); const compression = require('compression');
const passport = require('./lib/passport'); const passport = require('./lib/passport');
const tools = require('./lib/tools'); const tools = require('./lib/tools');
const contextHelpers = require('./lib/context-helpers');
const routes = require('./routes/index'); const routes = require('./routes/index');
const lists = require('./routes/lists'); const lists = require('./routes/lists');
@ -220,10 +221,7 @@ app.use((req, res, next) => {
}); });
app.use((req, res, next) => { app.use((req, res, next) => {
req.context = { req.context = contextHelpers.getRequestContext(req);
user: req.user
};
next(); next();
}); });

View file

@ -35,7 +35,7 @@ export default class CUD extends Component {
} }
isEditGlobal() { isEditGlobal() {
return this.state.entityId === 1; return this.state.entityId === 1; /* Global namespace id */
} }
isDelete() { isDelete() {

View file

@ -245,6 +245,8 @@ export default class CUD extends Component {
} }
} }
// FIXME - filter namespaces by permission
return ( return (
<div> <div>
{edit && {edit &&

View file

@ -2,7 +2,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { translate } from 'react-i18next'; import { translate } from 'react-i18next';
import { withPageHelpers, Title, Toolbar, NavButton } from '../lib/page'; import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton } from '../lib/page';
import { Table } from '../lib/table'; import { Table } from '../lib/table';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import moment from 'moment'; import moment from 'moment';
@ -12,7 +12,42 @@ import { ReportState } from '../../../shared/reports';
@translate() @translate()
@withErrorHandling @withErrorHandling
@withPageHelpers @withPageHelpers
@requiresAuthenticatedUser
export default class List extends Component { export default class List extends Component {
constructor(props) {
super(props);
this.state = {};
}
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
createReport: {
entityTypeId: 'namespace',
requiredOperations: ['createReport']
},
executeReportTemplate: {
entityTypeId: 'reportTemplate',
requiredOperations: ['execute']
},
viewReportTemplate: {
entityTypeId: 'reportTemplate',
requiredOperations: ['view']
},
};
const result = await axios.post('/rest/permissions-check', request);
this.setState({
createPermitted: result.data.createReport && result.data.executeReportTemplate,
templatesPermitted: result.data.viewReportTemplate
});
}
componentDidMount() {
this.fetchPermissions();
}
@withAsyncErrorHandler @withAsyncErrorHandler
async stop(table, id) { async stop(table, id) {
@ -30,14 +65,18 @@ export default class List extends Component {
const t = this.props.t; const t = this.props.t;
const actions = data => { const actions = data => {
let view, startStop, refreshTimeout; const actions = [];
const perms = data[8];
const permsReportTemplate = data[9];
let viewContent, startStop, refreshTimeout;
const state = data[6]; const state = data[6];
const id = data[0]; const id = data[0];
const mimeType = data[7]; const mimeType = data[7];
if (state === ReportState.PROCESSING || state === ReportState.SCHEDULED) { if (state === ReportState.PROCESSING || state === ReportState.SCHEDULED) {
view = { viewContent = {
label: <span className="glyphicon glyphicon-hourglass" aria-hidden="true" title="Processing"></span>, label: <span className="glyphicon glyphicon-hourglass" aria-hidden="true" title="Processing"></span>,
}; };
@ -49,12 +88,12 @@ export default class List extends Component {
refreshTimeout = 1000; refreshTimeout = 1000;
} else if (state === ReportState.FINISHED) { } else if (state === ReportState.FINISHED) {
if (mimeType === 'text/html') { if (mimeType === 'text/html') {
view = { viewContent = {
label: <span className="glyphicon glyphicon-eye-open" aria-hidden="true" title="View"></span>, label: <span className="glyphicon glyphicon-eye-open" aria-hidden="true" title="View"></span>,
link: `reports/view/${id}` link: `reports/view/${id}`
}; };
} else if (mimeType === 'text/csv') { } else if (mimeType === 'text/csv') {
view = { viewContent = {
label: <span className="glyphicon glyphicon-download-alt" aria-hidden="true" title="Download"></span>, label: <span className="glyphicon glyphicon-download-alt" aria-hidden="true" title="Download"></span>,
href: `reports/download/${id}` href: `reports/download/${id}`
}; };
@ -66,7 +105,7 @@ export default class List extends Component {
}; };
} else if (state === ReportState.FAILED) { } else if (state === ReportState.FAILED) {
view = { viewContent = {
label: <span className="glyphicon glyphicon-thumbs-down" aria-hidden="true" title="Report generation failed"></span>, label: <span className="glyphicon glyphicon-thumbs-down" aria-hidden="true" title="Report generation failed"></span>,
}; };
@ -76,25 +115,38 @@ export default class List extends Component {
}; };
} }
return { if (perms.includes('viewContent')) {
refreshTimeout, actions.push(viewContent);
actions: [ }
view,
if (perms.includes('viewOutput')) {
actions.push(
{ {
label: <span className="glyphicon glyphicon-modal-window" aria-hidden="true" title="View console output"></span>, label: <span className="glyphicon glyphicon-modal-window" aria-hidden="true" title="View console output"></span>,
link: `reports/output/${id}` link: `reports/output/${id}`
},
startStop,
{
label: <span className="glyphicon glyphicon-wrench" aria-hidden="true" title="Edit"></span>,
link: `/reports/edit/${id}`
},
{
label: <span className="glyphicon glyphicon-share" aria-hidden="true" title="Share"></span>,
link: `/reports/share/${id}`
} }
] );
}; }
if (perms.includes('execute') && permsReportTemplate.includes('execute')) {
actions.push(startStop);
}
if (perms.includes('edit') && permsReportTemplate.includes('execute')) {
actions.push({
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
link: `/reports/edit/${id}`
});
}
if (perms.includes('share')) {
actions.push({
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
link: `/reports/share/${id}`
});
}
return { refreshTimeout, actions };
}; };
const columns = [ const columns = [
@ -106,11 +158,16 @@ export default class List extends Component {
{ data: 5, title: t('Namespace') } { data: 5, title: t('Namespace') }
]; ];
return ( return (
<div> <div>
<Toolbar> <Toolbar>
{this.state.createPermitted &&
<NavButton linkTo="/reports/create" className="btn-primary" icon="plus" label={t('Create Report')}/> <NavButton linkTo="/reports/create" className="btn-primary" icon="plus" label={t('Create Report')}/>
}
{this.state.templatesPermitted &&
<NavButton linkTo="/reports/templates" className="btn-primary" label={t('Report Templates')}/> <NavButton linkTo="/reports/templates" className="btn-primary" label={t('Report Templates')}/>
}
</Toolbar> </Toolbar>
<Title>{t('Reports')}</Title> <Title>{t('Reports')}</Title>

View file

@ -3,7 +3,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next'; import { translate, Trans } from 'react-i18next';
import { withPageHelpers, Title } from '../../lib/page' import { requiresAuthenticatedUser, withPageHelpers, Title } from '../../lib/page'
import { withForm, Form, FormSendMethod, InputField, TextArea, Dropdown, ACEEditor, ButtonRow, Button } from '../../lib/form'; import { withForm, Form, FormSendMethod, InputField, TextArea, Dropdown, ACEEditor, ButtonRow, Button } from '../../lib/form';
import axios from '../../lib/axios'; import axios from '../../lib/axios';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling'; import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
@ -14,6 +14,7 @@ import { validateNamespace, NamespaceSelect } from '../../lib/namespace';
@withForm @withForm
@withPageHelpers @withPageHelpers
@withErrorHandling @withErrorHandling
@requiresAuthenticatedUser
export default class CUD extends Component { export default class CUD extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -300,6 +301,8 @@ export default class CUD extends Component {
const t = this.props.t; const t = this.props.t;
const edit = this.props.edit; const edit = this.props.edit;
// FIXME - filter namespaces by permission
return ( return (
<div> <div>
{edit && {edit &&

View file

@ -4,30 +4,65 @@ import React, { Component } from 'react';
import { translate } from 'react-i18next'; import { translate } from 'react-i18next';
import { DropdownMenu } from '../../lib/bootstrap-components'; import { DropdownMenu } from '../../lib/bootstrap-components';
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, DropdownLink } from '../../lib/page'; import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, DropdownLink } from '../../lib/page';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import { Table } from '../../lib/table'; import { Table } from '../../lib/table';
import axios from '../../lib/axios';
import moment from 'moment'; import moment from 'moment';
@translate() @translate()
@withPageHelpers @withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser @requiresAuthenticatedUser
export default class List extends Component { export default class List extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {};
}
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
createReportTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createReportTemplate']
}
};
const result = await axios.post('/rest/permissions-check', request);
this.setState({
createPermitted: result.data.createReportTemplate
});
}
componentDidMount() {
this.fetchPermissions();
} }
render() { render() {
const t = this.props.t; const t = this.props.t;
const actions = data => [ const actions = data => {
{ const actions = [];
label: 'Edit', const perms = data[5];
if (perms.includes('view')) {
actions.push({
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
link: '/reports/templates/edit/' + data[0] link: '/reports/templates/edit/' + data[0]
}, });
{
label: 'Share',
link: '/reports/templates/share/' + data[0]
} }
];
if (perms.includes('share')) {
actions.push({
label: <span className="glyphicon glyphicon-share-alt" aria-hidden="true" title="Share"></span>,
link: '/reports/templates/share/' + data[0]
});
}
return actions;
};
const columns = [ const columns = [
{ data: 0, title: "#" }, { data: 0, title: "#" },
@ -39,6 +74,7 @@ export default class List extends Component {
return ( return (
<div> <div>
{this.state.createPermitted &&
<Toolbar> <Toolbar>
<DropdownMenu className="btn-primary" label={t('Create Report Template')}> <DropdownMenu className="btn-primary" label={t('Create Report Template')}>
<DropdownLink to="/reports/templates/create">{t('Blank')}</DropdownLink> <DropdownLink to="/reports/templates/create">{t('Blank')}</DropdownLink>
@ -47,6 +83,7 @@ export default class List extends Component {
<DropdownLink to="/reports/templates/create/export-list-csv">{t('Export List as CSV')}</DropdownLink> <DropdownLink to="/reports/templates/create/export-list-csv">{t('Export List as CSV')}</DropdownLink>
</DropdownMenu> </DropdownMenu>
</Toolbar> </Toolbar>
}
<Title>{t('Report Templates')}</Title> <Title>{t('Report Templates')}</Title>

View file

@ -4,12 +4,11 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { translate } from 'react-i18next'; import { translate } from 'react-i18next';
import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'; import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page';
import { withForm, Form, FormSendMethod, InputField, ButtonRow, Button, TreeTableSelect } from '../lib/form'; import { withForm, Form, FormSendMethod, InputField, ButtonRow, Button, TableSelect } from '../lib/form';
import axios from '../lib/axios'; import axios from '../lib/axios';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import interoperableErrors from '../../../shared/interoperable-errors'; import interoperableErrors from '../../../shared/interoperable-errors';
import passwordValidator from '../../../shared/password-validator'; import passwordValidator from '../../../shared/password-validator';
import validators from '../../../shared/validators';
import { ModalDialog } from '../lib/bootstrap-components'; import { ModalDialog } from '../lib/bootstrap-components';
import mailtrainConfig from 'mailtrainConfig'; import mailtrainConfig from 'mailtrainConfig';
import { validateNamespace, NamespaceSelect } from '../lib/namespace'; import { validateNamespace, NamespaceSelect } from '../lib/namespace';
@ -25,7 +24,9 @@ export default class CUD extends Component {
this.passwordValidator = passwordValidator(props.t); this.passwordValidator = passwordValidator(props.t);
this.state = {}; this.state = {
globalRoles: []
};
if (props.edit) { if (props.edit) {
this.state.entityId = parseInt(props.match.params.id); this.state.entityId = parseInt(props.match.params.id);
@ -48,6 +49,14 @@ export default class CUD extends Component {
return this.props.match.params.action === 'delete'; return this.props.match.params.action === 'delete';
} }
@withAsyncErrorHandler
async fetchGlobalRoles() {
const result = await axios.get('/rest/users-global-roles');
this.setState({
globalRoles: result.data
});
}
@withAsyncErrorHandler @withAsyncErrorHandler
async loadFormValues() { async loadFormValues() {
await this.getFormValuesFromURL(`/rest/users/${this.state.entityId}`, data => { await this.getFormValuesFromURL(`/rest/users/${this.state.entityId}`, data => {
@ -80,8 +89,6 @@ export default class CUD extends Component {
if (!username) { if (!username) {
state.setIn(['username', 'error'], t('User name must not be empty')); state.setIn(['username', 'error'], t('User name must not be empty'));
} else if (!validators.usernameValid(username)) {
state.setIn(['username', 'error'], t('User name may contain only the following characters: A-Z, a-z, 0-9, "_", "-", "." and may start only with A-Z, a-z, 0-9.'));
} else if (!usernameServerValidation || usernameServerValidation.exists) { } else if (!usernameServerValidation || usernameServerValidation.exists) {
state.setIn(['username', 'error'], t('The user name already exists in the system.')); state.setIn(['username', 'error'], t('The user name already exists in the system.'));
} else { } else {
@ -214,6 +221,20 @@ export default class CUD extends Component {
const userId = this.getFormValue('id'); const userId = this.getFormValue('id');
const canDelete = userId !== 1 && mailtrainConfig.userId !== userId; const canDelete = userId !== 1 && mailtrainConfig.userId !== userId;
const roles = mailtrainConfig.roles.global;
const rolesColumns = [
{ data: 1, title: "Name" },
{ data: 2, title: "Description" },
];
const rolesData = [];
for (const key in roles) {
const role = roles[key];
rolesData.push([ key, role.name, role.description ]);
}
return ( return (
<div> <div>
{edit && canDelete && {edit && canDelete &&
@ -229,10 +250,15 @@ export default class CUD extends Component {
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}> <Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="username" label={t('User Name')}/> <InputField id="username" label={t('User Name')}/>
{mailtrainConfig.isAuthMethodLocal &&
<div>
<InputField id="name" label={t('Full Name')}/> <InputField id="name" label={t('Full Name')}/>
<InputField id="email" label={t('Email')}/> <InputField id="email" label={t('Email')}/>
<InputField id="password" label={t('Password')} type="password"/> <InputField id="password" label={t('Password')} type="password"/>
<InputField id="password2" label={t('Repeat Password')} type="password"/> <InputField id="password2" label={t('Repeat Password')} type="password"/>
</div>
}
<TableSelect id="role" label={t('Role')} withHeader dropdown data={rolesData} columns={rolesColumns} selectionLabelIndex={1}/>
<NamespaceSelect/> <NamespaceSelect/>
<ButtonRow> <ButtonRow>

View file

@ -17,7 +17,12 @@ export default class List extends Component {
render() { render() {
const t = this.props.t; const t = this.props.t;
let actions; const actions = data => [
{
label: 'Edit',
link: '/users/edit/' + data[0]
}
];
const columns = [ const columns = [
{ data: 0, title: "#" }, { data: 0, title: "#" },
@ -26,13 +31,6 @@ export default class List extends Component {
if (mailtrainConfig.isAuthMethodLocal) { if (mailtrainConfig.isAuthMethodLocal) {
columns.push({ data: 2, title: "Full Name" }); columns.push({ data: 2, title: "Full Name" });
actions = data => [
{
label: 'Edit',
link: '/users/edit/' + data[0]
}
];
} }
columns.push({ data: 3, title: "Namespace" }); columns.push({ data: 3, title: "Namespace" });

View file

@ -122,6 +122,9 @@ uidTag="username"
# nameTag identifies the attribute to be used for user's full name # nameTag identifies the attribute to be used for user's full name
nameTag="username" nameTag="username"
passwordresetlink="" passwordresetlink=""
newUserRole="master"
# Global namespace id
newUserNamespaceId=1
[postfixbounce] [postfixbounce]
# Enable to allow writing Postfix bounce log to Mailtrain listener # Enable to allow writing Postfix bounce log to Mailtrain listener
@ -188,28 +191,72 @@ logger=false
browser="phantomjs" browser="phantomjs"
[roles.global.master]
name="Master"
admin=true
description="All permissions"
permissions=["rebuildPermissions"]
rootNamespaceRole="master"
[roles.reportTemplate.master] [roles.reportTemplate.master]
name="Master" name="Master"
description="All permissions" description="All permissions"
permissions=["view", "edit", "delete"] permissions=["view", "edit", "delete", "share", "execute"]
[roles.report.master] [roles.report.master]
name="Master" name="Master"
description="All permissions" description="All permissions"
permissions=["view", "edit", "delete"] permissions=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
[roles.list.master] [roles.list.master]
name="Master" name="Master"
description="All permissions" description="All permissions"
permissions=["view"] permissions=[]
[roles.namespace.master] [roles.namespace.master]
name="Master" name="Master"
description="All permissions" description="All permissions"
permissions=["view", "edit", "create", "delete", "create list"] permissions=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
[roles.namespace.master.childperms] [roles.namespace.master.children]
reportTemplate=["view", "edit", "delete"] reportTemplate=["view", "edit", "delete", "share", "execute"]
report=["view", "edit", "delete"] report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
list=["view"] list=[]
namespace=["view", "edit", "create", "delete", "create list"] namespace=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
[roles.global.editor]
name="Editor"
description="Anything under own namespace except operations related to sending and doing reports"
permissions=[]
ownNamespaceRole="editor"
[roles.reportTemplate.editor]
name="Editor"
description="Anything under own namespace except operations related to sending and doing reports"
permissions=[]
[roles.report.editor]
name="Editor"
description="Anything under own namespace except operations related to sending and doing reports"
permissions=["view", "viewContent", "viewOutput"]
[roles.list.editor]
name="Editor"
description="Anything under own namespace except operations related to sending and doing reports"
permissions=[]
[roles.namespace.editor]
name="Editor"
description="Anything under own namespace except operations related to sending and doing reports"
permissions=["view", "edit", "delete"]
[roles.namespace.editor.children]
reportTemplate=[]
report=["view", "viewContent", "viewOutput"]
list=[]
namespace=["view", "edit", "delete"]

View file

@ -21,6 +21,8 @@ const senders = require('./lib/senders');
const reportProcessor = require('./lib/report-processor'); const reportProcessor = require('./lib/report-processor');
const executor = require('./lib/executor'); const executor = require('./lib/executor');
const privilegeHelpers = require('./lib/privilege-helpers'); const privilegeHelpers = require('./lib/privilege-helpers');
const knex = require('./lib/knex');
const shares = require('./models/shares');
let port = config.www.port; let port = config.www.port;
let host = config.www.host; let host = config.www.host;
@ -38,19 +40,6 @@ app.set('port', port);
let server = http.createServer(app); let server = http.createServer(app);
// Check if database needs upgrading before starting the server
dbcheck(err => {
if (err) {
log.error('DB', err.message || err);
return process.exit(1);
}
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port, host);
});
server.on('error', err => { server.on('error', err => {
if (err.syscall !== 'listen') { if (err.syscall !== 'listen') {
throw err; throw err;
@ -145,3 +134,20 @@ server.on('listening', () => {
startNextServices(); startNextServices();
} }
}); });
// Check if database needs upgrading before starting the server
// First, the legacy migration
dbcheck(err => {
if (err) {
log.error('DB', err.message || err);
return process.exit(1);
}
// And now the current migration with Knex
knex.migrate.latest()
.then(() => shares.rebuildPermissions())
.then(() => server.listen(port, host)); // Listen on provided port, on all network interfaces.
});

29
lib/context-helpers.js Normal file
View file

@ -0,0 +1,29 @@
'use strict';
const knex = require('../lib/knex');
function getRequestContext(req) {
const context = {
user: req.user
};
return context;
}
function getServiceContext() {
const context = {
user: {
id: 1,
username: 'admin',
name: 'Service worker',
email: ''
}
};
return context;
}
module.exports = {
getRequestContext,
getServiceContext
};

View file

@ -1,36 +1,64 @@
'use strict'; 'use strict';
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const permissions = require('../lib/permissions');
async function ajaxList(params, queryFun, columns) { async function ajaxList(params, queryFun, columns, mapFun) {
return await knex.transaction(async (tx) => { return await knex.transaction(async (tx) => {
const query = queryFun(tx); const columnsNames = [];
const columnsSelect = [];
for (const col of columns) {
if (typeof col === 'string') {
columnsNames.push(col);
columnsSelect.push(col);
} else {
columnsNames.push(col.name);
if (col.raw) {
columnsSelect.push(tx.raw(col.raw));
} else if (col.query) {
columnsSelect.push(function () { return col.query(this); });
}
}
}
if (params.operation === 'getBy') { if (params.operation === 'getBy') {
query.whereIn(columns[parseInt(params.column)], params.values); const query = queryFun(tx);
query.select(columns); query.whereIn(columnsNames[parseInt(params.column)], params.values);
query.select(columnsSelect);
const rows = await query; const rows = await query;
const rowsOfArray = rows.map(row => Object.keys(row).map(key => row[key])); const rowsOfArray = rows.map(row => Object.keys(row).map(key => row[key]));
return rowsOfArray; return rowsOfArray;
} else { } else {
const recordsTotalQuery = query.clone().count('* as recordsTotal').first(); const whereFun = function() {
const recordsTotal = (await recordsTotalQuery).recordsTotal;
query.where(function() {
let searchVal = '%' + params.search.value.replace(/\\/g, '\\\\').replace(/([%_])/g, '\\$1') + '%'; let searchVal = '%' + params.search.value.replace(/\\/g, '\\\\').replace(/([%_])/g, '\\$1') + '%';
for (let colIdx = 0; colIdx < params.columns.length; colIdx++) { for (let colIdx = 0; colIdx < params.columns.length; colIdx++) {
const col = params.columns[colIdx]; const col = params.columns[colIdx];
if (col.searchable) { if (col.searchable) {
this.orWhere(columns[parseInt(col.data)], 'like', searchVal); this.orWhere(columnsNames[parseInt(col.data)], 'like', searchVal);
}
} }
} }
});
const recordsFilteredQuery = query.clone().count('* as recordsFiltered').first(); /* There are a few SQL peculiarities that make this query a bit weird:
- Group by (which is used in getting permissions) don't go well with count(*). Thus we run the actual query
as a sub-query and then count the number of results.
- SQL does not like if it have columns with the same name in the subquery. This happens multiple tables are joined.
To circumvent this, we select only the first column (whatever it is). Since this is not "distinct", it is supposed
to give us the right number of rows anyway.
*/
const recordsTotalQuery = tx.count('* as recordsTotal').from(function () { return queryFun(this).select(columnsSelect[0]).as('records'); }).first();
const recordsTotal = (await recordsTotalQuery).recordsTotal;
const recordsFilteredQuery = tx.count('* as recordsFiltered').from(function () { return queryFun(this).select(columnsSelect[0]).where(whereFun).as('records'); }).first();
const recordsFiltered = (await recordsFilteredQuery).recordsFiltered; const recordsFiltered = (await recordsFilteredQuery).recordsFiltered;
const query = queryFun(tx);
query.where(whereFun);
query.offset(parseInt(params.start)); query.offset(parseInt(params.start));
const limit = parseInt(params.length); const limit = parseInt(params.length);
@ -38,16 +66,25 @@ async function ajaxList(params, queryFun, columns) {
query.limit(limit); query.limit(limit);
} }
query.select(columns); query.select(columnsSelect);
for (const order of params.order) { for (const order of params.order) {
query.orderBy(columns[params.columns[order.column].data], order.dir); query.orderBy(columnsNames[params.columns[order.column].data], order.dir);
} }
query.options({rowsAsArray:true}); query.options({rowsAsArray:true});
const rows = await query; const rows = await query;
const rowsOfArray = rows.map(row => Object.keys(row).map(field => row[field])); const rowsOfArray = rows.map(row => {
const arr = Object.keys(row).map(field => row[field]);
if (mapFun) {
const result = mapFun(arr);
return result || arr;
} else {
return arr;
}
});
const result = { const result = {
draw: params.draw, draw: params.draw,
@ -61,6 +98,51 @@ async function ajaxList(params, queryFun, columns) {
}); });
} }
async function ajaxListWithPermissions(context, fetchSpecs, params, queryFun, columns) {
const permCols = [];
for (const fetchSpec of fetchSpecs) {
const entityType = permissions.getEntityType(fetchSpec.entityTypeId);
permCols.push({
name: `permissions_${fetchSpec.entityTypeId}`,
query: builder => builder
.from(entityType.permissionsTable)
.select(knex.raw('GROUP_CONCAT(operation SEPARATOR \';\')'))
.whereRaw(`${entityType.permissionsTable}.entity = ${entityType.entitiesTable}.id`)
.where(`${entityType.permissionsTable}.user`, context.user.id)
.as(`permissions_${fetchSpec.entityTypeId}`)
});
}
return await ajaxList(
params,
builder => {
let query = queryFun(builder);
for (const fetchSpec of fetchSpecs) {
const entityType = permissions.getEntityType(fetchSpec.entityTypeId);
query = query.innerJoin(
function () {
return this.from(entityType.permissionsTable).select('entity').where('user', context.user.id).whereIn('operation', fetchSpec.requiredOperations).as(`permitted__${fetchSpec.entityTypeId}`);
},
`permitted__${fetchSpec.entityTypeId}.entity`, `${entityType.entitiesTable}.id`)
}
return query;
},
[
...columns,
...permCols
],
data => {
for (let idx = 0; idx < fetchSpecs.length; idx++) {
data[columns.length + idx] = data[columns.length + idx].split(';');
}
}
);
}
module.exports = { module.exports = {
ajaxList ajaxList,
ajaxListWithPermissions
}; };

View file

@ -11,6 +11,4 @@ const knex = require('knex')({
// , debug: true // , debug: true
}); });
knex.migrate.latest();
module.exports = knex; module.exports = knex;

View file

@ -1,15 +1,15 @@
'use strict'; 'use strict';
let config = require('config'); const config = require('config');
let log = require('npmlog'); const log = require('npmlog');
let _ = require('./translate')._; const _ = require('./translate')._;
let util = require('util'); const util = require('util');
let passport = require('passport'); const passport = require('passport');
let LocalStrategy = require('passport-local').Strategy; const LocalStrategy = require('passport-local').Strategy;
let csrf = require('csurf'); const csrf = require('csurf');
let bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const users = require('../models/users'); const users = require('../models/users');
const { nodeifyFunction, nodeifyPromise } = require('./nodeify'); const { nodeifyFunction, nodeifyPromise } = require('./nodeify');
@ -104,20 +104,24 @@ if (config.ldap.enabled && LdapStrategy) {
id: user.id, id: user.id,
username: user.username, username: user.username,
name: profile[config.ldap.nameTag], name: profile[config.ldap.nameTag],
email: profile.mail email: profile.mail,
role: user.role
}; };
} catch (err) { } catch (err) {
if (err instanceof interoperableErrors.NotFoundError) { if (err instanceof interoperableErrors.NotFoundError) {
const userId = await users.createExternal({ const userId = await users.create({
username: profile[config.ldap.uidTag], username: profile[config.ldap.uidTag],
role: config.ldap.newUserRole,
namespace: config.ldap.newUserNamespaceId
}); });
return { return {
id: userId, id: userId,
username: profile[config.ldap.uidTag], username: profile[config.ldap.uidTag],
name: profile[config.ldap.nameTag], name: profile[config.ldap.nameTag],
email: profile.mail email: profile.mail,
role: config.ldap.newUserRole
}; };
} else { } else {
throw err; throw err;
@ -139,6 +143,6 @@ if (config.ldap.enabled && LdapStrategy) {
}))); })));
passport.serializeUser((user, done) => done(null, user.id)); passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser((id, done) => nodeifyPromise(users.getById(id), done)); passport.deserializeUser((id, done) => nodeifyPromise(users.getByIdNoPerms(id), done));
} }

View file

@ -1,75 +1,38 @@
'use strict'; 'use strict';
const config = require('config'); const entityTypes = {
namespace: {
entitiesTable: 'namespaces',
// FIXME - redo or delete sharesTable: 'shares_namespace',
permissionsTable: 'permissions_namespace'
/* },
class ReportTemplatePermission { report: {
constructor(name) { entitiesTable: 'reports',
this.name = name; sharesTable: 'shares_report',
this.entityType = 'report-template'; permissionsTable: 'permissions_report'
},
reportTemplate: {
entitiesTable: 'report_templates',
sharesTable: 'shares_report_template',
permissionsTable: 'permissions_report_template'
} }
}
const ReportTemplatePermissions = {
View: new ReportTemplatePermission('view'),
Edit: new ReportTemplatePermission('edit'),
Delete: new ReportTemplatePermission('delete')
}; };
function getEntityTypes() {
class ListPermission { return entityTypes;
constructor(name) {
this.name = name;
this.entityType = 'list';
}
} }
const ListPermissions = { function getEntityType(entityTypeId) {
View: new ListPermissions('view') const entityType = entityTypes[entityTypeId];
};
class NamespacePermission { if (!entityType) {
constructor(name) { throw new Error(`Unknown entity type ${entityTypeId}`);
this.name = name;
this.entityType = 'namespace';
}
} }
const NamespacePermissions = { return entityType
View: new NamespacePermission('view'),
Edit: new NamespacePermission('edit'),
Create: new NamespacePermission('create'),
Delete: new NamespacePermission('delete'),
CreateList: new NamespacePermission('create list')
};
*/
/*
async function can(context, operation, entityId) {
if (!context.user) {
return false;
} }
const result = await knex('permissions_' + operation.entityType).select(['id']).where({
entity: entityId,
user: context.user.id,
operation: operation.name
}).first();
return !!result;
}
async function buildPermissions() {
}
can(ctx, ListPermissions.View, 3)
can(ctx, NamespacePermissions.CreateList, 2)
can(ctx, ReportTemplatePermissions.ViewReport, 5)
*/
module.exports = { module.exports = {
getEntityTypes,
getEntityType
} }

View file

@ -3,6 +3,7 @@
const log = require('npmlog'); const log = require('npmlog');
const reports = require('../models/reports'); const reports = require('../models/reports');
const executor = require('./executor'); const executor = require('./executor');
const contextHelpers = require('../lib/context-helpers');
let runningWorkersCount = 0; let runningWorkersCount = 0;
let maxWorkersCount = 1; let maxWorkersCount = 1;
@ -99,7 +100,7 @@ async function tryStartWorkers() {
isStartingWorkers = false; isStartingWorkers = false;
} }
module.exports.start = async reportId => { module.exports.start = async (reportId) => {
if (!workers[reportId]) { if (!workers[reportId]) {
log.info('ReportProcessor', 'Scheduling report id: %s', reportId); log.info('ReportProcessor', 'Scheduling report id: %s', reportId);
await reports.updateFields(reportId, { state: reports.ReportState.SCHEDULED, last_run: null}); await reports.updateFields(reportId, { state: reports.ReportState.SCHEDULED, last_run: null});

View file

@ -5,7 +5,7 @@ const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors'); const interoperableErrors = require('../shared/interoperable-errors');
async function listDTAjax(params) { async function listDTAjax(params) {
return await dtHelpers.ajaxList(params, tx => tx('campaigns'), ['campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.status', 'campaigns.created']); return await dtHelpers.ajaxList(params, builder => builder.from('campaigns'), ['campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.status', 'campaigns.created']);
} }
async function getById(id) { async function getById(id) {

View file

@ -5,7 +5,7 @@ const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors'); const interoperableErrors = require('../shared/interoperable-errors');
async function listDTAjax(params) { async function listDTAjax(params) {
return await dtHelpers.ajaxList(params, tx => tx('lists'), ['lists.id', 'lists.name', 'lists.cid', 'lists.subscribers', 'lists.description']); return await dtHelpers.ajaxList(params, builder => builder.from('lists'), ['lists.id', 'lists.name', 'lists.cid', 'lists.subscribers', 'lists.description']);
} }
async function getById(id) { async function getById(id) {

View file

@ -16,7 +16,9 @@ function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys)); return hasher.hash(filterObject(entity, allowedKeys));
} }
async function getById(id) { async function getById(context, id) {
await shares.enforceEntityPermission(context, 'namespace', id, 'view');
const entity = await knex('namespaces').where('id', id).first(); const entity = await knex('namespaces').where('id', id).first();
if (!entity) { if (!entity) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
@ -25,15 +27,16 @@ async function getById(id) {
return entity; return entity;
} }
async function create(entity) { async function create(context, entity) {
await knex.transaction(async tx => { enforce(entity.namespace, 'Parent namespace must be set');
const id = await tx('namespaces').insert(filterObject(entity, allowedKeys)); await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createNamespace');
if (entity.namespace) { await knex.transaction(async tx => {
if (!await tx('namespaces').select(['id']).where('id', entity.namespace).first()) { if (!await tx('namespaces').select(['id']).where('id', entity.namespace).first()) {
throw new interoperableErrors.DependencyNotFoundError(); throw new interoperableErrors.DependencyNotFoundError();
} }
}
const id = await tx('namespaces').insert(filterObject(entity, allowedKeys));
// We don't have to rebuild all entity types, because no entity can be a child of the namespace at this moment. // 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 }); await shares.rebuildPermissions(tx, { entityTypeId: 'namespace', entityId: id });
@ -42,12 +45,13 @@ async function create(entity) {
}); });
} }
async function updateWithConsistencyCheck(entity) { async function updateWithConsistencyCheck(context, entity) {
enforce(entity.id !== 1 || entity.namespace === null, 'Cannot assign a parent to the root namespace.'); enforce(entity.id !== 1 || entity.namespace === null, 'Cannot assign a parent to the root namespace.');
await shares.enforceEntityPermission(context, 'namespace', entity.id, 'edit');
await knex.transaction(async tx => { await knex.transaction(async tx => {
const existing = await tx('namespaces').where('id', entity.id).first(); const existing = await tx('namespaces').where('id', entity.id).first();
if (!entity) { if (!existing) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
@ -75,8 +79,9 @@ async function updateWithConsistencyCheck(entity) {
}); });
} }
async function remove(id) { async function remove(context, id) {
enforce(id !== 1, 'Cannot delete the root namespace.'); enforce(id !== 1, 'Cannot delete the root namespace.');
await shares.enforceEntityPermission(context, 'namespace', id, 'delete');
await knex.transaction(async tx => { await knex.transaction(async tx => {
const childNs = await tx('namespaces').where('namespace', id).first(); const childNs = await tx('namespaces').where('namespace', id).first();

View file

@ -14,7 +14,9 @@ function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys)); return hasher.hash(filterObject(entity, allowedKeys));
} }
async function getById(id) { async function getById(context, id) {
await shares.enforceEntityPermission(context, 'reportTemplate', id, 'view');
const entity = await knex('report_templates').where('id', id).first(); const entity = await knex('report_templates').where('id', id).first();
if (!entity) { if (!entity) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
@ -23,15 +25,19 @@ async function getById(id) {
return entity; return entity;
} }
async function listDTAjax(params) { async function listDTAjax(context, params) {
return await dtHelpers.ajaxList( return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'reportTemplate', requiredOperations: ['view'] }],
params, params,
tx => tx('report_templates').innerJoin('namespaces', 'namespaces.id', 'report_templates.namespace'), builder => builder.from('report_templates').innerJoin('namespaces', 'namespaces.id', 'report_templates.namespace'),
[ 'report_templates.id', 'report_templates.name', 'report_templates.description', 'report_templates.created', 'namespaces.name' ] [ 'report_templates.id', 'report_templates.name', 'report_templates.description', 'report_templates.created', 'namespaces.name' ]
); );
} }
async function create(entity) { async function create(context, entity) {
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createReportTemplate');
await knex.transaction(async tx => { await knex.transaction(async tx => {
await namespaceHelpers.validateEntity(tx, entity); await namespaceHelpers.validateEntity(tx, entity);
@ -43,10 +49,12 @@ async function create(entity) {
}); });
} }
async function updateWithConsistencyCheck(entity) { async function updateWithConsistencyCheck(context, entity) {
await shares.enforceEntityPermission(context, 'reportTemplate', entity.id, 'edit');
await knex.transaction(async tx => { await knex.transaction(async tx => {
const existing = await tx('report_templates').where('id', entity.id).first(); const existing = await tx('report_templates').where('id', entity.id).first();
if (!entity) { if (!existing) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
@ -57,17 +65,26 @@ async function updateWithConsistencyCheck(entity) {
await namespaceHelpers.validateEntity(tx, entity); await namespaceHelpers.validateEntity(tx, entity);
if (existing.namespace !== entity.namespace) {
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createReport');
await shares.enforceEntityPermission(context, 'reportTemplate', entity.id, 'delete');
}
await tx('report_templates').where('id', entity.id).update(filterObject(entity, allowedKeys)); await tx('report_templates').where('id', entity.id).update(filterObject(entity, allowedKeys));
await shares.rebuildPermissions(tx, { entityTypeId: 'reportTemplate', entityId: entity.id }); await shares.rebuildPermissions(tx, { entityTypeId: 'reportTemplate', entityId: entity.id });
}); });
} }
async function remove(id) { async function remove(context, id) {
await shares.enforceEntityPermission(context, 'reportTemplate', id, 'delete');
await knex('report_templates').where('id', id).del(); await knex('report_templates').where('id', id).del();
} }
async function getUserFieldsById(id) { async function getUserFieldsById(context, id) {
await shares.enforceEntityPermission(context, 'reportTemplate', id, 'view');
const entity = await knex('report_templates').select(['user_fields']).where('id', id).first(); const entity = await knex('report_templates').select(['user_fields']).where('id', id).first();
if (!entity) { if (!entity) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();

View file

@ -18,7 +18,13 @@ function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys)); return hasher.hash(filterObject(entity, allowedKeys));
} }
async function getByIdWithTemplate(id) { async function getByIdWithTemplate(context, id) {
await shares.enforceEntityPermission(context, 'report', id, 'view');
return await getByIdWithTemplateNoPerms(id);
}
async function getByIdWithTemplateNoPerms(id) {
const entity = await knex('reports') const entity = await knex('reports')
.where('reports.id', id) .where('reports.id', id)
.innerJoin('report_templates', 'reports.report_template', 'report_templates.id') .innerJoin('report_templates', 'reports.report_template', 'report_templates.id')
@ -35,17 +41,28 @@ async function getByIdWithTemplate(id) {
return entity; return entity;
} }
async function listDTAjax(params) { async function listDTAjax(context, params) {
return await dtHelpers.ajaxList( return await dtHelpers.ajaxListWithPermissions(
context,
[
{ entityTypeId: 'report', requiredOperations: ['view'] },
{ entityTypeId: 'reportTemplate', requiredOperations: ['view'] }
],
params, params,
tx => tx('reports') builder => builder.from('reports')
.innerJoin('report_templates', 'reports.report_template', 'report_templates.id') .innerJoin('report_templates', 'reports.report_template', 'report_templates.id')
.innerJoin('namespaces', 'namespaces.id', 'reports.namespace'), .innerJoin('namespaces', 'namespaces.id', 'reports.namespace'),
['reports.id', 'reports.name', 'report_templates.name', 'reports.description', 'reports.last_run', 'namespaces.name', 'reports.state', 'report_templates.mime_type'] [
'reports.id', 'reports.name', 'report_templates.name', 'reports.description',
'reports.last_run', 'namespaces.name', 'reports.state', 'report_templates.mime_type'
]
); );
} }
async function create(entity) { async function create(context, entity) {
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createReport');
await shares.enforceEntityPermission(context, 'reportTemplate', entity.report_template, 'execute');
let id; let id;
await knex.transaction(async tx => { await knex.transaction(async tx => {
await namespaceHelpers.validateEntity(tx, entity); await namespaceHelpers.validateEntity(tx, entity);
@ -66,10 +83,13 @@ async function create(entity) {
return id; return id;
} }
async function updateWithConsistencyCheck(entity) { async function updateWithConsistencyCheck(context, entity) {
await shares.enforceEntityPermission(context, 'report', entity.id, 'edit');
await shares.enforceEntityPermission(context, 'reportTemplate', entity.report_template, 'execute');
await knex.transaction(async tx => { await knex.transaction(async tx => {
const existing = await tx('reports').where('id', entity.id).first(); const existing = await tx('reports').where('id', entity.id).first();
if (!entity) { if (!existing) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
@ -86,6 +106,11 @@ async function updateWithConsistencyCheck(entity) {
await namespaceHelpers.validateEntity(tx, entity); await namespaceHelpers.validateEntity(tx, entity);
if (existing.namespace !== entity.namespace) {
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createReport');
await shares.enforceEntityPermission(context, 'report', entity.id, 'delete');
}
entity.params = JSON.stringify(entity.params); entity.params = JSON.stringify(entity.params);
const filteredUpdates = filterObject(entity, allowedKeys); const filteredUpdates = filterObject(entity, allowedKeys);
@ -101,7 +126,9 @@ async function updateWithConsistencyCheck(entity) {
await reportProcessor.start(entity.id); await reportProcessor.start(entity.id);
} }
async function remove(id) { async function remove(context, id) {
await shares.enforceEntityPermission(context, 'report', id, 'delete');
await knex('reports').where('id', id).del(); await knex('reports').where('id', id).del();
} }
@ -132,7 +159,7 @@ function customFieldName(id) {
return id.replace(/MERGE_/, 'CUSTOM_').toLowerCase(); return id.replace(/MERGE_/, 'CUSTOM_').toLowerCase();
} }
async function getCampaignResults(campaign, select, extra) { async function getCampaignResults(context, campaign, select, extra) {
const fieldList = await fields.list(campaign.list); const fieldList = await fields.list(campaign.list);
const fieldsMapping = fieldList.reduce((map, field) => { const fieldsMapping = fieldList.reduce((map, field) => {
@ -174,6 +201,7 @@ module.exports = {
ReportState, ReportState,
hash, hash,
getByIdWithTemplate, getByIdWithTemplate,
getByIdWithTemplateNoPerms,
listDTAjax, listDTAjax,
create, create,
updateWithConsistencyCheck, updateWithConsistencyCheck,

View file

@ -1,55 +1,39 @@
'use strict'; 'use strict';
let _ = require('../lib/translate')._;
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const config = require('config'); const config = require('config');
const { enforce } = require('../lib/helpers'); const { enforce } = require('../lib/helpers');
const dtHelpers = require('../lib/dt-helpers'); const dtHelpers = require('../lib/dt-helpers');
const permissions = require('../lib/permissions');
const interoperableErrors = require('../shared/interoperable-errors'); const interoperableErrors = require('../shared/interoperable-errors');
const entityTypes = {
namespace: {
entitiesTable: 'namespaces',
sharesTable: 'shares_namespace',
permissionsTable: 'permissions_namespace'
},
report: {
entitiesTable: 'reports',
sharesTable: 'shares_report',
permissionsTable: 'permissions_report'
},
reportTemplate: {
entitiesTable: 'report_templates',
sharesTable: 'shares_report_template',
permissionsTable: 'permissions_report_template'
}
};
function getEntityType(entityTypeId) { async function listDTAjax(context, entityTypeId, entityId, params) {
const entityType = entityTypes[entityTypeId]; const entityType = permissions.getEntityType(entityTypeId);
if (!entityType) { await enforceEntityPermission(context, entityTypeId, entityId, 'share');
throw new Error(`Unknown entity type ${entityTypeId}`);
return await dtHelpers.ajaxList(params, builder => builder.from(entityType.sharesTable).innerJoin('users', entityType.sharesTable + '.user', 'users.id').where(`${entityType.sharesTable}.entity`, entityId), [entityType.sharesTable + '.id', 'users.username', 'users.name', entityType.sharesTable + '.role', 'users.id']);
} }
return entityType async function listUnassignedUsersDTAjax(context, entityTypeId, entityId, params) {
} const entityType = permissions.getEntityType(entityTypeId);
async function listDTAjax(entityTypeId, entityId, params) { await enforceEntityPermission(context, entityTypeId, entityId, 'share');
const entityType = getEntityType(entityTypeId);
return await dtHelpers.ajaxList(params, tx => tx(entityType.sharesTable).innerJoin('users', entityType.sharesTable + '.user', 'users.id').where(`${entityType.sharesTable}.entity`, entityId), [entityType.sharesTable + '.id', 'users.username', 'users.name', entityType.sharesTable + '.role', 'users.id']);
}
async function listUnassignedUsersDTAjax(entityTypeId, entityId, params) {
const entityType = getEntityType(entityTypeId);
return await dtHelpers.ajaxList( return await dtHelpers.ajaxList(
params, params,
tx => tx('users').whereNotExists(function() { return this.select('*').from(entityType.sharesTable).whereRaw(`users.id = ${entityType.sharesTable}.user`).andWhere(`${entityType.sharesTable}.entity`, entityId); }), 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']); ['users.id', 'users.username', 'users.name']);
} }
async function assign(entityTypeId, entityId, userId, role) { async function assign(context, entityTypeId, entityId, userId, role) {
const entityType = getEntityType(entityTypeId); const entityType = permissions.getEntityType(entityTypeId);
await enforceEntityPermission(context, entityTypeId, entityId, 'share');
await knex.transaction(async tx => { await knex.transaction(async tx => {
enforce(await tx('users').where('id', userId).select('id').first(), 'Invalid user id'); 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'); enforce(await tx(entityType.entitiesTable).where('id', entityId).select('id').first(), 'Invalid entity id');
@ -79,26 +63,92 @@ async function assign(entityTypeId, entityId, userId, role) {
}); });
} }
async function rebuildPermissions(tx, restriction) { async function _rebuildPermissions(tx, restriction) {
restriction = restriction || {}; const namespaceEntityType = permissions.getEntityType('namespace');
// Collect entity types we care about
let restrictedEntityTypes; let restrictedEntityTypes;
if (restriction.entityTypeId) { if (restriction.entityTypeId) {
const entityType = permissions.getEntityType(restriction.entityTypeId);
restrictedEntityTypes = { restrictedEntityTypes = {
[restriction.entityTypeId]: entityTypes[restriction.entityTypeId] [restriction.entityTypeId]: entityType
}; };
} else { } else {
restrictedEntityTypes = entityTypes; restrictedEntityTypes = permissions.getEntityTypes();
} }
const namespaces = await tx('namespaces').select(['id', 'namespace']); // 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;
}
}
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('user', 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`)
.where(`${namespaceEntityType.sharesTable}.entity`, 1 /* Global namespace id */)
.select(['users.id', 'users.role as userRole', `${namespaceEntityType.sharesTable}.role`]);
if (restriction.userId) {
usersWithRoleInRootNamespaceQuery.andWhere('user', 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: // nsMap is a map of namespaces - each of the following shape:
// .id - id of the namespace // .id - id of the namespace
// .namespace - id of the parent or null if no parent // .namespace - id of the parent or null if no parent
// .userPermissions - Map userId -> [entityTypeId] -> array of permissions // .userPermissions - Map userId -> [entityTypeId] -> array of permissions
// .transitiveUserPermissions - the same as above, but taking into account transitive permission obtained from namespace parents // .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(); const nsMap = new Map();
for (const namespace of namespaces) { for (const namespace of namespaces) {
namespace.userPermissions = new Map(); namespace.userPermissions = new Map();
@ -106,9 +156,9 @@ async function rebuildPermissions(tx, restriction) {
} }
// This populates .userPermissions // This populates .userPermissions
const nsSharesQuery = tx(entityTypes.namespace.sharesTable).select(['entity', 'user', 'role']); const nsSharesQuery = tx(namespaceEntityType.sharesTable).select(['entity', 'user', 'role']);
if (restriction.userId) { if (restriction.userId) {
nsSharesQuery.andWhere('user', restriction.userId); nsSharesQuery.where('user', restriction.userId);
} }
const nsShares = await nsSharesQuery; const nsShares = await nsSharesQuery;
@ -120,10 +170,10 @@ async function rebuildPermissions(tx, restriction) {
for (const entityTypeId in restrictedEntityTypes) { for (const entityTypeId in restrictedEntityTypes) {
if (config.roles.namespace[nsShare.role] && if (config.roles.namespace[nsShare.role] &&
config.roles.namespace[nsShare.role].childperms && config.roles.namespace[nsShare.role].children &&
config.roles.namespace[nsShare.role].childperms[entityTypeId]) { config.roles.namespace[nsShare.role].children[entityTypeId]) {
userPerms[entityTypeId] = new Set(config.roles.namespace[nsShare.role].childperms[entityTypeId]); userPerms[entityTypeId] = new Set(config.roles.namespace[nsShare.role].children[entityTypeId]);
} else { } else {
userPerms[entityTypeId] = new Set(); userPerms[entityTypeId] = new Set();
@ -174,22 +224,22 @@ async function rebuildPermissions(tx, restriction) {
} }
// This reads direct shares from DB, joins it with the permissions from namespaces and stores the permissions into DB // This reads direct shares from DB, joins each with the permissions from namespaces and stores the permissions into DB
for (const entityTypeId in restrictedEntityTypes) { for (const entityTypeId in restrictedEntityTypes) {
const entityType = restrictedEntityTypes[entityTypeId]; const entityType = restrictedEntityTypes[entityTypeId];
const expungeQuery = tx(entityType.permissionsTable).del(); const expungeQuery = tx(entityType.permissionsTable).del();
if (restriction.entityId) { if (restriction.entityId) {
expungeQuery.andWhere('entity', restriction.entityId); expungeQuery.where('entity', restriction.entityId);
} }
if (restriction.userId) { if (restriction.userId) {
expungeQuery.andWhere('user', restriction.userId); expungeQuery.where('user', restriction.userId);
} }
await expungeQuery; await expungeQuery;
const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace']); const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace']);
if (restriction.entityId) { if (restriction.entityId) {
entitiesQuery.andWhere('id', restriction.entityId); entitiesQuery.where('id', restriction.entityId);
} }
const entities = await entitiesQuery; const entities = await entitiesQuery;
@ -199,7 +249,7 @@ async function rebuildPermissions(tx, restriction) {
if (entity.namespace) { // The root namespace has not parent namespace, thus the test if (entity.namespace) { // The root namespace has not parent namespace, thus the test
const transitiveUserPermissions = nsMap.get(entity.namespace).transitiveUserPermissions; const transitiveUserPermissions = nsMap.get(entity.namespace).transitiveUserPermissions;
for (const transitivePermsPair of transitiveUserPermissions.entries()) { for (const transitivePermsPair of transitiveUserPermissions.entries()) {
permsPerUser.set(transitivePermsPair[0], [...transitivePermsPair[1][entityTypeId]]); permsPerUser.set(transitivePermsPair[0], new Set(transitivePermsPair[1][entityTypeId]));
} }
} }
@ -242,9 +292,96 @@ async function rebuildPermissions(tx, restriction) {
} }
} }
async function rebuildPermissions(tx, restriction) {
restriction = restriction || {};
if (tx) {
await _rebuildPermissions(tx, restriction);
} else {
await knex.transaction(async tx => {
await _rebuildPermissions(tx, restriction);
});
}
}
function throwPermissionDenied() {
throw new interoperableErrors.PermissionDeniedError(_('Permission denied'));
}
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 = await 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 _checkEntityPermission(context, entityTypeId, entityId, requiredOperations);
}
async function checkTypePermission(context, entityTypeId, requiredOperations) {
return await _checkEntityPermission(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 = { module.exports = {
listDTAjax, listDTAjax,
listUnassignedUsersDTAjax, listUnassignedUsersDTAjax,
assign, assign,
rebuildPermissions rebuildPermissions,
enforceEntityPermission,
enforceTypePermission,
checkEntityPermission,
checkTypePermission,
enforceGlobalPermission,
throwPermissionDenied
}; };

View file

@ -1,11 +1,11 @@
'use strict'; 'use strict';
const config = require('config');
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const hasher = require('node-object-hash')(); const hasher = require('node-object-hash')();
const { enforce, filterObject } = require('../lib/helpers'); const { enforce, filterObject } = require('../lib/helpers');
const interoperableErrors = require('../shared/interoperable-errors'); const interoperableErrors = require('../shared/interoperable-errors');
const passwordValidator = require('../shared/password-validator')(); const passwordValidator = require('../shared/password-validator')();
const validators = require('../shared/validators');
const dtHelpers = require('../lib/dt-helpers'); const dtHelpers = require('../lib/dt-helpers');
const tools = require('../lib/tools-async'); const tools = require('../lib/tools-async');
let crypto = require('crypto'); let crypto = require('crypto');
@ -26,20 +26,19 @@ const passport = require('../lib/passport');
const namespaceHelpers = require('../lib/namespace-helpers'); const namespaceHelpers = require('../lib/namespace-helpers');
const allowedKeys = new Set(['username', 'name', 'email', 'password', 'namespace']); const allowedKeys = new Set(['username', 'name', 'email', 'password', 'namespace', 'role']);
const allowedKeysExternal = new Set(['username']);
const ownAccountAllowedKeys = new Set(['name', 'email', 'password']); const ownAccountAllowedKeys = new Set(['name', 'email', 'password']);
const hashKeys = new Set(['username', 'name', 'email', 'namespace']); const allowedKeysExternal = new Set(['username', 'namespace', 'role']);
const hashKeys = new Set(['username', 'name', 'email', 'namespace', 'role']);
const defaultNamespace = 1; const shares = require('../../models/shares');
function hash(entity) { function hash(entity) {
return hasher.hash(filterObject(entity, hashKeys)); return hasher.hash(filterObject(entity, hashKeys));
} }
async function _getBy(key, value, extraColumns) { async function _getBy(context, key, value, extraColumns) {
const columns = ['id', 'username', 'name', 'email', 'namespace']; const columns = ['id', 'username', 'name', 'email', 'namespace', 'role'];
if (extraColumns) { if (extraColumns) {
columns.push(...extraColumns); columns.push(...extraColumns);
@ -48,19 +47,35 @@ async function _getBy(key, value, extraColumns) {
const user = await knex('users').select(columns).where(key, value).first(); const user = await knex('users').select(columns).where(key, value).first();
if (!user) { if (!user) {
if (context) {
shares.throwPermissionDenied();
} else {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
}
if (context) {
await shares.enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers');
}
return user; return user;
} }
async function getById(id) { async function getById(context, id) {
return await _getBy('id', id); return await _getBy(context, 'id', id);
} }
async function serverValidate(data, isOwnAccount) { async function getByIdNoPerms(id) {
return await _getBy(null, 'id', id);
}
async function serverValidate(context, data, isOwnAccount) {
const result = {}; const result = {};
if (!isOwnAccount) {
await shares.enforceTypePermission(context, 'namespace', 'manageUsers');
}
if (!isOwnAccount && data.username) { if (!isOwnAccount && data.username) {
const query = knex('users').select(['id']).where('username', data.username); const query = knex('users').select(['id']).where('username', data.username);
@ -100,11 +115,17 @@ async function serverValidate(data, isOwnAccount) {
return result; return result;
} }
async function listDTAjax(params) { async function listDTAjax(context, params) {
return await dtHelpers.ajaxList( return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'namespace', requiredOperations: ['manageUsers'] }],
params, params,
tx => tx('users').innerJoin('namespaces', 'namespaces.id', 'users.namespace'), builder => builder.from('users').innerJoin('namespaces', 'namespaces.id', 'users.namespace'),
['users.id', 'users.username', 'users.name', 'namespaces.name'] ['users.id', 'users.username', 'users.name', 'namespaces.name', 'users.role'],
data => {
const role = data[4];
data[4] = config.roles.global[role] ? config.roles.global[role].name : role;
}
); );
} }
@ -124,8 +145,6 @@ async function _validateAndPreprocess(tx, user, isCreate, isOwnAccount) {
if (!isOwnAccount) { if (!isOwnAccount) {
enforce(validators.usernameValid(user.username), 'Invalid username');
const otherUserWithSameUsernameQuery = tx('users').where('username', user.username); const otherUserWithSameUsernameQuery = tx('users').where('username', user.username);
if (user.id) { if (user.id) {
otherUserWithSameUsernameQuery.andWhereNot('id', user.id); otherUserWithSameUsernameQuery.andWhereNot('id', user.id);
@ -136,6 +155,7 @@ async function _validateAndPreprocess(tx, user, isCreate, isOwnAccount) {
} }
} }
enforce(user.role in config.roles.global, 'Unknown role');
enforce(!isCreate || user.password.length > 0, 'Password not set'); enforce(!isCreate || user.password.length > 0, 'Password not set');
@ -153,70 +173,89 @@ async function _validateAndPreprocess(tx, user, isCreate, isOwnAccount) {
} }
async function create(user) { async function create(user) {
enforce(passport.isAuthMethodLocal, 'Local user management is required'); await shares.enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers');
await knex.transaction(async tx => { await knex.transaction(async tx => {
if (passport.isAuthMethodLocal) {
await _validateAndPreprocess(tx, user, true); await _validateAndPreprocess(tx, user, true);
const userId = await tx('users').insert(filterObject(user, allowedKeys)); const userId = await tx('users').insert(filterObject(user, allowedKeys));
return userId; return userId;
});
}
async function createExternal(user) {
enforce(!passport.isAuthMethodLocal, 'External user management is required');
} else {
const filteredUser = filterObject(user, allowedKeysExternal); const filteredUser = filterObject(user, allowedKeysExternal);
filteredUser.namespace = defaultNamespace; enforce(user.role in config.roles.global, 'Unknown role');
await namespaceHelpers.validateEntity(tx, user);
const userId = await knex('users').insert(filteredUser); const userId = await knex('users').insert(filteredUser);
return userId; return userId;
} }
});
async function updateWithConsistencyCheck(user, isOwnAccount) {
enforce(passport.isAuthMethodLocal, 'Local user management is required');
await knex.transaction(async tx => {
await _validateAndPreprocess(tx, user, false, isOwnAccount);
const existingUser = await tx('users').where('id', user.id).first();
if (!user) {
throw new interoperableErrors.NotFoundError();
} }
const existingUserHash = hash(existingUser); async function updateWithConsistencyCheck(user, isOwnAccount) {
if (existingUserHash !== user.originalHash) { await knex.transaction(async tx => {
const existing = await tx('users').where('id', user.id).first();
if (!existing) {
shares.throwPermissionDenied();
}
const existingHash = hash(existing);
if (existingHash !== user.originalHash) {
throw new interoperableErrors.ChangedError(); throw new interoperableErrors.ChangedError();
} }
if (!isOwnAccount) {
await shares.enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers');
await shares.enforceEntityPermission(context, 'namespace', existing.namespace, 'manageUsers');
}
if (passport.isAuthMethodLocal) {
await _validateAndPreprocess(tx, user, false, isOwnAccount);
if (isOwnAccount && user.password) { if (isOwnAccount && user.password) {
if (!await bcryptCompare(user.currentPassword, existingUser.password)) { if (!await bcryptCompare(user.currentPassword, existing.password)) {
throw new interoperableErrors.IncorrectPasswordError(); throw new interoperableErrors.IncorrectPasswordError();
} }
} }
await tx('users').where('id', user.id).update(filterObject(user, isOwnAccount ? ownAccountAllowedKeys : allowedKeys)); await tx('users').where('id', user.id).update(filterObject(user, isOwnAccount ? ownAccountAllowedKeys : allowedKeys));
} else {
enforce(isOwnAccount, 'Local user management is required');
enforce(user.role in config.roles.global, 'Unknown role');
await namespaceHelpers.validateEntity(tx, user);
await tx('users').where('id', user.id).update(filterObject(user, allowedKeysExternal));
}
}); });
} }
async function remove(context, userId) { async function remove(context, userId) {
enforce(passport.isAuthMethodLocal, 'Local user management is required');
enforce(userId !== 1, 'Admin cannot be deleted'); enforce(userId !== 1, 'Admin cannot be deleted');
enforce(context.user.id !== userId, 'User cannot delete himself/herself'); enforce(context.user.id !== userId, 'User cannot delete himself/herself');
await knex.transaction(async tx => {
const existing = await tx('users').where('id', userId).first();
if (!existing) {
shares.throwPermissionDenied();
}
await shares.enforceEntityPermission(context, 'namespace', existing.namespace, 'manageUsers');
await knex('users').where('id', userId).del(); await knex('users').where('id', userId).del();
});
} }
async function getByAccessToken(accessToken) { async function getByAccessToken(accessToken) {
return await _getBy('access_token', accessToken); return await _getBy(null, 'access_token', accessToken);
} }
async function getByUsername(username) { async function getByUsername(username) {
return await _getBy('username', username); return await _getBy(null, 'username', username);
} }
async function getByUsernameIfPasswordMatch(username, password) { async function getByUsernameIfPasswordMatch(username, password) {
try { try {
const user = await _getBy('username', username, ['password']); const user = await _getBy(null, 'username', username, ['password']);
if (!await bcryptCompare(password, user.password)) { if (!await bcryptCompare(password, user.password)) {
throw new interoperableErrors.IncorrectPasswordError(); throw new interoperableErrors.IncorrectPasswordError();
@ -234,19 +273,13 @@ async function getByUsernameIfPasswordMatch(username, password) {
} }
async function getAccessToken(userId) { async function getAccessToken(userId) {
const user = await _getBy('id', userId, ['access_token']); const user = await _getBy(null, 'id', userId, ['access_token']);
return user.access_token; return user.access_token;
} }
async function resetAccessToken(userId) { async function resetAccessToken(userId) {
const token = crypto.randomBytes(20).toString('hex').toLowerCase(); const token = crypto.randomBytes(20).toString('hex').toLowerCase();
await knex('users').where({id: userId}).update({access_token: token});
const affectedRows = await knex('users').where({id: userId}).update({access_token: token});
if (!affectedRows) {
throw new interoperableErrors.NotFoundError();
}
return token; return token;
} }
@ -331,9 +364,9 @@ module.exports = {
remove, remove,
updateWithConsistencyCheck, updateWithConsistencyCheck,
create, create,
createExternal,
hash, hash,
getById, getById,
getByIdNoPerms,
serverValidate, serverValidate,
getByAccessToken, getByAccessToken,
getByUsername, getByUsername,

View file

@ -4,11 +4,14 @@ const passport = require('../lib/passport');
const _ = require('../lib/translate')._; const _ = require('../lib/translate')._;
const reports = require('../models/reports'); const reports = require('../models/reports');
const fileHelpers = require('../lib/file-helpers'); const fileHelpers = require('../lib/file-helpers');
const shares = require('../models/shares');
const router = require('../lib/router-async').create(); const router = require('../lib/router-async').create();
router.getAsync('/download/:id', passport.loggedIn, async (req, res) => { router.getAsync('/download/:id', passport.loggedIn, async (req, res) => {
const report = await reports.getByIdWithTemplate(req.params.id); await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewContent');
const report = await reports.getByIdWithTemplateNoPerms(req.params.id);
if (report.state == reports.ReportState.FINISHED) { if (report.state == reports.ReportState.FINISHED) {
const headers = { const headers = {

View file

@ -8,7 +8,7 @@ const router = require('../../lib/router-async').create();
router.getAsync('/account', passport.loggedIn, async (req, res) => { router.getAsync('/account', passport.loggedIn, async (req, res) => {
const user = await users.getById(req.user.id); const user = await users.getByIdNoPerms(req.user.id);
user.hash = users.hash(user); user.hash = users.hash(user);
return res.json(user); return res.json(user);
}); });

View file

@ -8,7 +8,7 @@ const router = require('../../lib/router-async').create();
router.getAsync('/namespaces/:nsId', passport.loggedIn, async (req, res) => { router.getAsync('/namespaces/:nsId', passport.loggedIn, async (req, res) => {
const ns = await namespaces.getById(req.params.nsId); const ns = await namespaces.getById(req.context, req.params.nsId);
ns.hash = namespaces.hash(ns); ns.hash = namespaces.hash(ns);
@ -16,7 +16,7 @@ router.getAsync('/namespaces/:nsId', passport.loggedIn, async (req, res) => {
}); });
router.postAsync('/namespaces', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.postAsync('/namespaces', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await namespaces.create(req.body); await namespaces.create(req.context, req.body);
return res.json(); return res.json();
}); });
@ -24,16 +24,19 @@ router.putAsync('/namespaces/:nsId', passport.loggedIn, passport.csrfProtection,
const ns = req.body; const ns = req.body;
ns.id = parseInt(req.params.nsId); ns.id = parseInt(req.params.nsId);
await namespaces.updateWithConsistencyCheck(ns); await namespaces.updateWithConsistencyCheck(req.context, ns);
return res.json(); return res.json();
}); });
router.deleteAsync('/namespaces/:nsId', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.deleteAsync('/namespaces/:nsId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await namespaces.remove(req.params.nsId); await namespaces.remove(req.context, req.params.nsId);
return res.json(); return res.json();
}); });
router.getAsync('/namespaces-tree', passport.loggedIn, async (req, res) => { router.getAsync('/namespaces-tree', passport.loggedIn, async (req, res) => {
// FIXME - process permissions
const entries = {}; const entries = {};
let root; // Only the Root namespace is without a parent let root; // Only the Root namespace is without a parent
const rows = await namespaces.list(); const rows = await namespaces.list();

View file

@ -8,13 +8,13 @@ const router = require('../../lib/router-async').create();
router.getAsync('/report-templates/:reportTemplateId', passport.loggedIn, async (req, res) => { router.getAsync('/report-templates/:reportTemplateId', passport.loggedIn, async (req, res) => {
const reportTemplate = await reportTemplates.getById(req.params.reportTemplateId); const reportTemplate = await reportTemplates.getById(req.context, req.params.reportTemplateId);
reportTemplate.hash = reportTemplates.hash(reportTemplate); reportTemplate.hash = reportTemplates.hash(reportTemplate);
return res.json(reportTemplate); return res.json(reportTemplate);
}); });
router.postAsync('/report-templates', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.postAsync('/report-templates', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await reportTemplates.create(req.body); await reportTemplates.create(req.context, req.body);
return res.json(); return res.json();
}); });
@ -22,23 +22,27 @@ router.putAsync('/report-templates/:reportTemplateId', passport.loggedIn, passpo
const reportTemplate = req.body; const reportTemplate = req.body;
reportTemplate.id = parseInt(req.params.reportTemplateId); reportTemplate.id = parseInt(req.params.reportTemplateId);
await reportTemplates.updateWithConsistencyCheck(reportTemplate); await reportTemplates.updateWithConsistencyCheck(req.context, reportTemplate);
return res.json(); return res.json();
}); });
router.deleteAsync('/report-templates/:reportTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.deleteAsync('/report-templates/:reportTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await reportTemplates.remove(req.params.reportTemplateId); await reportTemplates.remove(req.context, req.params.reportTemplateId);
return res.json(); return res.json();
}); });
router.postAsync('/report-templates-table', passport.loggedIn, async (req, res) => { router.postAsync('/report-templates-table', passport.loggedIn, async (req, res) => {
return res.json(await reportTemplates.listDTAjax(req.body)); return res.json(await reportTemplates.listDTAjax(req.context, req.body));
}); });
router.getAsync('/report-template-user-fields/:reportTemplateId', passport.loggedIn, async (req, res) => { router.getAsync('/report-template-user-fields/:reportTemplateId', passport.loggedIn, async (req, res) => {
const userFields = await reportTemplates.getUserFieldsById(req.params.reportTemplateId); const userFields = await reportTemplates.getUserFieldsById(req.context, req.params.reportTemplateId);
return res.json(userFields); return res.json(userFields);
}); });
router.getAsync('/report-templates-create-permitted', passport.loggedIn, async (req, res) => {
return res.json(await shares.checkTypePermission(req.context, 'namespace', 'createReportTemplate'));
});
module.exports = router; module.exports = router;

View file

@ -5,18 +5,19 @@ const _ = require('../../lib/translate')._;
const reports = require('../../models/reports'); const reports = require('../../models/reports');
const reportProcessor = require('../../lib/report-processor'); const reportProcessor = require('../../lib/report-processor');
const fileHelpers = require('../../lib/file-helpers'); const fileHelpers = require('../../lib/file-helpers');
const shares = require('../../models/shares');
const router = require('../../lib/router-async').create(); const router = require('../../lib/router-async').create();
router.getAsync('/reports/:reportId', passport.loggedIn, async (req, res) => { router.getAsync('/reports/:reportId', passport.loggedIn, async (req, res) => {
const report = await reports.getByIdWithTemplate(req.params.reportId); const report = await reports.getByIdWithTemplate(req.context, req.params.reportId);
report.hash = reports.hash(report); report.hash = reports.hash(report);
return res.json(report); return res.json(report);
}); });
router.postAsync('/reports', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.postAsync('/reports', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await reports.create(req.body); await reports.create(req.context, req.body);
return res.json(); return res.json();
}); });
@ -24,36 +25,50 @@ router.putAsync('/reports/:reportId', passport.loggedIn, passport.csrfProtection
const report = req.body; const report = req.body;
report.id = parseInt(req.params.reportId); report.id = parseInt(req.params.reportId);
await reports.updateWithConsistencyCheck(report); await reports.updateWithConsistencyCheck(req.context, report);
return res.json(); return res.json();
}); });
router.deleteAsync('/reports/:reportId', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.deleteAsync('/reports/:reportId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await reports.remove(req.params.reportId); await reports.remove(req.context, req.params.reportId);
return res.json(); return res.json();
}); });
router.postAsync('/reports-table', passport.loggedIn, async (req, res) => { router.postAsync('/reports-table', passport.loggedIn, async (req, res) => {
return res.json(await reports.listDTAjax(req.body)); return res.json(await reports.listDTAjax(req.context, req.body));
}); });
router.postAsync('/report-start/:id', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.postAsync('/report-start/:id', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'execute');
const report = await reports.getByIdWithTemplateNoPerms(req.params.id);
await shares.enforceEntityPermission(req.context, 'reportTemplate', report.report_template, 'execute');
await reportProcessor.start(req.params.id); await reportProcessor.start(req.params.id);
res.json(); res.json();
}); });
router.postAsync('/report-stop/:id', async (req, res) => { router.postAsync('/report-stop/:id', async (req, res) => {
await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'execute');
const report = await reports.getByIdWithTemplateNoPerms(req.params.id);
await shares.enforceEntityPermission(req.context, 'reportTemplate', report.report_template, 'execute');
await reportProcessor.stop(req.params.id); await reportProcessor.stop(req.params.id);
res.json(); res.json();
}); });
router.getAsync('/report-content/:id', async (req, res) => { router.getAsync('/report-content/:id', async (req, res) => {
const report = await reports.getByIdWithTemplate(req.params.id); await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewContent');
const report = await reports.getByIdWithTemplateNoPerms(req.params.id);
res.sendFile(fileHelpers.getReportContentFile(report)); res.sendFile(fileHelpers.getReportContentFile(report));
}); });
router.getAsync('/report-output/:id', async (req, res) => { router.getAsync('/report-output/:id', async (req, res) => {
const report = await reports.getByIdWithTemplate(req.params.id); await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewOutput');
const report = await reports.getByIdWithTemplateNoPerms(req.params.id);
res.sendFile(fileHelpers.getReportOutputFile(report)); res.sendFile(fileHelpers.getReportOutputFile(report));
}); });

View file

@ -8,20 +8,63 @@ const permissions = require('../../lib/permissions')
const router = require('../../lib/router-async').create(); const router = require('../../lib/router-async').create();
router.postAsync('/shares-table/:entityTypeId/:entityId', passport.loggedIn, async (req, res) => { router.postAsync('/shares-table/:entityTypeId/:entityId', passport.loggedIn, async (req, res) => {
return res.json(await shares.listDTAjax(req.params.entityTypeId, req.params.entityId, req.body)); return res.json(await shares.listDTAjax(req.context, req.params.entityTypeId, req.params.entityId, req.body));
}); });
router.postAsync('/shares-users-table/:entityTypeId/:entityId', passport.loggedIn, async (req, res) => { router.postAsync('/shares-users-table/:entityTypeId/:entityId', passport.loggedIn, async (req, res) => {
return res.json(await shares.listUnassignedUsersDTAjax(req.params.entityTypeId, req.params.entityId, req.body)); return res.json(await shares.listUnassignedUsersDTAjax(req.context, req.params.entityTypeId, req.params.entityId, req.body));
}); });
router.putAsync('/shares', passport.loggedIn, async (req, res) => { router.putAsync('/shares', passport.loggedIn, async (req, res) => {
// FIXME: Check that the user has the right to assign the role
const body = req.body; const body = req.body;
await shares.assign(body.entityTypeId, body.entityId, body.userId, body.role); await shares.assign(req.context, body.entityTypeId, body.entityId, body.userId, body.role);
return res.json(); return res.json();
}); });
/*
Checks if entities with a given permission exist.
Accepts format:
{
XXX1: {
entityTypeId: ...
requiredOperations: [ ... ]
},
XXX2: {
entityTypeId: ...
requiredOperations: [ ... ]
}
}
Returns:
{
XXX1: true
XXX2: false
}
*/
router.postAsync('/permissions-check', passport.loggedIn, async (req, res) => {
const body = req.body;
const result = {};
for (const reqKey in body) {
if (body[reqKey].entityId) {
result[reqKey] = await shares.checkEntityPermission(req.context, body[reqKey].entityTypeId, body[reqKey].entityId, body[reqKey].requiredOperations);
} else {
result[reqKey] = await shares.checkTypePermission(req.context, body[reqKey].entityTypeId, body[reqKey].requiredOperations);
}
}
return res.json(result);
});
router.postAsync('/permissions-rebuild', passport.loggedIn, async (req, res) => {
shares.enforceGlobalPermission(req.context, 'rebuildPermissions');
shares.rebuildPermissions();
return res.json(result);
});
module.exports = router; module.exports = router;

View file

@ -1,14 +1,16 @@
'use strict'; 'use strict';
const config = require('config');
const passport = require('../../lib/passport'); const passport = require('../../lib/passport');
const _ = require('../../lib/translate')._; const _ = require('../../lib/translate')._;
const users = require('../../models/users'); const users = require('../../models/users');
const shares = require('../../models/shares');
const router = require('../../lib/router-async').create(); const router = require('../../lib/router-async').create();
router.getAsync('/users/:userId', passport.loggedIn, async (req, res) => { router.getAsync('/users/:userId', passport.loggedIn, async (req, res) => {
const user = await users.getById(req.params.userId); const user = await users.getById(req.context, req.params.userId);
user.hash = users.hash(user); user.hash = users.hash(user);
return res.json(user); return res.json(user);
}); });
@ -32,11 +34,12 @@ router.deleteAsync('/users/:userId', passport.loggedIn, passport.csrfProtection,
}); });
router.postAsync('/users-validate', passport.loggedIn, async (req, res) => { router.postAsync('/users-validate', passport.loggedIn, async (req, res) => {
return res.json(await users.serverValidate(req.body)); return res.json(await users.serverValidate(req.context, req.body));
}); });
router.postAsync('/users-table', passport.loggedIn, async (req, res) => { router.postAsync('/users-table', passport.loggedIn, async (req, res) => {
return res.json(await users.listDTAjax(req.body)); return res.json(await users.listDTAjax(req.context, req.body));
}); });
module.exports = router; module.exports = router;

View file

@ -9,7 +9,7 @@ exports.up = function(knex, Promise) {
table.integer('namespace').unsigned().references('namespaces.id').onDelete('CASCADE'); table.integer('namespace').unsigned().references('namespaces.id').onDelete('CASCADE');
}) })
.then(() => knex('namespaces').insert({ .then(() => knex('namespaces').insert({
id: 1, id: 1, /* Global namespace id */
name: 'Global', name: 'Global',
description: 'Global namespace' description: 'Global namespace'
})); }));
@ -20,7 +20,7 @@ exports.up = function(knex, Promise) {
table.integer('namespace').unsigned().notNullable(); table.integer('namespace').unsigned().notNullable();
})) }))
.then(() => knex(`${entityType}s`).update({ .then(() => knex(`${entityType}s`).update({
namespace: 1 namespace: 1 /* Global namespace id */
})) }))
.then(() => knex.schema.table(`${entityType}s`, table => { .then(() => knex.schema.table(`${entityType}s`, table => {
table.foreign('namespace').references('namespaces.id').onDelete('CASCADE'); table.foreign('namespace').references('namespaces.id').onDelete('CASCADE');

View file

@ -21,12 +21,7 @@ exports.up = function(knex, Promise) {
}); });
} }
schema = schema.then(() => knex('shares_namespace').insert({ /* The global share for admin is set automatically in rebuild permissions, which is called upon every start */
id: 1,
entity: 1,
user: 1,
role: 'master'
}));
return schema; return schema;
}; };

View file

@ -4,7 +4,7 @@ exports.up = function(knex, Promise) {
table.string('name'); table.string('name');
table.string('password').alter(); table.string('password').alter();
}) })
.then(() => knex('users').where('id', 1).update({ .then(() => knex('users').where('id', 1 /* Admin user id */).update({
name: 'Administrator' name: 'Administrator'
})); }));
}; };

View file

@ -0,0 +1,9 @@
exports.up = function(knex, Promise) {
return knex.schema.table('users', table => {
table.string('role');
});
/* The user role is set automatically in rebuild permissions, which is called upon every start */
};
exports.down = function(knex, Promise) {
};

View file

@ -68,6 +68,12 @@ class DependencyNotFoundError extends InteroperableError {
} }
} }
class PermissionDeniedError extends InteroperableError {
constructor(msg, data) {
super('PermissionDeniedError', msg, data);
}
}
const errorTypes = { const errorTypes = {
InteroperableError, InteroperableError,
NotLoggedInError, NotLoggedInError,
@ -79,7 +85,8 @@ const errorTypes = {
DuplicitEmailError, DuplicitEmailError,
IncorrectPasswordError, IncorrectPasswordError,
InvalidTokenError, InvalidTokenError,
DependencyNotFoundError DependencyNotFoundError,
PermissionDeniedError
}; };
function deserialize(errorObj) { function deserialize(errorObj) {

View file

@ -1,9 +0,0 @@
'use strict';
function usernameValid(username) {
return /^[a-zA-Z0-9][a-zA-Z0-9_\-.]*$/.test(username);
}
module.exports = {
usernameValid
};

View file

@ -13,6 +13,7 @@ const vm = require('vm');
const log = require('npmlog'); const log = require('npmlog');
const fs = require('fs'); const fs = require('fs');
const knex = require('../../lib/knex'); const knex = require('../../lib/knex');
const contextHelpers = require('../../lib/context-helpers');
handlebarsHelpers.registerHelpers(handlebars); handlebarsHelpers.registerHelpers(handlebars);
@ -25,9 +26,11 @@ const userFieldGetters = {
async function main() { async function main() {
try { try {
const context = contextHelpers.getServiceContext();
const reportId = Number(process.argv[2]); const reportId = Number(process.argv[2]);
const report = await reports.getByIdWithTemplate(reportId); const report = await reports.getByIdWithTemplate(context, reportId);
const inputs = {}; const inputs = {};
@ -50,7 +53,7 @@ async function main() {
} }
const campaignsProxy = { const campaignsProxy = {
getResults: reports.getCampaignResults, getResults: (campaign, select, extra) => reports.getCampaignResults(context, campaign, select, extra),
getById: campaigns.getById getById: campaigns.getById
}; };