diff --git a/client/src/account/API.js b/client/src/account/API.js index 42636879..bd97455c 100644 --- a/client/src/account/API.js +++ b/client/src/account/API.js @@ -10,6 +10,7 @@ import {Button} from '../lib/bootstrap-components'; import {getUrl} from "../lib/urls"; import {withComponentMixins} from "../lib/decorator-helpers"; import styles from "./styles.scss" +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -35,6 +36,10 @@ export default class API extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageApi) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageApi'); + } // noinspection JSIgnoredPromiseFromCall this.loadAccessToken(); } diff --git a/client/src/blacklist/List.js b/client/src/blacklist/List.js index 9429b446..24df22c2 100644 --- a/client/src/blacklist/List.js +++ b/client/src/blacklist/List.js @@ -10,6 +10,7 @@ import {Button} from "../lib/bootstrap-components"; import {HTTPMethod} from "../lib/axios"; import {tableAddRestActionButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {withComponentMixins} from "../lib/decorator-helpers"; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -87,6 +88,10 @@ export default class List extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageBlacklist) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageBlacklist'); + } this.clearFields(); } @@ -139,4 +144,4 @@ export default class List extends Component { ); } -} \ No newline at end of file +} diff --git a/client/src/campaigns/CUD.js b/client/src/campaigns/CUD.js index 666025ac..6e78b855 100644 --- a/client/src/campaigns/CUD.js +++ b/client/src/campaigns/CUD.js @@ -254,6 +254,10 @@ export default class CUD extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageCampaigns) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageCampaigns'); + } if (this.props.entity) { this.getFormValuesFromEntity(this.props.entity); diff --git a/client/src/campaigns/List.js b/client/src/campaigns/List.js index e8db5b36..b98dea0f 100644 --- a/client/src/campaigns/List.js +++ b/client/src/campaigns/List.js @@ -13,6 +13,7 @@ import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRe import {withComponentMixins} from "../lib/decorator-helpers"; import styles from "./styles.scss"; import PropTypes from 'prop-types'; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -34,6 +35,13 @@ export default class List extends Component { tableRestActionDialogInit(this); } + componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageCampaigns) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageCampaigns'); + } + } + static propTypes = { permissions: PropTypes.object, channel: PropTypes.object diff --git a/client/src/channels/CUD.js b/client/src/channels/CUD.js index 6ef6da1c..21fcdf8e 100644 --- a/client/src/channels/CUD.js +++ b/client/src/channels/CUD.js @@ -254,6 +254,10 @@ export default class CUD extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageChannels) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageChannels'); + } if (this.props.entity) { this.getFormValuesFromEntity(this.props.entity); diff --git a/client/src/channels/List.js b/client/src/channels/List.js index 1d03efe6..3d3a8ad6 100644 --- a/client/src/channels/List.js +++ b/client/src/channels/List.js @@ -10,6 +10,7 @@ import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRe import {withComponentMixins} from "../lib/decorator-helpers"; import styles from "./styles.scss"; import PropTypes from 'prop-types'; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -31,6 +32,13 @@ export default class List extends Component { permissions: PropTypes.object } + componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageChannels) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageChannels'); + } + } + render() { const t = this.props.t; diff --git a/client/src/lists/CUD.js b/client/src/lists/CUD.js index 588282fa..bc651622 100644 --- a/client/src/lists/CUD.js +++ b/client/src/lists/CUD.js @@ -27,6 +27,7 @@ import {FieldWizard, UnsubscriptionMode} from '../../../shared/lists'; import styles from "../lib/styles.scss"; import {getMailerTypes} from "../send-configurations/helpers"; import {withComponentMixins} from "../lib/decorator-helpers"; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -73,6 +74,10 @@ export default class CUD extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageLists) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageLists'); + } if (this.props.entity) { this.getFormValuesFromEntity(this.props.entity); diff --git a/client/src/lists/List.js b/client/src/lists/List.js index f7751ebd..511b6fe6 100644 --- a/client/src/lists/List.js +++ b/client/src/lists/List.js @@ -10,6 +10,7 @@ import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRe import {withComponentMixins} from "../lib/decorator-helpers"; import {withForm} from "../lib/form"; import PropTypes from 'prop-types'; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -26,6 +27,13 @@ export default class List extends Component { tableRestActionDialogInit(this); } + componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageLists) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageLists'); + } + } + static propTypes = { permissions: PropTypes.object } @@ -132,6 +140,7 @@ export default class List extends Component { this.table = node} withHeader dataUrl="rest/lists-table" columns={columns} /> - ); + ) + } -} \ No newline at end of file +} diff --git a/client/src/lists/fields/CUD.js b/client/src/lists/fields/CUD.js index f99bb0ce..61c5d792 100644 --- a/client/src/lists/fields/CUD.js +++ b/client/src/lists/fields/CUD.js @@ -32,6 +32,7 @@ import styles from "../../lib/styles.scss"; import 'ace-builds/src-noconflict/mode-json'; import 'ace-builds/src-noconflict/mode-handlebars'; import {withComponentMixins} from "../../lib/decorator-helpers"; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -178,6 +179,10 @@ export default class CUD extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageLists) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageLists'); + } if (this.props.entity) { this.getFormValuesFromEntity(this.props.entity); diff --git a/client/src/lists/fields/List.js b/client/src/lists/fields/List.js index 33461a41..0431b669 100644 --- a/client/src/lists/fields/List.js +++ b/client/src/lists/fields/List.js @@ -10,6 +10,7 @@ import {getFieldTypes} from './helpers'; import {Icon} from "../../lib/bootstrap-components"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals"; import {withComponentMixins} from "../../lib/decorator-helpers"; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -32,6 +33,10 @@ export default class List extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageLists) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageLists'); + } } render() { @@ -77,4 +82,4 @@ export default class List extends Component { ); } -} \ No newline at end of file +} diff --git a/client/src/lists/imports/CUD.js b/client/src/lists/imports/CUD.js index 887df141..96cf548e 100644 --- a/client/src/lists/imports/CUD.js +++ b/client/src/lists/imports/CUD.js @@ -30,6 +30,7 @@ import listStyles from "../styles.scss"; import styles from "../../lib/styles.scss"; import interoperableErrors from "../../../../shared/interoperable-errors"; import {withComponentMixins} from "../../lib/decorator-helpers"; +import mailtrainConfig from 'mailtrainConfig'; function truncate(str, len, ending = '...') { @@ -209,6 +210,10 @@ export default class CUD extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageLists) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageLists'); + } if (this.props.entity) { this.initFromEntity(this.props.entity); } else { @@ -469,4 +474,4 @@ export default class CUD extends Component { ); } -} \ No newline at end of file +} diff --git a/client/src/lists/imports/List.js b/client/src/lists/imports/List.js index 7e077748..1a960844 100644 --- a/client/src/lists/imports/List.js +++ b/client/src/lists/imports/List.js @@ -37,6 +37,10 @@ export default class List extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageLists) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageLists'); + } } render() { @@ -95,4 +99,4 @@ export default class List extends Component { ); } -} \ No newline at end of file +} diff --git a/client/src/lists/segments/CUD.js b/client/src/lists/segments/CUD.js index af737d10..18e578f9 100644 --- a/client/src/lists/segments/CUD.js +++ b/client/src/lists/segments/CUD.js @@ -28,6 +28,7 @@ import {getRuleHelpers} from "./helpers"; import RuleSettingsPane from "./RuleSettingsPane"; import {withComponentMixins} from "../../lib/decorator-helpers"; import clone from "clone"; +import mailtrainConfig from 'mailtrainConfig'; // https://stackoverflow.com/a/4819886/1601953 const isTouchDevice = !!('ontouchstart' in window || navigator.maxTouchPoints); @@ -123,6 +124,10 @@ export default class CUD extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageLists) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageLists'); + } if (this.props.entity) { this.getFormValuesFromEntity(this.props.entity); @@ -401,4 +406,4 @@ export default class CUD extends Component { ); } -} \ No newline at end of file +} diff --git a/client/src/lists/segments/List.js b/client/src/lists/segments/List.js index b95c134c..e71d7139 100644 --- a/client/src/lists/segments/List.js +++ b/client/src/lists/segments/List.js @@ -9,6 +9,7 @@ import {Table} from '../../lib/table'; import {Icon} from "../../lib/bootstrap-components"; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals"; import {withComponentMixins} from "../../lib/decorator-helpers"; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -29,6 +30,10 @@ export default class List extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageLists) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageLists'); + } } render() { @@ -69,4 +74,4 @@ export default class List extends Component { ); } -} \ No newline at end of file +} diff --git a/client/src/lists/subscriptions/CUD.js b/client/src/lists/subscriptions/CUD.js index 22c2c6f2..df20ed37 100644 --- a/client/src/lists/subscriptions/CUD.js +++ b/client/src/lists/subscriptions/CUD.js @@ -25,6 +25,7 @@ import {getFieldColumn, SubscriptionStatus} from '../../../../shared/lists'; import {getFieldTypes, getSubscriptionStatusLabels} from './helpers'; import moment from 'moment-timezone'; import {withComponentMixins} from "../../lib/decorator-helpers"; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -89,6 +90,11 @@ export default class CUD extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageLists) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageLists'); + } + if (this.props.entity) { this.getFormValuesFromEntity(this.props.entity); diff --git a/client/src/lists/subscriptions/List.js b/client/src/lists/subscriptions/List.js index 1938643a..3c154173 100644 --- a/client/src/lists/subscriptions/List.js +++ b/client/src/lists/subscriptions/List.js @@ -21,6 +21,7 @@ import { } from "../../lib/modals"; import listStyles from "../styles.scss"; import {withComponentMixins} from "../../lib/decorator-helpers"; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -58,6 +59,10 @@ export default class List extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageLists) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageLists'); + } this.populateFormValues({ segment: this.props.segmentId || '' }); @@ -188,4 +193,4 @@ export default class List extends Component { ); } -} \ No newline at end of file +} diff --git a/client/src/namespaces/CUD.js b/client/src/namespaces/CUD.js index b073266e..e9b21216 100644 --- a/client/src/namespaces/CUD.js +++ b/client/src/namespaces/CUD.js @@ -93,6 +93,10 @@ export default class CUD extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageNamespaces) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageNamespaces'); + } if (this.props.entity) { this.getFormValuesFromEntity(this.props.entity); } else { diff --git a/client/src/namespaces/List.js b/client/src/namespaces/List.js index dcb684c8..f791d54f 100644 --- a/client/src/namespaces/List.js +++ b/client/src/namespaces/List.js @@ -29,7 +29,12 @@ export default class List extends Component { static propTypes = { permissions: PropTypes.object } - + componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageNamespaces) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageNamespaces'); + } + } render() { const t = this.props.t; diff --git a/client/src/root.js b/client/src/root.js index f6e3dfea..b51af63b 100644 --- a/client/src/root.js +++ b/client/src/root.js @@ -28,10 +28,10 @@ import {getUrl} from "./lib/urls"; import {withComponentMixins} from "./lib/decorator-helpers"; import Update from "./settings/Update"; -const topLevelMenuKeys = ['lists', 'channels', 'templates', 'campaigns']; +const topLevelMenuKeys = ['Lists', 'Channels', 'Templates', 'Campaigns']; if (mailtrainConfig.reportsEnabled) { - topLevelMenuKeys.push('reports'); + topLevelMenuKeys.push('Reports'); } @@ -67,30 +67,43 @@ class Root extends Component { const topLevelMenu = []; - for (const entryKey of topLevelMenuKeys) { - const entry = topLevelItems[entryKey]; - const link = entry.link || entry.externalLink; - - if (link && path.startsWith(link)) { - topLevelMenu.push({entry.title} {t('current')}); - } else { - topLevelMenu.push({entry.title}); - } - } - if (mailtrainConfig.isAuthenticated) { + + const gP = mailtrainConfig.globalPermissions; + + for (const entryKey of topLevelMenuKeys) { + const entry = topLevelItems[entryKey.toLowerCase()]; + const link = entry.link || entry.externalLink; + + if (gP["manage"+entryKey]) { + if (link && path.startsWith(link)) { + topLevelMenu.push({entry.title} {t('current')}); + } else { + topLevelMenu.push({entry.title}); + } + } + } + return ( <> ); + } else { return ( <> diff --git a/client/src/send-configurations/CUD.js b/client/src/send-configurations/CUD.js index 4d972a28..3b72843e 100644 --- a/client/src/send-configurations/CUD.js +++ b/client/src/send-configurations/CUD.js @@ -91,6 +91,10 @@ export default class CUD extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageSendConfigurations) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageSendConfigurations'); + } if (this.props.entity) { this.getFormValuesFromEntity(this.props.entity); } else { diff --git a/client/src/send-configurations/List.js b/client/src/send-configurations/List.js index 0238265d..5bd71398 100644 --- a/client/src/send-configurations/List.js +++ b/client/src/send-configurations/List.js @@ -11,6 +11,7 @@ import {getMailerTypes} from './helpers'; import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../lib/modals"; import {withComponentMixins} from "../lib/decorator-helpers"; import PropTypes from 'prop-types'; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ @@ -33,6 +34,13 @@ export default class List extends Component { permissions: PropTypes.object } + componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageSendConfigurations) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageSendConfigurations'); + } + } + render() { const t = this.props.t; @@ -87,4 +95,4 @@ export default class List extends Component { ); } -} \ No newline at end of file +} diff --git a/client/src/settings/Update.js b/client/src/settings/Update.js index 27a26bd0..16dfbe54 100644 --- a/client/src/settings/Update.js +++ b/client/src/settings/Update.js @@ -19,6 +19,7 @@ import { } from '../lib/form'; import {withErrorHandling} from '../lib/error-handling'; import {withComponentMixins} from "../lib/decorator-helpers"; +import mailtrainConfig from 'mailtrainConfig'; @withComponentMixins([ withTranslation, @@ -45,6 +46,10 @@ export default class Update extends Component { } componentDidMount() { + const t = this.props.t; + if (!mailtrainConfig.globalPermissions.manageSettings) { + this.navigateToWithFlashMessage('/', 'danger', t('permissionDenied')+': manageSettings'); + } this.getFormValuesFromEntity(this.props.entity); } @@ -102,4 +107,4 @@ export default class Update extends Component { ); } -} \ No newline at end of file +} diff --git a/locales/de-DE/common.json b/locales/de-DE/common.json index e1f0faf2..434cb6c9 100644 --- a/locales/de-DE/common.json +++ b/locales/de-DE/common.json @@ -1065,5 +1065,6 @@ "channelName": "Channel \"{{name}}\"", "cloneCampaign": "Clone Campaign", "next": "Next", - "selectCampaignToBeCloned": "Select campaign to be cloned." + "selectCampaignToBeCloned": "Select campaign to be cloned.", + "permissionDenied": "Zugang verweigert" } diff --git a/locales/en-US/common.json b/locales/en-US/common.json index 25510369..a3ca6924 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -1071,5 +1071,6 @@ "channelName": "Channel \"{{name}}\"", "cloneCampaign": "Clone Campaign", "next": "Next", - "selectCampaignToBeCloned": "Select campaign to be cloned." + "selectCampaignToBeCloned": "Select campaign to be cloned.", + "permissionDenied": "Permission Denied" } diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index 341d60e6..e155fdcc 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -1095,5 +1095,6 @@ "selectCampaignToBeCloned": "Elige la campaña que será clonada.", "tagLanguage": "Lenguaje de marcado", "tagLanguageMustBeSelected": "Debes seleccionar un lenguaje de marcado", - "helpText": "Texto de ayuda" + "helpText": "Texto de ayuda", + "permissionDenied": "Permiso denegado" } diff --git a/locales/fr-FR/common.json b/locales/fr-FR/common.json index e25d07b1..3efacd8c 100644 --- a/locales/fr-FR/common.json +++ b/locales/fr-FR/common.json @@ -1066,5 +1066,6 @@ "channelName": "Channel \"{{name}}\"", "cloneCampaign": "Clone Campaign", "next": "Next", - "selectCampaignToBeCloned": "Select campaign to be cloned." + "selectCampaignToBeCloned": "Select campaign to be cloned.", + "permissionDenied": "Permission refusée" } diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json index d60be0ad..c1ee6bbf 100644 --- a/locales/pt-BR/common.json +++ b/locales/pt-BR/common.json @@ -1144,5 +1144,6 @@ "channelName": "Channel \"{{name}}\"", "cloneCampaign": "Clone Campaign", "next": "Next", - "selectCampaignToBeCloned": "Select campaign to be cloned." + "selectCampaignToBeCloned": "Select campaign to be cloned.", + "permissionDenied": "Permissão negada" } diff --git a/server/config/default.yaml b/server/config/default.yaml index 14b07bf9..46f7727d 100644 --- a/server/config/default.yaml +++ b/server/config/default.yaml @@ -277,12 +277,12 @@ defaultRoles: name: Global Master admin: true description: All permissions - permissions: [rebuildPermissions, createJavascriptWithROAccess, displayManageUsers, manageBlacklist, manageSettings, setupAutomation] + permissions: [rebuildPermissions, createJavascriptWithROAccess, manageBlacklist, manageSettings, manageUsers, manageLists, manageChannels, manageTemplates, manageCampaigns, manageReports, manageApi, manageSendConfigurations, manageNamespaces, setupAutomation] rootNamespaceRole: master campaignsAdmin: name: Campaigns Admin description: Under the namespace in which the user is located, the user has all permissions for managing lists, templates and campaigns and the permission to send to send configurations. - permissions: [setupAutomation] + permissions: [setupAutomation, manageLists, manageChannels, manageTemplates, manageCampaigns, manageReports, manageApi, manageSendConfigurations, manageNamespaces] ownNamespaceRole: campaignsAdmin campaignsAdminWithoutNamespace: name: Campaigns Admin (multiple namespaces) diff --git a/server/lib/translate.js b/server/lib/translate.js index ba272355..465cc6b4 100644 --- a/server/lib/translate.js +++ b/server/lib/translate.js @@ -15,6 +15,8 @@ function loadLanguage(longCode) { loadLanguage('en-US'); loadLanguage('es-ES'); loadLanguage('pt-BR'); +loadLanguage('de-DE'); +loadLanguage('fr-FR'); resourcesCommon['fk-FK'] = convertToFake(resourcesCommon['en-US']); const resources = {}; diff --git a/server/models/campaigns.js b/server/models/campaigns.js index d5e11ed0..ffe4351e 100644 --- a/server/models/campaigns.js +++ b/server/models/campaigns.js @@ -445,6 +445,7 @@ async function getByCid(context, cid) { } async function _validateAndPreprocess(tx, context, entity, isCreate, content) { + shares.enforceGlobalPermission(context, 'manageCampaigns'); if (content === Content.ALL || content === Content.WITHOUT_SOURCE_CUSTOM || content === Content.RSS_ENTRY) { await namespaceHelpers.validateEntity(tx, entity); @@ -481,6 +482,7 @@ async function _validateAndPreprocess(tx, context, entity, isCreate, content) { } async function _createTx(tx, context, entity, content) { + shares.enforceGlobalPermission(context, 'manageCampaigns'); return await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCampaign'); @@ -579,6 +581,7 @@ async function createRssTx(tx, context, entity) { } async function _validateChannelMoveTx(tx, context, entity, existing) { + shares.enforceGlobalPermission(context, 'manageCampaigns'); if (existing.channel !== entity.channel) { await shares.enforceEntityPermission(context, 'channel', entity.channel, 'createCampaign'); await shares.enforceEntityPermission(context, 'campaign', entity.id, 'delete'); @@ -637,6 +640,7 @@ async function updateWithConsistencyCheck(context, entity, content) { } async function _removeTx(tx, context, id, existing = null, overrideTypeCheck = false) { + shares.enforceGlobalPermission(context, 'manageCampaigns'); await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'delete'); if (!existing) { @@ -861,6 +865,7 @@ async function prepareCampaignMessages(campaignId) { } async function _changeStatus(context, campaignId, permittedCurrentStates, newState, invalidStateMessage, extraData) { + shares.enforceGlobalPermission(context, 'manageCampaigns'); await knex.transaction(async tx => { // This is quite inefficient because it selects the same row 3 times. However as status is changed // rather infrequently, we keep it this way for simplicity @@ -925,6 +930,7 @@ async function stop(context, campaignId) { } async function reset(context, campaignId) { + shares.enforceGlobalPermission(context, 'manageCampaigns'); await knex.transaction(async tx => { // This is quite inefficient because it selects the same row 3 times. However as RESET is // going to be called rather infrequently, we keep it this way for simplicity diff --git a/server/models/channels.js b/server/models/channels.js index 75151090..a96ba111 100644 --- a/server/models/channels.js +++ b/server/models/channels.js @@ -140,6 +140,7 @@ async function _validateAndPreprocess(tx, context, entity, isCreate) { } async function _createTx(tx, context, entity, content) { + shares.enforceGlobalPermission(context, 'manageChannels'); return await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCampaign'); @@ -169,6 +170,7 @@ async function create(context, entity) { } async function updateWithConsistencyCheck(context, entity) { + shares.enforceGlobalPermission(context, 'manageChannels'); await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'channel', entity.id, 'edit'); @@ -198,6 +200,7 @@ async function updateWithConsistencyCheck(context, entity) { async function remove(context, id) { + shares.enforceGlobalPermission(context, 'manageChannels'); await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'channel', id, 'delete'); diff --git a/server/models/lists.js b/server/models/lists.js index ada16cf8..22236652 100644 --- a/server/models/lists.js +++ b/server/models/lists.js @@ -68,6 +68,7 @@ async function listByNamespaceDTAjax(context, namespaceId, params) { } async function listWithSegmentByCampaignDTAjax(context, campaignId, params) { + await shares.enforceEntityPermissionTx(tx, context, 'list', id, 'view'); return await dtHelpers.ajaxListWithPermissions( context, [{ entityTypeId: 'list', requiredOperations: ['view'] }], @@ -84,7 +85,6 @@ async function listWithSegmentByCampaignDTAjax(context, campaignId, params) { } async function getByIdTx(tx, context, id) { - await shares.enforceEntityPermissionTx(tx, context, 'list', id, 'view'); const entity = await tx('lists').where('id', id).first(); return entity; } @@ -153,6 +153,7 @@ async function _validateAndPreprocess(tx, entity) { } async function create(context, entity) { + shares.enforceGlobalPermission(context, 'manageLists'); return await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createList'); @@ -248,6 +249,7 @@ async function create(context, entity) { } async function updateWithConsistencyCheck(context, entity) { + shares.enforceGlobalPermission(context, 'manageLists'); await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'edit'); @@ -274,6 +276,7 @@ async function updateWithConsistencyCheck(context, entity) { } async function remove(context, id) { + shares.enforceGlobalPermission(context, 'manageLists'); await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'list', id, 'delete'); diff --git a/server/models/namespaces.js b/server/models/namespaces.js index eb1744d9..d29282d2 100644 --- a/server/models/namespaces.js +++ b/server/models/namespaces.js @@ -120,7 +120,6 @@ async function getById(context, id) { async function getChildrenTx(tx, context, id) { await shares.enforceEntityPermissionTx(tx, context, 'namespace', id, 'view'); - const entityType = entitySettings.getEntityType('namespace'); const extraKeys = em.get('models.namespaces.extraKeys', []); @@ -162,6 +161,7 @@ async function getChildrenTx(tx, context, id) { } async function createTx(tx, context, entity) { + shares.enforceGlobalPermission(context, 'manageNamespaces'); enforce(entity.namespace, 'Parent namespace must be set'); await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createNamespace'); @@ -183,6 +183,7 @@ async function create(context, entity) { async function updateWithConsistencyCheck(context, entity) { enforce(entity.id !== 1 || entity.namespace === null, 'Cannot assign a parent to the root namespace.'); + shares.enforceGlobalPermission(context, 'manageNamespaces'); await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.id, 'edit'); @@ -221,6 +222,7 @@ async function updateWithConsistencyCheck(context, entity) { async function remove(context, id) { enforce(id !== 1, 'Cannot delete the root namespace.'); + shares.enforceGlobalPermission(context, 'manageNamespaces'); await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'namespace', id, 'delete'); diff --git a/server/models/reports.js b/server/models/reports.js index 90983580..9d31ed74 100644 --- a/server/models/reports.js +++ b/server/models/reports.js @@ -64,6 +64,7 @@ async function listDTAjax(context, params) { } async function create(context, entity) { + shares.enforceGlobalPermission(context, 'manageReports'); let id; await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createReport'); @@ -85,6 +86,7 @@ async function create(context, entity) { } async function updateWithConsistencyCheck(context, entity) { + shares.enforceGlobalPermission(context, 'manageReports'); await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'report', entity.id, 'edit'); await shares.enforceEntityPermissionTx(tx, context, 'reportTemplate', entity.report_template, 'execute'); @@ -120,6 +122,7 @@ async function updateWithConsistencyCheck(context, entity) { } async function removeTx(tx, context, id) { + shares.enforceGlobalPermission(context, 'manageReports'); await shares.enforceEntityPermissionTx(tx, context, 'report', id, 'delete'); const report = await tx('reports').where('id', id).first(); diff --git a/server/models/send-configurations.js b/server/models/send-configurations.js index 890c0d38..631d605b 100644 --- a/server/models/send-configurations.js +++ b/server/models/send-configurations.js @@ -120,6 +120,7 @@ async function _validateAndPreprocess(tx, entity, isCreate) { async function create(context, entity) { + shares.enforceGlobalPermission(context, 'manageSendConfigurations'); return await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createSendConfiguration'); @@ -138,6 +139,7 @@ async function create(context, entity) { } async function updateWithConsistencyCheck(context, entity) { + shares.enforceGlobalPermission(context, 'manageSendConfigurations'); await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', entity.id, 'edit'); @@ -167,6 +169,7 @@ async function updateWithConsistencyCheck(context, entity) { } async function remove(context, id) { + shares.enforceGlobalPermission(context, 'manageSendConfigurations'); if (id === getSystemSendConfigurationId()) { shares.throwPermissionDenied(); } diff --git a/server/models/shares.js b/server/models/shares.js index ce974cf8..2398a96e 100644 --- a/server/models/shares.js +++ b/server/models/shares.js @@ -10,6 +10,7 @@ const log = require('../lib/log'); const {getGlobalNamespaceId} = require('../../shared/namespaces'); const {getAdminId} = require('../../shared/users'); + // TODO: This would really benefit from some permission cache connected to rebuildPermissions // A bit of the problem is that the cache would have to expunged as the result of other processes modifying entites/permissions @@ -726,4 +727,4 @@ module.exports.regenerateRoleNamesTable = regenerateRoleNamesTable; module.exports.getGlobalPermissions = getGlobalPermissions; module.exports.getPermissionsTx = getPermissionsTx; module.exports.filterPermissionsByRestrictedAccessHandler = filterPermissionsByRestrictedAccessHandler; -module.exports.isAccessibleByRestrictedAccessHandler = isAccessibleByRestrictedAccessHandler; \ No newline at end of file +module.exports.isAccessibleByRestrictedAccessHandler = isAccessibleByRestrictedAccessHandler; diff --git a/server/models/templates.js b/server/models/templates.js index b6c53174..eaf49587 100644 --- a/server/models/templates.js +++ b/server/models/templates.js @@ -70,6 +70,7 @@ async function _validateAndPreprocess(tx, entity) { } async function create(context, entity) { + shares.enforceGlobalPermission(context, 'manageTemplates'); return await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createTemplate'); @@ -114,6 +115,7 @@ async function create(context, entity) { } async function updateWithConsistencyCheck(context, entity) { + shares.enforceGlobalPermission(context, 'manageTemplates'); await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'template', entity.id, 'edit'); @@ -143,6 +145,7 @@ async function updateWithConsistencyCheck(context, entity) { } async function remove(context, id) { + shares.enforceGlobalPermission(context, 'manageTemplates'); await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'template', id, 'delete'); diff --git a/server/models/users.js b/server/models/users.js index 6ce7aee2..f1c7cedc 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -165,6 +165,7 @@ async function _validateAndPreprocess(tx, entity, isCreate, isOwnAccount) { } async function create(context, user) { + shares.enforceGlobalPermission(context, 'manageUsers'); let id; await knex.transaction(async tx => { await shares.enforceEntityPermissionTx(tx, context, 'namespace', user.namespace, 'manageUsers'); @@ -192,6 +193,7 @@ async function create(context, user) { } async function updateWithConsistencyCheck(context, user, isOwnAccount) { + shares.enforceGlobalPermission(context, 'manageUsers'); await knex.transaction(async tx => { const existing = await tx('users').where('id', user.id).first(); if (!existing) { @@ -240,6 +242,7 @@ async function updateWithConsistencyCheck(context, user, isOwnAccount) { async function remove(context, userId) { enforce(userId !== 1, 'Admin cannot be deleted'); enforce(context.user.id !== userId, 'User cannot delete himself/herself'); + shares.enforceGlobalPermission(context, 'manageUsers'); await knex.transaction(async tx => { const existing = await tx('users').where('id', userId).first();