Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PP-12058 View user services #4169

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions app/controllers/browser-as-a-user/browse-as-a-user.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict'

const { response } = require('../../utils/response')
const paths = require('../../paths')
const userService = require('../../services/user.service')
const { RESTClientError } = require('../../errors')

function get (req, res) {
return response(req, res, 'browse-as-a-user/index')
}

async function post (req, res, next) {
const { body } = req

const emailAddress = body.emailAddress

try {
const userFound = await userService.findUserByEmail(emailAddress)
req.session.assumedUserId = userFound.externalId
req.session.assumedUserEmail = userFound.email
req.flash('generic', `You are now viewing ${userFound.email} services`)
return res.redirect(paths.serviceSwitcher.index)
} catch (error) {
if (error instanceof RESTClientError && error.errorCode === 404) {
req.flash('genericError', `User with email ${emailAddress} not found`)
}
else {
req.flash('genericError', `Something has gone wrong`)
}
return response(req, res, 'browse-as-a-user/index', { emailAddress: emailAddress })
}
}

function clear (req, res) {
delete req.session.assumedUserId
delete req.session.assumedUserEmail

res.redirect(paths.serviceSwitcher.index)
}

module.exports = {
get,
post,
clear
}
15 changes: 13 additions & 2 deletions app/controllers/my-services/get-index.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const serviceService = require('../../services/service.service')
const { filterGatewayAccountIds } = require('../../utils/permissions')
const getHeldPermissions = require('../../utils/get-held-permissions')
const { DEFAULT_SERVICE_NAME } = require('../../utils/constants')
const getAdminUsersClient = require('../../services/clients/adminusers.client')
const adminUsersClient = getAdminUsersClient()

