Skip to content

Commit 89d3a4c

Browse files
authored
feat: premium and x509 (#61)
1 parent fc18b82 commit 89d3a4c

File tree

5 files changed

+61
-25
lines changed

5 files changed

+61
-25
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ jobs:
2828
env:
2929
ALS_CREDS_OAUTH2: ${{ secrets.ALS_CREDS_OAUTH2 }}
3030
ALS_CREDS_STANDARD: ${{ secrets.ALS_CREDS_STANDARD }}
31+
ALS_CREDS_PREMIUM: ${{ secrets.ALS_CREDS_PREMIUM }}

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
env:
2626
ALS_CREDS_OAUTH2: ${{ secrets.ALS_CREDS_OAUTH2 }}
2727
ALS_CREDS_STANDARD: ${{ secrets.ALS_CREDS_STANDARD }}
28+
ALS_CREDS_PREMIUM: ${{ secrets.ALS_CREDS_PREMIUM }}
2829
- name: get version
2930
id: package-version
3031
uses: martinbeentjes/[email protected]

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ All notable changes to this project will be documented in this file.
44
This project adheres to [Semantic Versioning](http://semver.org/).
55
The format is based on [Keep a Changelog](http://keepachangelog.com/).
66

7-
## Version 0.4.0 - TBD
7+
## Version 0.4.0 - 2023-10-24
88

99
### Added
1010

11+
- Support for Premium plan of SAP Audit Log Service
12+
- Support for XSUAA credential type `x509`
1113
- Support for generic outbox
1214

1315
### Changed
@@ -16,7 +18,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
1618

1719
### Fixed
1820

19-
- Avoid dangling SELECTs to resolve data subject IDs, which resulted in "Transaction already closed" errors
21+
- Avoid dangling `SELECT`s to resolve data subject IDs, which resulted in "Transaction already closed" errors
2022

2123
## Version 0.3.2 - 2023-10-11
2224

srv/log2restv2.js

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
99
// credentials stuff
1010
const { credentials } = this.options
1111
if (!credentials) throw new Error('No or malformed credentials for "audit-log"')
12-
if (credentials.uaa) {
13-
this._oauth2 = true
14-
this._tokens = new Map()
15-
this._providerTenant = credentials.uaa.tenantid
16-
} else {
12+
if (!credentials.uaa) {
13+
this._plan = 'standard'
1714
this._auth = 'Basic ' + Buffer.from(credentials.user + ':' + credentials.password).toString('base64')
15+
} else {
16+
this._plan = credentials.url.match(/6081/) ? 'premium' : 'oauth2'
17+
this._tokens = new Map()
18+
this._provider = credentials.uaa.tenantid
1819
}
1920
this._vcap = process.env.VCAP_APPLICATION ? JSON.parse(process.env.VCAP_APPLICATION) : null
2021

@@ -49,21 +50,23 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
4950
const { _tokens: tokens } = this
5051
if (tokens.has(tenant)) return tokens.get(tenant)
5152

52-
const url = this.options.credentials.uaa.url + '/oauth/token'
53-
const data = {
54-
grant_type: 'client_credentials',
55-
response_type: 'token',
56-
client_id: this.options.credentials.uaa.clientid,
57-
client_secret: this.options.credentials.uaa.clientsecret
53+
const { uaa } = this.options.credentials
54+
const url = (uaa.certurl || uaa.url) + '/oauth/token'
55+
const data = { grant_type: 'client_credentials', response_type: 'token', client_id: uaa.clientid }
56+
const options = { headers: { 'content-type': 'application/x-www-form-urlencoded' } }
57+
if (tenant !== this._provider) options.headers['x-zid'] = tenant
58+
// certificate or secret?
59+
if (uaa['credential-type'] === 'x509') {
60+
options.agent = new https.Agent({ cert: uaa.certificate, key: uaa.key })
61+
} else {
62+
data.client_secret = uaa.clientsecret
5863
}
5964
const urlencoded = Object.keys(data).reduce((acc, cur) => {
6065
acc += (acc ? '&' : '') + cur + '=' + data[cur]
6166
return acc
6267
}, '')
63-
const headers = { 'content-type': 'application/x-www-form-urlencoded' }
64-
if (tenant !== this._providerTenant) headers['x-zid'] = tenant
6568
try {
66-
const { access_token, expires_in } = await _post(url, urlencoded, headers)
69+
const { access_token, expires_in } = await _post(url, urlencoded, options)
6770
tokens.set(tenant, access_token)
6871
// remove token from cache 60 seconds before it expires
6972
setTimeout(() => tokens.delete(tenant), (expires_in - 60) * 1000)
@@ -84,21 +87,21 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
8487
headers.XS_AUDIT_APP = this._vcap.application_name
8588
}
8689
let url
87-
if (this._oauth2) {
88-
url = this.options.credentials.url + PATHS.OAUTH2[path]
89-
data.tenant ??= this._providerTenant //> if request has no tenant, stay in provider account
90-
headers.authorization = 'Bearer ' + (await this._getToken(data.tenant))
91-
data.tenant = data.tenant === this._providerTenant ? '$PROVIDER' : '$SUBSCRIBER'
92-
} else {
90+
if (this._plan === 'standard') {
9391
url = this.options.credentials.url + PATHS.STANDARD[path]
9492
headers.authorization = this._auth
93+
} else {
94+
url = this.options.credentials.url + PATHS.OAUTH2[path]
95+
data.tenant ??= this._provider //> if request has no tenant, stay in provider account
96+
headers.authorization = 'Bearer ' + (await this._getToken(data.tenant))
97+
data.tenant = data.tenant === this._provider ? '$PROVIDER' : '$SUBSCRIBER'
9598
}
9699
if (LOG._debug) {
97100
const _headers = Object.assign({}, headers, { authorization: headers.authorization.split(' ')[0] + ' ***' })
98101
LOG.debug(`sending audit log to ${url} with tenant "${data.tenant}", user "${data.user}", and headers`, _headers)
99102
}
100103
try {
101-
await _post(url, data, headers)
104+
await _post(url, data, { headers })
102105
} catch (err) {
103106
LOG._trace && LOG.trace('error during log send:', err)
104107
// 429 (rate limit) is not unrecoverable
@@ -143,9 +146,10 @@ const PATHS = {
143146

144147
const https = require('https')
145148

146-
async function _post(url, data, headers) {
149+
async function _post(url, data, options) {
150+
options.method ??= 'POST'
147151
return new Promise((resolve, reject) => {
148-
const req = https.request(url, { method: 'POST', headers }, res => {
152+
const req = https.request(url, options, res => {
149153
const chunks = []
150154
res.on('data', chunk => chunks.push(chunk))
151155
res.on('end', () => {

test/integration/premium.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const cds = require('@sap/cds')
2+
3+
const { POST } = cds.test().in(__dirname)
4+
const log = cds.test.log()
5+
6+
cds.env.requires['audit-log'].credentials = process.env.ALS_CREDS_PREMIUM && JSON.parse(process.env.ALS_CREDS_PREMIUM)
7+
8+
// stay in provider account (i.e., use "$PROVIDER" and avoid x-zid header when fetching oauth2 token)
9+
cds.env.requires.auth.users.alice.tenant = cds.env.requires['audit-log'].credentials.uaa.tenantid
10+
11+
cds.env.log.levels['audit-log'] = 'debug'
12+
13+
describe('Log to Audit Log Service with premium plan', () => {
14+
if (!cds.env.requires['audit-log'].credentials)
15+
return test.skip('Skipping tests due to missing credentials', () => {})
16+
17+
// required for tests to exit correctly (cf. token expiration timeouts)
18+
jest.useFakeTimers()
19+
20+
require('./tests')(POST)
21+
22+
test('no tenant is handled correctly', async () => {
23+
const data = JSON.stringify({ data: { foo: 'bar' } })
24+
const res = await POST('/integration/passthrough', { event: 'SecurityEvent', data })
25+
expect(res).toMatchObject({ status: 204 })
26+
expect(log.output.match(/\$PROVIDER/)).toBeTruthy()
27+
})
28+
})

0 commit comments

Comments
 (0)