diff --git a/CHANGELOG.md b/CHANGELOG.md index c4d7204..b26505e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ 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 0.3.2 - 2023-10-11 + +### Fixed + +- If the request has no tenant (e.g., Unauthorized), the audit log shall be sent to the provider account + ## Version 0.3.1 - 2023-09-25 ### Fixed diff --git a/package.json b/package.json index 85919d3..10ed0ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cap-js/audit-logging", - "version": "0.3.1", + "version": "0.3.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)", diff --git a/srv/log2restv2.js b/srv/log2restv2.js index 6aaec02..57d9874 100644 --- a/srv/log2restv2.js +++ b/srv/log2restv2.js @@ -69,6 +69,7 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService { setTimeout(() => tokens.delete(tenant), (expires_in - 60) * 1000) return access_token } catch (err) { + LOG._trace && LOG.trace('error during token fetch:', err) // 401 could also mean x-zid is not valid if (String(err.response?.statusCode).match(/^4\d\d$/)) err.unrecoverable = true throw err @@ -76,25 +77,30 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService { } async _send(data, path) { - let url const headers = { 'content-type': 'application/json' } - // TODO: what are these for? if (this._vcap) { headers.XS_AUDIT_ORG = this._vcap.organization_name headers.XS_AUDIT_SPACE = this._vcap.space_name headers.XS_AUDIT_APP = this._vcap.application_name } + let url if (this._oauth2) { url = this.options.credentials.url + PATHS.OAUTH2[path] + data.tenant ??= this._providerTenant //> if request has no tenant, stay in provider account headers.authorization = 'Bearer ' + (await this._getToken(data.tenant)) data.tenant = data.tenant === this._providerTenant ? '$PROVIDER' : '$SUBSCRIBER' } else { url = this.options.credentials.url + PATHS.STANDARD[path] headers.authorization = this._auth } + if (LOG._debug) { + const _headers = Object.assign({}, headers, { authorization: headers.authorization.split(' ')[0] + ' ***' }) + LOG.debug(`sending audit log to ${url} with tenant "${data.tenant}", user "${data.user}", and headers`, _headers) + } try { await _post(url, data, headers) } catch (err) { + LOG._trace && LOG.trace('error during log send:', err) // 429 (rate limit) is not unrecoverable if (String(err.response?.statusCode).match(/^4\d\d$/) && err.response?.statusCode !== 429) err.unrecoverable = true diff --git a/test/api/api.test.js b/test/api/api.test.js index 92a69c0..cc2578b 100644 --- a/test/api/api.test.js +++ b/test/api/api.test.js @@ -1,6 +1,12 @@ const cds = require('@sap/cds') -const { POST, GET } = cds.test(__dirname) +const { POST, GET } = cds.test().in(__dirname) + +cds.env.requires['audit-log'] = { + kind: 'audit-log-to-console', + impl: '../../srv/log2console', + outbox: true +} const wait = require('util').promisify(setTimeout) diff --git a/test/api/custom.test.js b/test/api/custom.test.js index c06ea09..6909297 100644 --- a/test/api/custom.test.js +++ b/test/api/custom.test.js @@ -1,12 +1,12 @@ const cds = require('@sap/cds') +// set cwd for resolving impl +cds.test().in(__dirname) + cds.env.requires['audit-log'] = { impl: 'MyAuditLogService.js' } -// set cwd for resolving impl -cds.test(__dirname) - describe('Custom Implementation', () => { let __log, _logs const _log = (...args) => { diff --git a/test/api/package.json b/test/api/package.json deleted file mode 100644 index ecbadb4..0000000 --- a/test/api/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "api", - "version": "1.0.0", - "description": "A simple CAP project.", - "repository": "", - "license": "UNLICENSED", - "private": true, - "cds": { - "plugins": [ - "../../cds-plugin" - ], - "requires": { - "audit-log": { - "[test]": { - "kind": "audit-log-to-console", - "impl": "../../srv/log2console", - "outbox": true - } - } - } - } -} diff --git a/test/integration/oauth2.test.js b/test/integration/oauth2.test.js index 5584e32..d29e635 100644 --- a/test/integration/oauth2.test.js +++ b/test/integration/oauth2.test.js @@ -1,11 +1,16 @@ const cds = require('@sap/cds') +const { POST } = cds.test().in(__dirname) +const log = cds.test.log() + cds.env.requires['audit-log'] = { kind: 'audit-log-to-restv2', impl: '../../srv/log2restv2', credentials: process.env.ALS_CREDS_OAUTH2 && JSON.parse(process.env.ALS_CREDS_OAUTH2) } +cds.env.log.levels['audit-log'] = 'debug' + // stay in provider account (i.e., use "$PROVIDER" and avoid x-zid header when fetching oauth2 token) cds.env.requires.auth.users.alice.tenant = cds.env.requires['audit-log'].credentials.uaa.tenantid @@ -16,5 +21,12 @@ describe('Log to Audit Log Service with oauth2 plan', () => { // required for tests to exit correctly (cf. token expiration timeouts) jest.useFakeTimers() - require('./tests') + require('./tests')(POST) + + test('no tenant is handled correctly', async () => { + const data = JSON.stringify({ data: { foo: 'bar' } }) + const res = await POST('/integration/passthrough', { event: 'SecurityEvent', data }) + expect(res).toMatchObject({ status: 204 }) + expect(log.output.match(/\$PROVIDER/)).toBeTruthy() + }) }) diff --git a/test/integration/package.json b/test/integration/package.json deleted file mode 100644 index 82892c6..0000000 --- a/test/integration/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "integration", - "version": "1.0.0", - "description": "A simple CAP project.", - "repository": "", - "license": "UNLICENSED", - "private": true, - "cds": { - "plugins": [ - "../../cds-plugin" - ] - } -} diff --git a/test/integration/standard.test.js b/test/integration/standard.test.js index f30761a..163c8ac 100644 --- a/test/integration/standard.test.js +++ b/test/integration/standard.test.js @@ -1,5 +1,7 @@ const cds = require('@sap/cds') +const { POST } = cds.test().in(__dirname) + cds.env.requires['audit-log'] = { kind: 'audit-log-to-restv2', impl: '../../srv/log2restv2', @@ -10,5 +12,5 @@ describe('Log to Audit Log Service with standard plan', () => { if (!cds.env.requires['audit-log'].credentials) return test.skip('Skipping tests due to missing credentials', () => {}) - require('./tests') + require('./tests')(POST) }) diff --git a/test/integration/tests.js b/test/integration/tests.js index 0ce92c3..fd75436 100644 --- a/test/integration/tests.js +++ b/test/integration/tests.js @@ -1,63 +1,61 @@ -const cds = require('@sap/cds') - -const { POST } = cds.test(__dirname) - -const object = { type: 'foo.bar', id: { foo: 'bar' } } -const data_subject = Object.assign({ role: 'foo.bar' }, object) -const create_attributes = [{ name: 'foo', new: 'baz' }] -const update_attributes = [{ name: 'foo', old: 'bar', new: 'baz' }] -const delete_attributes = [{ name: 'foo', old: 'bar' }] - -const ALICE = { username: 'alice', password: 'password' } - -test('sensitive data read', async () => { - const data = JSON.stringify({ object, data_subject, attributes: [{ name: 'foo' }] }) - const res = await POST('/integration/passthrough', { event: 'SensitiveDataRead', data }, { auth: ALICE }) - expect(res).toMatchObject({ status: 204 }) -}) - -describe('personal data modified', () => { - test('create', async () => { - const data = JSON.stringify({ object, data_subject, attributes: create_attributes }) - const res = await POST('/integration/passthrough', { event: 'PersonalDataModified', data }, { auth: ALICE }) - expect(res).toMatchObject({ status: 204 }) - }) - - test('update', async () => { - const data = JSON.stringify({ object, data_subject, attributes: update_attributes }) - const res = await POST('/integration/passthrough', { event: 'PersonalDataModified', data }, { auth: ALICE }) +module.exports = POST => { + const object = { type: 'foo.bar', id: { foo: 'bar' } } + const data_subject = Object.assign({ role: 'foo.bar' }, object) + const create_attributes = [{ name: 'foo', new: 'baz' }] + const update_attributes = [{ name: 'foo', old: 'bar', new: 'baz' }] + const delete_attributes = [{ name: 'foo', old: 'bar' }] + + const ALICE = { username: 'alice', password: 'password' } + + test('sensitive data read', async () => { + const data = JSON.stringify({ object, data_subject, attributes: [{ name: 'foo' }] }) + const res = await POST('/integration/passthrough', { event: 'SensitiveDataRead', data }, { auth: ALICE }) expect(res).toMatchObject({ status: 204 }) }) - test('delete', async () => { - const data = JSON.stringify({ object, data_subject, attributes: delete_attributes }) - const res = await POST('/integration/passthrough', { event: 'PersonalDataModified', data }, { auth: ALICE }) - expect(res).toMatchObject({ status: 204 }) + describe('personal data modified', () => { + test('create', async () => { + const data = JSON.stringify({ object, data_subject, attributes: create_attributes }) + const res = await POST('/integration/passthrough', { event: 'PersonalDataModified', data }, { auth: ALICE }) + expect(res).toMatchObject({ status: 204 }) + }) + + test('update', async () => { + const data = JSON.stringify({ object, data_subject, attributes: update_attributes }) + const res = await POST('/integration/passthrough', { event: 'PersonalDataModified', data }, { auth: ALICE }) + expect(res).toMatchObject({ status: 204 }) + }) + + test('delete', async () => { + const data = JSON.stringify({ object, data_subject, attributes: delete_attributes }) + const res = await POST('/integration/passthrough', { event: 'PersonalDataModified', data }, { auth: ALICE }) + expect(res).toMatchObject({ status: 204 }) + }) }) -}) -describe('configuration modified', () => { - test('create', async () => { - const data = JSON.stringify({ object, attributes: create_attributes }) - const res = await POST('/integration/passthrough', { event: 'ConfigurationModified', data }, { auth: ALICE }) - expect(res).toMatchObject({ status: 204 }) + describe('configuration modified', () => { + test('create', async () => { + const data = JSON.stringify({ object, attributes: create_attributes }) + const res = await POST('/integration/passthrough', { event: 'ConfigurationModified', data }, { auth: ALICE }) + expect(res).toMatchObject({ status: 204 }) + }) + + test('update', async () => { + const data = JSON.stringify({ object, attributes: update_attributes }) + const res = await POST('/integration/passthrough', { event: 'ConfigurationModified', data }, { auth: ALICE }) + expect(res).toMatchObject({ status: 204 }) + }) + + test('delete', async () => { + const data = JSON.stringify({ object, attributes: delete_attributes }) + const res = await POST('/integration/passthrough', { event: 'ConfigurationModified', data }, { auth: ALICE }) + expect(res).toMatchObject({ status: 204 }) + }) }) - test('update', async () => { - const data = JSON.stringify({ object, attributes: update_attributes }) - const res = await POST('/integration/passthrough', { event: 'ConfigurationModified', data }, { auth: ALICE }) + test('security event', async () => { + const data = JSON.stringify({ data: { foo: 'bar' } }) + const res = await POST('/integration/passthrough', { event: 'SecurityEvent', data }, { auth: ALICE }) expect(res).toMatchObject({ status: 204 }) }) - - test('delete', async () => { - const data = JSON.stringify({ object, attributes: delete_attributes }) - const res = await POST('/integration/passthrough', { event: 'ConfigurationModified', data }, { auth: ALICE }) - expect(res).toMatchObject({ status: 204 }) - }) -}) - -test('security event', async () => { - const data = JSON.stringify({ data: { foo: 'bar' } }) - const res = await POST('/integration/passthrough', { event: 'SecurityEvent', data }, { auth: ALICE }) - expect(res).toMatchObject({ status: 204 }) -}) +} diff --git a/test/personal-data/crud.test.js b/test/personal-data/crud.test.js index 0b711d1..47aeddb 100644 --- a/test/personal-data/crud.test.js +++ b/test/personal-data/crud.test.js @@ -1,5 +1,11 @@ const cds = require('@sap/cds') +const { POST, PATCH, GET, DELETE, data } = cds.test().in(__dirname) + +cds.env.plugins['@cap-js/audit-logging'] = { + impl: require('path').join(__dirname, '../../cds-plugin.js') +} + cds.env.requires['audit-log'] = { kind: 'audit-log-to-console', impl: '../../srv/log2console', @@ -13,8 +19,6 @@ cds.env.requires['audit-log'] = { const _logger = require('../utils/logger')({ debug: true }) cds.log.Logger = _logger -const { POST, PATCH, GET, DELETE, data } = cds.test(__dirname) - describe('personal data audit logging in CRUD', () => { let __log, _logs const _log = (...args) => { diff --git a/test/personal-data/fiori.test.js b/test/personal-data/fiori.test.js index 8dbde79..27c6800 100644 --- a/test/personal-data/fiori.test.js +++ b/test/personal-data/fiori.test.js @@ -1,5 +1,11 @@ const cds = require('@sap/cds') +const { POST, PATCH, GET, DELETE, data } = cds.test().in(__dirname) + +cds.env.plugins['@cap-js/audit-logging'] = { + impl: require('path').join(__dirname, '../../cds-plugin.js') +} + cds.env.requires['audit-log'] = { kind: 'audit-log-to-console', impl: '../../srv/log2console', @@ -9,8 +15,6 @@ cds.env.requires['audit-log'] = { const _logger = require('../utils/logger')({ debug: true }) cds.log.Logger = _logger -const { POST, PATCH, GET, DELETE, data } = cds.test(__dirname) - describe('personal data audit logging in Fiori', () => { let __log, _logs const _log = (...args) => { diff --git a/test/personal-data/handle.test.js b/test/personal-data/handle.test.js index 16ca34a..a9b1306 100644 --- a/test/personal-data/handle.test.js +++ b/test/personal-data/handle.test.js @@ -1,13 +1,17 @@ const cds = require('@sap/cds') +const { GET } = cds.test().in(__dirname) + +cds.env.plugins['@cap-js/audit-logging'] = { + impl: require('path').join(__dirname, '../../cds-plugin.js') +} + cds.env.requires['audit-log'] = { kind: 'audit-log-to-console', impl: '../../srv/log2console', handle: ['WRITE'] } -const { GET } = cds.test(__dirname) - describe('handle', () => { let __log, _logs const _log = (...args) => { diff --git a/test/personal-data/package.json b/test/personal-data/package.json deleted file mode 100644 index 04af3bf..0000000 --- a/test/personal-data/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "personal-data", - "version": "1.0.0", - "description": "A simple CAP project.", - "repository": "", - "license": "UNLICENSED", - "private": true, - "cds": { - "plugins": [ - "../../cds-plugin" - ] - } -}