Skip to content
Open
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Version 1.0.2 - 2025-XX-XX
## Version 1.1.0 - 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
Expand Down
29 changes: 28 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
@@ -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')

Expand Down Expand Up @@ -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,
Expand All @@ -291,5 +317,6 @@ module.exports = {
addObjectID,
addDataSubject,
addDataSubjectForDetailsEntity,
resolveDataSubjects
resolveDataSubjects,
loadVCAPServices
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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)",
Expand All @@ -12,7 +12,7 @@
"srv"
],
"scripts": {
"lint": "npx eslint .",
"lint": "npx eslint . --max-warnings=0",
"test": "npx jest --silent"
},
"peerDependencies": {
Expand Down
6 changes: 4 additions & 2 deletions srv/log2als.js
Original file line number Diff line number Diff line change
@@ -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')
10 changes: 4 additions & 6 deletions srv/log2alsng.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
112 changes: 112 additions & 0 deletions test/utils/utils.test.js
Original file line number Diff line number Diff line change
@@ -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}`)
})
})