diff --git a/CHANGELOG.md b/CHANGELOG.md index ee77c17..3b7c213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## Version 1.0.2 - 2025-XX-XX + +### Added + +- Support for loading `VCAP_SERVICES` from a file specified by the `VCAP_SERVICES_FILE_PATH` environment variable + +### Fixed + +- Correctly retrieve `appId` from the `VCAP_APPLICATION` environment variable + ## Version 1.0.1 - 2025-08-05 ### Fixed diff --git a/lib/utils.js b/lib/utils.js index ffffc17..e360f8d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,7 @@ const cds = require('@sap/cds') +const fs = require('fs') +const path = require('path') +const LOG = cds.log('audit-log') const { Relation, exposeRelation, relationHandler } = require('./_relation') @@ -282,6 +285,29 @@ const resolveDataSubjects = (logs, req) => { }) } +const loadVCAPServices = () => { + let vcapServices = {} + const vcapServicesFilePath = process.env.VCAP_SERVICES_FILE_PATH + if (vcapServicesFilePath) { + try { + const vcapServicesFileContent = fs.readFileSync(path.resolve(vcapServicesFilePath), 'utf8') + vcapServices = JSON.parse(vcapServicesFileContent) + LOG.debug(`VCAP_SERVICES loaded from file at ${vcapServicesFilePath}`) + } catch (err) { + throw new Error(`Failed to read or parse VCAP_SERVICES from file at ${vcapServicesFilePath}: ${err.message}`) + } + } else if (process.env.VCAP_SERVICES) { + try { + vcapServices = JSON.parse(process.env.VCAP_SERVICES) + LOG.debug('VCAP_SERVICES loaded from environment variable') + } catch (err) { + throw new Error(`Failed to parse VCAP_SERVICES from environment variable: ${err.message}`) + } + } + + return vcapServices +} + module.exports = { hasPersonalData, getMapKeyForCurrentRequest, @@ -291,5 +317,6 @@ module.exports = { addObjectID, addDataSubject, addDataSubjectForDetailsEntity, - resolveDataSubjects + resolveDataSubjects, + loadVCAPServices } diff --git a/package.json b/package.json index eec10f5..67e393c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cap-js/audit-logging", - "version": "1.0.1", + "version": "1.0.2", "description": "CDS plugin providing integration to the SAP Audit Log service as well as out-of-the-box personal data-related audit logging based on annotations.", "repository": "cap-js/audit-logging", "author": "SAP SE (https://www.sap.com)", @@ -12,7 +12,7 @@ "srv" ], "scripts": { - "lint": "npx eslint .", + "lint": "npx eslint . --max-warnings=0", "test": "npx jest --silent" }, "peerDependencies": { diff --git a/srv/log2als.js b/srv/log2als.js index 5ecb159..faab4b0 100644 --- a/srv/log2als.js +++ b/srv/log2als.js @@ -1,4 +1,6 @@ -const credentials = JSON.parse(process.env.VCAP_SERVICES) || {} -const isV3 = credentials['user-provided']?.some(obj => obj.tags.includes('auditlog-ng')) +const { loadVCAPServices } = require('../lib/utils') + +const vcapServices = loadVCAPServices() +const isV3 = vcapServices['user-provided']?.some(obj => obj.tags.includes('auditlog-ng')) module.exports = isV3 ? require('./log2alsng') : require('./log2restv2') diff --git a/srv/log2alsng.js b/srv/log2alsng.js index 3208884..d860808 100644 --- a/srv/log2alsng.js +++ b/srv/log2alsng.js @@ -1,18 +1,16 @@ const cds = require('@sap/cds') - -const LOG = cds.log('audit-log') - const https = require('https') - const AuditLogService = require('./service') +const { loadVCAPServices } = require('../lib/utils') +const LOG = cds.log('audit-log') module.exports = class AuditLog2ALSNG extends AuditLogService { constructor() { super() - this._vcap = JSON.parse(process.env.VCAP_SERVICES || '{}') + this._vcap = loadVCAPServices() this._userProvided = this._vcap['user-provided']?.find(obj => obj.tags.includes('auditlog-ng')) || {} if (!this._userProvided.credentials) throw new Error('No credentials found for SAP Audit Log Service NG') - this._vcapApplication = this._vcap['VCAP_APPLICATION'] || {} + this._vcapApplication = JSON.parse(process.env.VCAP_APPLICATION || '{}') } async init() { diff --git a/test/utils/utils.test.js b/test/utils/utils.test.js new file mode 100644 index 0000000..22bcdd6 --- /dev/null +++ b/test/utils/utils.test.js @@ -0,0 +1,112 @@ +const path = require('path') +const { loadVCAPServices } = require('../../lib/utils') +const cds = require('@sap/cds') +const LOG = cds.log('audit-log') + +jest.mock('fs', () => ({ + readFileSync: jest.fn() +})) + +const fs = require('fs') + +describe('Test loadVCAPServices', () => { + const ORIGINAL_ENV = process.env + const FAKE_VCAP = { 'user-provided': [{ name: 'test', credentials: { token: 'abc' } }] } + const INVALID_JSON = 'invalid json' + const FAKE_PATH = '/path/to/vcap.json' + + let logSpy + + beforeEach(() => { + delete process.env.VCAP_SERVICES_FILE_PATH + delete process.env.VCAP_SERVICES + + jest.clearAllMocks() + logSpy = jest.spyOn(LOG, 'debug').mockImplementation(() => {}) + }) + + afterAll(() => { + process.env = ORIGINAL_ENV + }) + + test('loads and parses VCAP_SERVICES from VCAP_SERVICES_FILE_PATH', () => { + process.env.VCAP_SERVICES_FILE_PATH = FAKE_PATH + + fs.readFileSync.mockReturnValueOnce(JSON.stringify(FAKE_VCAP)) + + const result = loadVCAPServices() + + expect(fs.readFileSync).toHaveBeenCalledWith(path.resolve(FAKE_PATH), 'utf8') + expect(result).toEqual(FAKE_VCAP) + expect(logSpy).toHaveBeenCalledWith(`VCAP_SERVICES loaded from file at ${FAKE_PATH}`) + }) + + test('throws error when reading VCAP_SERVICES_FILE_PATH fails', () => { + const errorMessage = 'ENOENT: no such file or directory' + process.env.VCAP_SERVICES_FILE_PATH = FAKE_PATH + + fs.readFileSync.mockImplementationOnce(() => { + const err = new Error(errorMessage) + err.code = 'ENOENT' + throw err + }) + + expect(() => loadVCAPServices()).toThrow( + `Failed to read or parse VCAP_SERVICES from file at ${FAKE_PATH}: ${errorMessage}` + ) + expect(logSpy).not.toHaveBeenCalled() + }) + + test('throws error when JSON in VCAP_SERVICES_FILE_PATH is invalid', () => { + process.env.VCAP_SERVICES_FILE_PATH = FAKE_PATH + + fs.readFileSync.mockReturnValueOnce(INVALID_JSON) + + expect(() => loadVCAPServices()).toThrow( + new RegExp(`^Failed to read or parse VCAP_SERVICES from file at ${FAKE_PATH}:`) + ) + expect(logSpy).not.toHaveBeenCalled() + }) + + test('loads and parses VCAP_SERVICES from environment variable', () => { + process.env.VCAP_SERVICES = JSON.stringify(FAKE_VCAP) + + const result = loadVCAPServices() + + expect(result).toEqual(FAKE_VCAP) + expect(logSpy).toHaveBeenCalledWith('VCAP_SERVICES loaded from environment variable') + expect(fs.readFileSync).not.toHaveBeenCalled() + }) + + test('throws error when VCAP_SERVICES env var JSON is invalid', () => { + process.env.VCAP_SERVICES = INVALID_JSON + + expect(() => loadVCAPServices()).toThrow(new RegExp(`^Failed to parse VCAP_SERVICES from environment variable:`)) + expect(logSpy).not.toHaveBeenCalled() + expect(fs.readFileSync).not.toHaveBeenCalled() + }) + + test('returns empty object when neither VCAP_SERVICES_FILE_PATH nor VCAP_SERVICES is set', () => { + const result = loadVCAPServices() + + expect(result).toEqual({}) + expect(logSpy).not.toHaveBeenCalled() + expect(fs.readFileSync).not.toHaveBeenCalled() + }) + + test('VCAP_SERVICES_FILE_PATH takes precedence over VCAP_SERVICES', () => { + const fromFile = { from: 'file' } + const fromEnv = { from: 'env' } + + process.env.VCAP_SERVICES_FILE_PATH = FAKE_PATH + process.env.VCAP_SERVICES = JSON.stringify(fromEnv) + + fs.readFileSync.mockReturnValueOnce(JSON.stringify(fromFile)) + + const result = loadVCAPServices() + + expect(result).toEqual(fromFile) + expect(fs.readFileSync).toHaveBeenCalledTimes(1) + expect(logSpy).toHaveBeenCalledWith(`VCAP_SERVICES loaded from file at ${FAKE_PATH}`) + }) +})