forked from janus-idp/backstage-plugins
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(audit-log): add common audit-log package (janus-idp#1622)
* feat(audit-log): add common audit-log package * chore: add helper function to generate audit logs Signed-off-by: Frank Kong <frkong@redhat.com> * chore: fix yarn lint Signed-off-by: Frank Kong <frkong@redhat.com> * chore: add comment to request users to redact secrets in request body Signed-off-by: Frank Kong <frkong@redhat.com> * chore: set a default value for meta field. Signed-off-by: Frank Kong <frkong@redhat.com> * chore: update error handling Signed-off-by: Frank Kong <frkong@redhat.com> * chore: convert helper function into helper class Signed-off-by: Frank Kong <frkong@redhat.com> * chore: update type names Signed-off-by: Frank Kong <frkong@redhat.com> * chore: fix tsc errors Signed-off-by: Frank Kong <frkong@redhat.com> * chore: update error type Signed-off-by: Frank Kong <frkong@redhat.com> * chore: fix audit logging actor id check Signed-off-by: Frank Kong <frkong@redhat.com> * chore: add initial unit test (seems to be broken?) Signed-off-by: Frank Kong <frkong@redhat.com> * chore: support audit logging of unauthenticated requests Signed-off-by: Frank Kong <frkong@redhat.com> * chore: migrate existing common package into a node package to fix jest tests Signed-off-by: Frank Kong <frkong@redhat.com> * chore: update getActorId to not throw error when an invalid credential is provided Signed-off-by: Frank Kong <frkong@redhat.com> * chore(audit-log): add unit tests for the DefaultAuditLogger Signed-off-by: Frank Kong <frkong@redhat.com> * chore: fix yarn lint issues Signed-off-by: Frank Kong <frkong@redhat.com> * chore: address review comments Signed-off-by: Frank Kong <frkong@redhat.com> * chore: update yarn.lock Signed-off-by: Frank Kong <frkong@redhat.com> * chore: remove unnecessary console log Signed-off-by: Frank Kong <frkong@redhat.com> * chore: address review comments Signed-off-by: Frank Kong <frkong@redhat.com> * chore: update yarn.lock Signed-off-by: Frank Kong <frkong@redhat.com> * chore: fix yarn lint Signed-off-by: Frank Kong <frkong@redhat.com> * chore: add option to log to different log levels in auditLog Signed-off-by: Frank Kong <frkong@redhat.com> * chore: update yarn.lock Signed-off-by: Frank Kong <frkong@redhat.com> * chore: provide additional examples in README Signed-off-by: Frank Kong <frkong@redhat.com> * chore: update yarn.lock Signed-off-by: Frank Kong <frkong@redhat.com> --------- Signed-off-by: Frank Kong <frkong@redhat.com>
- Loading branch information
Showing
12 changed files
with
885 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
# @janus-idp/backstage-plugin-audit-log-node | ||
|
||
This package contains common types and utility functions for audit logging the backend | ||
|
||
## Installation | ||
|
||
To install this plugin in a package/plugin, run the following command: | ||
|
||
```console | ||
yarn workspace <package/plugin> add @janus-idp/backstage-plugin-audit-log-node | ||
``` | ||
|
||
### Usage | ||
|
||
The audit logging node package contains a helper class for generating audit logs with a common structure, as well as logging them. | ||
|
||
The `auditLog` and `auditErrorLog` functions can be used to log out an audit log using the backstage `LoggerService`. You can provide a log level to the `auditLog` function. The supported levels are: `info`, `debug`, `warn`, and `error`. | ||
|
||
Alternatively, if you want to generate the audit log object (does not contain message) without it being logged out for you, the `createAuditLogDetails` helper function of the `DefaultAuditLogger` can be used. | ||
|
||
The `DefaultAuditLogger.createAuditLogDetails` will generate the `actorId` of the actor with the following priority (highest to lowest): | ||
|
||
- The `actorId` provided in the arguments | ||
- The actor id generated from the `express.Request` object provided in the arguments | ||
- `null` if neither of the above fields were provided in the arguments | ||
|
||
--- | ||
|
||
**IMPORTANT** | ||
|
||
Any fields containing secrets provided to these helper functions should have secrets redacted or else they will be logged as is. | ||
|
||
For the `DefaultAuditLogger`, these fields would include: | ||
|
||
- The `metadata` field | ||
- The following fields in the `request`: | ||
- `request.body` | ||
- `request.params` | ||
- `request.query` | ||
- The `response.body` field | ||
|
||
--- | ||
|
||
The `getActorId` helper function grabs the specified entityRef of the user or service associated with the provided credentials in the provided express Request object. If no request is provided or no user/service was associated to the request, `undefined` is returned. | ||
|
||
### Example | ||
|
||
#### Audit Log Example | ||
|
||
In the following example, we add a simple audit log for the `/health` endpoint of a plugin's router. | ||
|
||
```ts plugins/test/src/service/router.ts | ||
/* highlight-add-start */ | ||
|
||
/* highlight-add-end */ | ||
|
||
import { | ||
AuthService, | ||
HttpAuthService, | ||
LoggerService, | ||
} from '@backstage/backend-plugin-api'; | ||
|
||
import { DefaultAuditLogger } from '@janus-idp/backstage-plugin-audit-log-node'; | ||
|
||
export interface RouterOptions { | ||
logger: LoggerService; | ||
auth: AuthService; | ||
httpAuth: HttpAuthService; | ||
} | ||
|
||
export async function createRouter( | ||
options: RouterOptions, | ||
): Promise<express.Router> { | ||
const { logger, auth, httpAuth } = options; | ||
|
||
/* highlight-add-start */ | ||
const auditLogger = new DefaultAuditLogger({ | ||
logger, | ||
auth, | ||
httpAuth, | ||
}); | ||
/* highlight-add-end */ | ||
|
||
const router = Router(); | ||
router.use(express.json()); | ||
|
||
router.get('/health', async (request, response) => { | ||
logger.info('PONG!'); | ||
response.json({ status: 'ok' }); | ||
|
||
/* highlight-add-start */ | ||
// Note: if `level` is not provided, it defaults to `info` | ||
auditLogger.auditLog({ | ||
eventName: 'HealthEndpointHit', | ||
stage: 'completion', | ||
level: 'debug', | ||
request, | ||
response: { | ||
status: 200, | ||
body: { status: 'ok' }, | ||
}, | ||
message: `The Health Endpoint was hit by ${await auditLogger.getActorId( | ||
request, | ||
)}`, | ||
}); | ||
/* highlight-add-end */ | ||
}); | ||
router.use(errorHandler()); | ||
return router; | ||
} | ||
``` | ||
|
||
Assuming the `user:default/tester` user hit requested this endpoint, something similar to the following would be outputted if the logger format is JSON: | ||
|
||
```JSON | ||
{"actor":{"actorId":"user:default/tester","hostname":"localhost","ip":"::1","userAgent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"},"eventName":"HealthEndpointHit","isAuditLog":true,"level":"debug","message":"The Health Endpoint was hit by user:default/tester","meta":{},"plugin":"test","request":{"body": "","method":"GET","params":{},"query":{},"url":"/api/test/health"},"service":"backstage","stage":"completion","status":"succeeded","timestamp":"2024-05-17 11:17:07","type":"plugin"} | ||
``` | ||
|
||
#### Audit Log Error Example | ||
|
||
In the following example, we utilize the `auditErrorLog` utility function to generate and output an error log: | ||
|
||
```ts plugins/test/src/service/router.ts | ||
/* highlight-add-start */ | ||
|
||
/* highlight-add-end */ | ||
|
||
import { | ||
AuthService, | ||
HttpAuthService, | ||
LoggerService, | ||
} from '@backstage/backend-plugin-api'; | ||
|
||
import { DefaultAuditLogger } from '@janus-idp/backstage-plugin-audit-log-node'; | ||
|
||
export interface RouterOptions { | ||
logger: LoggerService; | ||
auth: AuthService; | ||
httpAuth: HttpAuthService; | ||
} | ||
|
||
export async function createRouter( | ||
options: RouterOptions, | ||
): Promise<express.Router> { | ||
const { logger, auth, httpAuth } = options; | ||
|
||
/* highlight-add-start */ | ||
const auditLogger = new DefaultAuditLogger({ | ||
logger, | ||
auth, | ||
httpAuth, | ||
}); | ||
/* highlight-add-end */ | ||
|
||
const router = Router(); | ||
router.use(express.json()); | ||
|
||
router.get('/error', async (request, response) => { | ||
try { | ||
const customErr = new Error('Custom Error Occurred'); | ||
customErr.name = 'CustomError'; | ||
|
||
throw customErr; | ||
|
||
response.json({ | ||
status: 'ok', | ||
}); | ||
} catch (err) { | ||
/* highlight-add-start */ | ||
auditLogger.auditErrorLog({ | ||
eventName: 'ErrorEndpointHit', | ||
stage: 'completion', | ||
request, | ||
response: { | ||
status: 501, | ||
body: { | ||
errors: [ | ||
{ | ||
name: (err as Error).name, | ||
message: (err as Error).message, | ||
}, | ||
], | ||
}, | ||
}, | ||
errors: [customErr], | ||
message: `An error occurred when querying the '/errors' endpoint`, | ||
}); | ||
/* highlight-add-end */ | ||
// Do something with the caught error | ||
response.status(501).json({ | ||
errors: [ | ||
{ | ||
name: (err as Error).name, | ||
message: (err as Error).message, | ||
}, | ||
], | ||
}); | ||
} | ||
}); | ||
router.use(errorHandler()); | ||
return router; | ||
} | ||
``` | ||
|
||
An example error audit log would be in the following form: | ||
Note: the stack trace was removed redacted in this example due to its size. | ||
|
||
```JSON | ||
{"actor":{"actorId":"user:development/guest","hostname":"localhost","ip":"::1","userAgent":"curl/8.2.1"},"errors":[{"message":"Custom Error Occurred","name":"CustomError","stack":"CustomError: Custom Error Occurred\n at STACK_TRACE]"}],"eventName":"ErrorEndpointHit","isAuditLog":true,"level":"error","message":"An error occurred when querying the '/errors' endpoint","meta":{},"plugin":"test","request":{"body":{},"method":"GET","params":{},"query":{},"url":"/api/test/error"},"response":{"body":{"errors":[{"name":"CustomError","message":"Custom Error Occurred"}]},"status":501},"service":"backstage","stage":"completion","status":"failed","timestamp":"2024-05-23 10:09:04"} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
{ | ||
"name": "@janus-idp/backstage-plugin-audit-log-node", | ||
"description": "Node.js library for the audit-log plugin", | ||
"version": "0.1.0", | ||
"main": "src/index.ts", | ||
"types": "src/index.ts", | ||
"license": "Apache-2.0", | ||
"private": true, | ||
"publishConfig": { | ||
"access": "public", | ||
"main": "dist/index.cjs.js", | ||
"types": "dist/index.d.ts" | ||
}, | ||
"backstage": { | ||
"role": "node-library" | ||
}, | ||
"scripts": { | ||
"build": "backstage-cli package build", | ||
"clean": "backstage-cli package clean", | ||
"lint": "backstage-cli package lint", | ||
"postpack": "backstage-cli package postpack", | ||
"prepack": "backstage-cli package prepack", | ||
"start": "backstage-cli package start", | ||
"test": "backstage-cli package test --passWithNoTests --coverage", | ||
"tsc": "tsc" | ||
}, | ||
"devDependencies": { | ||
"@backstage/backend-common": "^0.21.7", | ||
"@backstage/backend-test-utils": "0.3.7", | ||
"@backstage/cli": "0.26.4", | ||
"jest-express": "^1.12.0" | ||
}, | ||
"files": [ | ||
"dist" | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/janus-idp/backstage-plugins", | ||
"directory": "plugins/audit-log-node" | ||
}, | ||
"keywords": [ | ||
"backstage", | ||
"plugin" | ||
], | ||
"homepage": "https://janus-idp.io/", | ||
"bugs": "https://github.com/janus-idp/backstage-plugins/issues", | ||
"dependencies": { | ||
"@backstage/backend-plugin-api": "^0.6.17", | ||
"@backstage/errors": "^1.2.4", | ||
"@backstage/types": "^1.1.1", | ||
"express": "^4.19.2" | ||
} | ||
} |
Oops, something went wrong.