diff --git a/README.md b/README.md index 34b4c0ae..5e6f462d 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,13 @@ Or run tests with the cypress UI: `npm run int-test-ui` +## Environment variables + +The following environment variables can be set to run the application: + +`ENABLE_AUTHORIZATION_CODE` - set to `true` to enable the authorization code grant type. Default is `false`. + +`ENABLE_SERVICE_DETAILS` - set to `true` to enable the service details section. Default is `false`. ## Change log diff --git a/feature.env b/feature.env index c26b4580..a65a621b 100644 --- a/feature.env +++ b/feature.env @@ -10,3 +10,4 @@ API_CLIENT_SECRET=clientsecret SYSTEM_CLIENT_ID=clientid SYSTEM_CLIENT_SECRET=clientsecret ENVIRONMENT_NAME=dev +ENABLE_AUTHORIZATION_CODE=true \ No newline at end of file diff --git a/integration_tests/e2e/add-base-client.cy.ts b/integration_tests/e2e/add-base-client.cy.ts index 32ef279f..24e1e9b3 100644 --- a/integration_tests/e2e/add-base-client.cy.ts +++ b/integration_tests/e2e/add-base-client.cy.ts @@ -72,8 +72,9 @@ context('Add client page', () => { addBaseClientGrantPage.clientCredentialsRadio().should('have.attr', 'checked') }) - it('Authorization code is disabled (for now)', () => { - addBaseClientGrantPage.authorizationCodeRadio().should('have.attr', 'disabled') + it('Authorization code is not disabled but is unchecked', () => { + addBaseClientGrantPage.authorizationCodeRadio().should('not.have.attr', 'disabled') + addBaseClientGrantPage.authorizationCodeRadio().should('not.have.attr', 'checked') }) it('User clicks cancel to return to home screen', () => { diff --git a/integration_tests/e2e/edit-base-client-deployment.cy.ts b/integration_tests/e2e/edit-base-client-deployment.cy.ts index c5a21ecf..5e5a882c 100644 --- a/integration_tests/e2e/edit-base-client-deployment.cy.ts +++ b/integration_tests/e2e/edit-base-client-deployment.cy.ts @@ -1,6 +1,7 @@ import Page from '../pages/page' import ViewBaseClientPage from '../pages/viewBaseClient' import EditBaseClientDeploymentDetailsPage from '../pages/editBaseClientDeploymentDetails' +import { GrantTypes } from '../../server/data/enums/grantTypes' import AuthSignInPage from '../pages/authSignIn' import AuthErrorPage from '../pages/authError' @@ -15,7 +16,7 @@ context('Edit base client deployment: Auth', () => { cy.task('stubSignIn') cy.task('stubManageUser') cy.task('stubListBaseClients') - cy.task('stubGetBaseClient') + cy.task('stubGetBaseClient', { grantType: GrantTypes.ClientCredentials }) cy.task('stubGetListClientInstancesList') }) @@ -40,7 +41,7 @@ context('Edit base client deployment details page', () => { cy.task('stubSignIn') cy.task('stubManageUser') cy.task('stubListBaseClients') - cy.task('stubGetBaseClient') + cy.task('stubGetBaseClient', { grantType: GrantTypes.ClientCredentials }) cy.task('stubGetListClientInstancesList') editBaseClientDeploymentDetailsPage = visitEditBaseClientDeploymentDetailsPage() }) diff --git a/integration_tests/e2e/edit-base-client-details.cy.ts b/integration_tests/e2e/edit-base-client-details.cy.ts index 91421288..f185f646 100644 --- a/integration_tests/e2e/edit-base-client-details.cy.ts +++ b/integration_tests/e2e/edit-base-client-details.cy.ts @@ -1,6 +1,7 @@ import Page from '../pages/page' import EditBaseClientDetailsPage from '../pages/editBaseClientDetails' import ViewBaseClientPage from '../pages/viewBaseClient' +import { GrantTypes } from '../../server/data/enums/grantTypes' import AuthSignInPage from '../pages/authSignIn' import AuthErrorPage from '../pages/authError' @@ -15,7 +16,7 @@ context('Edit base client details: Auth', () => { cy.task('stubSignIn') cy.task('stubManageUser') cy.task('stubListBaseClients') - cy.task('stubGetBaseClient') + cy.task('stubGetBaseClient', { grantType: GrantTypes.ClientCredentials }) cy.task('stubGetListClientInstancesList') }) @@ -32,7 +33,7 @@ context('Edit base client details: Auth', () => { }) }) -context('Edit base client details page', () => { +context('Edit base client details page - client-credentials flow', () => { let editBaseClientDetailsPage: EditBaseClientDetailsPage beforeEach(() => { @@ -40,7 +41,7 @@ context('Edit base client details page', () => { cy.task('stubSignIn') cy.task('stubManageUser') cy.task('stubListBaseClients') - cy.task('stubGetBaseClient') + cy.task('stubGetBaseClient', { grantType: GrantTypes.ClientCredentials }) cy.task('stubGetListClientInstancesList') editBaseClientDetailsPage = visitEditBaseClientDetailsPage() }) @@ -55,10 +56,18 @@ context('Edit base client details page', () => { editBaseClientDetailsPage.auditTrailDetailsInput().should('be.visible') }) - it('User can see grant details form inputs', () => { - editBaseClientDetailsPage.grantTypeInput().should('be.visible') - editBaseClientDetailsPage.grantAuthoritiesInput().should('be.visible') - editBaseClientDetailsPage.grantDatabaseUsernameInput().should('be.visible') + context('Grant section for client-credentials flow', () => { + it('User can see client credentials form inputs', () => { + editBaseClientDetailsPage.grantTypeInput().should('be.visible') + editBaseClientDetailsPage.grantAuthoritiesInput().should('be.visible') + editBaseClientDetailsPage.grantDatabaseUsernameInput().should('be.visible') + }) + + it('User cannot see authorization code form inputs', () => { + editBaseClientDetailsPage.grantRedirectUrisInput().should('not.exist') + editBaseClientDetailsPage.grantJwtFieldsInput().should('not.exist') + editBaseClientDetailsPage.grantAzureAdLoginFlowCheckboxes().should('not.exist') + }) }) it('User can see config form inputs', () => { @@ -107,3 +116,29 @@ context('Edit base client details page', () => { editBaseClientDetailsPage.saveButton().click() }) }) + +context('Edit base client details page - authorization-code flow', () => { + let editBaseClientDetailsPage: EditBaseClientDetailsPage + + beforeEach(() => { + cy.task('reset') + cy.task('stubSignIn') + cy.task('stubManageUser') + cy.task('stubListBaseClients') + cy.task('stubGetBaseClient', { grantType: GrantTypes.AuthorizationCode }) + cy.task('stubGetListClientInstancesList') + editBaseClientDetailsPage = visitEditBaseClientDetailsPage() + }) + + it('User cannot see client credentials form inputs', () => { + editBaseClientDetailsPage.grantAuthoritiesInput().should('not.exist') + editBaseClientDetailsPage.grantDatabaseUsernameInput().should('not.exist') + }) + + it('User can see authorization code form inputs', () => { + editBaseClientDetailsPage.grantTypeInput().should('be.visible') + editBaseClientDetailsPage.grantRedirectUrisInput().should('be.visible') + editBaseClientDetailsPage.grantJwtFieldsInput().should('be.visible') + editBaseClientDetailsPage.grantAzureAdLoginFlowCheckboxes().should('exist') + }) +}) diff --git a/integration_tests/e2e/edit-client-instances.cy.ts b/integration_tests/e2e/edit-client-instances.cy.ts index 1a86f024..189d60cc 100644 --- a/integration_tests/e2e/edit-client-instances.cy.ts +++ b/integration_tests/e2e/edit-client-instances.cy.ts @@ -2,6 +2,7 @@ import Page from '../pages/page' import ViewBaseClientPage from '../pages/viewBaseClient' import ViewClientSecretsPage from '../pages/viewClientSecrets' import ConfirmDeleteClientPage from '../pages/confirmDeleteClient' +import { GrantTypes } from '../../server/data/enums/grantTypes' const visitBaseClientPage = (): ViewBaseClientPage => { cy.signIn({ failOnStatusCode: true, redirectPath: '/base-clients/base_client_id_1' }) @@ -20,7 +21,7 @@ context('Base client page - client instances', () => { beforeEach(() => { cy.task('reset') cy.task('stubSignIn') - cy.task('stubGetBaseClient') + cy.task('stubGetBaseClient', { grantType: GrantTypes.ClientCredentials }) cy.task('stubManageUser') cy.task('stubGetListClientInstancesList') }) diff --git a/integration_tests/e2e/login.cy.ts b/integration_tests/e2e/login.cy.ts index e281a854..fe2bb62e 100644 --- a/integration_tests/e2e/login.cy.ts +++ b/integration_tests/e2e/login.cy.ts @@ -2,6 +2,7 @@ import IndexPage from '../pages/index' import AuthSignInPage from '../pages/authSignIn' import Page from '../pages/page' import AuthManageDetailsPage from '../pages/authManageDetails' +import { GrantTypes } from '../../server/data/enums/grantTypes' import AuthErrorPage from '../pages/authError' context('SignIn', () => { @@ -9,7 +10,7 @@ context('SignIn', () => { cy.task('reset') cy.task('stubSignIn', ['ROLE_OAUTH_ADMIN']) cy.task('stubListBaseClients') - cy.task('stubGetBaseClient') + cy.task('stubGetBaseClient', { grantType: GrantTypes.ClientCredentials }) cy.task('stubManageUser') }) diff --git a/integration_tests/e2e/view-base-client-list.cy.ts b/integration_tests/e2e/view-base-client-list.cy.ts index a8130bba..e0438a04 100644 --- a/integration_tests/e2e/view-base-client-list.cy.ts +++ b/integration_tests/e2e/view-base-client-list.cy.ts @@ -2,6 +2,7 @@ import Page from '../pages/page' import ViewBaseClientListPage from '../pages/viewBaseClientList' import ViewBaseClientPage from '../pages/viewBaseClient' import NewBaseClientGrantPage from '../pages/newBaseClientGrant' +import { GrantTypes } from '../../server/data/enums/grantTypes' import AuthSignInPage from '../pages/authSignIn' import AuthErrorPage from '../pages/authError' @@ -16,7 +17,7 @@ context('Homepage - Auth', () => { cy.task('reset') cy.task('stubSignIn') cy.task('stubListBaseClients') - cy.task('stubGetBaseClient') + cy.task('stubGetBaseClient', { grantType: GrantTypes.ClientCredentials }) cy.task('stubManageUser') cy.task('stubGetListClientInstancesList') }) @@ -41,7 +42,7 @@ context('Homepage - list base-clients', () => { cy.task('reset') cy.task('stubSignIn') cy.task('stubListBaseClients') - cy.task('stubGetBaseClient') + cy.task('stubGetBaseClient', { grantType: GrantTypes.ClientCredentials }) cy.task('stubManageUser') cy.task('stubGetListClientInstancesList') diff --git a/integration_tests/e2e/view-base-client.cy.ts b/integration_tests/e2e/view-base-client.cy.ts index 19ac3e73..98e7304f 100644 --- a/integration_tests/e2e/view-base-client.cy.ts +++ b/integration_tests/e2e/view-base-client.cy.ts @@ -4,6 +4,7 @@ import ViewClientSecretsPage from '../pages/viewClientSecrets' import ConfirmDeleteClientPage from '../pages/confirmDeleteClient' import EditBaseClientDetailsPage from '../pages/editBaseClientDetails' import EditBaseClientDeploymentDetailsPage from '../pages/editBaseClientDeploymentDetails' +import { GrantTypes } from '../../server/data/enums/grantTypes' import AuthSignInPage from '../pages/authSignIn' import AuthErrorPage from '../pages/authError' @@ -16,7 +17,7 @@ context('Base client page - Auth', () => { beforeEach(() => { cy.task('reset') cy.task('stubSignIn') - cy.task('stubGetBaseClient') + cy.task('stubGetBaseClient', { grantType: GrantTypes.ClientCredentials }) cy.task('stubManageUser') cy.task('stubGetListClientInstancesList') cy.task('stubAddClientInstance') @@ -35,13 +36,13 @@ context('Base client page - Auth', () => { }) }) -context('Base client page', () => { +context('Base client page - client credentials flow', () => { let baseClientsPage: ViewBaseClientPage beforeEach(() => { cy.task('reset') cy.task('stubSignIn') - cy.task('stubGetBaseClient') + cy.task('stubGetBaseClient', { grantType: GrantTypes.ClientCredentials }) cy.task('stubManageUser') cy.task('stubGetListClientInstancesList') cy.task('stubAddClientInstance') @@ -78,6 +79,10 @@ context('Base client page', () => { baseClientsPage.baseClientClientCredentialsTable().should('be.visible') }) + it('User cannot see authorization-code table', () => { + baseClientsPage.baseClientAuthorizationCodeTable().should('not.exist') + }) + it('User can see config table', () => { baseClientsPage.baseClientConfigTable().should('be.visible') }) @@ -103,3 +108,40 @@ context('Base client page', () => { }) }) }) + +context('Base client page - authorization-code flow', () => { + let baseClientsPage: ViewBaseClientPage + + beforeEach(() => { + cy.task('reset') + cy.task('stubSignIn') + cy.task('stubGetBaseClient', { grantType: GrantTypes.AuthorizationCode }) + cy.task('stubManageUser') + cy.task('stubGetListClientInstancesList') + cy.task('stubAddClientInstance') + + baseClientsPage = visitBaseClientPage() + }) + + context('Base client details', () => { + it('User can see base client details table', () => { + baseClientsPage.baseClientDetailsTable().should('be.visible') + }) + + it('User can see audit trail table', () => { + baseClientsPage.baseClientAuditTable().should('be.visible') + }) + + it('User cannot see client credentials table', () => { + baseClientsPage.baseClientClientCredentialsTable().should('not.exist') + }) + + it('User can see authorization-code table', () => { + baseClientsPage.baseClientAuthorizationCodeTable().should('be.visible') + }) + + it('User can see config table', () => { + baseClientsPage.baseClientConfigTable().should('be.visible') + }) + }) +}) diff --git a/integration_tests/mockApis/baseClientsApi.ts b/integration_tests/mockApis/baseClientsApi.ts index 1f1bf9a4..e7b682d2 100644 --- a/integration_tests/mockApis/baseClientsApi.ts +++ b/integration_tests/mockApis/baseClientsApi.ts @@ -5,6 +5,7 @@ import { getListClientInstancesResponseMock, getSecretsResponseMock, } from '../../server/data/localMockData/baseClientsResponseMock' +import { GrantTypes } from '../../server/data/enums/grantTypes' export default { stubListBaseClients: () => { @@ -23,7 +24,7 @@ export default { }) }, - stubGetBaseClient: () => { + stubGetBaseClient: (config: { grantType: GrantTypes }) => { return stubFor({ request: { method: 'GET', @@ -34,7 +35,7 @@ export default { headers: { 'Content-Type': 'application/json;charset=UTF-8', }, - jsonBody: getBaseClientResponseMock, + jsonBody: getBaseClientResponseMock(config.grantType), }, }) }, diff --git a/server/config.ts b/server/config.ts index f610f1e5..7db0ae6b 100755 --- a/server/config.ts +++ b/server/config.ts @@ -43,6 +43,8 @@ export default { password: process.env.REDIS_PASSWORD, tls_enabled: get('REDIS_TLS_ENABLED', 'false'), }, + enableAuthorizationCode: get('ENABLE_AUTHORIZATION_CODE', 'false') === 'true', + enableServiceDetails: get('ENABLE_SERVICE_DETAILS', 'false') === 'true', session: { secret: get('SESSION_SECRET', 'app-insecure-default-session', requiredInProduction), expiryMinutes: Number(get('WEB_SESSION_TIMEOUT_IN_MINUTES', 120)), diff --git a/server/controllers/baseClientController.test.ts b/server/controllers/baseClientController.test.ts index 4023b6d4..dad1b7c6 100644 --- a/server/controllers/baseClientController.test.ts +++ b/server/controllers/baseClientController.test.ts @@ -177,7 +177,7 @@ describe('BaseClientController', () => { await baseClientController.displayNewBaseClient()(request, response, next) // THEN the choose client type page is rendered - expect(response.render).toHaveBeenCalledWith('pages/new-base-client-grant.njk') + expect(response.render).toHaveBeenCalledWith('pages/new-base-client-grant.njk', expect.anything()) }) it('if grant is specified with client-credentials renders the details screen', async () => { @@ -218,7 +218,7 @@ describe('BaseClientController', () => { await baseClientController.displayNewBaseClient()(request, response, next) // THEN the choose client type page is rendered - expect(response.render).toHaveBeenCalledWith('pages/new-base-client-grant.njk') + expect(response.render).toHaveBeenCalledWith('pages/new-base-client-grant.njk', expect.anything()) }) it('if validation fails because no id specified renders the details screen with error message', async () => { diff --git a/server/controllers/baseClientController.ts b/server/controllers/baseClientController.ts index 69b50b80..97cea587 100644 --- a/server/controllers/baseClientController.ts +++ b/server/controllers/baseClientController.ts @@ -13,6 +13,9 @@ import baseClientAudit, { BaseClientAuditFunction } from '../audit/baseClientAud import { BaseClientEvent } from '../audit/baseClientEvent' import { Client } from '../interfaces/baseClientApi/client' import { mapFilterToUrlQuery, mapListBaseClientRequest } from '../mappers/baseClientApi/listBaseClients' +import config from '../config' + +const { enableAuthorizationCode } = config export default class BaseClientController { constructor(private readonly baseClientService: BaseClientService) {} @@ -69,7 +72,7 @@ export default class BaseClientController { return async (req, res) => { const { grant } = req.query if (!(grant === kebab(GrantTypes.ClientCredentials) || grant === kebab(GrantTypes.AuthorizationCode))) { - res.render('pages/new-base-client-grant.njk') + res.render('pages/new-base-client-grant.njk', { enableAuthorizationCode }) return } res.render('pages/new-base-client-details.njk', { @@ -283,7 +286,7 @@ export default class BaseClientController { private renderCreateBaseClientErrorPage(res: Response, error: string, baseClient: BaseClient) { res.render('pages/new-base-client-details.njk', { errorMessage: { text: error }, - grant: baseClient.grantType, + grant: kebab(baseClient.grantType), baseClient, presenter: editBaseClientPresenter(baseClient), ...nunjucksUtils, diff --git a/server/data/enums/mfaTypes.ts b/server/data/enums/mfaTypes.ts new file mode 100644 index 00000000..56bf3e1e --- /dev/null +++ b/server/data/enums/mfaTypes.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line no-shadow,import/prefer-default-export +export enum MfaType { + None = 'NONE', + All = 'ALL', + Untrusted = 'UNTRUSTED', +} diff --git a/server/data/localMockData/baseClientsResponseMock.ts b/server/data/localMockData/baseClientsResponseMock.ts index dfc13897..c114b771 100644 --- a/server/data/localMockData/baseClientsResponseMock.ts +++ b/server/data/localMockData/baseClientsResponseMock.ts @@ -4,6 +4,9 @@ import { ListBaseClientsResponse, ListClientInstancesResponse, } from '../../interfaces/baseClientApi/baseClientResponse' +import { GrantTypes } from '../enums/grantTypes' +import { MfaType } from '../enums/mfaTypes' +import { snake } from '../../utils/utils' export const listBaseClientsResponseMock: ListBaseClientsResponse = { clients: [ @@ -33,28 +36,46 @@ export const listBaseClientsResponseMock: ListBaseClientsResponse = { ], } -export const getBaseClientResponseMock: GetBaseClientResponse = { - clientId: 'base_client_id_1', - scopes: ['read', 'write'], - authorities: ['ROLE_CLIENT_CREDENTIALS'], - ips: [], - jiraNumber: 'jiraNumber', - databaseUserName: 'databaseUserName', - validDays: 1, - accessTokenValidityMinutes: 60, - deployment: { - clientType: 'service', - team: 'deployment team', - teamContact: 'deployment team contact', - teamSlack: 'deployment team slack', - hosting: 'other', - namespace: 'deployment namespace', - deployment: 'deployment deployment', - secretName: 'deployment secret name', - clientIdKey: 'deployment client id key', - secretKey: 'deployment secret key', - deploymentInfo: 'deployment deployment info', - }, +export const getBaseClientResponseMock: (grantType: GrantTypes) => GetBaseClientResponse = (grantType: GrantTypes) => { + const response: GetBaseClientResponse = { + grantType: 'CLIENT_CREDENTIALS', + clientId: 'base_client_id_1', + scopes: ['read', 'write'], + ips: [], + jiraNumber: 'jiraNumber', + validDays: 1, + accessTokenValidityMinutes: 60, + deployment: { + clientType: 'service', + team: 'deployment team', + teamContact: 'deployment team contact', + teamSlack: 'deployment team slack', + hosting: 'other', + namespace: 'deployment namespace', + deployment: 'deployment deployment', + secretName: 'deployment secret name', + clientIdKey: 'deployment client id key', + secretKey: 'deployment secret key', + deploymentInfo: 'deployment deployment info', + }, + } + if (snake(grantType) === GrantTypes.ClientCredentials) { + return { + ...response, + grantType: 'CLIENT_CREDENTIALS', + authorities: ['ROLE_CLIENT_CREDENTIALS'], + databaseUserName: 'databaseUserName', + } + } + + return { + ...response, + grantType: 'AUTHORIZATION_CODE', + redirectUris: ['redirectUri1', 'redirectUri2'], + jwtFields: '+alpha,-beta', + mfa: MfaType.None, + mfaRememberMe: false, + } } export const getListClientInstancesResponseMock: ListClientInstancesResponse = { diff --git a/server/interfaces/baseClientApi/baseClient.ts b/server/interfaces/baseClientApi/baseClient.ts index 072b31b3..67798f59 100644 --- a/server/interfaces/baseClientApi/baseClient.ts +++ b/server/interfaces/baseClientApi/baseClient.ts @@ -1,3 +1,5 @@ +import { MfaType } from '../../data/enums/mfaTypes' + export interface BaseClient { baseClientId: string accessTokenValidity: number @@ -23,6 +25,8 @@ interface AuthorisationCodeDetails { registeredRedirectURIs: string[] jwtFields: string azureAdLoginFlow: boolean + mfaRememberMe: boolean + mfa: MfaType } interface ServiceDetails { diff --git a/server/interfaces/baseClientApi/baseClientRequestBody.ts b/server/interfaces/baseClientApi/baseClientRequestBody.ts index 0c4b4d87..9fca9f61 100644 --- a/server/interfaces/baseClientApi/baseClientRequestBody.ts +++ b/server/interfaces/baseClientApi/baseClientRequestBody.ts @@ -7,6 +7,11 @@ export interface AddBaseClientRequest { databaseUserName?: string validDays?: number accessTokenValidityMinutes?: number + grantType?: string + mfa?: string + mfaRememberMe?: boolean + jwtFields?: string + redirectUris?: string } export interface UpdateBaseClientRequest { @@ -17,6 +22,11 @@ export interface UpdateBaseClientRequest { databaseUserName?: string validDays?: number accessTokenValidityMinutes?: number + grantType?: string + mfa?: string + mfaRememberMe?: boolean + jwtFields?: string + redirectUris?: string } export interface UpdateBaseClientDeploymentRequest { diff --git a/server/interfaces/baseClientApi/baseClientResponse.ts b/server/interfaces/baseClientApi/baseClientResponse.ts index fb165b65..b9658088 100644 --- a/server/interfaces/baseClientApi/baseClientResponse.ts +++ b/server/interfaces/baseClientApi/baseClientResponse.ts @@ -22,6 +22,11 @@ export interface GetBaseClientResponse { databaseUserName?: string validDays?: number accessTokenValidityMinutes?: number + grantType?: string + mfa?: string + mfaRememberMe?: boolean + jwtFields?: string + redirectUris?: string[] deployment: { clientType: string team: string diff --git a/server/mappers/baseClientApi/addBaseClient.ts b/server/mappers/baseClientApi/addBaseClient.ts index 995a01b8..45daca56 100644 --- a/server/mappers/baseClientApi/addBaseClient.ts +++ b/server/mappers/baseClientApi/addBaseClient.ts @@ -1,6 +1,7 @@ import { BaseClient } from '../../interfaces/baseClientApi/baseClient' import { AddBaseClientRequest } from '../../interfaces/baseClientApi/baseClientRequestBody' import { daysRemaining } from '../../utils/utils' +import { GrantTypes } from '../../data/enums/grantTypes' export default (baseClient: BaseClient): AddBaseClientRequest => { return { @@ -12,5 +13,10 @@ export default (baseClient: BaseClient): AddBaseClientRequest => { databaseUserName: baseClient.clientCredentials.databaseUserName, validDays: baseClient.config.expiryDate ? daysRemaining(baseClient.config.expiryDate) : null, accessTokenValidityMinutes: baseClient.accessTokenValidity ? baseClient.accessTokenValidity / 60 : null, + grantType: baseClient.grantType === GrantTypes.ClientCredentials ? 'CLIENT_CREDENTIALS' : 'AUTHORIZATION_CODE', + mfa: baseClient.authorisationCode.mfa, + mfaRememberMe: baseClient.authorisationCode.mfaRememberMe, + jwtFields: baseClient.authorisationCode.jwtFields, + redirectUris: baseClient.authorisationCode.registeredRedirectURIs.join(','), } } diff --git a/server/mappers/baseClientApi/getBaseClient.ts b/server/mappers/baseClientApi/getBaseClient.ts index a540c535..8815bed2 100644 --- a/server/mappers/baseClientApi/getBaseClient.ts +++ b/server/mappers/baseClientApi/getBaseClient.ts @@ -10,7 +10,8 @@ export default (response: GetBaseClientResponse): BaseClient => { baseClientId: response.clientId, accessTokenValidity: response.accessTokenValidityMinutes ? response.accessTokenValidityMinutes * 60 : 0, scopes: response.scopes ? response.scopes : [], - grantType: GrantTypes.ClientCredentials, + grantType: + response.grantType === 'CLIENT_CREDENTIALS' ? GrantTypes.ClientCredentials : GrantTypes.AuthorizationCode, audit: response.jiraNumber ? response.jiraNumber : '', count: 1, clientCredentials: { @@ -18,9 +19,11 @@ export default (response: GetBaseClientResponse): BaseClient => { databaseUserName: response.databaseUserName ? response.databaseUserName : '', }, authorisationCode: { - registeredRedirectURIs: [], - jwtFields: '', + registeredRedirectURIs: response.redirectUris, + jwtFields: response.jwtFields ? response.jwtFields : '', azureAdLoginFlow: false, + mfaRememberMe: response.mfaRememberMe ? response.mfaRememberMe : false, + mfa: response.mfa ? response.mfa : '', }, service: { serviceName: '', diff --git a/server/mappers/baseClientApi/updateBaseClient.ts b/server/mappers/baseClientApi/updateBaseClient.ts index 1a84b95a..6e9f5f9b 100644 --- a/server/mappers/baseClientApi/updateBaseClient.ts +++ b/server/mappers/baseClientApi/updateBaseClient.ts @@ -1,6 +1,7 @@ import { BaseClient } from '../../interfaces/baseClientApi/baseClient' import { UpdateBaseClientRequest } from '../../interfaces/baseClientApi/baseClientRequestBody' import { daysRemaining } from '../../utils/utils' +import { GrantTypes } from '../../data/enums/grantTypes' export default (baseClient: BaseClient): UpdateBaseClientRequest => { return { @@ -11,5 +12,10 @@ export default (baseClient: BaseClient): UpdateBaseClientRequest => { databaseUserName: baseClient.clientCredentials.databaseUserName, validDays: baseClient.config.expiryDate ? daysRemaining(baseClient.config.expiryDate) : null, accessTokenValidityMinutes: baseClient.accessTokenValidity ? baseClient.accessTokenValidity / 60 : null, + grantType: baseClient.grantType === GrantTypes.ClientCredentials ? 'CLIENT_CREDENTIALS' : 'AUTHORIZATION_CODE', + mfa: baseClient.authorisationCode.mfa, + mfaRememberMe: baseClient.authorisationCode.mfaRememberMe, + jwtFields: baseClient.authorisationCode.jwtFields, + redirectUris: baseClient.authorisationCode.registeredRedirectURIs.join(','), } } diff --git a/server/mappers/forms/mapCreateBaseClientForm.ts b/server/mappers/forms/mapCreateBaseClientForm.ts index 989fd934..57ebc6d0 100644 --- a/server/mappers/forms/mapCreateBaseClientForm.ts +++ b/server/mappers/forms/mapCreateBaseClientForm.ts @@ -1,6 +1,7 @@ import type { Request } from 'express' import { BaseClient } from '../../interfaces/baseClientApi/baseClient' import { getAccessTokenValiditySeconds, getDayOfExpiry, multiSeparatorSplit, snake } from '../../utils/utils' +import { MfaType } from '../../data/enums/mfaTypes' export default (request: Request): BaseClient => { // valid days is calculated from expiry date @@ -24,9 +25,11 @@ export default (request: Request): BaseClient => { databaseUserName: data.databaseUserName, }, authorisationCode: { - registeredRedirectURIs: [], - jwtFields: '', - azureAdLoginFlow: false, + registeredRedirectURIs: multiSeparatorSplit(data.redirectUris, [',', '\r\n', '\n']), + jwtFields: data.jwtFields, + azureAdLoginFlow: data.azureAdLoginFlow === 'redirect', + mfa: MfaType.None, + mfaRememberMe: false, }, service: { serviceName: '', diff --git a/server/mappers/forms/mapEditBaseClientDetailsForm.test.ts b/server/mappers/forms/mapEditBaseClientDetailsForm.test.ts index 9e7a3eed..357b7fb9 100644 --- a/server/mappers/forms/mapEditBaseClientDetailsForm.test.ts +++ b/server/mappers/forms/mapEditBaseClientDetailsForm.test.ts @@ -3,6 +3,8 @@ import { createMock } from '@golevelup/ts-jest' import { baseClientFactory } from '../../testutils/factories' import { dateISOString, offsetNow } from '../../utils/utils' import { mapEditBaseClientDetailsForm } from '../index' +import { GrantTypes } from '../../data/enums/grantTypes' +import { MfaType } from '../../data/enums/mfaTypes' const formRequest = (form: Record) => { return createMock({ body: form }) @@ -80,7 +82,7 @@ describe('mapEditBaseClientDetailsForm', () => { }) describe('updates details only', () => { - it('updates details only', () => { + it('updates Client_credentials details', () => { // Given a base client with fields populated const detailedBaseClient = baseClientFactory.build({ accessTokenValidity: 3600, @@ -92,12 +94,12 @@ describe('mapEditBaseClientDetailsForm', () => { }, }) - // and given an edit request with all fields populated + // and given an edit request with client credentials fields populated const request = formRequest({ baseClientId: detailedBaseClient.baseClientId, approvedScopes: 'requestscope1,requestscope2', audit: 'request audit', - grant: 'request grant', + grantType: GrantTypes.ClientCredentials, authorities: 'requestauthority1\r\nrequestauthority2', databaseUsername: 'request databaseUsername', allowedIPs: 'requestallowedIP1\r\nrequestallowedIP2', @@ -112,7 +114,7 @@ describe('mapEditBaseClientDetailsForm', () => { expect(update.baseClientId).toEqual(detailedBaseClient.baseClientId) expect(update.scopes).toEqual(['requestscope1', 'requestscope2']) expect(update.audit).toEqual('request audit') - expect(update.grantType).toEqual('request_grant') + expect(update.grantType).toEqual(GrantTypes.ClientCredentials) expect(update.clientCredentials.authorities).toEqual(['requestauthority1', 'requestauthority2']) expect(update.clientCredentials.databaseUserName).toEqual('request databaseUsername') expect(update.config.allowedIPs).toEqual(['requestallowedIP1', 'requestallowedIP2']) @@ -121,5 +123,43 @@ describe('mapEditBaseClientDetailsForm', () => { expect(update.deployment.team).toEqual(detailedBaseClient.deployment.team) expect(update.service.serviceName).toEqual(detailedBaseClient.service.serviceName) }) + + it('updates Authorization code details', () => { + // Given a base client with fields populated + const detailedBaseClient = baseClientFactory.build({ + accessTokenValidity: 3600, + deployment: { + team: 'deployment team', + }, + service: { + serviceName: 'service serviceName', + }, + }) + + // and given an edit request with client credentials fields populated + const request = formRequest({ + baseClientId: detailedBaseClient.baseClientId, + approvedScopes: 'requestscope1,requestscope2', + audit: 'request audit', + grantType: GrantTypes.AuthorizationCode, + redirectUris: 'requestredirectUri1\r\nrequestredirectUri2', + jwtFields: 'request jwtFields', + azureAdLoginFlow: 'redirect', + mfa: MfaType.None, + mfaRememberMe: false, + expiry: true, + expiryDays: '1', + }) + + // when the form is mapped + const update = mapEditBaseClientDetailsForm(detailedBaseClient, request) + + // then the authorisationCode details are updated + expect(update.authorisationCode.registeredRedirectURIs).toEqual(['requestredirectUri1', 'requestredirectUri2']) + expect(update.authorisationCode.jwtFields).toEqual('request jwtFields') + expect(update.authorisationCode.azureAdLoginFlow).toEqual(true) + expect(update.authorisationCode.mfa).toEqual(MfaType.None) + expect(update.authorisationCode.mfaRememberMe).toEqual(false) + }) }) }) diff --git a/server/mappers/forms/mapEditBaseClientDetailsForm.ts b/server/mappers/forms/mapEditBaseClientDetailsForm.ts index 695a7496..36b7102c 100644 --- a/server/mappers/forms/mapEditBaseClientDetailsForm.ts +++ b/server/mappers/forms/mapEditBaseClientDetailsForm.ts @@ -1,6 +1,7 @@ import type { Request } from 'express' import { BaseClient } from '../../interfaces/baseClientApi/baseClient' import { getAccessTokenValiditySeconds, getDayOfExpiry, multiSeparatorSplit, snake } from '../../utils/utils' +import { MfaType } from '../../data/enums/mfaTypes' export default (baseClient: BaseClient, request: Request): BaseClient => { const data = request.body @@ -14,11 +15,18 @@ export default (baseClient: BaseClient, request: Request): BaseClient => { accessTokenValidity: accessTokenValiditySeconds, scopes: multiSeparatorSplit(data.approvedScopes, [',', '\r\n', '\n']), audit: data.audit, - grantType: snake(data.grant), + grantType: snake(data.grantType), clientCredentials: { authorities: multiSeparatorSplit(data.authorities, [',', '\r\n', '\n']), databaseUserName: data.databaseUsername, }, + authorisationCode: { + registeredRedirectURIs: multiSeparatorSplit(data.redirectUris, [',', '\r\n', '\n']), + jwtFields: data.jwtFields, + azureAdLoginFlow: data.azureAdLoginFlow === 'redirect', + mfa: MfaType.None, + mfaRememberMe: false, + }, config: { allowedIPs: multiSeparatorSplit(data.allowedIPs, [',', '\r\n', '\n']), expiryDate: dayOfExpiry, diff --git a/server/testutils/factories/baseClient.ts b/server/testutils/factories/baseClient.ts index f5cc579a..30b1632e 100644 --- a/server/testutils/factories/baseClient.ts +++ b/server/testutils/factories/baseClient.ts @@ -2,6 +2,7 @@ import { Factory } from 'fishery' import { faker } from '@faker-js/faker' import { BaseClient } from '../../interfaces/baseClientApi/baseClient' import { HostingType } from '../../data/enums/hostingTypes' +import { MfaType } from '../../data/enums/mfaTypes' export default Factory.define(() => ({ baseClientId: faker.string.uuid(), @@ -20,6 +21,8 @@ export default Factory.define(() => ({ registeredRedirectURIs: ['https://localhost:3000'], jwtFields: 'jwt fields', azureAdLoginFlow: false, + mfaRememberMe: false, + mfa: MfaType.None, }, service: { serviceName: 'service name', diff --git a/server/views/pages/base-client.njk b/server/views/pages/base-client.njk index 10d1d3df..74b09eab 100644 --- a/server/views/pages/base-client.njk +++ b/server/views/pages/base-client.njk @@ -180,7 +180,7 @@ { text: "Registered redirect URIs" },{ - text: toLinesHtml(baseClient.authorisationCode.registeredRedirectURIs) + html: toLinesHtml(baseClient.authorisationCode.registeredRedirectURIs) } ],[ { @@ -192,7 +192,7 @@ { text: "Azure Ad login flow" },{ - text: presenter.skipToAzureField + text: baseClient.authorisationCode.azureAdLoginFlow } ] ], @@ -232,7 +232,7 @@ }) }} - {% if baseClient.grantType == "authorization_code" %} + {% if baseClient.grantType == "authorization_code" and presenter.enableServiceDetails %}

