WiP on permissions
Doesn't run. This commit is just to backup the changes.
This commit is contained in:
parent
5df444f641
commit
89c9615592
37 changed files with 913 additions and 366 deletions
6
app.js
6
app.js
|
@ -19,6 +19,7 @@ const handlebarsHelpers = require('./lib/handlebars-helpers');
|
|||
const compression = require('compression');
|
||||
const passport = require('./lib/passport');
|
||||
const tools = require('./lib/tools');
|
||||
const contextHelpers = require('./lib/context-helpers');
|
||||
|
||||
const routes = require('./routes/index');
|
||||
const lists = require('./routes/lists');
|
||||
|
@ -220,10 +221,7 @@ app.use((req, res, next) => {
|
|||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
req.context = {
|
||||
user: req.user
|
||||
};
|
||||
|
||||
req.context = contextHelpers.getRequestContext(req);
|
||||
next();
|
||||
});
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
isEditGlobal() {
|
||||
return this.state.entityId === 1;
|
||||
return this.state.entityId === 1; /* Global namespace id */
|
||||
}
|
||||
|
||||
isDelete() {
|
||||
|
|
|
@ -245,6 +245,8 @@ export default class CUD extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME - filter namespaces by permission
|
||||
|
||||
return (
|
||||
<div>
|
||||
{edit &&
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
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 { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import moment from 'moment';
|
||||
|
@ -12,7 +12,42 @@ import { ReportState } from '../../../shared/reports';
|
|||
@translate()
|
||||
@withErrorHandling
|
||||
@withPageHelpers
|
||||
@requiresAuthenticatedUser
|
||||
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
|
||||
async stop(table, id) {
|
||||
|
@ -30,14 +65,18 @@ export default class List extends Component {
|
|||
const t = this.props.t;
|
||||
|
||||
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 id = data[0];
|
||||
const mimeType = data[7];
|
||||
|
||||
if (state === ReportState.PROCESSING || state === ReportState.SCHEDULED) {
|
||||
view = {
|
||||
viewContent = {
|
||||
label: <span className="glyphicon glyphicon-hourglass" aria-hidden="true" title="Processing"></span>,
|
||||
};
|
||||
|
||||
|
@ -49,12 +88,12 @@ export default class List extends Component {
|
|||
refreshTimeout = 1000;
|
||||
} else if (state === ReportState.FINISHED) {
|
||||
if (mimeType === 'text/html') {
|
||||
view = {
|
||||
viewContent = {
|
||||
label: <span className="glyphicon glyphicon-eye-open" aria-hidden="true" title="View"></span>,
|
||||
link: `reports/view/${id}`
|
||||
};
|
||||
} else if (mimeType === 'text/csv') {
|
||||
view = {
|
||||
viewContent = {
|
||||
label: <span className="glyphicon glyphicon-download-alt" aria-hidden="true" title="Download"></span>,
|
||||
href: `reports/download/${id}`
|
||||
};
|
||||
|
@ -66,7 +105,7 @@ export default class List extends Component {
|
|||
};
|
||||
|
||||
} else if (state === ReportState.FAILED) {
|
||||
view = {
|
||||
viewContent = {
|
||||
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 {
|
||||
refreshTimeout,
|
||||
actions: [
|
||||
view,
|
||||
if (perms.includes('viewContent')) {
|
||||
actions.push(viewContent);
|
||||
}
|
||||
|
||||
if (perms.includes('viewOutput')) {
|
||||
actions.push(
|
||||
{
|
||||
label: <span className="glyphicon glyphicon-modal-window" aria-hidden="true" title="View console output"></span>,
|
||||
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 = [
|
||||
|
@ -106,11 +158,16 @@ export default class List extends Component {
|
|||
{ data: 5, title: t('Namespace') }
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Toolbar>
|
||||
{this.state.createPermitted &&
|
||||
<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')}/>
|
||||
}
|
||||
</Toolbar>
|
||||
|
||||
<Title>{t('Reports')}</Title>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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 axios from '../../lib/axios';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
||||
|
@ -14,6 +14,7 @@ import { validateNamespace, NamespaceSelect } from '../../lib/namespace';
|
|||
@withForm
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export default class CUD extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -300,6 +301,8 @@ export default class CUD extends Component {
|
|||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
// FIXME - filter namespaces by permission
|
||||
|
||||
return (
|
||||
<div>
|
||||
{edit &&
|
||||
|
|
|
@ -4,30 +4,65 @@ import React, { Component } from 'react';
|
|||
import { translate } from 'react-i18next';
|
||||
import { DropdownMenu } from '../../lib/bootstrap-components';
|
||||
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, DropdownLink } from '../../lib/page';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
||||
import { Table } from '../../lib/table';
|
||||
import axios from '../../lib/axios';
|
||||
import moment from 'moment';
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export default class List extends Component {
|
||||
constructor(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() {
|
||||
const t = this.props.t;
|
||||
|
||||
const actions = data => [
|
||||
{
|
||||
label: 'Edit',
|
||||
const actions = data => {
|
||||
const actions = [];
|
||||
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]
|
||||
},
|
||||
{
|
||||
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 = [
|
||||
{ data: 0, title: "#" },
|
||||
|
@ -39,6 +74,7 @@ export default class List extends Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{this.state.createPermitted &&
|
||||
<Toolbar>
|
||||
<DropdownMenu className="btn-primary" label={t('Create Report Template')}>
|
||||
<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>
|
||||
</DropdownMenu>
|
||||
</Toolbar>
|
||||
}
|
||||
|
||||
<Title>{t('Report Templates')}</Title>
|
||||
|
||||
|
|
|
@ -4,12 +4,11 @@ import React, { Component } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
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 { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
import passwordValidator from '../../../shared/password-validator';
|
||||
import validators from '../../../shared/validators';
|
||||
import { ModalDialog } from '../lib/bootstrap-components';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
||||
|
@ -25,7 +24,9 @@ export default class CUD extends Component {
|
|||
|
||||
this.passwordValidator = passwordValidator(props.t);
|
||||
|
||||
this.state = {};
|
||||
this.state = {
|
||||
globalRoles: []
|
||||
};
|
||||
|
||||
if (props.edit) {
|
||||
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';
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async fetchGlobalRoles() {
|
||||
const result = await axios.get('/rest/users-global-roles');
|
||||
this.setState({
|
||||
globalRoles: result.data
|
||||
});
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(`/rest/users/${this.state.entityId}`, data => {
|
||||
|
@ -80,8 +89,6 @@ export default class CUD extends Component {
|
|||
|
||||
if (!username) {
|
||||
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) {
|
||||
state.setIn(['username', 'error'], t('The user name already exists in the system.'));
|
||||
} else {
|
||||
|
@ -214,6 +221,20 @@ export default class CUD extends Component {
|
|||
const userId = this.getFormValue('id');
|
||||
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 (
|
||||
<div>
|
||||
{edit && canDelete &&
|
||||
|
@ -229,10 +250,15 @@ export default class CUD extends Component {
|
|||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="username" label={t('User Name')}/>
|
||||
{mailtrainConfig.isAuthMethodLocal &&
|
||||
<div>
|
||||
<InputField id="name" label={t('Full Name')}/>
|
||||
<InputField id="email" label={t('Email')}/>
|
||||
<InputField id="password" label={t('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/>
|
||||
|
||||
<ButtonRow>
|
||||
|
|
|
@ -17,7 +17,12 @@ export default class List extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
let actions;
|
||||
const actions = data => [
|
||||
{
|
||||
label: 'Edit',
|
||||
link: '/users/edit/' + data[0]
|
||||
}
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{ data: 0, title: "#" },
|
||||
|
@ -26,13 +31,6 @@ export default class List extends Component {
|
|||
|
||||
if (mailtrainConfig.isAuthMethodLocal) {
|
||||
columns.push({ data: 2, title: "Full Name" });
|
||||
|
||||
actions = data => [
|
||||
{
|
||||
label: 'Edit',
|
||||
link: '/users/edit/' + data[0]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
columns.push({ data: 3, title: "Namespace" });
|
||||
|
|
|
@ -122,6 +122,9 @@ uidTag="username"
|
|||
# nameTag identifies the attribute to be used for user's full name
|
||||
nameTag="username"
|
||||
passwordresetlink=""
|
||||
newUserRole="master"
|
||||
# Global namespace id
|
||||
newUserNamespaceId=1
|
||||
|
||||
[postfixbounce]
|
||||
# Enable to allow writing Postfix bounce log to Mailtrain listener
|
||||
|
@ -188,28 +191,72 @@ logger=false
|
|||
browser="phantomjs"
|
||||
|
||||
|
||||
|
||||
[roles.global.master]
|
||||
name="Master"
|
||||
admin=true
|
||||
description="All permissions"
|
||||
permissions=["rebuildPermissions"]
|
||||
rootNamespaceRole="master"
|
||||
|
||||
|
||||
[roles.reportTemplate.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete"]
|
||||
permissions=["view", "edit", "delete", "share", "execute"]
|
||||
|
||||
[roles.report.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete"]
|
||||
permissions=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||
|
||||
[roles.list.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view"]
|
||||
permissions=[]
|
||||
|
||||
[roles.namespace.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "create", "delete", "create list"]
|
||||
permissions=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
|
||||
|
||||
[roles.namespace.master.childperms]
|
||||
reportTemplate=["view", "edit", "delete"]
|
||||
report=["view", "edit", "delete"]
|
||||
list=["view"]
|
||||
namespace=["view", "edit", "create", "delete", "create list"]
|
||||
[roles.namespace.master.children]
|
||||
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
||||
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||
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"]
|
||||
|
|
32
index.js
32
index.js
|
@ -21,6 +21,8 @@ const senders = require('./lib/senders');
|
|||
const reportProcessor = require('./lib/report-processor');
|
||||
const executor = require('./lib/executor');
|
||||
const privilegeHelpers = require('./lib/privilege-helpers');
|
||||
const knex = require('./lib/knex');
|
||||
const shares = require('./models/shares');
|
||||
|
||||
let port = config.www.port;
|
||||
let host = config.www.host;
|
||||
|
@ -38,19 +40,6 @@ app.set('port', port);
|
|||
|
||||
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 => {
|
||||
if (err.syscall !== 'listen') {
|
||||
throw err;
|
||||
|
@ -145,3 +134,20 @@ server.on('listening', () => {
|
|||
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
29
lib/context-helpers.js
Normal 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
|
||||
};
|
|
@ -1,36 +1,64 @@
|
|||
'use strict';
|
||||
|
||||
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) => {
|
||||
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') {
|
||||
query.whereIn(columns[parseInt(params.column)], params.values);
|
||||
query.select(columns);
|
||||
const query = queryFun(tx);
|
||||
query.whereIn(columnsNames[parseInt(params.column)], params.values);
|
||||
query.select(columnsSelect);
|
||||
|
||||
const rows = await query;
|
||||
const rowsOfArray = rows.map(row => Object.keys(row).map(key => row[key]));
|
||||
return rowsOfArray;
|
||||
|
||||
} else {
|
||||
const recordsTotalQuery = query.clone().count('* as recordsTotal').first();
|
||||
const recordsTotal = (await recordsTotalQuery).recordsTotal;
|
||||
|
||||
query.where(function() {
|
||||
const whereFun = function() {
|
||||
let searchVal = '%' + params.search.value.replace(/\\/g, '\\\\').replace(/([%_])/g, '\\$1') + '%';
|
||||
for (let colIdx = 0; colIdx < params.columns.length; colIdx++) {
|
||||
const col = params.columns[colIdx];
|
||||
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 query = queryFun(tx);
|
||||
query.where(whereFun);
|
||||
|
||||
query.offset(parseInt(params.start));
|
||||
|
||||
const limit = parseInt(params.length);
|
||||
|
@ -38,16 +66,25 @@ async function ajaxList(params, queryFun, columns) {
|
|||
query.limit(limit);
|
||||
}
|
||||
|
||||
query.select(columns);
|
||||
query.select(columnsSelect);
|
||||
|
||||
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});
|
||||
|
||||
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 = {
|
||||
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 = {
|
||||
ajaxList
|
||||
ajaxList,
|
||||
ajaxListWithPermissions
|
||||
};
|
|
@ -11,6 +11,4 @@ const knex = require('knex')({
|
|||
// , debug: true
|
||||
});
|
||||
|
||||
knex.migrate.latest();
|
||||
|
||||
module.exports = knex;
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
let config = require('config');
|
||||
let log = require('npmlog');
|
||||
let _ = require('./translate')._;
|
||||
let util = require('util');
|
||||
const config = require('config');
|
||||
const log = require('npmlog');
|
||||
const _ = require('./translate')._;
|
||||
const util = require('util');
|
||||
|
||||
let passport = require('passport');
|
||||
let LocalStrategy = require('passport-local').Strategy;
|
||||
const passport = require('passport');
|
||||
const LocalStrategy = require('passport-local').Strategy;
|
||||
|
||||
let csrf = require('csurf');
|
||||
let bodyParser = require('body-parser');
|
||||
const csrf = require('csurf');
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
const users = require('../models/users');
|
||||
const { nodeifyFunction, nodeifyPromise } = require('./nodeify');
|
||||
|
@ -104,20 +104,24 @@ if (config.ldap.enabled && LdapStrategy) {
|
|||
id: user.id,
|
||||
username: user.username,
|
||||
name: profile[config.ldap.nameTag],
|
||||
email: profile.mail
|
||||
email: profile.mail,
|
||||
role: user.role
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
if (err instanceof interoperableErrors.NotFoundError) {
|
||||
const userId = await users.createExternal({
|
||||
const userId = await users.create({
|
||||
username: profile[config.ldap.uidTag],
|
||||
role: config.ldap.newUserRole,
|
||||
namespace: config.ldap.newUserNamespaceId
|
||||
});
|
||||
|
||||
return {
|
||||
id: userId,
|
||||
username: profile[config.ldap.uidTag],
|
||||
name: profile[config.ldap.nameTag],
|
||||
email: profile.mail
|
||||
email: profile.mail,
|
||||
role: config.ldap.newUserRole
|
||||
};
|
||||
} else {
|
||||
throw err;
|
||||
|
@ -139,6 +143,6 @@ if (config.ldap.enabled && LdapStrategy) {
|
|||
})));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,75 +1,38 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
|
||||
|
||||
// FIXME - redo or delete
|
||||
|
||||
/*
|
||||
class ReportTemplatePermission {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.entityType = 'report-template';
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
const ReportTemplatePermissions = {
|
||||
View: new ReportTemplatePermission('view'),
|
||||
Edit: new ReportTemplatePermission('edit'),
|
||||
Delete: new ReportTemplatePermission('delete')
|
||||
};
|
||||
|
||||
|
||||
class ListPermission {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.entityType = 'list';
|
||||
}
|
||||
function getEntityTypes() {
|
||||
return entityTypes;
|
||||
}
|
||||
|
||||
const ListPermissions = {
|
||||
View: new ListPermissions('view')
|
||||
};
|
||||
function getEntityType(entityTypeId) {
|
||||
const entityType = entityTypes[entityTypeId];
|
||||
|
||||
class NamespacePermission {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.entityType = 'namespace';
|
||||
}
|
||||
if (!entityType) {
|
||||
throw new Error(`Unknown entity type ${entityTypeId}`);
|
||||
}
|
||||
|
||||
const NamespacePermissions = {
|
||||
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;
|
||||
return entityType
|
||||
}
|
||||
|
||||
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 = {
|
||||
getEntityTypes,
|
||||
getEntityType
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
const log = require('npmlog');
|
||||
const reports = require('../models/reports');
|
||||
const executor = require('./executor');
|
||||
const contextHelpers = require('../lib/context-helpers');
|
||||
|
||||
let runningWorkersCount = 0;
|
||||
let maxWorkersCount = 1;
|
||||
|
@ -99,7 +100,7 @@ async function tryStartWorkers() {
|
|||
isStartingWorkers = false;
|
||||
}
|
||||
|
||||
module.exports.start = async reportId => {
|
||||
module.exports.start = async (reportId) => {
|
||||
if (!workers[reportId]) {
|
||||
log.info('ReportProcessor', 'Scheduling report id: %s', reportId);
|
||||
await reports.updateFields(reportId, { state: reports.ReportState.SCHEDULED, last_run: null});
|
||||
|
|
|
@ -5,7 +5,7 @@ const dtHelpers = require('../lib/dt-helpers');
|
|||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
|
||||
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) {
|
||||
|
|
|
@ -5,7 +5,7 @@ const dtHelpers = require('../lib/dt-helpers');
|
|||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
|
||||
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) {
|
||||
|
|
|
@ -16,7 +16,9 @@ function hash(entity) {
|
|||
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();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
|
@ -25,15 +27,16 @@ async function getById(id) {
|
|||
return entity;
|
||||
}
|
||||
|
||||
async function create(entity) {
|
||||
await knex.transaction(async tx => {
|
||||
const id = await tx('namespaces').insert(filterObject(entity, allowedKeys));
|
||||
async function create(context, entity) {
|
||||
enforce(entity.namespace, 'Parent namespace must be set');
|
||||
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()) {
|
||||
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.
|
||||
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.');
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.id, 'edit');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
const existing = await tx('namespaces').where('id', entity.id).first();
|
||||
if (!entity) {
|
||||
if (!existing) {
|
||||
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.');
|
||||
await shares.enforceEntityPermission(context, 'namespace', id, 'delete');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
const childNs = await tx('namespaces').where('namespace', id).first();
|
||||
|
|
|
@ -14,7 +14,9 @@ function hash(entity) {
|
|||
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();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
|
@ -23,15 +25,19 @@ async function getById(id) {
|
|||
return entity;
|
||||
}
|
||||
|
||||
async function listDTAjax(params) {
|
||||
return await dtHelpers.ajaxList(
|
||||
async function listDTAjax(context, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'reportTemplate', requiredOperations: ['view'] }],
|
||||
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' ]
|
||||
);
|
||||
}
|
||||
|
||||
async function create(entity) {
|
||||
async function create(context, entity) {
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createReportTemplate');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
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 => {
|
||||
const existing = await tx('report_templates').where('id', entity.id).first();
|
||||
if (!entity) {
|
||||
if (!existing) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
|
@ -57,17 +65,26 @@ async function updateWithConsistencyCheck(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 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();
|
||||
}
|
||||
|
||||
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();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
|
|
|
@ -18,7 +18,13 @@ function hash(entity) {
|
|||
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')
|
||||
.where('reports.id', id)
|
||||
.innerJoin('report_templates', 'reports.report_template', 'report_templates.id')
|
||||
|
@ -35,17 +41,28 @@ async function getByIdWithTemplate(id) {
|
|||
return entity;
|
||||
}
|
||||
|
||||
async function listDTAjax(params) {
|
||||
return await dtHelpers.ajaxList(
|
||||
async function listDTAjax(context, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[
|
||||
{ entityTypeId: 'report', requiredOperations: ['view'] },
|
||||
{ entityTypeId: 'reportTemplate', requiredOperations: ['view'] }
|
||||
],
|
||||
params,
|
||||
tx => tx('reports')
|
||||
builder => builder.from('reports')
|
||||
.innerJoin('report_templates', 'reports.report_template', 'report_templates.id')
|
||||
.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;
|
||||
await knex.transaction(async tx => {
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
|
@ -66,10 +83,13 @@ async function create(entity) {
|
|||
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 => {
|
||||
const existing = await tx('reports').where('id', entity.id).first();
|
||||
if (!entity) {
|
||||
if (!existing) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
|
@ -86,6 +106,11 @@ async function updateWithConsistencyCheck(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);
|
||||
|
||||
const filteredUpdates = filterObject(entity, allowedKeys);
|
||||
|
@ -101,7 +126,9 @@ async function updateWithConsistencyCheck(entity) {
|
|||
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();
|
||||
}
|
||||
|
||||
|
@ -132,7 +159,7 @@ function customFieldName(id) {
|
|||
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 fieldsMapping = fieldList.reduce((map, field) => {
|
||||
|
@ -174,6 +201,7 @@ module.exports = {
|
|||
ReportState,
|
||||
hash,
|
||||
getByIdWithTemplate,
|
||||
getByIdWithTemplateNoPerms,
|
||||
listDTAjax,
|
||||
create,
|
||||
updateWithConsistencyCheck,
|
||||
|
|
233
models/shares.js
233
models/shares.js
|
@ -1,55 +1,39 @@
|
|||
'use strict';
|
||||
|
||||
let _ = require('../lib/translate')._;
|
||||
const knex = require('../lib/knex');
|
||||
const config = require('config');
|
||||
const { enforce } = require('../lib/helpers');
|
||||
const dtHelpers = require('../lib/dt-helpers');
|
||||
const permissions = require('../lib/permissions');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
|
||||
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) {
|
||||
const entityType = entityTypes[entityTypeId];
|
||||
async function listDTAjax(context, entityTypeId, entityId, params) {
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
|
||||
if (!entityType) {
|
||||
throw new Error(`Unknown entity type ${entityTypeId}`);
|
||||
await enforceEntityPermission(context, entityTypeId, entityId, 'share');
|
||||
|
||||
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) {
|
||||
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']);
|
||||
}
|
||||
await enforceEntityPermission(context, entityTypeId, entityId, 'share');
|
||||
|
||||
async function listUnassignedUsersDTAjax(entityTypeId, entityId, params) {
|
||||
const entityType = getEntityType(entityTypeId);
|
||||
return await dtHelpers.ajaxList(
|
||||
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']);
|
||||
}
|
||||
|
||||
|
||||
async function assign(entityTypeId, entityId, userId, role) {
|
||||
const entityType = getEntityType(entityTypeId);
|
||||
async function assign(context, entityTypeId, entityId, userId, role) {
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
|
||||
await enforceEntityPermission(context, entityTypeId, entityId, 'share');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
enforce(await tx('users').where('id', userId).select('id').first(), 'Invalid user id');
|
||||
enforce(await tx(entityType.entitiesTable).where('id', entityId).select('id').first(), 'Invalid entity id');
|
||||
|
@ -79,26 +63,92 @@ async function assign(entityTypeId, entityId, userId, role) {
|
|||
});
|
||||
}
|
||||
|
||||
async function rebuildPermissions(tx, restriction) {
|
||||
restriction = restriction || {};
|
||||
async function _rebuildPermissions(tx, restriction) {
|
||||
const namespaceEntityType = permissions.getEntityType('namespace');
|
||||
|
||||
// Collect entity types we care about
|
||||
let restrictedEntityTypes;
|
||||
if (restriction.entityTypeId) {
|
||||
const entityType = permissions.getEntityType(restriction.entityTypeId);
|
||||
restrictedEntityTypes = {
|
||||
[restriction.entityTypeId]: entityTypes[restriction.entityTypeId]
|
||||
[restriction.entityTypeId]: entityType
|
||||
};
|
||||
} 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:
|
||||
// .id - id of the namespace
|
||||
// .namespace - id of the parent or null if no parent
|
||||
// .userPermissions - Map userId -> [entityTypeId] -> array of permissions
|
||||
// .transitiveUserPermissions - the same as above, but taking into account transitive permission obtained from namespace parents
|
||||
|
||||
const namespaces = await tx('namespaces').select(['id', 'namespace']);
|
||||
|
||||
const nsMap = new Map();
|
||||
for (const namespace of namespaces) {
|
||||
namespace.userPermissions = new Map();
|
||||
|
@ -106,9 +156,9 @@ async function rebuildPermissions(tx, restriction) {
|
|||
}
|
||||
|
||||
// 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) {
|
||||
nsSharesQuery.andWhere('user', restriction.userId);
|
||||
nsSharesQuery.where('user', restriction.userId);
|
||||
}
|
||||
|
||||
const nsShares = await nsSharesQuery;
|
||||
|
@ -120,10 +170,10 @@ async function rebuildPermissions(tx, restriction) {
|
|||
|
||||
for (const entityTypeId in restrictedEntityTypes) {
|
||||
if (config.roles.namespace[nsShare.role] &&
|
||||
config.roles.namespace[nsShare.role].childperms &&
|
||||
config.roles.namespace[nsShare.role].childperms[entityTypeId]) {
|
||||
config.roles.namespace[nsShare.role].children &&
|
||||
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 {
|
||||
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) {
|
||||
const entityType = restrictedEntityTypes[entityTypeId];
|
||||
|
||||
const expungeQuery = tx(entityType.permissionsTable).del();
|
||||
if (restriction.entityId) {
|
||||
expungeQuery.andWhere('entity', restriction.entityId);
|
||||
expungeQuery.where('entity', restriction.entityId);
|
||||
}
|
||||
if (restriction.userId) {
|
||||
expungeQuery.andWhere('user', restriction.userId);
|
||||
expungeQuery.where('user', restriction.userId);
|
||||
}
|
||||
await expungeQuery;
|
||||
|
||||
const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace']);
|
||||
if (restriction.entityId) {
|
||||
entitiesQuery.andWhere('id', restriction.entityId);
|
||||
entitiesQuery.where('id', restriction.entityId);
|
||||
}
|
||||
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
|
||||
const transitiveUserPermissions = nsMap.get(entity.namespace).transitiveUserPermissions;
|
||||
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 = {
|
||||
listDTAjax,
|
||||
listUnassignedUsersDTAjax,
|
||||
assign,
|
||||
rebuildPermissions
|
||||
rebuildPermissions,
|
||||
enforceEntityPermission,
|
||||
enforceTypePermission,
|
||||
checkEntityPermission,
|
||||
checkTypePermission,
|
||||
enforceGlobalPermission,
|
||||
throwPermissionDenied
|
||||
};
|
135
models/users.js
135
models/users.js
|
@ -1,11 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const knex = require('../lib/knex');
|
||||
const hasher = require('node-object-hash')();
|
||||
const { enforce, filterObject } = require('../lib/helpers');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
const passwordValidator = require('../shared/password-validator')();
|
||||
const validators = require('../shared/validators');
|
||||
const dtHelpers = require('../lib/dt-helpers');
|
||||
const tools = require('../lib/tools-async');
|
||||
let crypto = require('crypto');
|
||||
|
@ -26,20 +26,19 @@ const passport = require('../lib/passport');
|
|||
|
||||
const namespaceHelpers = require('../lib/namespace-helpers');
|
||||
|
||||
const allowedKeys = new Set(['username', 'name', 'email', 'password', 'namespace']);
|
||||
const allowedKeysExternal = new Set(['username']);
|
||||
const allowedKeys = new Set(['username', 'name', 'email', 'password', 'namespace', 'role']);
|
||||
const ownAccountAllowedKeys = new Set(['name', 'email', 'password']);
|
||||
const hashKeys = new Set(['username', 'name', 'email', 'namespace']);
|
||||
|
||||
const defaultNamespace = 1;
|
||||
const allowedKeysExternal = new Set(['username', 'namespace', 'role']);
|
||||
const hashKeys = new Set(['username', 'name', 'email', 'namespace', 'role']);
|
||||
const shares = require('../../models/shares');
|
||||
|
||||
|
||||
function hash(entity) {
|
||||
return hasher.hash(filterObject(entity, hashKeys));
|
||||
}
|
||||
|
||||
async function _getBy(key, value, extraColumns) {
|
||||
const columns = ['id', 'username', 'name', 'email', 'namespace'];
|
||||
async function _getBy(context, key, value, extraColumns) {
|
||||
const columns = ['id', 'username', 'name', 'email', 'namespace', 'role'];
|
||||
|
||||
if (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();
|
||||
|
||||
if (!user) {
|
||||
if (context) {
|
||||
shares.throwPermissionDenied();
|
||||
} else {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
}
|
||||
|
||||
if (context) {
|
||||
await shares.enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function getById(id) {
|
||||
return await _getBy('id', id);
|
||||
async function getById(context, 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 = {};
|
||||
|
||||
if (!isOwnAccount) {
|
||||
await shares.enforceTypePermission(context, 'namespace', 'manageUsers');
|
||||
}
|
||||
|
||||
if (!isOwnAccount && data.username) {
|
||||
const query = knex('users').select(['id']).where('username', data.username);
|
||||
|
||||
|
@ -100,11 +115,17 @@ async function serverValidate(data, isOwnAccount) {
|
|||
return result;
|
||||
}
|
||||
|
||||
async function listDTAjax(params) {
|
||||
return await dtHelpers.ajaxList(
|
||||
async function listDTAjax(context, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'namespace', requiredOperations: ['manageUsers'] }],
|
||||
params,
|
||||
tx => tx('users').innerJoin('namespaces', 'namespaces.id', 'users.namespace'),
|
||||
['users.id', 'users.username', 'users.name', 'namespaces.name']
|
||||
builder => builder.from('users').innerJoin('namespaces', 'namespaces.id', 'users.namespace'),
|
||||
['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) {
|
||||
enforce(validators.usernameValid(user.username), 'Invalid username');
|
||||
|
||||
const otherUserWithSameUsernameQuery = tx('users').where('username', user.username);
|
||||
if (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');
|
||||
|
||||
|
@ -153,70 +173,89 @@ async function _validateAndPreprocess(tx, user, isCreate, isOwnAccount) {
|
|||
}
|
||||
|
||||
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 => {
|
||||
if (passport.isAuthMethodLocal) {
|
||||
await _validateAndPreprocess(tx, user, true);
|
||||
const userId = await tx('users').insert(filterObject(user, allowedKeys));
|
||||
return userId;
|
||||
});
|
||||
}
|
||||
|
||||
async function createExternal(user) {
|
||||
enforce(!passport.isAuthMethodLocal, 'External user management is required');
|
||||
|
||||
} else {
|
||||
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);
|
||||
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);
|
||||
if (existingUserHash !== user.originalHash) {
|
||||
async function updateWithConsistencyCheck(user, isOwnAccount) {
|
||||
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();
|
||||
}
|
||||
|
||||
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 (!await bcryptCompare(user.currentPassword, existingUser.password)) {
|
||||
if (!await bcryptCompare(user.currentPassword, existing.password)) {
|
||||
throw new interoperableErrors.IncorrectPasswordError();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
enforce(passport.isAuthMethodLocal, 'Local user management is required');
|
||||
enforce(userId !== 1, 'Admin cannot be deleted');
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
async function getByAccessToken(accessToken) {
|
||||
return await _getBy('access_token', accessToken);
|
||||
return await _getBy(null, 'access_token', accessToken);
|
||||
}
|
||||
|
||||
async function getByUsername(username) {
|
||||
return await _getBy('username', username);
|
||||
return await _getBy(null, 'username', username);
|
||||
}
|
||||
|
||||
async function getByUsernameIfPasswordMatch(username, password) {
|
||||
try {
|
||||
const user = await _getBy('username', username, ['password']);
|
||||
const user = await _getBy(null, 'username', username, ['password']);
|
||||
|
||||
if (!await bcryptCompare(password, user.password)) {
|
||||
throw new interoperableErrors.IncorrectPasswordError();
|
||||
|
@ -234,19 +273,13 @@ async function getByUsernameIfPasswordMatch(username, password) {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function resetAccessToken(userId) {
|
||||
const token = crypto.randomBytes(20).toString('hex').toLowerCase();
|
||||
|
||||
const affectedRows = await knex('users').where({id: userId}).update({access_token: token});
|
||||
|
||||
if (!affectedRows) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
await knex('users').where({id: userId}).update({access_token: token});
|
||||
return token;
|
||||
}
|
||||
|
||||
|
@ -331,9 +364,9 @@ module.exports = {
|
|||
remove,
|
||||
updateWithConsistencyCheck,
|
||||
create,
|
||||
createExternal,
|
||||
hash,
|
||||
getById,
|
||||
getByIdNoPerms,
|
||||
serverValidate,
|
||||
getByAccessToken,
|
||||
getByUsername,
|
||||
|
|
|
@ -4,11 +4,14 @@ const passport = require('../lib/passport');
|
|||
const _ = require('../lib/translate')._;
|
||||
const reports = require('../models/reports');
|
||||
const fileHelpers = require('../lib/file-helpers');
|
||||
const shares = require('../models/shares');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
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) {
|
||||
const headers = {
|
||||
|
|
|
@ -8,7 +8,7 @@ const router = require('../../lib/router-async').create();
|
|||
|
||||
|
||||
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);
|
||||
return res.json(user);
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ const router = require('../../lib/router-async').create();
|
|||
|
||||
|
||||
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);
|
||||
|
||||
|
@ -16,7 +16,7 @@ router.getAsync('/namespaces/:nsId', passport.loggedIn, 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();
|
||||
});
|
||||
|
||||
|
@ -24,16 +24,19 @@ router.putAsync('/namespaces/:nsId', passport.loggedIn, passport.csrfProtection,
|
|||
const ns = req.body;
|
||||
ns.id = parseInt(req.params.nsId);
|
||||
|
||||
await namespaces.updateWithConsistencyCheck(ns);
|
||||
await namespaces.updateWithConsistencyCheck(req.context, ns);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
router.getAsync('/namespaces-tree', passport.loggedIn, async (req, res) => {
|
||||
|
||||
// FIXME - process permissions
|
||||
|
||||
const entries = {};
|
||||
let root; // Only the Root namespace is without a parent
|
||||
const rows = await namespaces.list();
|
||||
|
|
|
@ -8,13 +8,13 @@ const router = require('../../lib/router-async').create();
|
|||
|
||||
|
||||
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);
|
||||
return res.json(reportTemplate);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -22,23 +22,27 @@ router.putAsync('/report-templates/:reportTemplateId', passport.loggedIn, passpo
|
|||
const reportTemplate = req.body;
|
||||
reportTemplate.id = parseInt(req.params.reportTemplateId);
|
||||
|
||||
await reportTemplates.updateWithConsistencyCheck(reportTemplate);
|
||||
await reportTemplates.updateWithConsistencyCheck(req.context, reportTemplate);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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) => {
|
||||
const userFields = await reportTemplates.getUserFieldsById(req.params.reportTemplateId);
|
||||
const userFields = await reportTemplates.getUserFieldsById(req.context, req.params.reportTemplateId);
|
||||
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;
|
|
@ -5,18 +5,19 @@ const _ = require('../../lib/translate')._;
|
|||
const reports = require('../../models/reports');
|
||||
const reportProcessor = require('../../lib/report-processor');
|
||||
const fileHelpers = require('../../lib/file-helpers');
|
||||
const shares = require('../../models/shares');
|
||||
|
||||
const router = require('../../lib/router-async').create();
|
||||
|
||||
|
||||
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);
|
||||
return res.json(report);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -24,36 +25,50 @@ router.putAsync('/reports/:reportId', passport.loggedIn, passport.csrfProtection
|
|||
const report = req.body;
|
||||
report.id = parseInt(req.params.reportId);
|
||||
|
||||
await reports.updateWithConsistencyCheck(report);
|
||||
await reports.updateWithConsistencyCheck(req.context, report);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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) => {
|
||||
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);
|
||||
res.json();
|
||||
});
|
||||
|
||||
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);
|
||||
res.json();
|
||||
});
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
|
|
|
@ -8,20 +8,63 @@ const permissions = require('../../lib/permissions')
|
|||
const router = require('../../lib/router-async').create();
|
||||
|
||||
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) => {
|
||||
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) => {
|
||||
// FIXME: Check that the user has the right to assign the role
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
/*
|
||||
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;
|
|
@ -1,14 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const passport = require('../../lib/passport');
|
||||
const _ = require('../../lib/translate')._;
|
||||
const users = require('../../models/users');
|
||||
const shares = require('../../models/shares');
|
||||
|
||||
const router = require('../../lib/router-async').create();
|
||||
|
||||
|
||||
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);
|
||||
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) => {
|
||||
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) => {
|
||||
return res.json(await users.listDTAjax(req.body));
|
||||
return res.json(await users.listDTAjax(req.context, req.body));
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -9,7 +9,7 @@ exports.up = function(knex, Promise) {
|
|||
table.integer('namespace').unsigned().references('namespaces.id').onDelete('CASCADE');
|
||||
})
|
||||
.then(() => knex('namespaces').insert({
|
||||
id: 1,
|
||||
id: 1, /* Global namespace id */
|
||||
name: 'Global',
|
||||
description: 'Global namespace'
|
||||
}));
|
||||
|
@ -20,7 +20,7 @@ exports.up = function(knex, Promise) {
|
|||
table.integer('namespace').unsigned().notNullable();
|
||||
}))
|
||||
.then(() => knex(`${entityType}s`).update({
|
||||
namespace: 1
|
||||
namespace: 1 /* Global namespace id */
|
||||
}))
|
||||
.then(() => knex.schema.table(`${entityType}s`, table => {
|
||||
table.foreign('namespace').references('namespaces.id').onDelete('CASCADE');
|
||||
|
|
|
@ -21,12 +21,7 @@ exports.up = function(knex, Promise) {
|
|||
});
|
||||
}
|
||||
|
||||
schema = schema.then(() => knex('shares_namespace').insert({
|
||||
id: 1,
|
||||
entity: 1,
|
||||
user: 1,
|
||||
role: 'master'
|
||||
}));
|
||||
/* The global share for admin is set automatically in rebuild permissions, which is called upon every start */
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@ exports.up = function(knex, Promise) {
|
|||
table.string('name');
|
||||
table.string('password').alter();
|
||||
})
|
||||
.then(() => knex('users').where('id', 1).update({
|
||||
.then(() => knex('users').where('id', 1 /* Admin user id */).update({
|
||||
name: 'Administrator'
|
||||
}));
|
||||
};
|
||||
|
|
9
setup/knex/migrations/20170726155118_create_user_role.js
Normal file
9
setup/knex/migrations/20170726155118_create_user_role.js
Normal 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) {
|
||||
};
|
|
@ -68,6 +68,12 @@ class DependencyNotFoundError extends InteroperableError {
|
|||
}
|
||||
}
|
||||
|
||||
class PermissionDeniedError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('PermissionDeniedError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
const errorTypes = {
|
||||
InteroperableError,
|
||||
NotLoggedInError,
|
||||
|
@ -79,7 +85,8 @@ const errorTypes = {
|
|||
DuplicitEmailError,
|
||||
IncorrectPasswordError,
|
||||
InvalidTokenError,
|
||||
DependencyNotFoundError
|
||||
DependencyNotFoundError,
|
||||
PermissionDeniedError
|
||||
};
|
||||
|
||||
function deserialize(errorObj) {
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
function usernameValid(username) {
|
||||
return /^[a-zA-Z0-9][a-zA-Z0-9_\-.]*$/.test(username);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
usernameValid
|
||||
};
|
|
@ -13,6 +13,7 @@ const vm = require('vm');
|
|||
const log = require('npmlog');
|
||||
const fs = require('fs');
|
||||
const knex = require('../../lib/knex');
|
||||
const contextHelpers = require('../../lib/context-helpers');
|
||||
|
||||
|
||||
handlebarsHelpers.registerHelpers(handlebars);
|
||||
|
@ -25,9 +26,11 @@ const userFieldGetters = {
|
|||
|
||||
async function main() {
|
||||
try {
|
||||
const context = contextHelpers.getServiceContext();
|
||||
|
||||
const reportId = Number(process.argv[2]);
|
||||
|
||||
const report = await reports.getByIdWithTemplate(reportId);
|
||||
const report = await reports.getByIdWithTemplate(context, reportId);
|
||||
|
||||
const inputs = {};
|
||||
|
||||
|
@ -50,7 +53,7 @@ async function main() {
|
|||
}
|
||||
|
||||
const campaignsProxy = {
|
||||
getResults: reports.getCampaignResults,
|
||||
getResults: (campaign, select, extra) => reports.getCampaignResults(context, campaign, select, extra),
|
||||
getById: campaigns.getById
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue