diff --git a/client/src/lists/segments/helpers.js b/client/src/lists/segments/helpers.js index b9da6bca..47be0799 100644 --- a/client/src/lists/segments/helpers.js +++ b/client/src/lists/segments/helpers.js @@ -406,6 +406,11 @@ export function getRuleHelpers(t, fields) { column: 'latest_click', name: t('latestClick'), type: 'date' + }, + { + column: 'is_test', + name: t('Test user'), + type: 'option' } ]; diff --git a/server/index.js b/server/index.js index 491445d6..33860774 100644 --- a/server/index.js +++ b/server/index.js @@ -93,6 +93,24 @@ dbcheck(err => { // Check if database needs upgrading before starting the server ); */ + .then(() => + startHTTPServer(AppType.TRUSTED, 'trusted', trustedPort, () => + startHTTPServer(AppType.SANDBOXED, 'sandbox', sandboxPort, () => + startHTTPServer(AppType.PUBLIC, 'public', publicPort, async () => { + + await privilegeHelpers.ensureMailtrainDir(uploadedFilesDir); + + privilegeHelpers.dropRootPrivileges(); + + tzupdate.start(); + + log.info('Service', 'All services started'); + appBuilder.setReady(); + }) + ) + ) + ); +/* .then(() => executor.spawn(() => testServer(() => @@ -130,6 +148,7 @@ dbcheck(err => { // Check if database needs upgrading before starting the server ) ) ); +*/ }); diff --git a/server/lib/campaign-sender.js b/server/lib/campaign-sender.js index 09e92861..cacb4a1b 100644 --- a/server/lib/campaign-sender.js +++ b/server/lib/campaign-sender.js @@ -20,6 +20,7 @@ const htmlToText = require('html-to-text'); const {getPublicUrl} = require('./urls'); const blacklist = require('../models/blacklist'); const libmime = require('libmime'); +const shares = require('../models/shares') class CampaignSender { @@ -30,7 +31,7 @@ class CampaignSender { let sendConfiguration, list, fieldsGrouped, campaign, subscriptionGrouped, useVerp, useVerpSenderHeader, mergeTags, attachments; await knex.transaction(async tx => { - sendConfiguration = await sendConfigurations.getByIdTx(tx, context, sendConfigurationId, false, true); + sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), sendConfigurationId, false, true); list = await lists.getByCidTx(tx, context, listCid); fieldsGrouped = await fields.listGroupedTx(tx, list.id); @@ -42,7 +43,10 @@ class CampaignSender { if (campaignId) { campaign = await campaigns.getByIdTx(tx, context, campaignId, false, campaigns.Content.WITHOUT_SOURCE_CUSTOM); + await campaigns.enforceSendPermissionTx(tx, context, campaign); } else { + await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', sendConfigurationId, 'sendWithoutOverrides'); + // This is to fake the campaign for getMessageLinks, which is called inside formatMessage campaign = { cid: '[CAMPAIGN_ID]' diff --git a/server/models/campaigns.js b/server/models/campaigns.js index 8096f49f..3be43381 100644 --- a/server/models/campaigns.js +++ b/server/models/campaigns.js @@ -19,6 +19,7 @@ const segments = require('./segments'); const senders = require('../lib/senders'); const {LinkId} = require('./links'); const feedcheck = require('../lib/feedcheck'); +const contextHelpers = require('../lib/context-helpers'); const allowedKeysCommon = ['name', 'description', 'segment', 'namespace', 'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url']; @@ -637,8 +638,15 @@ async function remove(context, id) { } async function enforceSendPermissionTx(tx, context, campaignId) { - const campaign = await getByIdTx(tx, context, campaignId, false); - const sendConfiguration = await sendConfigurations.getByIdTx(tx, context, campaign.send_configuration, false, false); + let campaign; + + if (typeof campaignId === 'object') { + campaign = campaignId; + } else { + campaign = await getByIdTx(tx, context, campaignId, false); + } + + const sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), campaign.send_configuration, false, false); const requiredPermission = getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration); @@ -840,13 +848,13 @@ async function getSubscribersQueryGeneratorTx(tx, campaignId) { async function _changeStatus(context, campaignId, permittedCurrentStates, newState, invalidStateMessage, scheduled = null) { await knex.transaction(async tx => { - await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'send'); - const entity = await tx('campaigns').where('id', campaignId).first(); if (!entity) { throw new interoperableErrors.NotFoundError(); } + await enforceSendPermissionTx(tx, context, campaign); + if (!permittedCurrentStates.includes(entity.status)) { throw new interoperableErrors.InvalidStateError(invalidStateMessage); } diff --git a/server/models/lists.js b/server/models/lists.js index 0d35a4d6..b91a7d84 100644 --- a/server/models/lists.js +++ b/server/models/lists.js @@ -34,14 +34,17 @@ async function listDTAjax(context, params) { .from('lists') .innerJoin('namespaces', 'namespaces.id', 'lists.namespace'), ['lists.id', 'lists.name', 'lists.cid', 'lists.subscribers', 'lists.description', 'namespaces.name', - { query: builder => - builder.from('campaigns') - .innerJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign') - .innerJoin('triggers', 'campaigns.id', 'triggers.campaign') - .innerJoin(campaignEntityType.permissionsTable, 'campaigns.id', `${campaignEntityType.permissionsTable}.entity`) - .whereRaw('campaign_lists.list = lists.id') - .where(`${campaignEntityType.permissionsTable}.operation`, 'viewTriggers') - .count() + { + name: 'triggerCount', + query: builder => + builder.from('campaigns') + .innerJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign') + .innerJoin('triggers', 'campaigns.id', 'triggers.campaign') + .innerJoin(campaignEntityType.permissionsTable, 'campaigns.id', `${campaignEntityType.permissionsTable}.entity`) + .whereRaw('campaign_lists.list = lists.id') + .where(`${campaignEntityType.permissionsTable}.operation`, 'viewTriggers') + .count() + .as('triggerCount') } ] ); diff --git a/server/models/segments.js b/server/models/segments.js index e6cfadf5..676cf88c 100644 --- a/server/models/segments.js +++ b/server/models/segments.js @@ -35,6 +35,10 @@ const predefColumns = [ { column: 'latest_click', type: 'date' + }, + { + column: 'is_test', + type: 'option' } ]; diff --git a/server/models/shares.js b/server/models/shares.js index 7ca9eb61..55fda903 100644 --- a/server/models/shares.js +++ b/server/models/shares.js @@ -2,7 +2,7 @@ const knex = require('../lib/knex'); const config = require('config'); -const { enforce } = require('../lib/helpers'); +const { enforce, castToInteger } = require('../lib/helpers'); const dtHelpers = require('../lib/dt-helpers'); const entitySettings = require('../lib/entity-settings'); const interoperableErrors = require('../../shared/interoperable-errors'); @@ -170,56 +170,45 @@ async function rebuildPermissionsTx(tx, restriction) { } - // 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`]); + // Reset root, own and shared namespaces shares as per the user roles + const usersAutoSharesQry = tx('users') + .select(['users.id', 'users.role', 'users.namespace']); if (restriction.userId) { - usersWithRoleInOwnNamespaceQuery.where('users.id', restriction.userId); + usersAutoSharesQry.where('users.id', restriction.userId); } - const usersWithRoleInOwnNamespace = await usersWithRoleInOwnNamespaceQuery; + const usersAutoShares = await usersAutoSharesQry; - for (const user of usersWithRoleInOwnNamespace) { - const roleConf = config.roles.global[user.userRole]; + for (const user of usersAutoShares) { + const roleConf = config.roles.global[user.role]; 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, auto: true }); + const desiredRoles = new Map(); + + if (roleConf.sharedNamespaces) { + for (const shrKey in roleConf.sharedNamespaces) { + const shrRole = roleConf.sharedNamespaces[shrKey]; + const shrNsId = castToInteger(shrKey); + + desiredRoles.set(shrNsId, shrRole); + } + } + + if (roleConf.ownNamespaceRole) { + desiredRoles.set(user.namespace, roleConf.ownNamespaceRole); + } + + if (roleConf.rootNamespaceRole) { + desiredRoles.set(getGlobalNamespaceId(), roleConf.rootNamespaceRole); + } + + for (const [nsId, role] of desiredRoles.entries()) { + await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: nsId }).del(); + await tx(namespaceEntityType.sharesTable).insert({ user: user.id, entity: nsId, role: role, auto: true }); } } } - const usersWithRoleInRootNamespaceQuery = tx('users') - .leftJoin(namespaceEntityType.sharesTable, { - 'users.id': `${namespaceEntityType.sharesTable}.user`, - [`${namespaceEntityType.sharesTable}.entity`]: getGlobalNamespaceId() - }) - .select(['users.id', 'users.role as userRole', `${namespaceEntityType.sharesTable}.role`]); - if (restriction.userId) { - usersWithRoleInRootNamespaceQuery.andWhere('users.id', 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: getGlobalNamespaceId() }).del(); - await tx(namespaceEntityType.sharesTable).insert({ user: user.id, entity: getGlobalNamespaceId(), role: desiredRole, auto: 1 }); - } - } - } - - - // Build the map of all namespaces // nsMap is a map of namespaces - each of the following shape: // .id - id of the namespace diff --git a/server/models/subscriptions.js b/server/models/subscriptions.js index 1d549e00..f9276073 100644 --- a/server/models/subscriptions.js +++ b/server/models/subscriptions.js @@ -262,7 +262,7 @@ async function listDTAjax(context, listId, segmentId, params) { listTable + '.email', listTable + '.status', listTable + '.created', - { name: 'blacklisted', raw: 'not isnull(blacklist.email)' } + { name: 'blacklisted', raw: 'not isnull(blacklist.email) as `blacklisted`' } ]; const extraColumns = []; let listFldIdx = columns.length; @@ -275,9 +275,11 @@ async function listDTAjax(context, listId, segmentId, params) { if (fld.column) { columns.push(listTable + '.' + fld.column); } else { + const colName = listTable + '.' + fldCol; + columns.push({ - name: listTable + '.' + fldCol, - raw: '?', + name: colName, + raw: '? as `' + colName + '`', data: [0] }) }