function hasStripeAccount (gatewayAccounts) {
return gatewayAccounts.some(gatewayAccount =>
Expand All @@ -28,7 +30,14 @@ function sortServicesByLiveThenName (a, b) {
}

module.exports = async function getServiceList (req, res) {
const servicesRoles = lodash.get(req, 'user.serviceRoles', [])
let servicesRoles
if (req.session.assumedUserId) {
const assumedUser = await adminUsersClient.getUserByExternalId(req.session.assumedUserId)
servicesRoles = lodash.get(assumedUser, 'serviceRoles', [])
} else {
servicesRoles = lodash.get(req, 'user.serviceRoles', [])
}

const newServiceId = res.locals.flash && res.locals.flash.inviteSuccessServiceId &&
res.locals.flash.inviteSuccessServiceId[0]

Expand Down Expand Up @@ -62,7 +71,9 @@ module.exports = async function getServiceList (req, res) {
services_singular: servicesData.length === 1,
env: process.env,
has_account_with_payouts: hasStripeAccount(aggregatedGatewayAccounts),
has_live_account: filterGatewayAccountIds(aggregatedGatewayAccounts, true).length
has_live_account: filterGatewayAccountIds(aggregatedGatewayAccounts, true).length,
has_global_role: req.user.hasGlobalRole(),
assumedUserEmail: req.session.assumedUserEmail
}

if (newServiceId) {
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/my-services/post-index.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = (req, res) => {
const gatewayAccountId = req.body && req.body.gatewayAccountId
const gatewayAccountExternalId = req.body && req.body.gatewayAccountExternalId

if (validAccountId(gatewayAccountId, req.user)) {
if (validAccountId(gatewayAccountId, req.user) || req.user.hasGlobalRole()) {
res.redirect(302, formatAccountPathsFor(paths.account.dashboard.index, gatewayAccountExternalId))
} else {
logger.warn(`Attempted to switch to invalid account ${gatewayAccountId}`)
Expand Down
31 changes: 20 additions & 11 deletions app/middleware/get-service-and-gateway-account.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const { keys } = require('@govuk-pay/pay-js-commons').logging
const { addField } = require('../utils/request-context')
const { getSwitchingCredentialIfExists } = require('../utils/credentials')
const connectorClient = new Connector(process.env.CONNECTOR_URL)
const getAdminUsersClient = require('../services/clients/adminusers.client')
const adminUsersClient = getAdminUsersClient()

async function getGatewayAccountByExternalId (gatewayAccountExternalId) {
try {
Expand Down Expand Up @@ -47,21 +49,29 @@ async function getGatewayAccountByExternalId (gatewayAccountExternalId) {
}
}

function getService (user, serviceExternalId, gatewayAccount) {
async function getService (user, serviceExternalId, gatewayAccount) {
let service
const serviceRoles = _.get(user, 'serviceRoles', [])

if (serviceRoles.length > 0) {
if (user.role) {
if (serviceExternalId) {
service = _.get(serviceRoles.find(serviceRole => {
return (serviceRole.service.externalId === serviceExternalId &&
(!gatewayAccount || serviceRole.service.gatewayAccountIds.includes(String(gatewayAccount.gateway_account_id))))
}), 'service')
} else {
if (gatewayAccount) {
service = await adminUsersClient.findServiceByExternalId(serviceExternalId)
} else if (gatewayAccount) {
service = await adminUsersClient.findServiceByExternalId(gatewayAccount.service_id)
}
} else {
if (serviceRoles.length > 0) {
if (serviceExternalId) {
service = _.get(serviceRoles.find(serviceRole => {
return serviceRole.service.gatewayAccountIds.includes(String(gatewayAccount.gateway_account_id))
return (serviceRole.service.externalId === serviceExternalId &&
(!gatewayAccount || serviceRole.service.gatewayAccountIds.includes(String(gatewayAccount.gateway_account_id))))
}), 'service')
} else {
if (gatewayAccount) {
service = _.get(serviceRoles.find(serviceRole => {
return serviceRole.service.gatewayAccountIds.includes(String(gatewayAccount.gateway_account_id))
}), 'service')
}
}
}
}
Expand Down Expand Up @@ -98,12 +108,11 @@ module.exports = async function getServiceAndGatewayAccount (req, res, next) {

// uses req.user object which is set by passport (auth.service.js) and has all user services information to find service by serviceExternalId or gatewayAccountId.
// A separate API call to adminusers to find service makes it independent of user object but most of tests setup currently relies on req.user
const service = getService(req.user, serviceExternalId, gatewayAccount)
const service = await getService(req.user, serviceExternalId, gatewayAccount)
if (service) {
req.service = service
addField(keys.SERVICE_EXTERNAL_ID, service.externalId)
}

if (environment) {
req.isLive = environment === 'live'
}
Expand Down
19 changes: 11 additions & 8 deletions app/middleware/user-is-authorised.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@ module.exports = function userIsAuthorised (req, res, next) {
return next(new UserAccountDisabledError('User account is disabled'))
}

if (params[GATEWAY_ACCOUNT_EXTERNAL_ID] || params[SERVICE_EXTERNAL_ID]) {
if (!service) {
return next(new NotAuthorisedError('Service not found on request'))
}
if (!user.serviceRoles.find(serviceRole => serviceRole.service.externalId === service.externalId)) {
return next(new NotAuthorisedError('User does not have service role for service'))
if (user.role && user.role.name != null) {
next()
} else {
if (params[GATEWAY_ACCOUNT_EXTERNAL_ID] || params[SERVICE_EXTERNAL_ID]) {
if (!service) {
return next(new NotAuthorisedError('Service not found on request'))
}
if (!user.serviceRoles.find(serviceRole => serviceRole.service.externalId === service.externalId)) {
return next(new NotAuthorisedError('User does not have service role for service'))
}
}
next()
}

next()
}
20 changes: 18 additions & 2 deletions app/models/User.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class User {
}
this.externalId = userData.external_id
this.email = userData.email || ''
this.role = userData.role || ''
this.serviceRoles = userData.service_roles.map(serviceRoleData => new ServiceRole(serviceRoleData))
this.otpKey = userData.otp_key || ''
this.telephoneNumber = userData.telephone_number || ''
Expand Down Expand Up @@ -94,9 +95,16 @@ class User {
* @returns {boolean} Whether or not the user has the given permission
*/
hasPermission (serviceExternalId, permissionName) {
return _.get(this.getRoleForService(serviceExternalId), 'permissions', [])
let hasPermission = _.get(this.getRoleForService(serviceExternalId), 'permissions', [])
.map(permission => permission.name)
.includes(permissionName)

if (!hasPermission && this.role) {
return _.get(this, 'role.permissions', [])
.map(permission => permission.name)
.includes(permissionName)
}
return hasPermission
}

/**
Expand Down Expand Up @@ -127,11 +135,19 @@ class User {
return _.get(this.getRoleForService(serviceExternalId), 'permissions', []).map(permission => permission.name)
}

hasGlobalRole () {
return _.get(this, 'role') !== undefined
}

getGlobalPermissions () {
return _.get(this, 'role.permissions', []).map(permission => permission.name)
}

isAdminUserForService (serviceExternalId) {
return this.serviceRoles
.filter(serviceRole => serviceRole.role && serviceRole.role.name === 'admin' &&
serviceRole.service && serviceRole.service.externalId === serviceExternalId)
.length > 0
.length > 0 || (_.get(this, 'role.name') === 'admin')
}
}

Expand Down
4 changes: 4 additions & 0 deletions app/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ module.exports = {
switch: '/my-services/switch',
create: '/my-services/create'
},
browseAsUser: {
index: '/browse-as-a-user',
clear: '/browse-as-a-user/clear'
},
invite: {
validateInvite: '/invites/:code',
subscribeService: '/subscribe'
Expand Down
6 changes: 6 additions & 0 deletions app/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const digitalWalletController = require('./controllers/digital-wallet')
const emailNotificationsController = require('./controllers/email-notifications/email-notifications.controller')
const forgotPasswordController = require('./controllers/forgotten-password.controller')
const myServicesController = require('./controllers/my-services')
const browseAsAUserController = require('./controllers/browser-as-a-user/browse-as-a-user.controller')
const editServiceNameController = require('./controllers/edit-service-name/edit-service-name.controller')
const serviceUsersController = require('./controllers/service-users.controller')
const organisationDetailsController = require('./controllers/organisation-details.controller')
Expand Down Expand Up @@ -87,6 +88,7 @@ const agreementsController = require('./controllers/agreements/agreements.contro
const organisationUrlController = require('./controllers/switch-psp/organisation-url')
const registrationController = require('./controllers/registration/registration.controller')
const privacyController = require('./controllers/privacy/privacy.controller')
const { browseAsUser } = require('./paths')

// Assignments
const {
Expand Down Expand Up @@ -218,6 +220,10 @@ module.exports.bind = function (app) {
// OUTSIDE OF SERVICE ROUTES
// -------------------------

app.get(browseAsUser.index, userIsAuthorised, browseAsAUserController.get)
app.post(browseAsUser.index, userIsAuthorised, browseAsAUserController.post)
app.get(browseAsUser.clear, userIsAuthorised, browseAsAUserController.clear)

// Service switcher
app.get(serviceSwitcher.index, userIsAuthorised, myServicesController.getIndex)
app.post(serviceSwitcher.switch, userIsAuthorised, myServicesController.postIndex)
Expand Down
42 changes: 42 additions & 0 deletions app/services/clients/adminusers.client.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@ module.exports = function (clientOptions = {}) {
)
}

/**
* Get a User by email address
*
* @param {string} externalId
* @return {Promise<User>} A promise of a User
*/
function findUserByEmail (emailAddress) {
return baseClient.post(
{
baseUrl,
url: `${userResource}/find`,
body: {
'email': emailAddress
},
json: true,
description: 'find a user by email',
service: SERVICE_NAME,
transform: responseBodyToUserTransformer
}
)
}

/**
* Get a User by external id
*
Expand Down Expand Up @@ -499,6 +521,24 @@ module.exports = function (clientOptions = {}) {
)
}

/**
* Fidd a service by external ID
*
* @returns {*|promise|Constructor}
*/
function findServiceByExternalId (serviceExternalId) {
return baseClient.get(
{
baseUrl,
url: `${serviceResource}/${serviceExternalId}`,
json: true,
description: 'find a service by external ID',
service: SERVICE_NAME,
transform: responseBodyToServiceTransformer
}
)
}

/**
* Update service
*
Expand Down Expand Up @@ -750,6 +790,7 @@ module.exports = function (clientOptions = {}) {
createForgottenPassword,
incrementSessionVersionForUser,
getUserByExternalId,
findUserByEmail,
getUsersByExternalIds,
authenticateUser,
updatePasswordForUser,
Expand Down Expand Up @@ -779,6 +820,7 @@ module.exports = function (clientOptions = {}) {

// Service-related Methods
createService,
findServiceByExternalId,
updateService,
updateServiceName,
updateCollectBillingAddress,
Expand Down
8 changes: 8 additions & 0 deletions app/services/user.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ module.exports = {
return adminUsersClient.getUserByExternalId(externalId)
},

/**
* @param emailAddress
* @returns {Promise<User>}
*/
findUserByEmail: (emailAddress) => {
return adminUsersClient.findUserByEmail(emailAddress)
},

/**
* @param {Array} externalIds
* @returns {Promise<User>}
Expand Down
9 changes: 8 additions & 1 deletion app/utils/display-converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const hideServiceHeaderTemplates = [
]

const hideServiceNavTemplates = [
'browse-as-a-user/index',
'services/edit-service-name',
'merchant-details/merchant-details',
'merchant-details/edit-merchant-details',
Expand Down Expand Up @@ -71,7 +72,12 @@ const digitalWalletsSupportedProviders = [
const getPermissions = (user, service) => {
if (service) {
let userPermissions
const permissionsForService = user.getPermissionsForService(service.externalId)
let permissionsForService = user.getPermissionsForService(service.externalId)

if (permissionsForService.length === 0 && user.role) {
permissionsForService = user.getGlobalPermissions()
}

if (user && permissionsForService) {
userPermissions = _.clone(permissionsForService)
return getHeldPermissions(userPermissions)
Expand Down Expand Up @@ -118,6 +124,7 @@ module.exports = function (req, data, template) {
const paymentProvider = account && account.payment_provider
convertedData.loggedIn = user && session && user.sessionVersion === session.version
convertedData.paymentMethod = paymentMethod
convertedData.assumedUserEmail = session.assumedUserEmail
convertedData.permissions = permissions
convertedData.isAdminUser = isAdminUser
convertedData.hideServiceHeader = hideServiceHeader(template)
Expand Down
Loading
Loading