Skip to content

Commit 7e0a3dd

Browse files
authored
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 <[email protected]> * chore: fix yarn lint Signed-off-by: Frank Kong <[email protected]> * chore: add comment to request users to redact secrets in request body Signed-off-by: Frank Kong <[email protected]> * chore: set a default value for meta field. Signed-off-by: Frank Kong <[email protected]> * chore: update error handling Signed-off-by: Frank Kong <[email protected]> * chore: convert helper function into helper class Signed-off-by: Frank Kong <[email protected]> * chore: update type names Signed-off-by: Frank Kong <[email protected]> * chore: fix tsc errors Signed-off-by: Frank Kong <[email protected]> * chore: update error type Signed-off-by: Frank Kong <[email protected]> * chore: fix audit logging actor id check Signed-off-by: Frank Kong <[email protected]> * chore: add initial unit test (seems to be broken?) Signed-off-by: Frank Kong <[email protected]> * chore: support audit logging of unauthenticated requests Signed-off-by: Frank Kong <[email protected]> * chore: migrate existing common package into a node package to fix jest tests Signed-off-by: Frank Kong <[email protected]> * chore: update getActorId to not throw error when an invalid credential is provided Signed-off-by: Frank Kong <[email protected]> * chore(audit-log): add unit tests for the DefaultAuditLogger Signed-off-by: Frank Kong <[email protected]> * chore: fix yarn lint issues Signed-off-by: Frank Kong <[email protected]> * chore: address review comments Signed-off-by: Frank Kong <[email protected]> * chore: update yarn.lock Signed-off-by: Frank Kong <[email protected]> * chore: remove unnecessary console log Signed-off-by: Frank Kong <[email protected]> * chore: address review comments Signed-off-by: Frank Kong <[email protected]> * chore: update yarn.lock Signed-off-by: Frank Kong <[email protected]> * chore: fix yarn lint Signed-off-by: Frank Kong <[email protected]> * chore: add option to log to different log levels in auditLog Signed-off-by: Frank Kong <[email protected]> * chore: update yarn.lock Signed-off-by: Frank Kong <[email protected]> * chore: provide additional examples in README Signed-off-by: Frank Kong <[email protected]> * chore: update yarn.lock Signed-off-by: Frank Kong <[email protected]> --------- Signed-off-by: Frank Kong <[email protected]>
1 parent ec29d17 commit 7e0a3dd

File tree

12 files changed

+885
-5
lines changed

12 files changed

+885
-5
lines changed

