Skip to content
Open
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ 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
30 changes: 29 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const cds = require('@sap/cds')
const fs = require('fs');
const path = require('path');

const { Relation, exposeRelation, relationHandler } = require('./_relation')

Expand Down Expand Up @@ -282,6 +284,31 @@ 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);
// eslint-disable-next-line no-console
console.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);
// eslint-disable-next-line no-console
console.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 +318,6 @@ module.exports = {
addObjectID,
addDataSubject,
addDataSubjectForDetailsEntity,
resolveDataSubjects
resolveDataSubjects,
loadVCAPServices,
}
2 changes: 1 addition & 1 deletion 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 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');

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(console, '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}`);
});
});