Skip to content

Commit

Permalink
feat(audit-log): add common audit-log package (janus-idp#1622)
Browse files Browse the repository at this point in the history
* 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
Zaperex committed May 23, 2024
1 parent ec29d17 commit 7e0a3dd
Show file tree
Hide file tree
Showing 12 changed files with 885 additions and 5 deletions.
3 changes: 2 additions & 1 deletion .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@
},
{
"matchFileNames": [
"plugins/analytics-provider-segment*/**",
"plugins/analytics-provider-segment*/**",
"plugins/audit-log*/**",
"plugins/bulk-import*/**",
"plugins/dynamic-plugins-info*/**",
"plugins/keycloak*/**",
Expand Down
1 change: 1 addition & 0 deletions plugins/audit-log-node/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
210 changes: 210 additions & 0 deletions plugins/audit-log-node/README.md
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"}
```
53 changes: 53 additions & 0 deletions plugins/audit-log-node/package.json
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"
}
}
Loading

0 comments on commit 7e0a3dd

Please sign in to comment.