@parsifal-m/backstage-plugin-opa-node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # Backstage Plugin OPA Node
2
+
3
+ This package provides a Node.js service for integrating Open Policy Agent (OPA) with Backstage backend modules and plugins.
4
+
5
+ Its a nice way to secure your backend routes using OPA!
6
+
7
+ ## Pre-requisites
8
+
9
+ Simply run the yarn install command to add the package to your plugin:
10
+
11
+ ```bash
12
+ yarn add @parsifal-m/backstage-plugin-opa-node
13
+ ```
14
+
15
+ Add the dependency in your `plugin.ts` or `module.ts` file:
16
+
17
+ ```ts
18
+ import { opaService } from '@parsifal-m/backstage-plugin-opa-node';
19
+
20
+ export const yourPlugin = createBackendPlugin({
21
+ pluginId: 'your-plugin-id',
22
+ register(env) {
23
+ env.registerInit({
24
+ deps: {
25
+ // other dependencies...
26
+ userInfo: coreServices.userInfo,
27
+ httpRouter: coreServices.httpRouter,
28
+ catalog: catalogServiceRef,
29
+ // We add the OPA service as a dependency this will allow the plugin to use it
30
+ opa: opaService,
31
+ },
32
+ async init({
33
+ // other dependencies...
34
+ opa,
35
+ }) {
36
+ httpRouter.use(
37
+ await createRouter({
38
+ // other dependencies...
39
+ opa,
40
+ }),
41
+ );
42
+ },
43
+ });
44
+ },
45
+ });
46
+ ```
47
+
48
+ ## What is it for?
49
+
50
+ - Allows Backstage plugins and backend services to evaluate authorization and policy decisions using OPA.
51
+ - Provides a simple API for sending policy inputs and receiving policy results from OPA.
52
+ - Supports custom policy entry points and flexible input structures for fine-grained access control.
53
+ - Enables centralized, declarative policy management for your Backstage environment.
54
+
55
+ ## Simple Usage Example
56
+
57
+ Here's a minimal example of how to use `OpaService` in an Express route:
58
+
59
+ ```ts
60
+ import { opaService } from '@parsifal-m/backstage-plugin-opa-node';
61
+
62
+ app.post('/my-protected-route', async (req, res) => {
63
+ const input = {
64
+ method: req.method,
65
+ path: req.path,
66
+ headers: req.headers,
67
+ permission: { name: 'create-resource' },
68
+ // ...other input fields
69
+ };
70
+
71
+ const policyResult = await opa.evaluatePolicy(input, 'my_policy_entrypoint');
72
+ if (!policyResult.result || !policyResult.result.allow) {
73
+ return res.status(403).json({ error: 'Access Denied' });
74
+ }
75
+ // Proceed with your route logic
76
+ res.status(201).json({ success: true });
77
+ });
78
+ ```
79
+
80
+ ## Getting Started
81
+
82
+ I've set up a demo backend plugin that shows how to use this package to protect your backend endpoints using OPA. You can find it here: [opa-backend-demo](../opa-demo-backend/README.md)
83
+
84
+ If you check out the `router.ts` file in that plugin, you'll see how to use the `OpaService` to evaluate policies before allowing access to certain endpoints.
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ class OpaClient {
4
+ logger;
5
+ config;
6
+ baseUrl;
7
+ constructor(config, logger) {
8
+ this.config = config;
9
+ this.logger = logger;
10
+ this.baseUrl = this.config.getString("openPolicyAgent.baseUrl");
11
+ }
12
+ async evaluatePolicy(input, entryPoint) {
13
+ if (!this.baseUrl) {
14
+ this.logger.error("The OPA URL is not set in the app-config!");
15
+ throw new Error("The OPA URL is not set in the app-config!");
16
+ }
17
+ if (!entryPoint) {
18
+ this.logger.error(
19
+ "You have not defined a policy entrypoint! Please provide one."
20
+ );
21
+ throw new Error(
22
+ "You have not defined a policy entrypoint! Please provide one."
23
+ );
24
+ }
25
+ if (!input) {
26
+ this.logger.error("The policy input is missing!");
27
+ throw new Error("The policy input is missing!");
28
+ }
29
+ const opaUrl = `${this.baseUrl}/v1/data/${entryPoint}`;
30
+ try {
31
+ const response = await fetch(opaUrl, {
32
+ method: "POST",
33
+ headers: {
34
+ "Content-Type": "application/json"
35
+ },
36
+ body: JSON.stringify({ input })
37
+ });
38
+ if (!response.ok) {
39
+ const message = `An error response was returned after sending the policy input to the OPA server: ${response.status} - ${response.statusText}`;
40
+ this.logger.error(message);
41
+ throw new Error(message);
42
+ }
43
+ const evalResult = await response.json();
44
+ this.logger.debug(
45
+ `Received data from OPA: ${JSON.stringify(evalResult)}`
46
+ );
47
+ return evalResult;
48
+ } catch (error) {
49
+ this.logger.error(
50
+ `An error occurred while sending the policy input to the OPA server: ${error}`
51
+ );
52
+ throw new Error(
53
+ `An error occurred while sending the policy input to the OPA server: ${error}`
54
+ );
55
+ }
56
+ }
57
+ }
58
+
59
+ exports.OpaClient = OpaClient;
60
+ //# sourceMappingURL=opaClient.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opaClient.cjs.js","sources":["../../src/api/opaClient.ts"],"sourcesContent":["import { LoggerService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport {\n PolicyInput,\n PolicyResult,\n} from '@parsifal-m/backstage-plugin-opa-common';\n\nexport class OpaClient {\n private readonly logger: LoggerService;\n private readonly config: Config;\n private readonly baseUrl: string;\n\n constructor(config: Config, logger: LoggerService) {\n this.config = config;\n this.logger = logger;\n this.baseUrl = this.config.getString('openPolicyAgent.baseUrl');\n }\n\n async evaluatePolicy(\n input: PolicyInput,\n entryPoint: string,\n ): Promise<PolicyResult> {\n if (!this.baseUrl) {\n this.logger.error('The OPA URL is not set in the app-config!');\n throw new Error('The OPA URL is not set in the app-config!');\n }\n\n if (!entryPoint) {\n this.logger.error(\n 'You have not defined a policy entrypoint! Please provide one.',\n );\n throw new Error(\n 'You have not defined a policy entrypoint! Please provide one.',\n );\n }\n\n if (!input) {\n this.logger.error('The policy input is missing!');\n throw new Error('The policy input is missing!');\n }\n\n const opaUrl = `${this.baseUrl}/v1/data/${entryPoint}`;\n\n try {\n const response = await fetch(opaUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ input }),\n });\n\n if (!response.ok) {\n const message = `An error response was returned after sending the policy input to the OPA server: ${response.status} - ${response.statusText}`;\n this.logger.error(message);\n throw new Error(message);\n }\n\n const evalResult = (await response.json()) as PolicyResult;\n this.logger.debug(\n `Received data from OPA: ${JSON.stringify(evalResult)}`,\n );\n\n return evalResult;\n } catch (error: unknown) {\n this.logger.error(\n `An error occurred while sending the policy input to the OPA server: ${error}`,\n );\n throw new Error(\n `An error occurred while sending the policy input to the OPA server: ${error}`,\n );\n }\n }\n}\n"],"names":[],"mappings":";;AAOO,MAAM,SAAU,CAAA;AAAA,EACJ,MAAA;AAAA,EACA,MAAA;AAAA,EACA,OAAA;AAAA,EAEjB,WAAA,CAAY,QAAgB,MAAuB,EAAA;AACjD,IAAA,IAAA,CAAK,MAAS,GAAA,MAAA;AACd,IAAA,IAAA,CAAK,MAAS,GAAA,MAAA;AACd,IAAA,IAAA,CAAK,OAAU,GAAA,IAAA,CAAK,MAAO,CAAA,SAAA,CAAU,yBAAyB,CAAA;AAAA;AAChE,EAEA,MAAM,cACJ,CAAA,KAAA,EACA,UACuB,EAAA;AACvB,IAAI,IAAA,CAAC,KAAK,OAAS,EAAA;AACjB,MAAK,IAAA,CAAA,MAAA,CAAO,MAAM,2CAA2C,CAAA;AAC7D,MAAM,MAAA,IAAI,MAAM,2CAA2C,CAAA;AAAA;AAG7D,IAAA,IAAI,CAAC,UAAY,EAAA;AACf,MAAA,IAAA,CAAK,MAAO,CAAA,KAAA;AAAA,QACV;AAAA,OACF;AACA,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,OACF;AAAA;AAGF,IAAA,IAAI,CAAC,KAAO,EAAA;AACV,MAAK,IAAA,CAAA,MAAA,CAAO,MAAM,8BAA8B,CAAA;AAChD,MAAM,MAAA,IAAI,MAAM,8BAA8B,CAAA;AAAA;AAGhD,IAAA,MAAM,MAAS,GAAA,CAAA,EAAG,IAAK,CAAA,OAAO,YAAY,UAAU,CAAA,CAAA;AAEpD,IAAI,IAAA;AACF,MAAM,MAAA,QAAA,GAAW,MAAM,KAAA,CAAM,MAAQ,EAAA;AAAA,QACnC,MAAQ,EAAA,MAAA;AAAA,QACR,OAAS,EAAA;AAAA,UACP,cAAgB,EAAA;AAAA,SAClB;AAAA,QACA,IAAM,EAAA,IAAA,CAAK,SAAU,CAAA,EAAE,OAAO;AAAA,OAC/B,CAAA;AAED,MAAI,IAAA,CAAC,SAAS,EAAI,EAAA;AAChB,QAAA,MAAM,UAAU,CAAoF,iFAAA,EAAA,QAAA,CAAS,MAAM,CAAA,GAAA,EAAM,SAAS,UAAU,CAAA,CAAA;AAC5I,QAAK,IAAA,CAAA,MAAA,CAAO,MAAM,OAAO,CAAA;AACzB,QAAM,MAAA,IAAI,MAAM,OAAO,CAAA;AAAA;AAGzB,MAAM,MAAA,UAAA,GAAc,MAAM,QAAA,CAAS,IAAK,EAAA;AACxC,MAAA,IAAA,CAAK,MAAO,CAAA,KAAA;AAAA,QACV,CAA2B,wBAAA,EAAA,IAAA,CAAK,SAAU,CAAA,UAAU,CAAC,CAAA;AAAA,OACvD;AAEA,MAAO,OAAA,UAAA;AAAA,aACA,KAAgB,EAAA;AACvB,MAAA,IAAA,CAAK,MAAO,CAAA,KAAA;AAAA,QACV,uEAAuE,KAAK,CAAA;AAAA,OAC9E;AACA,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,uEAAuE,KAAK,CAAA;AAAA,OAC9E;AAAA;AACF;AAEJ;;;;"}
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ var opaClient = require('./api/opaClient.cjs.js');
4
+ var service = require('./lib/service.cjs.js');
5
+ var DefaultOpaService = require('./service/DefaultOpaService.cjs.js');
6
+
7
+
8
+
9
+ exports.OpaClient = opaClient.OpaClient;
10
+ exports.opaService = service.opaService;
11
+ exports.DefaultOpaService = DefaultOpaService.DefaultOpaService;
12
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
@@ -0,0 +1,41 @@
1
+ import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
+ import { LoggerService } from '@backstage/backend-plugin-api';
3
+ import { Config } from '@backstage/config';
4
+ import { PolicyInput, PolicyResult } from '@parsifal-m/backstage-plugin-opa-common';
5
+
6
+ declare class OpaClient {
7
+ private readonly logger;
8
+ private readonly config;
9
+ private readonly baseUrl;
10
+ constructor(config: Config, logger: LoggerService);
11
+ evaluatePolicy(input: PolicyInput, entryPoint: string): Promise<PolicyResult>;
12
+ }
13
+
14
+ /**
15
+ * OPA Service interface for evaluating policies.
16
+ *
17
+ * @public
18
+ */
19
+ interface OpaService {
20
+ /**
21
+ * Evaluate a policy at the given entry point.
22
+ *
23
+ * @param input - The policy input data
24
+ * @param entryPoint - The OPA policy entry point d
25
+ * @returns Promise resolving to the policy evaluation result
26
+ */
27
+ evaluatePolicy<T = PolicyResult>(input: PolicyInput, entryPoint: string): Promise<T>;
28
+ }
29
+ declare class DefaultOpaService implements OpaService {
30
+ private readonly opaClient;
31
+ constructor(opaClient: OpaClient);
32
+ static create(options: {
33
+ config: Config;
34
+ logger: LoggerService;
35
+ }): DefaultOpaService;
36
+ evaluatePolicy<T = PolicyResult>(input: PolicyInput, entryPoint: string): Promise<T>;
37
+ }
38
+
39
+ declare const opaService: _backstage_backend_plugin_api.ServiceRef<OpaService, "plugin", "singleton">;
40
+
41
+ export { DefaultOpaService, OpaClient, type OpaService, opaService };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var DefaultOpaService = require('../service/DefaultOpaService.cjs.js');
5
+
6
+ const opaService = backendPluginApi.createServiceRef({
7
+ id: "opa.service",
8
+ scope: "plugin",
9
+ defaultFactory: async (service) => backendPluginApi.createServiceFactory({
10
+ service,
11
+ deps: {
12
+ config: backendPluginApi.coreServices.rootConfig,
13
+ logger: backendPluginApi.coreServices.logger
14
+ },
15
+ factory({ config, logger }) {
16
+ return DefaultOpaService.DefaultOpaService.create({
17
+ config,
18
+ logger
19
+ });
20
+ }
21
+ })
22
+ });
23
+
24
+ exports.opaService = opaService;
25
+ //# sourceMappingURL=service.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.cjs.js","sources":["../../src/lib/service.ts"],"sourcesContent":["import {\n coreServices,\n createServiceFactory,\n createServiceRef,\n} from '@backstage/backend-plugin-api';\nimport { DefaultOpaService, OpaService } from '../../service';\n\nexport const opaService = createServiceRef<OpaService>({\n id: 'opa.service',\n scope: 'plugin',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n },\n factory({ config, logger }) {\n return DefaultOpaService.create({\n config,\n logger,\n });\n },\n }),\n});\n"],"names":["createServiceRef","createServiceFactory","coreServices","DefaultOpaService"],"mappings":";;;;;AAOO,MAAM,aAAaA,iCAA6B,CAAA;AAAA,EACrD,EAAI,EAAA,aAAA;AAAA,EACJ,KAAO,EAAA,QAAA;AAAA,EACP,cAAA,EAAgB,OAAM,OAAA,KACpBC,qCAAqB,CAAA;AAAA,IACnB,OAAA;AAAA,IACA,IAAM,EAAA;AAAA,MACJ,QAAQC,6BAAa,CAAA,UAAA;AAAA,MACrB,QAAQA,6BAAa,CAAA;AAAA,KACvB;AAAA,IACA,OAAQ,CAAA,EAAE,MAAQ,EAAA,MAAA,EAAU,EAAA;AAC1B,MAAA,OAAOC,oCAAkB,MAAO,CAAA;AAAA,QAC9B,MAAA;AAAA,QACA;AAAA,OACD,CAAA;AAAA;AACH,GACD;AACL,CAAC;;;;"}
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ var opaClient = require('../api/opaClient.cjs.js');
4
+
5
+ class DefaultOpaService {
6
+ opaClient;
7
+ constructor(opaClient) {
8
+ this.opaClient = opaClient;
9
+ }
10
+ static create(options) {
11
+ const opaClient$1 = new opaClient.OpaClient(options.config, options.logger);
12
+ return new DefaultOpaService(opaClient$1);
13
+ }
14
+ async evaluatePolicy(input, entryPoint) {
15
+ return this.opaClient.evaluatePolicy(input, entryPoint);
16
+ }
17
+ }
18
+
19
+ exports.DefaultOpaService = DefaultOpaService;
20
+ //# sourceMappingURL=DefaultOpaService.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DefaultOpaService.cjs.js","sources":["../../service/DefaultOpaService.ts"],"sourcesContent":["import { LoggerService } from '@backstage/backend-plugin-api';\nimport { OpaClient } from '../src/api/opaClient';\nimport { Config } from '@backstage/config';\nimport {\n PolicyResult,\n PolicyInput,\n} from '@parsifal-m/backstage-plugin-opa-common';\n\n/**\n * OPA Service interface for evaluating policies.\n *\n * @public\n */\nexport interface OpaService {\n /**\n * Evaluate a policy at the given entry point.\n *\n * @param input - The policy input data\n * @param entryPoint - The OPA policy entry point d\n * @returns Promise resolving to the policy evaluation result\n */\n evaluatePolicy<T = PolicyResult>(\n input: PolicyInput,\n entryPoint: string,\n ): Promise<T>;\n}\n\nexport class DefaultOpaService implements OpaService {\n private readonly opaClient: OpaClient;\n\n constructor(opaClient: OpaClient) {\n this.opaClient = opaClient;\n }\n\n static create(options: {\n config: Config;\n logger: LoggerService;\n }): DefaultOpaService {\n const opaClient = new OpaClient(options.config, options.logger);\n return new DefaultOpaService(opaClient);\n }\n\n async evaluatePolicy<T = PolicyResult>(\n input: PolicyInput,\n entryPoint: string,\n ): Promise<T> {\n return this.opaClient.evaluatePolicy(input, entryPoint) as Promise<T>;\n }\n}\n"],"names":["opaClient","OpaClient"],"mappings":";;;;AA2BO,MAAM,iBAAwC,CAAA;AAAA,EAClC,SAAA;AAAA,EAEjB,YAAY,SAAsB,EAAA;AAChC,IAAA,IAAA,CAAK,SAAY,GAAA,SAAA;AAAA;AACnB,EAEA,OAAO,OAAO,OAGQ,EAAA;AACpB,IAAA,MAAMA,cAAY,IAAIC,mBAAA,CAAU,OAAQ,CAAA,MAAA,EAAQ,QAAQ,MAAM,CAAA;AAC9D,IAAO,OAAA,IAAI,kBAAkBD,WAAS,CAAA;AAAA;AACxC,EAEA,MAAM,cACJ,CAAA,KAAA,EACA,UACY,EAAA;AACZ,IAAA,OAAO,IAAK,CAAA,SAAA,CAAU,cAAe,CAAA,KAAA,EAAO,UAAU,CAAA;AAAA;AAE1D;;;;"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@parsifal-m/backstage-plugin-opa-node",
3
+ "version": "0.1.0",
4
+ "license": "Apache-2.0",
5
+ "description": "Provides a Node.js service for integrating Open Policy Agent (OPA) with Backstage backend modules and plugins.",
6
+ "main": "dist/index.cjs.js",
7
+ "types": "dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "main": "dist/index.cjs.js",
11
+ "types": "dist/index.d.ts"
12
+ },
13
+ "backstage": {
14
+ "role": "node-library",
15
+ "pluginId": "opa",
16
+ "pluginPackages": [
17
+ "@parsifal-m/backstage-plugin-opa-common",
18
+ "@parsifal-m/backstage-plugin-opa-node",
19
+ "@parsifal-m/plugin-opa-backend"
20
+ ]
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/Parsifal-M/backstage-opa-plugins.git"
25
+ },
26
+ "keywords": [
27
+ "backstage",
28
+ "OPA",
29
+ "Open Policy Agent",
30
+ "authorization",
31
+ "RBAC"
32
+ ],
33
+ "scripts": {
34
+ "build": "backstage-cli package build",
35
+ "lint": "backstage-cli package lint",
36
+ "test": "backstage-cli package test",
37
+ "clean": "backstage-cli package clean",
38
+ "prepack": "backstage-cli package prepack",
39
+ "postpack": "backstage-cli package postpack"
40
+ },
41
+ "devDependencies": {
42
+ "@backstage/backend-test-utils": "^1.9.1",
43
+ "@backstage/cli": "^0.34.4"
44
+ },
45
+ "files": [
46
+ "dist"
47
+ ],
48
+ "dependencies": {
49
+ "@backstage/backend-plugin-api": "^1.4.4",
50
+ "@backstage/config": "^1.3.5",
51
+ "@parsifal-m/backstage-plugin-opa-common": "workspace:^"
52
+ },
53
+ "typesVersions": {
54
+ "*": {
55
+ "package.json": [
56
+ "package.json"
57
+ ]
58
+ }
59
+ }
60
+ }