From 0d7f962c86853ec51c6046c7493b625e8c022ab8 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sat, 12 Jan 2019 11:21:38 +0100 Subject: [PATCH] Fix - subscriber custom data were not listed in correct order in the subcribers list "Test user" field added to segment rules Configuration option to automatically share arbitrary namespace based on user role. --- client/src/lists/segments/helpers.js | 5 ++ server/index.js | 19 ++++++++ server/lib/campaign-sender.js | 6 ++- server/models/campaigns.js | 16 +++++-- server/models/lists.js | 19 ++++---- server/models/segments.js | 4 ++ server/models/shares.js | 71 ++++++++++++---------------- server/models/subscriptions.js | 8 ++-- 8 files changed, 91 insertions(+), 57 deletions(-) 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] }) }