Skip to content

Commit

Permalink
feat: premium and x509 (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
sjvans authored Oct 24, 2023
1 parent fc18b82 commit 89d3a4c
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 25 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ jobs:
env:
ALS_CREDS_OAUTH2: ${{ secrets.ALS_CREDS_OAUTH2 }}
ALS_CREDS_STANDARD: ${{ secrets.ALS_CREDS_STANDARD }}
ALS_CREDS_PREMIUM: ${{ secrets.ALS_CREDS_PREMIUM }}
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
env:
ALS_CREDS_OAUTH2: ${{ secrets.ALS_CREDS_OAUTH2 }}
ALS_CREDS_STANDARD: ${{ secrets.ALS_CREDS_STANDARD }}
ALS_CREDS_PREMIUM: ${{ secrets.ALS_CREDS_PREMIUM }}
- name: get version
id: package-version
uses: martinbeentjes/npm-get-version-action@v1.2.3
Expand Down
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +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.4.0 - TBD
## Version 0.4.0 - 2023-10-24

### Added

- Support for Premium plan of SAP Audit Log Service
- Support for XSUAA credential type `x509`
- Support for generic outbox

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

### Fixed

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

## Version 0.3.2 - 2023-10-11

Expand Down
50 changes: 27 additions & 23 deletions srv/log2restv2.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
// credentials stuff
const { credentials } = this.options
if (!credentials) throw new Error('No or malformed credentials for "audit-log"')
if (credentials.uaa) {
this._oauth2 = true
this._tokens = new Map()
this._providerTenant = credentials.uaa.tenantid
} else {
if (!credentials.uaa) {
this._plan = 'standard'
this._auth = 'Basic ' + Buffer.from(credentials.user + ':' + credentials.password).toString('base64')
} else {
this._plan = credentials.url.match(/6081/) ? 'premium' : 'oauth2'
this._tokens = new Map()
this._provider = credentials.uaa.tenantid
}
this._vcap = process.env.VCAP_APPLICATION ? JSON.parse(process.env.VCAP_APPLICATION) : null

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

const url = this.options.credentials.uaa.url + '/oauth/token'
const data = {
grant_type: 'client_credentials',
response_type: 'token',
client_id: this.options.credentials.uaa.clientid,
client_secret: this.options.credentials.uaa.clientsecret
const { uaa } = this.options.credentials
const url = (uaa.certurl || uaa.url) + '/oauth/token'
const data = { grant_type: 'client_credentials', response_type: 'token', client_id: uaa.clientid }
const options = { headers: { 'content-type': 'application/x-www-form-urlencoded' } }
if (tenant !== this._provider) options.headers['x-zid'] = tenant
// certificate or secret?
if (uaa['credential-type'] === 'x509') {
options.agent = new https.Agent({ cert: uaa.certificate, key: uaa.key })
} else {
data.client_secret = uaa.clientsecret
}
const urlencoded = Object.keys(data).reduce((acc, cur) => {
acc += (acc ? '&' : '') + cur + '=' + data[cur]
return acc
}, '')
const headers = { 'content-type': 'application/x-www-form-urlencoded' }
if (tenant !== this._providerTenant) headers['x-zid'] = tenant
try {
const { access_token, expires_in } = await _post(url, urlencoded, headers)
const { access_token, expires_in } = await _post(url, urlencoded, options)
tokens.set(tenant, access_token)
// remove token from cache 60 seconds before it expires
setTimeout(() => tokens.delete(tenant), (expires_in - 60) * 1000)
Expand All @@ -84,21 +87,21 @@ module.exports = class AuditLog2RESTv2 extends AuditLogService {
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 {
if (this._plan === 'standard') {
url = this.options.credentials.url + PATHS.STANDARD[path]
headers.authorization = this._auth
} else {
url = this.options.credentials.url + PATHS.OAUTH2[path]
data.tenant ??= this._provider //> if request has no tenant, stay in provider account
headers.authorization = 'Bearer ' + (await this._getToken(data.tenant))
data.tenant = data.tenant === this._provider ? '$PROVIDER' : '$SUBSCRIBER'
}
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)
await _post(url, data, { headers })
} catch (err) {
LOG._trace && LOG.trace('error during log send:', err)
// 429 (rate limit) is not unrecoverable
Expand Down Expand Up @@ -143,9 +146,10 @@ const PATHS = {

const https = require('https')

async function _post(url, data, headers) {
async function _post(url, data, options) {
options.method ??= 'POST'
return new Promise((resolve, reject) => {
const req = https.request(url, { method: 'POST', headers }, res => {
const req = https.request(url, options, res => {
const chunks = []
res.on('data', chunk => chunks.push(chunk))
res.on('end', () => {
Expand Down
28 changes: 28 additions & 0 deletions test/integration/premium.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const cds = require('@sap/cds')

const { POST } = cds.test().in(__dirname)
const log = cds.test.log()

cds.env.requires['audit-log'].credentials = process.env.ALS_CREDS_PREMIUM && JSON.parse(process.env.ALS_CREDS_PREMIUM)

// 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

cds.env.log.levels['audit-log'] = 'debug'

describe('Log to Audit Log Service with premium plan', () => {
if (!cds.env.requires['audit-log'].credentials)
return test.skip('Skipping tests due to missing credentials', () => {})

// required for tests to exit correctly (cf. token expiration timeouts)
jest.useFakeTimers()

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()
})
})

0 comments on commit 89d3a4c

Please sign in to comment.