Skip to content

Commit

Permalink
fix: if request has no tenant, stay in provider account (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
sjvans authored Oct 11, 2023
1 parent a09984e commit 5d2102e
Show file tree
Hide file tree
Showing 14 changed files with 111 additions and 117 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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": "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)",
Expand Down
10 changes: 8 additions & 2 deletions srv/log2restv2.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,32 +69,38 @@ 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
}
}

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
Expand Down
8 changes: 7 additions & 1 deletion test/api/api.test.js
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
6 changes: 3 additions & 3 deletions test/api/custom.test.js
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down
22 changes: 0 additions & 22 deletions test/api/package.json

This file was deleted.

14 changes: 13 additions & 1 deletion test/integration/oauth2.test.js
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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()
})
})
13 changes: 0 additions & 13 deletions test/integration/package.json

This file was deleted.

4 changes: 3 additions & 1 deletion test/integration/standard.test.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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)
})
106 changes: 52 additions & 54 deletions test/integration/tests.js
Original file line number Diff line number Diff line change
@@ -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 })
})
}
8 changes: 6 additions & 2 deletions test/personal-data/crud.test.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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) => {
Expand Down
8 changes: 6 additions & 2 deletions test/personal-data/fiori.test.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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) => {
Expand Down
8 changes: 6 additions & 2 deletions test/personal-data/handle.test.js
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down
13 changes: 0 additions & 13 deletions test/personal-data/package.json

This file was deleted.

0 comments on commit 5d2102e

Please sign in to comment.