From b8caf229365d3e7c44325812a5db418162fbe956 Mon Sep 17 00:00:00 2001 From: Krishna Bottla <40598480+kbottla@users.noreply.github.com> Date: Mon, 15 Jan 2024 21:10:24 +0000 Subject: [PATCH] PP-12058 View services as a user --- .../browse-as-a-user.controller.js | 45 ++++++++++++ .../my-services/get-index.controller.js | 15 +++- .../my-services/post-index.controller.js | 2 +- ...-service-and-gateway-account.middleware.js | 31 +++++--- app/middleware/user-is-authorised.js | 19 ++--- app/models/User.class.js | 20 +++++- app/paths.js | 4 ++ app/routes.js | 6 ++ app/services/clients/adminusers.client.js | 42 +++++++++++ app/services/user.service.js | 8 +++ app/utils/display-converter.js | 9 ++- app/views/browse-as-a-user/index.njk | 33 +++++++++ app/views/includes/browser-as-a-user-info.njk | 8 +++ app/views/layout.njk | 1 + app/views/services/index.njk | 71 +++++++++++++------ 15 files changed, 266 insertions(+), 48 deletions(-) create mode 100644 app/controllers/browser-as-a-user/browse-as-a-user.controller.js create mode 100644 app/views/browse-as-a-user/index.njk create mode 100644 app/views/includes/browser-as-a-user-info.njk diff --git a/app/controllers/browser-as-a-user/browse-as-a-user.controller.js b/app/controllers/browser-as-a-user/browse-as-a-user.controller.js new file mode 100644 index 0000000000..9d42c7f526 --- /dev/null +++ b/app/controllers/browser-as-a-user/browse-as-a-user.controller.js @@ -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 +} diff --git a/app/controllers/my-services/get-index.controller.js b/app/controllers/my-services/get-index.controller.js index 81e990dc6f..f45617f0ba 100644 --- a/app/controllers/my-services/get-index.controller.js +++ b/app/controllers/my-services/get-index.controller.js @@ -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 => @@ -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] @@ -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) { diff --git a/app/controllers/my-services/post-index.controller.js b/app/controllers/my-services/post-index.controller.js index 29b44590b2..d58b6e90c4 100644 --- a/app/controllers/my-services/post-index.controller.js +++ b/app/controllers/my-services/post-index.controller.js @@ -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}`) diff --git a/app/middleware/get-service-and-gateway-account.middleware.js b/app/middleware/get-service-and-gateway-account.middleware.js index e1ab8ec788..07f135ab0b 100644 --- a/app/middleware/get-service-and-gateway-account.middleware.js +++ b/app/middleware/get-service-and-gateway-account.middleware.js @@ -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 { @@ -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') + } } } } @@ -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' } diff --git a/app/middleware/user-is-authorised.js b/app/middleware/user-is-authorised.js index 74fcf8da9e..f1b7663308 100644 --- a/app/middleware/user-is-authorised.js +++ b/app/middleware/user-is-authorised.js @@ -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() } diff --git a/app/models/User.class.js b/app/models/User.class.js index e0837b574e..892566b940 100644 --- a/app/models/User.class.js +++ b/app/models/User.class.js @@ -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 || '' @@ -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 } /** @@ -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') } } diff --git a/app/paths.js b/app/paths.js index 7a150b0e17..28291ed17e 100644 --- a/app/paths.js +++ b/app/paths.js @@ -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' diff --git a/app/routes.js b/app/routes.js index 12ba9a48cf..c8d1425a75 100644 --- a/app/routes.js +++ b/app/routes.js @@ -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') @@ -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 { @@ -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) diff --git a/app/services/clients/adminusers.client.js b/app/services/clients/adminusers.client.js index 7b788228e0..54894df6d9 100644 --- a/app/services/clients/adminusers.client.js +++ b/app/services/clients/adminusers.client.js @@ -45,6 +45,28 @@ module.exports = function (clientOptions = {}) { ) } + /** + * Get a User by email address + * + * @param {string} externalId + * @return {Promise} 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 * @@ -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 * @@ -750,6 +790,7 @@ module.exports = function (clientOptions = {}) { createForgottenPassword, incrementSessionVersionForUser, getUserByExternalId, + findUserByEmail, getUsersByExternalIds, authenticateUser, updatePasswordForUser, @@ -779,6 +820,7 @@ module.exports = function (clientOptions = {}) { // Service-related Methods createService, + findServiceByExternalId, updateService, updateServiceName, updateCollectBillingAddress, diff --git a/app/services/user.service.js b/app/services/user.service.js index 5476887241..0639d844ed 100644 --- a/app/services/user.service.js +++ b/app/services/user.service.js @@ -38,6 +38,14 @@ module.exports = { return adminUsersClient.getUserByExternalId(externalId) }, + /** + * @param emailAddress + * @returns {Promise} + */ + findUserByEmail: (emailAddress) => { + return adminUsersClient.findUserByEmail(emailAddress) + }, + /** * @param {Array} externalIds * @returns {Promise} diff --git a/app/utils/display-converter.js b/app/utils/display-converter.js index a279ac19dd..21a269d667 100644 --- a/app/utils/display-converter.js +++ b/app/utils/display-converter.js @@ -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', @@ -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) @@ -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) diff --git a/app/views/browse-as-a-user/index.njk b/app/views/browse-as-a-user/index.njk new file mode 100644 index 0000000000..1ffac9e0ca --- /dev/null +++ b/app/views/browse-as-a-user/index.njk @@ -0,0 +1,33 @@ +{% extends "layout.njk" %} + +{% block pageTitle %} + Search user - {{ currentService.name }} {{ currentGatewayAccount.full_type }} - GOV.UK Pay +{% endblock %} + +{% block mainContent %} +
+ +

Search user by email

+ +
+ + + {{ govukInput({ + id: "emailAddress", + name: "emailAddress", + value: emailAddress, + label: { + text: "Enter full email address" + }, + type: "text", + classes: "govuk-input--width-200" + }) }} + + {{ govukButton({ + text: "Search", + classes: "govuk-!-margin-bottom-4" + }) }} + +
+
+{% endblock %} diff --git a/app/views/includes/browser-as-a-user-info.njk b/app/views/includes/browser-as-a-user-info.njk new file mode 100644 index 0000000000..c6e630b5a1 --- /dev/null +++ b/app/views/includes/browser-as-a-user-info.njk @@ -0,0 +1,8 @@ +{% from "../macro/breadcrumbs.njk" import breadcrumbs %} + +{% if assumedUserEmail %} +
+

Viewing user '{{ assumedUserEmail }}' services

+

Clear

+
+{% endif %} diff --git a/app/views/layout.njk b/app/views/layout.njk index 0bc47ddd06..51d7caca60 100644 --- a/app/views/layout.njk +++ b/app/views/layout.njk @@ -40,6 +40,7 @@ {% block beforeContent %} {% if loggedIn %} + {% include "includes/browser-as-a-user-info.njk" %} {% include "includes/phase-banner.njk" %} {% endif %} {% endblock %} diff --git a/app/views/services/index.njk b/app/views/services/index.njk index e5eb2ee230..57323aeece 100644 --- a/app/views/services/index.njk +++ b/app/views/services/index.njk @@ -5,14 +5,16 @@ Choose service - GOV.UK Pay {% endblock %} -{% block beforeContent %}{% endblock %} +{% block beforeContent %} + {% include "../includes/browser-as-a-user-info.njk" %} +{% endblock %} {% block mainContent %}
{% if new_service_name %} {% set html %}

- You have been added to {{new_service_name}} + You have been added to {{ new_service_name }}

{% endset %} @@ -25,29 +27,49 @@

Overview

+ {% if has_global_role and not assumedUserEmail %} +
+

Admin

+ +

+ + Browser as a user + +

+ +
+ +
+
+
+ {% endif %} + {% if services.length %}
-

Reports

+

Reports

- {% set allServiceTransactionsPath = routes.allServiceTransactions.index if has_live_account else - routes.formattedPathFor(routes.allServiceTransactions.indexStatusFilter, 'test') %} + {% set allServiceTransactionsPath = routes.allServiceTransactions.index if has_live_account else + routes.formattedPathFor(routes.allServiceTransactions.indexStatusFilter, 'test') %} +

+ + View transactions for all your services + +

+ + {% set payoutsPath = routes.payouts.list if has_live_account else + routes.formattedPathFor(routes.payouts.listStatusFilter, 'test') %} + + {% if has_account_with_payouts %}

- - View transactions for all your services + + View payments to your bank account

- - {% set payoutsPath = routes.payouts.list if has_live_account else - routes.formattedPathFor(routes.payouts.listStatusFilter, 'test') %} - - {% if has_account_with_payouts %} -

- - View payments to your bank account - -

- {% endif %} + {% endif %}
@@ -60,13 +82,15 @@

Services

-

- {{ govukButton({ + {% if not assumedUserEmail %} +

+ {{ govukButton({ classes: "govuk-button--secondary", text: "Add a new service", href: routes.serviceSwitcher.create }) }} -

+

+ {% endif %} {% if services.length %} {% if services.length > 7 %} @@ -89,10 +113,11 @@ {% else %}

You do not have any services set up at the moment. Either - create a new one + create a new + one or contact an administrator of an existing service to be added to it.

- {% endif %} + {% endif %} {% endblock %}