diff --git a/Jenkinsfile b/Jenkinsfile index 9bb3e55..633335f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,3 +1,3 @@ -@Library('defra-library@v-9') _ +@Library('defra-library@v-10') _ buildNodeJs() diff --git a/app/config/message.js b/app/config/message.js index 2c2abe5..41ed9d4 100644 --- a/app/config/message.js +++ b/app/config/message.js @@ -7,7 +7,8 @@ const schema = Joi.object({ username: Joi.string(), password: Joi.string(), useCredentialChain: Joi.bool().default(false), - appInsights: Joi.object() + appInsights: Joi.object(), + managedIdentityClientId: Joi.string().optional() }, alertSubscription: { address: Joi.string(), @@ -22,7 +23,8 @@ const config = { username: process.env.MESSAGE_QUEUE_USER, password: process.env.MESSAGE_QUEUE_PASSWORD, useCredentialChain: process.env.NODE_ENV === PRODUCTION, - appInsights: process.env.NODE_ENV === PRODUCTION ? require('applicationinsights') : undefined + appInsights: process.env.NODE_ENV === PRODUCTION ? require('applicationinsights') : undefined, + managedIdentityClientId: process.env.AZURE_CLIENT_ID }, alertSubscription: { address: process.env.ALERT_SUBSCRIPTION_ADDRESS, diff --git a/app/config/storage-config.js b/app/config/storage-config.js index 5c7a404..769463e 100644 --- a/app/config/storage-config.js +++ b/app/config/storage-config.js @@ -9,7 +9,8 @@ const schema = Joi.object({ quarantineFolder: Joi.string().default('quarantine'), returnFolder: Joi.string().default('return'), useConnectionStr: Joi.boolean().default(false), - createContainers: Joi.boolean().default(false) + createContainers: Joi.boolean().default(false), + managedIdentityClientId: Joi.string().optional() }) const config = { @@ -21,7 +22,8 @@ const config = { quarantineFolder: process.env.AZURE_STORAGE_QUARANTINE, returnFolder: process.env.AZURE_STORAGE_RETURN, useConnectionStr: process.env.AZURE_STORAGE_USE_CONNECTION_STRING, - createContainers: process.env.AZURE_STORAGE_CREATE_CONTAINERS + createContainers: process.env.AZURE_STORAGE_CREATE_CONTAINERS, + managedIdentityClientId: process.env.AZURE_CLIENT_ID } const result = schema.validate(config, { diff --git a/app/storage.js b/app/storage.js index 3a89276..36e9558 100644 --- a/app/storage.js +++ b/app/storage.js @@ -8,7 +8,7 @@ if (config.useConnectionStr) { blobServiceClient = BlobServiceClient.fromConnectionString(config.connectionStr) } else { const uri = `https://${config.storageAccount}.blob.core.windows.net` - blobServiceClient = new BlobServiceClient(uri, new DefaultAzureCredential()) + blobServiceClient = new BlobServiceClient(uri, new DefaultAzureCredential({ managedIdentityClientId: config.managedIdentityClientId })) } const container = blobServiceClient.getContainerClient(config.container) diff --git a/appconfig/common.yaml b/appconfig/common.yaml new file mode 100644 index 0000000..6e5f4f4 --- /dev/null +++ b/appconfig/common.yaml @@ -0,0 +1 @@ +container.alertTopicAddress: queue:ffc-pay-alert \ No newline at end of file diff --git a/appconfig/dev.yaml b/appconfig/dev.yaml new file mode 100644 index 0000000..e69de29 diff --git a/appconfig/post-deployment-test.yaml b/appconfig/post-deployment-test.yaml new file mode 100644 index 0000000..e69de29 diff --git a/appconfig/prd.yaml b/appconfig/prd.yaml new file mode 100644 index 0000000..e69de29 diff --git a/appconfig/pre.yaml b/appconfig/pre.yaml new file mode 100644 index 0000000..e69de29 diff --git a/appconfig/snd2.yaml b/appconfig/snd2.yaml new file mode 100644 index 0000000..e69de29 diff --git a/appconfig/test.yaml b/appconfig/test.yaml new file mode 100644 index 0000000..e69de29 diff --git a/helm/ffc-pay-alerting/Chart.yaml b/helm/ffc-pay-alerting/Chart.yaml index 9eecb43..0daff86 100644 --- a/helm/ffc-pay-alerting/Chart.yaml +++ b/helm/ffc-pay-alerting/Chart.yaml @@ -5,5 +5,5 @@ name: ffc-pay-alerting version: 1.0.0 dependencies: - name: ffc-helm-library - version: 4.0.0 + version: 4.7.2 repository: https://raw.githubusercontent.com/defra/ffc-helm-repository/master/ diff --git a/helm/ffc-pay-alerting/templates/azure-identity-binding.yaml b/helm/ffc-pay-alerting/templates/azure-identity-binding.yaml deleted file mode 100644 index b1c9d62..0000000 --- a/helm/ffc-pay-alerting/templates/azure-identity-binding.yaml +++ /dev/null @@ -1,5 +0,0 @@ -{{- if .Values.aadPodIdentity }} -{{- include "ffc-helm-library.azure-identity-binding" (list . "ffc-pay-alerting.azure-identity-binding") -}} -{{- end }} -{{- define "ffc-pay-alerting.azure-identity-binding" -}} -{{- end -}} diff --git a/helm/ffc-pay-alerting/templates/azure-identity.yaml b/helm/ffc-pay-alerting/templates/azure-identity.yaml deleted file mode 100644 index b5592cf..0000000 --- a/helm/ffc-pay-alerting/templates/azure-identity.yaml +++ /dev/null @@ -1,5 +0,0 @@ -{{- if .Values.aadPodIdentity }} -{{- include "ffc-helm-library.azure-identity" (list . "ffc-pay-alerting.azure-identity") -}} -{{- end }} -{{- define "ffc-pay-alerting.azure-identity" -}} -{{- end -}} diff --git a/helm/ffc-pay-alerting/templates/service-account.yaml b/helm/ffc-pay-alerting/templates/service-account.yaml new file mode 100644 index 0000000..4d5f562 --- /dev/null +++ b/helm/ffc-pay-alerting/templates/service-account.yaml @@ -0,0 +1,3 @@ +{{- include "ffc-helm-library.service-account" (list . "ffc-pay-alerting.service-account") -}} +{{- define "ffc-pay-alerting.service-account" -}} +{{- end -}} diff --git a/helm/ffc-pay-alerting/values.yaml b/helm/ffc-pay-alerting/values.yaml index c86ac71..27f48da 100644 --- a/helm/ffc-pay-alerting/values.yaml +++ b/helm/ffc-pay-alerting/values.yaml @@ -61,7 +61,7 @@ container: azureStorageUseConnectionString: false azureStorageCreateContainers: false -aadPodIdentity: true +workloadIdentity: true azureIdentity: clientID: not-a-real-clientID diff --git a/jest.config.js b/jest.config.js index 763abae..10531b7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,7 +13,8 @@ module.exports = { '/node_modules/', '/test-output/', '/test/', - '/jest.config.js' + '/jest.config.js', + '/app/config' ], modulePathIgnorePatterns: [ 'node_modules' diff --git a/package-lock.json b/package-lock.json index bd3049d..8bd2149 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "ffc-pay-alerting", - "version": "1.3.19", + "version": "1.3.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ffc-pay-alerting", - "version": "1.3.19", + "version": "1.3.20", "license": "OGL-UK-3.0", "dependencies": { - "@azure/identity": "4.3.0", + "@azure/identity": "4.4.1", "@azure/storage-blob": "12.15.0", "applicationinsights": "2.9.6", - "ffc-messaging": "2.9.1", + "ffc-messaging": "2.10.1", "joi": "17.7.1", "log-timestamp": "0.3.0", "moment": "2.29.4", @@ -298,9 +298,10 @@ } }, "node_modules/@azure/identity": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.3.0.tgz", - "integrity": "sha512-LHZ58/RsIpIWa4hrrE2YuJ/vzG1Jv9f774RfTTAVDZDriubvJ0/S5u4pnw4akJDlS0TiJb6VMphmVUFsWmgodQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.4.1.tgz", + "integrity": "sha512-DwnG4cKFEM7S3T+9u05NstXU/HN0dk45kPOinUyNKsn5VWwpXd9sbPKEg6kgJzGbm1lMuhx9o31PVbCtM5sfBA==", + "license": "MIT", "dependencies": { "@azure/abort-controller": "^1.0.0", "@azure/core-auth": "^1.5.0", @@ -309,7 +310,7 @@ "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.3.0", "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^3.11.1", + "@azure/msal-browser": "^3.14.0", "@azure/msal-node": "^2.9.2", "events": "^3.0.0", "jws": "^4.0.0", @@ -3681,9 +3682,10 @@ } }, "node_modules/ffc-messaging": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/ffc-messaging/-/ffc-messaging-2.9.1.tgz", - "integrity": "sha512-GCQ0ZRjRfYx39/kRzc408Bd/fICy614qjqgb8Fi17MDUdWRZ65bAhvqPDRNXoEgIKW8c97OcAtzUKiVuPOuspQ==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/ffc-messaging/-/ffc-messaging-2.10.1.tgz", + "integrity": "sha512-1TpRnZF6YmR4Xepal9QQUCDbXreTwyZNxbqOGL5py2aGiGLmhSCOhdlvXALiiEGuYDvHIvIaccZJw1Osv3D0ZA==", + "license": "OGL-UK-3.0", "dependencies": { "@azure/identity": "4.2.1", "@azure/service-bus": "7.9.4", diff --git a/package.json b/package.json index c557a42..24a2c2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ffc-pay-alerting", - "version": "1.3.19", + "version": "1.3.20", "description": "Publish alerts from Payment Hub warnings", "homepage": "https://github.com/DEFRA/ffc-pay-alerting", "main": "app/index.js", @@ -21,10 +21,10 @@ ], "license": "OGL-UK-3.0", "dependencies": { - "@azure/identity": "4.3.0", + "@azure/identity": "4.4.1", "@azure/storage-blob": "12.15.0", "applicationinsights": "2.9.6", - "ffc-messaging": "2.9.1", + "ffc-messaging": "2.10.1", "joi": "17.7.1", "log-timestamp": "0.3.0", "moment": "2.29.4", diff --git a/provision.azure.yaml b/provision.azure.yaml index 3eaad7f..36c0c8b 100644 --- a/provision.azure.yaml +++ b/provision.azure.yaml @@ -1,3 +1,7 @@ resources: + identity: pay-alerting topics: - - name: alert + - name: ffc-pay-alert + role: receiver + subscriptions: + - name: ffc-pay-alerting diff --git a/sonar-project.properties b/sonar-project.properties index 5d2e91b..d5630f9 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,3 +1,3 @@ -sonar.javascript.exclusions=**/jest.config.js,**/__mocks__/**,**/node_modules/**,**/test/**,**/test-output/** +sonar.javascript.exclusions=**/jest.config.js,**/__mocks__/**,**/node_modules/**,**/test/**,**/test-output/**,**/app/config/** sonar.javascript.lcov.reportPaths=test-output/lcov.info sonar.exclusions=/test/**,**/*.test.js,*snyk_report.html,*snyk_report.css diff --git a/test/unit/storage.test.js b/test/unit/storage.test.js index 82e9490..acc83b3 100644 --- a/test/unit/storage.test.js +++ b/test/unit/storage.test.js @@ -1,43 +1,98 @@ -const { getInboundBlobClient, blobServiceClient } = require('../../app/storage') -const config = require('../../app/config').storageConfig - -jest.mock('@azure/storage-blob', () => { - const mBlobServiceClient = { - getContainerClient: jest.fn().mockReturnThis(), - getBlockBlobClient: jest.fn().mockReturnThis(), - upload: jest.fn().mockResolvedValue(true), - createIfNotExists: jest.fn().mockResolvedValue(true) +jest.mock('@azure/storage-blob') +jest.mock('@azure/identity') + +describe('storage', () => { + let storage + const mockstorage = { + upload: jest.fn().mockResolvedValue({}), + url: 'test-url' } - return { - BlobServiceClient: { - fromConnectionString: jest.fn(() => mBlobServiceClient) - }, - ContainerClient: jest.fn(() => mBlobServiceClient), - BlockBlobClient: jest.fn(() => mBlobServiceClient) + + const mockContainer = { + createIfNotExists: jest.fn(), + getBlockBlobClient: jest.fn().mockReturnValue(mockstorage) + } + + const mockStorageConfig = { + useConnectionStr: true, + connectionStr: 'connection-string', + createContainers: true, + storageAccount: 'fakestorageaccount', + managedIdentityClientId: 'fake-client-id', + container: 'test-container', + inboundFolder: 'test-folder' + } + + const mockBlobServiceClient = { + getContainerClient: jest.fn().mockReturnValue(mockContainer) } -}) -describe('Azure Blob Storage Module', () => { beforeEach(() => { + jest.resetModules() jest.clearAllMocks() + + require('@azure/storage-blob').BlobServiceClient.fromConnectionString = jest + .fn() + .mockReturnValue(mockBlobServiceClient) + + require('@azure/storage-blob').BlobServiceClient.mockImplementation(() => mockBlobServiceClient) + + require('@azure/identity').DefaultAzureCredential.mockImplementation(() => ({})) + + jest.mock('../../app/config', () => ({ + storageConfig: mockStorageConfig + })) + + storage = require('../../app/storage') + }) + + test('uses connection string when config.useConnectionStr is true', async () => { + expect(require('@azure/storage-blob').BlobServiceClient.fromConnectionString) + .toHaveBeenCalledWith(mockStorageConfig.connectionStr) }) - test('should initialize containers and folders', async () => { - const containerClient = blobServiceClient.getContainerClient(config.container) - await containerClient.createIfNotExists() - expect(containerClient.createIfNotExists).toHaveBeenCalled() + test('uses DefaultAzureCredential when config.useConnectionStr is false', async () => { + jest.resetModules() + mockStorageConfig.useConnectionStr = false + + jest.mock('../../app/config', () => ({ + storageConfig: mockStorageConfig + })) + + storage = require('../../app/storage') + + expect(require('@azure/identity').DefaultAzureCredential).toHaveBeenCalledWith({ + managedIdentityClientId: 'fake-client-id' + }) + + expect(require('@azure/storage-blob').BlobServiceClient).toHaveBeenCalledWith( + `https://${mockStorageConfig.storageAccount}.blob.core.windows.net`, + expect.any(Object) + ) }) - test('should upload placeholder files to folders', async () => { - const containerClient = blobServiceClient.getContainerClient(config.container) - const blockBlobClient = containerClient.getBlockBlobClient(`${config.inboundFolder}/default.txt`) - await blockBlobClient.upload('Placeholder', 'Placeholder'.length) - expect(blockBlobClient.upload).toHaveBeenCalledWith('Placeholder', 'Placeholder'.length) + test('gets outbound blob client', async () => { + const result = await storage.getInboundBlobClient('test-file.txt') + expect(result.url).toBe('test-url') + expect(mockContainer.getBlockBlobClient).toHaveBeenCalledWith('test-folder/test-file.txt') }) - test('should return the correct blob client for inbound folder', async () => { - const blobClient = await getInboundBlobClient('testfile.txt') - expect(blobClient).toBeDefined() - expect(blobClient.upload).toBeDefined() + describe('when using managed identity', () => { + test('creates blob service client with DefaultAzureCredential', () => { + jest.resetModules() + mockStorageConfig.useConnectionStr = false + + jest.mock('../../app/config', () => ({ + storageConfig: mockStorageConfig + })) + + require('../../app/storage') + + expect(require('@azure/storage-blob').BlobServiceClient) + .toHaveBeenCalledWith( + `https://${mockStorageConfig.storageAccount}.blob.core.windows.net`, + expect.any(Object) + ) + }) }) })