.github/renovate.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@
187187
},
188188
{
189189
"matchFileNames": [
190-
"plugins/analytics-provider-segment*/**",
190+
"plugins/analytics-provider-segment*/**",
191+
"plugins/audit-log*/**",
191192
"plugins/bulk-import*/**",
192193
"plugins/dynamic-plugins-info*/**",
193194
"plugins/keycloak*/**",

plugins/audit-log-node/.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);

plugins/audit-log-node/README.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# @janus-idp/backstage-plugin-audit-log-node
2+
3+
This package contains common types and utility functions for audit logging the backend
4+
5+
## Installation
6+
7+
To install this plugin in a package/plugin, run the following command:
8+
9+
```console
10+
yarn workspace <package/plugin> add @janus-idp/backstage-plugin-audit-log-node
11+
```
12+
13+
### Usage
14+
15+
The audit logging node package contains a helper class for generating audit logs with a common structure, as well as logging them.
16+
17+
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`.
18+
19+
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.
20+
21+
The `DefaultAuditLogger.createAuditLogDetails` will generate the `actorId` of the actor with the following priority (highest to lowest):
22+
23+
- The `actorId` provided in the arguments
24+
- The actor id generated from the `express.Request` object provided in the arguments
25+
- `null` if neither of the above fields were provided in the arguments
26+
27+
---
28+
29+
**IMPORTANT**
30+
31+
Any fields containing secrets provided to these helper functions should have secrets redacted or else they will be logged as is.
32+
33+
For the `DefaultAuditLogger`, these fields would include:
34+
35+
- The `metadata` field
36+
- The following fields in the `request`:
37+
- `request.body`
38+
- `request.params`
39+
- `request.query`
40+
- The `response.body` field
41+
42+
---
43+
44+
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.
45+
46+
### Example
47+
48+
#### Audit Log Example
49+
50+
In the following example, we add a simple audit log for the `/health` endpoint of a plugin's router.
51+
52+
```ts plugins/test/src/service/router.ts
53+
/* highlight-add-start */
54+
55+
/* highlight-add-end */
56+
57+
import {
58+
AuthService,
59+
HttpAuthService,
60+
LoggerService,
61+
} from '@backstage/backend-plugin-api';
62+
63+
import { DefaultAuditLogger } from '@janus-idp/backstage-plugin-audit-log-node';
64+
65+
export interface RouterOptions {
66+
logger: LoggerService;
67+
auth: AuthService;
68+
httpAuth: HttpAuthService;
69+
}
70+
71+
export async function createRouter(
72+
options: RouterOptions,
73+
): Promise<express.Router> {
74+
const { logger, auth, httpAuth } = options;
75+
76+
/* highlight-add-start */
77+
const auditLogger = new DefaultAuditLogger({
78+
logger,
79+
auth,
80+
httpAuth,
81+
});
82+
/* highlight-add-end */
83+
84+
const router = Router();
85+
router.use(express.json());
86+
87+
router.get('/health', async (request, response) => {
88+
logger.info('PONG!');
89+
response.json({ status: 'ok' });
90+
91+
/* highlight-add-start */
92+
// Note: if `level` is not provided, it defaults to `info`
93+
auditLogger.auditLog({
94+
eventName: 'HealthEndpointHit',
95+
stage: 'completion',
96+
level: 'debug',
97+
request,
98+
response: {
99+
status: 200,
100+
body: { status: 'ok' },
101+
},
102+
message: `The Health Endpoint was hit by ${await auditLogger.getActorId(
103+
request,
104+
)}`,
105+
});
106+
/* highlight-add-end */
107+
});
108+
router.use(errorHandler());
109+
return router;
110+
}
111+
```
112+
113+
Assuming the `user:default/tester` user hit requested this endpoint, something similar to the following would be outputted if the logger format is JSON:
114+
115+
```JSON
116+
{"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"}
117+
```
118+
119+
#### Audit Log Error Example
120+
121+
In the following example, we utilize the `auditErrorLog` utility function to generate and output an error log:
122+
123+
```ts plugins/test/src/service/router.ts
124+
/* highlight-add-start */
125+
126+
/* highlight-add-end */
127+
128+
import {
129+
AuthService,
130+
HttpAuthService,
131+
LoggerService,
132+
} from '@backstage/backend-plugin-api';
133+
134+
import { DefaultAuditLogger } from '@janus-idp/backstage-plugin-audit-log-node';
135+
136+
export interface RouterOptions {
137+
logger: LoggerService;
138+
auth: AuthService;
139+
httpAuth: HttpAuthService;
140+
}
141+
142+
export async function createRouter(
143+
options: RouterOptions,
144+
): Promise<express.Router> {
145+
const { logger, auth, httpAuth } = options;
146+
147+
/* highlight-add-start */
148+
const auditLogger = new DefaultAuditLogger({
149+
logger,
150+
auth,
151+
httpAuth,
152+
});
153+
/* highlight-add-end */
154+
155+
const router = Router();
156+
router.use(express.json());
157+
158+
router.get('/error', async (request, response) => {
159+
try {
160+
const customErr = new Error('Custom Error Occurred');
161+
customErr.name = 'CustomError';
162+
163+
throw customErr;
164+
165+
response.json({
166+
status: 'ok',
167+
});
168+
} catch (err) {
169+
/* highlight-add-start */
170+
auditLogger.auditErrorLog({
171+
eventName: 'ErrorEndpointHit',
172+
stage: 'completion',
173+
request,
174+
response: {
175+
status: 501,
176+
body: {
177+
errors: [
178+
{
179+
name: (err as Error).name,
180+
message: (err as Error).message,
181+
},
182+
],
183+
},
184+
},
185+
errors: [customErr],
186+
message: `An error occurred when querying the '/errors' endpoint`,
187+
});
188+
/* highlight-add-end */
189+
// Do something with the caught error
190+
response.status(501).json({
191+
errors: [
192+
{
193+
name: (err as Error).name,
194+
message: (err as Error).message,
195+
},
196+
],
197+
});
198+
}
199+
});
200+
router.use(errorHandler());
201+
return router;
202+
}
203+
```
204+
205+
An example error audit log would be in the following form:
206+
Note: the stack trace was removed redacted in this example due to its size.
207+
208+
```JSON
209+
{"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"}
210+
```

plugins/audit-log-node/package.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "@janus-idp/backstage-plugin-audit-log-node",
3+
"description": "Node.js library for the audit-log plugin",
4+
"version": "0.1.0",
5+
"main": "src/index.ts",
6+
"types": "src/index.ts",
7+
"license": "Apache-2.0",
8+
"private": true,
9+
"publishConfig": {
10+
"access": "public",
11+
"main": "dist/index.cjs.js",
12+
"types": "dist/index.d.ts"
13+
},
14+
"backstage": {
15+
"role": "node-library"
16+
},
17+
"scripts": {
18+
"build": "backstage-cli package build",
19+
"clean": "backstage-cli package clean",
20+
"lint": "backstage-cli package lint",
21+
"postpack": "backstage-cli package postpack",
22+
"prepack": "backstage-cli package prepack",
23+
"start": "backstage-cli package start",
24+
"test": "backstage-cli package test --passWithNoTests --coverage",
25+
"tsc": "tsc"
26+
},
27+
"devDependencies": {
28+
"@backstage/backend-common": "^0.21.7",
29+
"@backstage/backend-test-utils": "0.3.7",
30+
"@backstage/cli": "0.26.4",
31+
"jest-express": "^1.12.0"
32+
},
33+
"files": [
34+
"dist"
35+
],
36+
"repository": {
37+
"type": "git",
38+
"url": "https://github.com/janus-idp/backstage-plugins",
39+
"directory": "plugins/audit-log-node"
40+
},
41+
"keywords": [
42+
"backstage",
43+
"plugin"
44+
],
45+
"homepage": "https://janus-idp.io/",
46+
"bugs": "https://github.com/janus-idp/backstage-plugins/issues",
47+
"dependencies": {
48+
"@backstage/backend-plugin-api": "^0.6.17",
49+
"@backstage/errors": "^1.2.4",
50+
"@backstage/types": "^1.1.1",
51+
"express": "^4.19.2"
52+
}
53+
}

0 commit comments

Comments
 (0)