Service details

@@ -256,31 +256,31 @@ { text: "Name" },{ - text: baseClient.serviceDetails.serviceName + text: baseClient.service.serviceName } ],[ { text: "Description" },{ - text: baseClient.serviceDetails.serviceDescription + text: baseClient.service.description } ],[ { text: "Authorised roles" },{ - text: baseClient.serviceDetails.serviceAuthorisedRoles.join('
') + html: toLinesHtml(baseClient.service.authorisedRoles) } ],[ { text: "URL" },{ - text: baseClient.serviceDetails.serviceURL + text: baseClient.service.url } ],[ { text: "Contact URL/email" },{ - text: baseClient.serviceDetails.contactUsURL + text: baseClient.service.contact } ],[ { diff --git a/server/views/pages/edit-base-client-details.njk b/server/views/pages/edit-base-client-details.njk index f73e6da5..e360e13f 100644 --- a/server/views/pages/edit-base-client-details.njk +++ b/server/views/pages/edit-base-client-details.njk @@ -31,6 +31,7 @@
+

Edit base client details @@ -199,7 +200,7 @@ text: "Registered redirect URIs", isPageHeading: false }, - value: toLines(baseClient.authorizationCode.redirectUris), + value: toLines(baseClient.authorisationCode.registeredRedirectURIs), attributes: { "data-qa": "grant-redirect-uris-input" } @@ -214,7 +215,7 @@ classes: "govuk-!-width-one-third", id: "jwt-fields", name: "jwtFields", - value: baseClient.authorizationCode.jwtFields, + value: baseClient.authorisationCode.jwtFields, attributes: { "data-qa": "grant-jwt-fields-input" } diff --git a/server/views/pages/new-base-client-details.njk b/server/views/pages/new-base-client-details.njk index 7dce8a75..739d18ec 100644 --- a/server/views/pages/new-base-client-details.njk +++ b/server/views/pages/new-base-client-details.njk @@ -24,7 +24,7 @@ {% block content %}
- + @@ -132,7 +132,7 @@

Grant details

- {% if grant == "client-credentials" %} + {% if kebab(grant) == "client-credentials" %} {{ govukInput({ label: { text: "Grant type" @@ -174,7 +174,7 @@ }) }} {% endif %} - {% if grant == "authorization-code" %} + {% if kebab(grant) == "authorization-code" %} {{ govukInput({ label: { text: "Grant type" diff --git a/server/views/pages/new-base-client-grant.njk b/server/views/pages/new-base-client-grant.njk index b8c5bb5a..e156575e 100644 --- a/server/views/pages/new-base-client-grant.njk +++ b/server/views/pages/new-base-client-grant.njk @@ -38,7 +38,7 @@ { value: "authorization-code", text: "Authorization code", - disabled: true, + disabled: enableAuthorizationCode === false, attributes: { "data-qa": "authorization-code-radio" } diff --git a/server/views/presenters/viewBaseClientPresenter.ts b/server/views/presenters/viewBaseClientPresenter.ts index 1e8da82e..c160a186 100644 --- a/server/views/presenters/viewBaseClientPresenter.ts +++ b/server/views/presenters/viewBaseClientPresenter.ts @@ -1,6 +1,7 @@ import { BaseClient } from '../../interfaces/baseClientApi/baseClient' import { Client } from '../../interfaces/baseClientApi/client' import { dateTimeFormat, daysRemaining } from '../../utils/utils' +import config from '../../config' export default (baseClient: BaseClient, clients: Client[]) => { return { @@ -20,6 +21,6 @@ export default (baseClient: BaseClient, clients: Client[]) => { ]), expiry: baseClient.config.expiryDate ? `Yes - days remaining ${daysRemaining(baseClient.config.expiryDate)}` : 'No', skipToAzureField: '', - serviceEnabledCode: '', + enableServiceDetails: config.enableServiceDetails, } }