From f7f9c6d8662ada32f03e78f181b0ffbf1f12539b Mon Sep 17 00:00:00 2001 From: Peter Macdonald Date: Thu, 31 Oct 2024 12:30:20 +0100 Subject: [PATCH] =?UTF-8?q?[=F0=9F=9A=A8]=20Feature=20using=20OPA=20for=20?= =?UTF-8?q?Permissions=20(without=20the=20permissions=20framework)=20(#230?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhancement to OPA suite for Backstage allowing more flexible REBAC/RBAC for custom plugins! --------- Signed-off-by: Peter Macdonald --- .github/labeler.yml | 10 + README.md | 7 + docker-compose.yaml | 3 +- docs/ADOPTERS.md | 2 + docs/CONTRIBUTING.md | 4 +- docs/README.md | 38 - docs/_sidebar.md | 41 +- docs/deploying-opa/deploying-opa.md | 90 + docs/home/home.md | 57 + docs/index.html | 18 +- docs/opa-authz-react/introduction.md | 118 + docs/opa-authz/introduction.md | 136 ++ docs/opa-backend/introduction.md | 35 +- .../introduction.md | 3 +- .../local-development.md | 2 +- .../quick-start.md | 12 +- package.json | 16 +- packages/app/package.json | 6 +- packages/app/src/App.tsx | 4 + packages/app/src/apis.ts | 12 + packages/app/src/components/Root/Root.tsx | 6 + packages/backend/package.json | 1 + packages/backend/src/index.ts | 1 + packages/opa-authz/.eslintrc.js | 1 + packages/opa-authz/README.md | 127 + packages/opa-authz/config.d.ts | 12 + packages/opa-authz/package.json | 41 + packages/opa-authz/src/api/index.ts | 1 + packages/opa-authz/src/api/opaClient.test.ts | 72 + packages/opa-authz/src/api/opaClient.ts | 75 + packages/opa-authz/src/index.ts | 2 + .../src/middleware/opaMiddleware.test.ts | 148 ++ .../opa-authz/src/middleware/opaMiddleware.ts | 74 + packages/opa-authz/src/setupTests.ts | 1 + packages/opa-authz/src/types.ts | 42 + plugins/backstage-opa-backend/README.md | 22 +- plugins/backstage-opa-backend/config.d.ts | 3 +- plugins/backstage-opa-backend/package.json | 4 +- plugins/backstage-opa-backend/src/lib/read.ts | 9 +- plugins/backstage-opa-backend/src/plugin.ts | 7 +- .../src/service/router.test.ts | 239 +- .../src/service/router.ts | 93 +- .../src/service/routers/authz.test.ts | 106 + .../src/service/routers/authz.ts | 74 + .../src/service/routers/entityChecker.test.ts | 123 + .../src/service/routers/entityChecker.ts | 56 + .../src/service/routers/policyContent.test.ts | 39 + .../src/service/routers/policyContent.ts | 33 + .../backstage-opa-entity-checker/README.md | 38 +- .../OpaMetadataAnalysisCard.test.tsx | 4 +- plugins/backstage-opa-policies/README.md | 12 + plugins/backstage-opa-policies/package.json | 2 +- .../backstage-opa-policies/src/api/types.ts | 2 +- .../OpaPolicyComponent.test.tsx | 8 +- .../OpaPolicyComponent/OpaPolicyComponent.tsx | 4 +- plugins/opa-authz-react/.eslintrc.js | 1 + plugins/opa-authz-react/README.md | 116 + plugins/opa-authz-react/package.json | 63 + plugins/opa-authz-react/src/api/api.test.ts | 90 + plugins/opa-authz-react/src/api/api.ts | 36 + plugins/opa-authz-react/src/api/index.ts | 2 + plugins/opa-authz-react/src/api/types.ts | 19 + .../RequireOpaAuthz.test.tsx | 92 + .../OpaAuthzComponent/RequireOpaAuthz.tsx | 24 + .../src/components/OpaAuthzComponent/index.ts | 1 + .../src/hooks/useOpaAuthz/index.ts | 1 + .../src/hooks/useOpaAuthz/useOpaAuthz.test.ts | 31 + .../src/hooks/useOpaAuthz/useOpaAuthz.ts | 34 + plugins/opa-authz-react/src/index.ts | 3 + plugins/opa-authz-react/src/setupTests.ts | 1 + plugins/opa-demo-backend/.eslintrc.js | 1 + plugins/opa-demo-backend/README.md | 9 + plugins/opa-demo-backend/dev/index.ts | 60 + plugins/opa-demo-backend/package.json | 47 + plugins/opa-demo-backend/src/index.ts | 1 + plugins/opa-demo-backend/src/plugin.ts | 44 + plugins/opa-demo-backend/src/router.ts | 83 + .../TodoListService/createTodoListService.ts | 94 + .../src/services/TodoListService/index.ts | 1 + .../src/services/TodoListService/types.ts | 19 + plugins/opa-demo-backend/src/setupTests.ts | 1 + plugins/opa-frontend-demo/.eslintrc.js | 1 + plugins/opa-frontend-demo/README.md | 9 + plugins/opa-frontend-demo/dev/index.tsx | 12 + plugins/opa-frontend-demo/package.json | 53 + .../ExampleComponent.test.tsx | 26 + .../ExampleComponent/ExampleComponent.tsx | 77 + .../src/components/ExampleComponent/index.ts | 1 + .../ExampleFetchComponent.test.tsx | 19 + .../ExampleFetchComponent.tsx | 308 +++ .../components/ExampleFetchComponent/index.ts | 1 + plugins/opa-frontend-demo/src/index.ts | 1 + plugins/opa-frontend-demo/src/plugin.test.ts | 7 + plugins/opa-frontend-demo/src/plugin.ts | 22 + plugins/opa-frontend-demo/src/routes.ts | 5 + plugins/opa-frontend-demo/src/setupTests.ts | 1 + .../config.d.ts | 32 + .../package.json | 3 +- policies/opa_demo.rego | 42 + policies/rbac_policy.rego | 2 +- yarn.lock | 2048 ++++++++++------- 101 files changed, 4385 insertions(+), 1255 deletions(-) delete mode 100644 docs/README.md create mode 100644 docs/deploying-opa/deploying-opa.md create mode 100644 docs/home/home.md create mode 100644 docs/opa-authz-react/introduction.md create mode 100644 docs/opa-authz/introduction.md create mode 100644 packages/opa-authz/.eslintrc.js create mode 100644 packages/opa-authz/README.md create mode 100644 packages/opa-authz/config.d.ts create mode 100644 packages/opa-authz/package.json create mode 100644 packages/opa-authz/src/api/index.ts create mode 100644 packages/opa-authz/src/api/opaClient.test.ts create mode 100644 packages/opa-authz/src/api/opaClient.ts create mode 100644 packages/opa-authz/src/index.ts create mode 100644 packages/opa-authz/src/middleware/opaMiddleware.test.ts create mode 100644 packages/opa-authz/src/middleware/opaMiddleware.ts create mode 100644 packages/opa-authz/src/setupTests.ts create mode 100644 packages/opa-authz/src/types.ts create mode 100644 plugins/backstage-opa-backend/src/service/routers/authz.test.ts create mode 100644 plugins/backstage-opa-backend/src/service/routers/authz.ts create mode 100644 plugins/backstage-opa-backend/src/service/routers/entityChecker.test.ts create mode 100644 plugins/backstage-opa-backend/src/service/routers/entityChecker.ts create mode 100644 plugins/backstage-opa-backend/src/service/routers/policyContent.test.ts create mode 100644 plugins/backstage-opa-backend/src/service/routers/policyContent.ts create mode 100644 plugins/opa-authz-react/.eslintrc.js create mode 100644 plugins/opa-authz-react/README.md create mode 100644 plugins/opa-authz-react/package.json create mode 100644 plugins/opa-authz-react/src/api/api.test.ts create mode 100644 plugins/opa-authz-react/src/api/api.ts create mode 100644 plugins/opa-authz-react/src/api/index.ts create mode 100644 plugins/opa-authz-react/src/api/types.ts create mode 100644 plugins/opa-authz-react/src/components/OpaAuthzComponent/RequireOpaAuthz.test.tsx create mode 100644 plugins/opa-authz-react/src/components/OpaAuthzComponent/RequireOpaAuthz.tsx create mode 100644 plugins/opa-authz-react/src/components/OpaAuthzComponent/index.ts create mode 100644 plugins/opa-authz-react/src/hooks/useOpaAuthz/index.ts create mode 100644 plugins/opa-authz-react/src/hooks/useOpaAuthz/useOpaAuthz.test.ts create mode 100644 plugins/opa-authz-react/src/hooks/useOpaAuthz/useOpaAuthz.ts create mode 100644 plugins/opa-authz-react/src/index.ts create mode 100644 plugins/opa-authz-react/src/setupTests.ts create mode 100644 plugins/opa-demo-backend/.eslintrc.js create mode 100644 plugins/opa-demo-backend/README.md create mode 100644 plugins/opa-demo-backend/dev/index.ts create mode 100644 plugins/opa-demo-backend/package.json create mode 100644 plugins/opa-demo-backend/src/index.ts create mode 100644 plugins/opa-demo-backend/src/plugin.ts create mode 100644 plugins/opa-demo-backend/src/router.ts create mode 100644 plugins/opa-demo-backend/src/services/TodoListService/createTodoListService.ts create mode 100644 plugins/opa-demo-backend/src/services/TodoListService/index.ts create mode 100644 plugins/opa-demo-backend/src/services/TodoListService/types.ts create mode 100644 plugins/opa-demo-backend/src/setupTests.ts create mode 100644 plugins/opa-frontend-demo/.eslintrc.js create mode 100644 plugins/opa-frontend-demo/README.md create mode 100644 plugins/opa-frontend-demo/dev/index.tsx create mode 100644 plugins/opa-frontend-demo/package.json create mode 100644 plugins/opa-frontend-demo/src/components/ExampleComponent/ExampleComponent.test.tsx create mode 100644 plugins/opa-frontend-demo/src/components/ExampleComponent/ExampleComponent.tsx create mode 100644 plugins/opa-frontend-demo/src/components/ExampleComponent/index.ts create mode 100644 plugins/opa-frontend-demo/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx create mode 100644 plugins/opa-frontend-demo/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx create mode 100644 plugins/opa-frontend-demo/src/components/ExampleFetchComponent/index.ts create mode 100644 plugins/opa-frontend-demo/src/index.ts create mode 100644 plugins/opa-frontend-demo/src/plugin.test.ts create mode 100644 plugins/opa-frontend-demo/src/plugin.ts create mode 100644 plugins/opa-frontend-demo/src/routes.ts create mode 100644 plugins/opa-frontend-demo/src/setupTests.ts create mode 100644 plugins/permission-backend-module-opa-wrapper/config.d.ts create mode 100644 policies/opa_demo.rego diff --git a/.github/labeler.yml b/.github/labeler.yml index b9572d10..1afbcf86 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -15,6 +15,16 @@ opa-permissions-wrapper: - any-glob-to-any-file: ['plugins/permissions-backend-module-opa-wrapper/**/*'] +opa-authz: + - changed-files: + - any-glob-to-any-file: + ['packages/opa-authz/**/*'] + +opa-authz-react: + - changed-files: + - any-glob-to-any-file: + ['plugins/opa-authz-react/**/*'] + cicd: - changed-files: - any-glob-to-any-file: ['.github/workflows/**/*'] diff --git a/README.md b/README.md index 884aa1ed..87dd9372 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Welcome to the OPA Plugins Repository for Backstage +[![codecov](https://codecov.io/gh/Parsifal-M/backstage-opa-plugins/graph/badge.svg?token=IHZGVSXZY7)](https://codecov.io/gh/Parsifal-M/backstage-opa-plugins) + This repository contains a collection of plugins for [Backstage](https://backstage.io) that integrate with [Open Policy Agent](https://www.openpolicyagent.org/). ## Blogs @@ -17,6 +19,11 @@ This repository contains a collection of plugins for [Backstage](https://backsta - [backstage-opa-entity-checker](./plugins/backstage-opa-entity-checker/README.md) - A frontend plugin that provides a component card that displays if an entity has the expected entity metadata according to an opa policy. - [backstage-opa-policies](./plugins/backstage-opa-policies/README.md) - A frontend component designed to be added to entity pages to fetch and display the OPA policy that entity uses based on a URL provided in an annotation in the `catalog-info.yaml` file. +## Beta Plugins + +- [backstage-opa-authz](./plugins/opa-authz-react/README.md) - A frontend plugin that allows you to control the visibility of components based on the result of an OPA policy evaluation. +- [backstage-opa-authz-backend](./packages/opa-authz/README.md) - A Backstage backend plugin that allows you to use OPA for authorization in the Backstage backend allowing you to protect api routes. + ## Policies - [backstage-opa-policies](https://github.com/Parsifal-M/backstage-opa-policies#hello) - A collection of policies that can be used with the plugins in this repository. (WIP) diff --git a/docker-compose.yaml b/docker-compose.yaml index 3e28d005..a8eaf036 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,8 +14,7 @@ services: - '--watch' - '--log-format=json-pretty' - '--set=decision_logs.console=true' - - '/policies/rbac_policy.rego' - - '/policies/entity_checker.rego' + - '/policies/' ports: - 8181:8181 volumes: diff --git a/docs/ADOPTERS.md b/docs/ADOPTERS.md index 565281a1..60bd27c5 100644 --- a/docs/ADOPTERS.md +++ b/docs/ADOPTERS.md @@ -10,6 +10,8 @@ It's awesome to see that this project is being used by other companies and proje ## Open Source Projects +None yet! Be the first to add your project here! + ## How to Add Your Organization If you're using our project and want to be featured on this list, please follow these steps: diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 07ac51b8..2c3bd2cb 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,5 +1,7 @@ # Contributing -I am happy to accept contributions and suggestions for this plugin. Please fork the repository and open a PR with your changes. If you have any questions, please feel free to reach out to me on [Mastodon](https://hachyderm.io/@parcifal). +I am happy to accept contributions and suggestions for these plugins, if you are looking to make significant changes, please open an issue first to discuss the changes you would like to make! + +Please fork the repository and open a PR with your changes. If you have any questions, please feel free to reach out to me on [Mastodon](https://hachyderm.io/@parcifal). Please remember to sign your commits with `git commit -s` so that your commits are signed! diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index e38f5379..00000000 --- a/docs/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Welcome to the OPA Plugins Repository for Backstage! - -This repository contains a collection of plugins for [Backstage](https://backstage.io) that integrate with [Open Policy Agent](https://www.openpolicyagent.org/). - -## Plugins - -- [backstage-opa-backend](../plugins/backstage-opa-backend/README.md) - A Backend Plugin that the [backstage-opa-entity-checker](./plugins/backstage-opa-entity-checker/README.md) consumes to evaluate policies. -- [plugin-permission-backend-module-opa-wrapper](/opa-permissions-wrapper-module/introduction.md) - An isolated OPA Client and a Policy Evaluator that integrates with the Backstage permissions framework and uses OPA to evaluate policies, making it possible to use OPA for permissions (like RBAC). -- [backstage-opa-entity-checker](../plugins/backstage-opa-entity-checker/README.md) - A frontend plugin that provides a component card that displays if an entity has the expected entity metadata according to an opa policy. -- [backstage-opa-policies](../plugins/backstage-opa-policies/README.md) - A frontend component designed to be added to entity pages to fetch and display the OPA policy that entity uses based on a URL provided in an annotation in the `catalog-info.yaml` file. - -## Policies - -- [backstage-opa-policies](https://github.com/Parsifal-M/backstage-opa-policies#hello) - A collection of policies that can be used with the plugins in this repository. (WIP) - -## Additional Documentation - -Each plugin also has its own documentation in the README file in the plugin folder. - -## Local Development - -Step by step guide to developing locally: - -1. Clone this repository -2. Create an `app-config.local.yaml` file in the root of the repository copying the contents from `app-config.yaml` -3. Create a PAT (Personal Access Token) for your GitHub account with these scopes: `read:org`, `read:user`, `user:email`. This token should be placed under `integrations.github.token` in the `app-config.local.yaml` file. -4. Run `yarn install --immutable` in the root of the repository -5. Use `docker-compose up -d` to start the OPA server and postgres database (this will also load the two policies in the `example-opa-policies` folder automatically) -6. Update the OPA rbac policy in here [rbac_policy.rego](./example-opa-policies/rbac_policy.rego), or use your own! If you want to use the default policy, you'll have to update `is_admin if "group:twocodersbrewing/maintainers" in claims` to what ever your user entity claims are. -7. Run `yarn dev` or `yarn debug` in the root of the repository to start the Backstage app (use debug if you want to see what is happening in the OPA plugin) - -# Contributing - -Contributions are welcome! However, still figuring out the best approach as this does require user and group entities to be in the system. - -Please open an issue or a pull request. You can also contact me on mastodon at [@parcifal](https://hachyderm.io/@parcifal). - -Please remember to sign your commits with `git commit -s` so that your commits are signed! diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 9887a0b6..872abc13 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,19 +1,22 @@ -- [Home](/) -- [OPA Permissions Wrapper Module](opa-permissions-wrapper-module/introduction.md) - - [Quick Start](opa-permissions-wrapper-module/quick-start.md) - - [Example Permissions (RBAC) Policy](opa-permissions-wrapper-module/example-rbac-policy.md) - - [Catalog Rules](opa-permissions-wrapper-module/catalog-rules.md) - - [Scaffolder Rules](opa-permissions-wrapper-module/scaffolder-rules.md) - - [Local Development](opa-permissions-wrapper-module/local-development.md) - - [Example Inputs and Outputs](opa-permissions-wrapper-module/inputs-and-outputs.md) - - [Architecture](opa-permissions-wrapper-module/architecture.md) -- [OPA Backend](opa-backend/introduction.md) - - [Quick Start](opa-backend/quick-start.md) -- [OPA Entity Checker](opa-entity-checker/introduction.md) - - [Quick Start](opa-entity-checker/quick-start.md) - - [Local Development](opa-entity-checker/local-development.md) - - [Example Entity Checker Policy](opa-entity-checker/example-entity-checker-policy.md) -- [OPA Policies](opa-policies/introduction.md) - - [Quick Start](opa-policies/quick-start.md) -- [Contributing](CONTRIBUTING.md) -- [Adopters](ADOPTERS.md) +- [Home](home/home.md#welcome-to-the-opa-plugins-repository-for-backstage) +- [Deploying OPA](deploying-opa/deploying-opa.md#how-to-deploy-opa) +- [OPA Permissions Wrapper Module](opa-permissions-wrapper-module/introduction.md#simplify-permissions-with-opa-in-backstage) + - [Quick Start](opa-permissions-wrapper-module/quick-start.md#quick-start) + - [Example Permissions (RBAC) Policy](opa-permissions-wrapper-module/example-rbac-policy.md#example-permissions-rbac-policy) + - [Catalog Rules](opa-permissions-wrapper-module/catalog-rules.md#catalog-rules) + - [Scaffolder Rules](opa-permissions-wrapper-module/scaffolder-rules.md#scaffolder-rules) + - [Local Development](opa-permissions-wrapper-module/local-development.md#using-the-plugin-in-local-development) + - [Example Inputs and Outputs](opa-permissions-wrapper-module/inputs-and-outputs.md#example-inputs-and-outputs-for-policy-evaluation) + - [Architecture](opa-permissions-wrapper-module/architecture.md#open-policy-agent-opa-plugins-architecture) +- [OPA Authz](opa-authz/introduction.md#opa-authz-client) +- [OPA Backend](opa-backend/introduction.md#backstage-opa-backend-plugin) + - [Quick Start](opa-backend/quick-start.md#quick-start) +- [OPA Entity Checker](opa-entity-checker/introduction.md#keep-your-entity-data-in-check-with-opa-entity-checker) + - [Quick Start](opa-entity-checker/quick-start.md#quick-start) + - [Local Development](opa-entity-checker/local-development.md#using-the-plugin-in-local-development) + - [Example Entity Checker Policy](opa-entity-checker/example-entity-checker-policy.md#example-entity-checker-policy) +- [OPA Authz React](opa-authz-react/introduction.md#opa-authz-react) +- [OPA Policies](opa-policies/introduction.md#opa-policies-plugin-overview) + - [Quick Start](opa-policies/quick-start.md#quick-start) +- [Contributing](CONTRIBUTING.md#contributing) +- [Adopters](ADOPTERS.md#project-adopters) diff --git a/docs/deploying-opa/deploying-opa.md b/docs/deploying-opa/deploying-opa.md new file mode 100644 index 00000000..f14af592 --- /dev/null +++ b/docs/deploying-opa/deploying-opa.md @@ -0,0 +1,90 @@ +# How To Deploy OPA! + +The official documentation provided by the Open Policy Agent (OPA) community is a great resource to get started with OPA. You can find the documentation [here](https://www.openpolicyagent.org/docs/latest/deployments/). + +However, if you're looking for just a quick way to get started with OPA, here's a simple guide to help you deploy OPA as a sidecar to your Backstage instance. + +## Deploying OPA + +There are many ways to deploy OPA, and there is no one size fits all. A good way is to deploy OPA as a sidecar to your Backstage instance. This way, you can ensure that OPA is always available when your Backstage instance is running. + +Here is an example of how you could update your Backstage `k8s` deployment to include OPA, this would be an extension of the `k8s` deployment that you are using for your Backstage instance. + +```yaml +#... Backstage deployment configuration with OPA +spec: + containers: + - name: backstage + image: your-backstage-image + ports: + - name: http + containerPort: 7007 + - name: opa + image: openpolicyagent/opa:0.65.0 # Pin a version of your choice + ports: + - name: http + containerPort: 8181 + args: + - 'run' + - '--server' + - '--log-format=json-pretty' + - '--set=decision_logs.console=true' + - '--ignore=.*' + - '--watch' # Watch for policy changes, this allows updating the policy without restarting OPA + - '/policies' + volumeMounts: + - readOnly: true + name: opa-rbac-policy + mountPath: /policies + volumes: + - name: opa-rbac-policy + configMap: + name: opa-rbac-policy +``` + +> [!ATTENTION|style:flat] +> The below is a policy designed to work with the [OPA Permissions Wrapper Module](../opa-permissions-wrapper-module/introduction.md#simplify-permissions-with-opa-in-backstage). If you are using [opa-authz](../opa-authz/introduction.md#opa-authz-client) or [opa-authz-react](../opa-authz-react/introduction.md#opa-authz-react), you will need to adjust the policy accordingly! + +For simplicity you can then create a policy in a `ConfigMap` and mount it into the OPA container. + +> [!NOTE|style:flat] +> Note: Update "kind:namespace/name" in the policy to match your user entity claims. + +```yaml +# opa-rbac-policy.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: opa-rbac-policy +data: + rbac_policy.rego: | + package rbac_policy + + import rego.v1 + + # Helper method for constructing a conditional decision + conditional(plugin_id, resource_type, conditions) := { + "result": "CONDITIONAL", + "pluginId": plugin_id, + "resourceType": resource_type, + "conditions": conditions, + } + + permission := input.permission.name + + claims := input.identity.claims + + # An Example Admin Group + is_admin if "kind:namespace/name" in claims + + # Catalog Permission: Allow users to only delete entities they claim ownership of. + # Allow admins to delete any entity regardless of ownership. + decision := conditional("catalog", "catalog-entity", {"anyOf": [{ + "resourceType": "catalog-entity", + "rule": "IS_ENTITY_OWNER", + "params": {"claims": claims}, + }]}) if { + permission == "catalog.entity.delete" + not is_admin + } +``` diff --git a/docs/home/home.md b/docs/home/home.md new file mode 100644 index 00000000..87dd9372 --- /dev/null +++ b/docs/home/home.md @@ -0,0 +1,57 @@ +# Welcome to the OPA Plugins Repository for Backstage + +[![codecov](https://codecov.io/gh/Parsifal-M/backstage-opa-plugins/graph/badge.svg?token=IHZGVSXZY7)](https://codecov.io/gh/Parsifal-M/backstage-opa-plugins) + +This repository contains a collection of plugins for [Backstage](https://backstage.io) that integrate with [Open Policy Agent](https://www.openpolicyagent.org/). + +## Blogs + +- [Going Backstage with OPA](https://www.styra.com/blog/going-backstage-with-opa/) + +## Talks + +- [Can It Be Done? Building Fine-Grained Access Control for Backstage with OPA](https://www.youtube.com/watch?v=N0n_czYo_kE&list=PLj6h78yzYM2P4KPyeDFexAVm6ZvfAWMU8&index=15&ab_channel=CNCF%5BCloudNativeComputingFoundation%5D) + +## Plugins + +- [backstage-opa-backend](./plugins/backstage-opa-backend/README.md) - A Backend Plugin that the [backstage-opa-entity-checker](./plugins/backstage-opa-entity-checker/README.md) consumes to evaluate policies. +- [plugin-permission-backend-module-opa-wrapper](./plugins/permission-backend-module-opa-wrapper/README.md) - An isolated OPA Client and a Policy Evaluator that integrates with the Backstage permissions framework and uses OPA to evaluate policies, making it possible to use OPA for permissions (like RBAC). Does not require the `backstage-opa-backend` plugin! +- [backstage-opa-entity-checker](./plugins/backstage-opa-entity-checker/README.md) - A frontend plugin that provides a component card that displays if an entity has the expected entity metadata according to an opa policy. +- [backstage-opa-policies](./plugins/backstage-opa-policies/README.md) - A frontend component designed to be added to entity pages to fetch and display the OPA policy that entity uses based on a URL provided in an annotation in the `catalog-info.yaml` file. + +## Beta Plugins + +- [backstage-opa-authz](./plugins/opa-authz-react/README.md) - A frontend plugin that allows you to control the visibility of components based on the result of an OPA policy evaluation. +- [backstage-opa-authz-backend](./packages/opa-authz/README.md) - A Backstage backend plugin that allows you to use OPA for authorization in the Backstage backend allowing you to protect api routes. + +## Policies + +- [backstage-opa-policies](https://github.com/Parsifal-M/backstage-opa-policies#hello) - A collection of policies that can be used with the plugins in this repository. (WIP) + +## Additional Documentation + +Each Plugin has its own documentation in the [Plugins](./plugins/) Folder, I am however, slowly moving things to [Github pages](https://parsifal-m.github.io/backstage-opa-plugins/#/). Feel free to help out! + +## Local Development + +Step by step guide to developing locally: + +1. Clone this repository +2. Create an `app-config.local.yaml` file in the root of the repository copying the contents from `app-config.yaml` +3. Create a PAT (Personal Access Token) for your GitHub account with these scopes: `read:org`, `read:user`, `user:email`. This token should be placed under `integrations.github.token` in the `app-config.local.yaml` file. +4. Run `yarn install --immutable` in the root of the repository +5. Use `docker-compose up -d` to start the OPA server and postgres database (this will also load the two policies in the `example-opa-policies` folder automatically) +6. Update the OPA rbac policy in here [rbac_policy.rego](./example-opa-policies/rbac_policy.rego), or use your own! If you want to use the default policy, you'll have to update `is_admin if "group:twocodersbrewing/maintainers" in claims` to what ever your user entity claims are. +7. Run `yarn dev` or `yarn debug` in the root of the repository to start the Backstage app (use debug if you want to see what is happening in the OPA plugin) + +## Ecosystem + +- [PlaTT Policy Template](https://github.com/ap-communications/platt-policy-template) contains policy templates that will work with the [plugin-permission-backend-module-opa-wrapper](./plugins/permission-backend-module-opa-wrapper/README.md) plugin! + +## Contributing + +Contributions are welcome! However, still figuring out the best approach as this does require user and group entities to be in the system. + +Please open an issue or a pull request. You can also contact me on mastodon at [@parcifal](https://hachyderm.io/@parcifal). + +Please remember to sign your commits with `git commit -s` so that your commits are signed! diff --git a/docs/index.html b/docs/index.html index 41821302..946bb1a3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -17,13 +17,7 @@ - @@ -56,7 +50,6 @@ - @@ -70,5 +63,14 @@ window.mermaid = mermaid; + + + + + + + + + diff --git a/docs/opa-authz-react/introduction.md b/docs/opa-authz-react/introduction.md new file mode 100644 index 00000000..18405b1b --- /dev/null +++ b/docs/opa-authz-react/introduction.md @@ -0,0 +1,118 @@ +# OPA Authz React + +This is a React component library for Backstage that provides a way to interact with an OPA (Open Policy Agent) server for Authorization in the frontend. + +You can wrap your components with the `RequireOpaAuthz` component to control the visibility of components based on the result of a policy evaluation. + +The component uses the `useOpaAuthz` hook to perform the policy evaluation, and it will render the children only if the policy evaluation `allow` is `true`. + +## Why use this library? + +Although the Backstage Permissions framework works well for most cases, sometimes you need to add a little more information to your policy input which is not available or possible in the framework. This library aims to provide a more generic way to interact with OPA, and can be used in any part of the Backstage application, and is not tied to the permissions framework in any way, meaning: + +- Flexibility to pass your own policy input to OPA. +- Decouple the Authorization logic from the application meaning no rebuilding the application to change the authorization logic. +- More control over the Authorization logic for your own plugins. + +Sadly, not all core and community plugins will work with this library for permissions, so you can still use the [plugin-permission-backend-module-opa-wrapper](https://parsifal-m.github.io/backstage-opa-plugins/#/opa-permissions-wrapper-module/introduction) in conjunction with this library if needed which supports the permissions framework. + +## Quick Start + +### Install the library + +Run the yarn install command! + +```bash +yarn add --cwd packages/app @parsifal-m/backstage-plugin-opa-authz-react +``` + +### Add the API + +In your `app/src/apis.ts` file, add the following: + +```ts +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: scmIntegrationsApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), + }), + // Add the OPA Authz API + createApiFactory({ + api: opaAuthzBackendApiRef, + deps: { + fetchApi: fetchApiRef, + }, + factory: ({ fetchApi }) => new OpaAuthzClientReact({ fetchApi }), + }), + ScmAuth.createDefaultApiFactory(), +]; +``` + +### Using The `RequireOpaAuthz` Component (Recommended) + +To control and hide a component based on the result of a policy evaluation, you can use the `RequireOpaAuthz` component. + +Install the library first to your Backstage plugin: + +```bash +yarn add @parsifal-m/backstage-plugin-opa-authz-react +``` + +```tsx +import { RequireOpaAuthz } from '@parsifal-m/backstage-plugin-opa-authz-react'; + +// Some code... + +return ( + + + +); +``` + +The above will render `MyComponent` only if the policy evaluation `allow` is `true`. It will send to OPA the input `{ action: 'read-policy' }` and the entry point `authz`. + +### Using The `useOpaAuthz` Hook Directly (Optional) + +If you want to use the `useOpaAuthz` hook directly, you can do so: + +```tsx +import React from 'react'; +import { useOpaAuthz } from '@parsifal-m/backstage-plugin-opa-authz-react'; + +const MyComponent = () => { + const { loading, data, error } = useOpaAuthz( + { action: 'read-policy' }, + 'authz', + ); + + if (loading) { + return
Loading...
; + } + + if (error || !data?.result.allow) { + return
Access Denied
; + } + + return
Content
; +}; +``` + +## Join The Community + +This project is a part of the broader Backstage and Open Policy Agent ecosystems. Explore more about these communities: + +- [Backstage Community](https://backstage.io) +- [Open Policy Agent Community](https://www.openpolicyagent.org) +- [Styra](https://www.styra.com) +- [Join OPA on Slack](https://slack.openpolicyagent.org/) +- [Backstage Discord](https://discord.com/invite/MUpMjP2) + +## Get Involved + +Your contributions can make this plugin even better. Fork the repository, make your changes, and submit a PR! If you have questions or ideas, reach out on [Mastodon](https://hachyderm.io/@parcifal). + +## License + +This project is licensed under the Apache 2.0 License. diff --git a/docs/opa-authz/introduction.md b/docs/opa-authz/introduction.md new file mode 100644 index 00000000..8e5fbdc1 --- /dev/null +++ b/docs/opa-authz/introduction.md @@ -0,0 +1,136 @@ +# OPA Authz Client + +> [!TIP|style:flat] +> This plugin can be used together with [plugin-permission-backend-module-opa-wrapper](https://parsifal-m.github.io/backstage-opa-plugins/#/opa-permissions-wrapper-module/introduction) which may be necessary for some **core** and **community plugins**. + +This is a node-library package for Backstage that provides a client and middleware for interacting with an OPA (Open Policy Agent) server for Authorization. + +## Why use this library? + +The [plugin-permission-backend-module-opa-wrapper](https://parsifal-m.github.io/backstage-opa-plugins/#/opa-permissions-wrapper-module/introduction) is a great way to integrate OPA with Backstage, but it has some limitations, because it is tightly coupled with the Backstage permissions framework sometimes +you need to add a little more information to your policy input which is not available or possible in the wrapper. + +This library is a more generic way to interact with OPA, and can be used in any part of the Backstage application, and is not tied to the permissions framework in any way, meaning: + +- You can still protect your API endpoints with OPA, but you can also use OPA for other things, like controlling the visibility of components in the frontend. +- You are not limited in terms of what you can send as input to OPA, want to restrict access to something in Backstage based on the time of day? You can do that with this library. +- You can still use [plugin-permission-backend-module-opa-wrapper](https://parsifal-m.github.io/backstage-opa-plugins/#/opa-permissions-wrapper-module/introduction) in conjunction with this library, + not all core and community plugins will natively work with this client, so you can use the wrapper to handle those cases. +- Has a middleware that can be used in the backend to protect your API endpoints, simply add it to your express routes and you are good to go. + +## How It Works + +### Using the OpaAuthzClient + +The `OpaAuthzClient` allows you to interact with an Open Policy Agent (OPA) server to evaluate policies against given inputs. + +You are pretty much free to use this in any way you like in your Backstage Backend. + +```typescript +import express from 'express'; +import { Config } from '@backstage/config'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { OpaAuthzClient } from '@parsifal-m/backstage-opa-authz'; + +export type someRouteOptions = { + logger: LoggerService; + config: Config; + // more options... +}; +// Some code here +// ... +// Instantiate the OpaAuthzClient +const opaClient = new OpaAuthzClient(config, logger); + +// Define the policy input and entry point +const policyInput = { user: 'alice', action: 'read', resource: 'document' }; +const entryPoint = 'example/allow'; + +// Evaluate the policy +opaClient + .evaluatePolicy(policyInput, entryPoint) + .then(result => { + logger.info('Policy evaluation result:', result); + }) + .catch(error => { + logger.info('Error evaluating policy:', error); + }); + +// Some more code here +// ... +``` + +### Using the opaAuthzMiddleware + +You'll probably want to use the `opaAuthzMiddleware` in your express routes to protect your API endpoints instead of using the `OpaAuthzClient` directly. + +```typescript +import express from 'express'; +import { + OpaAuthzClient, + opaAuthzMiddleware, +} from '@parsifal-m/backstage-opa-authz'; +import { Config } from '@backstage/config'; +import { LoggerService } from '@backstage/backend-plugin-api'; + +export type someRoutesOptions = { + logger: LoggerService; + config: Config; + // more options... +}; + +export const someRoutes = (options: someRoutesOptions): express.Router => { + const { logger, config } = options; + const router = express.Router(); + + // Instantiate the OpaAuthzClient + const opaAuthzClient = new OpaAuthzClient(logger, config); + + // Define the entry point + const entryPoint = 'authz'; + + // Define the input + const setInput = (req: express.Request) => { + return { + method: req.method, + path: req.path, + permission: { name: 'read' }, + someFoo: 'bar', + dateTime: new Date().toISOString(), + }; + }; + + // Define the route + router.get( + '/some-route', + opaAuthzMiddleware(opaAuthzClient, entryPoint, setInput, logger), + (req, res) => { + res.send('Hello, World!'); + }, + ); + + return router; +}; +``` + +## Example Demo Plugin(s) + +To help visualize how this library can be used, we have created a demo plugin that demonstrates how to use the `opaAuthzMiddleware` in the backend, you can find the demo code [here](../../plugins/opa-demo-backend/README.md). + +## Join The Community + +This project is a part of the broader Backstage and Open Policy Agent ecosystems. Explore more about these communities: + +- [Backstage Community](https://backstage.io) +- [Open Policy Agent Community](https://www.openpolicyagent.org) +- [Styra](https://www.styra.com) +- [Join OPA on Slack](https://slack.openpolicyagent.org/) +- [Backstage Discord](https://discord.com/invite/MUpMjP2) + +## Get Involved + +Your contributions can make this plugin even better. Fork the repository, make your changes, and submit a PR! If you have questions or ideas, reach out on [Mastodon](https://hachyderm.io/@parcifal). + +## License + +This project is licensed under the Apache 2.0 License. diff --git a/docs/opa-backend/introduction.md b/docs/opa-backend/introduction.md index 2322e1e7..62df1c2f 100644 --- a/docs/opa-backend/introduction.md +++ b/docs/opa-backend/introduction.md @@ -1,19 +1,38 @@ -![NPM Version](https://img.shields.io/npm/v/%40parsifal-m%2Fplugin-opa-backend?logo=npm) ![NPM Downloads](https://img.shields.io/npm/dw/%40parsifal-m%2Fplugin-opa-backend) +![NPM Version](https://img.shields.io/npm/v/%40parsifal-m%2Fplugin-opa-backend?logo=npm) -# backstage-opa-backend +# Backstage OPA Backend Plugin -A backend plugin for Backstage, this plugin integrates with the Open Policy Agent (OPA) to facilitate policy evaluation. It's designed to work with the frontend plugins [OPA Entity Checker](../opa-entity-checker/introduction.md) and [OPA Policies](../opa-policies/introduction.md). By itself, this plugin does not provide any user-facing features. +A backend plugin for Backstage, this plugin integrates with the Open Policy Agent (OPA) to facilitate policy evaluation. -> Note: This plugin is **NOT** required for the [OPA Permissions Wrapper Module](../opa-permissions-wrapper-module/introduction.md). +It's a dependency of the following plugins: + +- [OPA Entity Checker](../opa-entity-checker/introduction.md) +- [OPA Policies](../opa-policies/introduction.md) +- [OPA Authz React](../opa-authz-react/introduction.md) + +By itself, this plugin does not provide any user-facing features. + +> [!ATTENTION|style:flat] +> This plugin is **NOT** required for the [OPA Permissions Wrapper Module](../opa-permissions-wrapper-module/introduction.md). To quickly get started with this plugin, follow the steps below. -- [Quick-start Guide](./quick-start.md) +- [Quick-start Guide](/opa-backend/quick-start.md) + +## Join The Community + +This project is a part of the broader Backstage and Open Policy Agent ecosystems. Explore more about these communities: + +- [Backstage Community](https://backstage.io) +- [Open Policy Agent Community](https://www.openpolicyagent.org) +- [Styra](https://www.styra.com) +- [Join OPA on Slack](https://slack.openpolicyagent.org/) +- [Backstage Discord](https://discord.com/invite/MUpMjP2) -## Contributing +## Get Involved -Contributions are welcome! If you're interested in enhancing this plugin, please fork the repository and submit a PR with your changes. For any questions or discussions, feel free to reach out on [Mastodon](https://hachyderm.io/@parcifal). +Your contributions can make this plugin even better. Fork the repository, make your changes, and submit a PR! If you have questions or ideas, reach out on [Mastodon](https://hachyderm.io/@parcifal). ## License -Licensed under the Apache 2.0 License. +This project is licensed under the Apache 2.0 License. diff --git a/docs/opa-permissions-wrapper-module/introduction.md b/docs/opa-permissions-wrapper-module/introduction.md index 3a405d81..c8e8cde5 100644 --- a/docs/opa-permissions-wrapper-module/introduction.md +++ b/docs/opa-permissions-wrapper-module/introduction.md @@ -2,7 +2,8 @@ # Simplify Permissions with OPA in Backstage -> Does not require the `backstage-opa-backend` plugin! +> [!ATTENTION|style:flat] +> Does **NOT** require the `backstage-opa-backend` plugin! Integrate dynamic policy management into your Backstage instance with the OPA Permissions Wrapper Module. This tool leverages [Open Policy Agent (OPA)](https://github.com/open-policy-agent/opa) for flexible, easy-to-update permissions management within the [Backstage Permission Framework](https://backstage.io/docs/permissions/overview). diff --git a/docs/opa-permissions-wrapper-module/local-development.md b/docs/opa-permissions-wrapper-module/local-development.md index a95b4312..7f8e05b1 100644 --- a/docs/opa-permissions-wrapper-module/local-development.md +++ b/docs/opa-permissions-wrapper-module/local-development.md @@ -4,7 +4,7 @@ If you are using this plugin and want to know how to use it in local development ## Pre-requisites -- You have a Backstage instance set up and running and the permission framework set up as outlined [here](https://backstage.io/docs/permissions/getting-started--new/). +- You have a Backstage instance set up and running and the permission framework set up as outlined [here](https://backstage.io/docs/permissions/getting-started/). - **Note** do not set a policy, just enable the framework. - This assumes you are using `Postgres` as your database in your `app-config.yaml` file, although this is not mandatory. diff --git a/docs/opa-permissions-wrapper-module/quick-start.md b/docs/opa-permissions-wrapper-module/quick-start.md index 787ce45c..f74073bb 100644 --- a/docs/opa-permissions-wrapper-module/quick-start.md +++ b/docs/opa-permissions-wrapper-module/quick-start.md @@ -4,7 +4,7 @@ This guide will help you get started with the OPA Permissions Wrapper module for ## Pre-requisites -- You have a Backstage instance set up and running and the permission framework set up as outlined [here](https://backstage.io/docs/permissions/getting-started--new/). +- You have a Backstage instance set up and running and the permission framework set up as outlined [here](https://backstage.io/docs/permissions/getting-started/). - **Note** do not set a policy, just enable the framework. - You have deployed OPA, kindly see how to do that [here](https://www.openpolicyagent.org/docs/latest/deployments/), or see below. @@ -80,12 +80,12 @@ data: # Catalog Permission: Allow users to only delete entities they claim ownership of. # Allow admins to delete any entity regardless of ownership. decision := conditional("catalog", "catalog-entity", {"anyOf": [{ - "resourceType": "catalog-entity", - "rule": "IS_ENTITY_OWNER", - "params": {"claims": claims}, + "resourceType": "catalog-entity", + "rule": "IS_ENTITY_OWNER", + "params": {"claims": claims}, }]}) if { - permission == "catalog.entity.delete" - not is_admin + permission == "catalog.entity.delete" + not is_admin } ``` diff --git a/package.json b/package.json index 3eca74ed..ed3d0ffc 100644 --- a/package.json +++ b/package.json @@ -48,8 +48,8 @@ "typescript": "~5.3.3" }, "resolutions": { - "@types/react": "^17", - "@types/react-dom": "^17", + "@types/react": "^18", + "@types/react-dom": "^18", "@yarnpkg/parsers": "3.0.0-rc.4", "swagger-ui-react": "5.10.5" }, @@ -63,10 +63,16 @@ "prettier:check" ] }, - "jest":{ - "coveragePathIgnorePatterns" : [ + "jest": { + "coveragePathIgnorePatterns": [ "packages/app", - "packages/backend" + "packages/backend", + "plugins/opa-frontend-demo", + "plugins/opa-backend-demo" + ], + "testPathIgnorePatterns": [ + "plugins/opa-frontend-demo", + "plugins/opa-backend-demo" ] }, "packageManager": "yarn@3.6.4" diff --git a/packages/app/package.json b/packages/app/package.json index 49112f5e..a6933ada 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -43,14 +43,16 @@ "@backstage/plugin-techdocs-react": "^1.2.9", "@backstage/plugin-user-settings": "^0.8.14", "@backstage/theme": "^0.6.0", + "@internal/backstage-plugin-opa-frontend-demo": "^0.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", + "@parsifal-m/backstage-plugin-opa-authz-react": "workspace:^", "@parsifal-m/plugin-dev-quotes-homepage": "^3.0.3", "@parsifal-m/plugin-opa-entity-checker": "workspace:*", "@parsifal-m/plugin-opa-policies": "workspace:*", "history": "^5.0.0", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.0.0", + "react-dom": "^18.0.0", "react-router-dom": "^6.3.0", "react-use": "^17.2.4" }, diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 0272a798..b548174d 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -38,6 +38,7 @@ import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; import { RequirePermission } from '@backstage/plugin-permission-react'; import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { OpaFrontendDemoPage } from '@internal/backstage-plugin-opa-frontend-demo'; const app = createApp({ components: { @@ -95,10 +96,12 @@ const routes = ( } /> } /> + } /> + } /> } /> + } /> ); diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index c89753aa..40492dda 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -7,7 +7,12 @@ import { AnyApiFactory, configApiRef, createApiFactory, + fetchApiRef, } from '@backstage/core-plugin-api'; +import { + opaAuthzBackendApiRef, + OpaAuthzClientReact, +} from '@parsifal-m/backstage-plugin-opa-authz-react'; export const apis: AnyApiFactory[] = [ createApiFactory({ @@ -15,5 +20,12 @@ export const apis: AnyApiFactory[] = [ deps: { configApi: configApiRef }, factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), }), + createApiFactory({ + api: opaAuthzBackendApiRef, + deps: { + fetchApi: fetchApiRef, + }, + factory: ({ fetchApi }) => new OpaAuthzClientReact({ fetchApi }), + }), ScmAuth.createDefaultApiFactory(), ]; diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 6768b48d..a629a1e7 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -26,6 +26,7 @@ import { } from '@backstage/core-components'; import MenuIcon from '@material-ui/icons/Menu'; import SearchIcon from '@material-ui/icons/Search'; +import SecurityIcon from '@material-ui/icons/Security'; const useSidebarLogoStyles = makeStyles({ root: { @@ -71,6 +72,11 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( {/* End global nav */} + diff --git a/packages/backend/package.json b/packages/backend/package.json index 080bc600..d052805e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -43,6 +43,7 @@ "@backstage/plugin-search-backend-module-techdocs": "^0.3.0", "@backstage/plugin-search-backend-node": "^1.3.3", "@backstage/plugin-techdocs-backend": "^1.11.0", + "@internal/backstage-plugin-opa-demo-backend": "^0.1.0", "@parsifal-m/plugin-opa-backend": "workspace:*", "@parsifal-m/plugin-permission-backend-module-opa-wrapper": "workspace:*", "app": "link:../app", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index afc7d1b0..ffbbc270 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -19,4 +19,5 @@ backend.add(import('@backstage/plugin-search-backend/alpha')); backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); backend.add(import('@backstage/plugin-techdocs-backend/alpha')); +backend.add(import('@internal/backstage-plugin-opa-demo-backend')); backend.start(); diff --git a/packages/opa-authz/.eslintrc.js b/packages/opa-authz/.eslintrc.js new file mode 100644 index 00000000..e2a53a6a --- /dev/null +++ b/packages/opa-authz/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/opa-authz/README.md b/packages/opa-authz/README.md new file mode 100644 index 00000000..88edb6b6 --- /dev/null +++ b/packages/opa-authz/README.md @@ -0,0 +1,127 @@ +# OPA Authz Client + +This is a node-library package for Backstage that provides a client and middleware for interacting with an OPA (Open Policy Agent) server for Authorization. + +## Why use this library? + +The [plugin-permission-backend-module-opa-wrapper](https://parsifal-m.github.io/backstage-opa-plugins/#/opa-permissions-wrapper-module/introduction) is a great way to integrate OPA with Backstage, but it has some limitations, because it is tightly coupled with the Backstage permissions framework sometimes +you need to add a little more information to your policy input which is not available or possible in the wrapper. + +This library is a more generic way to interact with OPA, and can be used in any part of the Backstage application, and is not tied to the permissions framework in any way, meaning: + +- You can still protect your API endpoints with OPA, but you can also use OPA for other things, like controlling the visibility of components in the frontend. +- You are not limited in terms of what you can send as input to OPA, want to restrict access to something in Backstage based on the time of day? You can do that with this library. +- You can still use [plugin-permission-backend-module-opa-wrapper](https://parsifal-m.github.io/backstage-opa-plugins/#/opa-permissions-wrapper-module/introduction) in conjunction with this library, + not all core and community plugins will natively work with this client, so you can use the wrapper to handle those cases. +- Has a middleware that can be used in the backend to protect your API endpoints, simply add it to your express routes and you are good to go. + +## Usage + +### Using the OpaAuthzClient + +The `OpaAuthzClient` allows you to interact with an Open Policy Agent (OPA) server to evaluate policies against given inputs. + +You are pretty much free to use this in any way you like in your Backstage Backend. + +```typescript +import express from 'express'; +import { Config } from '@backstage/config'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { OpaAuthzClient } from '@parsifal-m/backstage-opa-authz'; + +export type someRouteOptions = { + logger: LoggerService; + config: Config; + // more options... +}; +// Some code here +// ... +// Instantiate the OpaAuthzClient +const opaClient = new OpaAuthzClient(config, logger); + +// Define the policy input and entry point +const policyInput = { user: 'alice', action: 'read', resource: 'document' }; +const entryPoint = 'example/allow'; + +// Evaluate the policy +opaClient + .evaluatePolicy(policyInput, entryPoint) + .then(result => { + logger.info('Policy evaluation result:', result); + }) + .catch(error => { + logger.info('Error evaluating policy:', error); + }); + +// Some more code here +// ... +``` + +### Using the opaAuthzMiddleware + +You'll probably want to use the `opaAuthzMiddleware` in your express routes to protect your API endpoints instead of using the `OpaAuthzClient` directly. + +```typescript +import express from 'express'; +import { + OpaAuthzClient, + opaAuthzMiddleware, +} from '@parsifal-m/backstage-opa-authz'; +import { Config } from '@backstage/config'; +import { LoggerService } from '@backstage/backend-plugin-api'; + +export type someRoutesOptions = { + logger: LoggerService; + config: Config; + // more options... +}; + +export const someRoutes = (options: someRoutesOptions): express.Router => { + const { logger, config } = options; + const router = express.Router(); + + // Instantiate the OpaAuthzClient + const opaAuthzClient = new OpaAuthzClient(logger, config); + + // Define the entry point + const entryPoint = 'authz'; + + // Define the input + const setInput = (req: express.Request) => { + return { + method: req.method, + path: req.path, + permission: { name: 'read' }, + someFoo: 'bar', + dateTime: new Date().toISOString(), + }; + }; + + // Define the route + router.get( + '/some-route', + opaAuthzMiddleware(opaAuthzClient, entryPoint, setInput, logger), + (req, res) => { + res.send('Hello, World!'); + }, + ); + + return router; +}; +``` + +## Example Demo Plugin(s) + +To help visualize how this library can be used, we have created a demo plugin that demonstrates how to use the `opaAuthzMiddleware` in the backend, you can find the demo code [here](../../plugins/opa-demo-backend/README.md). + +## Contributing + +I am happy to accept contributions and suggestions for these plugins, if you are looking to make significant changes, please open an issue first to discuss the changes you would like to make! + +Please fork the repository and open a PR with your changes. If you have any questions, please feel free to reach out to me on [Mastodon](https://hachyderm.io/@parcifal). + +Please remember to sign your commits with `git commit -s` so that your commits are signed! + +## License + +This project is released under the Apache 2.0 License. diff --git a/packages/opa-authz/config.d.ts b/packages/opa-authz/config.d.ts new file mode 100644 index 00000000..ffee7f38 --- /dev/null +++ b/packages/opa-authz/config.d.ts @@ -0,0 +1,12 @@ +export interface Config { + /** + * Configuration options for the OpaClient plugin + */ + opaClient?: { + /** + * The base url of the OPA server used for all OPA plugins. + * This is used across all the OPA plugins. + */ + baseUrl?: string; + }; +} diff --git a/packages/opa-authz/package.json b/packages/opa-authz/package.json new file mode 100644 index 00000000..0fd352c2 --- /dev/null +++ b/packages/opa-authz/package.json @@ -0,0 +1,41 @@ +{ + "name": "@parsifal-m/backstage-opa-authz", + "description": "A Backstage backend plugin that allows you to use OPA for authorization in the Backstage backend", + "version": "1.0.0-beta", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "fix": "backstage-cli repo fix --publish" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^0.4.3", + "@backstage/cli": "^0.26.10", + "supertest": "^7.0.0" + }, + "files": [ + "dist", + "config.d.ts" + ], + "dependencies": { + "@backstage/backend-plugin-api": "^0.6.21", + "@backstage/config": "^1.2.0", + "express": "^4.17.1", + "node-fetch": "^2.6.7" + } +} diff --git a/packages/opa-authz/src/api/index.ts b/packages/opa-authz/src/api/index.ts new file mode 100644 index 00000000..1d241171 --- /dev/null +++ b/packages/opa-authz/src/api/index.ts @@ -0,0 +1 @@ +export * from './opaClient'; diff --git a/packages/opa-authz/src/api/opaClient.test.ts b/packages/opa-authz/src/api/opaClient.test.ts new file mode 100644 index 00000000..9ed3d321 --- /dev/null +++ b/packages/opa-authz/src/api/opaClient.test.ts @@ -0,0 +1,72 @@ +import fetch from 'node-fetch'; +import { mockServices } from '@backstage/backend-test-utils'; +import { OpaAuthzClient } from './opaClient'; +import { ConfigReader } from '@backstage/config'; + +jest.mock('node-fetch'); + +const mockConfig = { + opaClient: { + baseUrl: 'http://localhost:8181', + }, +}; + +describe('OpaAuthzClient', () => { + const config = new ConfigReader(mockConfig); + let opaAuthzClient: OpaAuthzClient; + const mockLogger = mockServices.logger.mock(); + + beforeAll(() => { + opaAuthzClient = new OpaAuthzClient(mockLogger, config); + }); + + it('should evaluate policy correctly', async () => { + const mockInput = { + permission: { name: 'read' }, + identity: { user: 'testUser', claims: ['claim1', 'claim2'] }, + }; + + const mockOpaEntrypoint = 'some/admin'; + const url = `http://localhost:8181/v1/data/${mockOpaEntrypoint}`; + (fetch as jest.MockedFunction).mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValueOnce({ result: 'ALLOW' }), + } as any); + + const result = await opaAuthzClient.evaluatePolicy( + mockInput, + mockOpaEntrypoint, + ); + + expect(fetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ input: mockInput }), + }), + ); + expect(result).toEqual({ result: 'ALLOW' }); + }); + + it('should throw an error if the request to the OPA server fails', async () => { + const mockInput = { + permission: { name: 'read' }, + identity: { user: 'anders', claims: ['claim1', 'claim2'] }, + }; + const mockOpaEntrypoint = 'some/admin'; + (fetch as jest.MockedFunction).mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + } as any); + + await expect( + opaAuthzClient.evaluatePolicy(mockInput, mockOpaEntrypoint), + ).rejects.toThrow( + `An error response was returned after sending the policy input to the OPA server: 500 - Internal Server Error`, + ); + }); +}); diff --git a/packages/opa-authz/src/api/opaClient.ts b/packages/opa-authz/src/api/opaClient.ts new file mode 100644 index 00000000..3df2b11c --- /dev/null +++ b/packages/opa-authz/src/api/opaClient.ts @@ -0,0 +1,75 @@ +import fetch from 'node-fetch'; +import { Config } from '@backstage/config'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { PolicyInput, PolicyResult } from '../types'; + +/** + * OpaAuthzClient is a client for interacting with an Open Policy Agent (OPA) server. + * It allows evaluating policies against given inputs and retrieving the results. + */ +export class OpaAuthzClient { + private readonly baseUrl: string; + private readonly logger: LoggerService; + + /** + * Constructs a new OpaAuthzClient. + * + * @param config - The backend configuration object. + * @param logger - A logger instance used for logging. + */ + constructor(logger: LoggerService, config: Config) { + this.baseUrl = + config.getOptionalString('opaClient.baseUrl') || 'http://localhost:8181'; + this.logger = logger; + } + + /** + * Evaluates a policy against a given input. + * + * @param input - The input to evaluate the policy against. + * @param entryPoint - The entry point into the OPA policy to use. + * @returns A promise that resolves to the result of the policy evaluation. + * @throws An error if the OPA URL is not set or if the request to the OPA server fails. + */ + async evaluatePolicy( + input: PolicyInput, + entryPoint: string, + ): Promise { + const url = `${this.baseUrl}/v1/data/${entryPoint}`; + + this.logger.debug( + `OpaAuthzClient sending data to OPA: ${JSON.stringify(input)}`, + ); + + try { + const opaResponse = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ input }), + }); + + if (!opaResponse.ok) { + const message = `An error response was returned after sending the policy input to the OPA server: ${opaResponse.status} - ${opaResponse.statusText}`; + this.logger.error(message); + throw new Error(message); + } + + const opaPermissionsResponse = (await opaResponse.json()) as PolicyResult; + + this.logger.debug( + `Received data from OPA: ${JSON.stringify(opaPermissionsResponse)}`, + ); + + return opaPermissionsResponse; + } catch (error: unknown) { + this.logger.error( + `An error occurred while sending the policy input to the OPA server: ${error}`, + ); + throw new Error( + `An error occurred while sending the policy input to the OPA server: ${error}`, + ); + } + } +} diff --git a/packages/opa-authz/src/index.ts b/packages/opa-authz/src/index.ts new file mode 100644 index 00000000..3bc1cd69 --- /dev/null +++ b/packages/opa-authz/src/index.ts @@ -0,0 +1,2 @@ +export { OpaAuthzClient } from './api/opaClient'; +export { opaAuthzMiddleware } from './middleware/opaMiddleware'; diff --git a/packages/opa-authz/src/middleware/opaMiddleware.test.ts b/packages/opa-authz/src/middleware/opaMiddleware.test.ts new file mode 100644 index 00000000..9b0fe070 --- /dev/null +++ b/packages/opa-authz/src/middleware/opaMiddleware.test.ts @@ -0,0 +1,148 @@ +import { PolicyInput } from '../types'; +import { opaAuthzMiddleware } from './opaMiddleware'; +import { mockServices } from '@backstage/backend-test-utils'; +import request from 'supertest'; +import express from 'express'; +import { OpaAuthzClient } from '../api'; + +jest.mock('../api'); + +const mockOpaAuthzClient = { + evaluatePolicy: jest.fn(), +} as unknown as jest.Mocked; + +describe('opaAuthzMiddleware', () => { + let app: express.Express; + const mockLogger = mockServices.logger.mock(); + const entryPoint = 'testEntryPoint'; + const setInput = (_: express.Request): PolicyInput => ({ + method: 'GET', + path: '/', + headers: 'testHeaders', + user: 'testUser', + }); + + beforeEach(() => { + app = express(); + jest.clearAllMocks(); + }); + + it('should call next() if the policy allows the request', async () => { + mockOpaAuthzClient.evaluatePolicy.mockResolvedValueOnce({ + decision_id: 'test-decision-id', + result: { allow: true }, + }); + + app.use( + opaAuthzMiddleware(mockOpaAuthzClient, entryPoint, setInput, mockLogger), + ); + app.use((_, res) => res.status(200).send('OK')); + + await request(app).get('/').expect(200, 'OK'); + + expect(mockLogger.debug).toHaveBeenCalledWith( + `OPA middleware sending input to OPA: ${JSON.stringify({ + method: 'GET', + path: '/', + headers: 'testHeaders', + user: 'testUser', + })}`, + ); + expect(mockLogger.debug).toHaveBeenCalledWith( + `OPA middleware response: ${JSON.stringify({ + decision_id: 'test-decision-id', + result: { allow: true }, + })}`, + ); + }); + + it('should return a 403 status code if the policy denies the request', async () => { + mockOpaAuthzClient.evaluatePolicy.mockResolvedValueOnce({ + decision_id: 'test-decision-id', + result: { allow: false }, + }); + + app.use( + opaAuthzMiddleware(mockOpaAuthzClient, entryPoint, setInput, mockLogger), + ); + app.use((_req, res) => res.status(200).send('OK')); + + await request(app).get('/').expect(403, { error: 'Forbidden' }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + `OPA middleware sending input to OPA: ${JSON.stringify({ + method: 'GET', + path: '/', + headers: 'testHeaders', + user: 'testUser', + })}`, + ); + expect(mockLogger.debug).toHaveBeenCalledWith( + `OPA middleware response: ${JSON.stringify({ + decision_id: 'test-decision-id', + result: { allow: false }, + })}`, + ); + }); + + it('should return a custom error message if provided when access is forbidden', async () => { + mockOpaAuthzClient.evaluatePolicy.mockResolvedValueOnce({ + decision_id: 'test-decision-id', + result: { allow: false }, + }); + + const customErrorMessage = 'Custom Forbidden Message'; + app.use( + opaAuthzMiddleware( + mockOpaAuthzClient, + entryPoint, + setInput, + mockLogger, + customErrorMessage, + ), + ); + app.use((_req, res) => res.status(200).send('OK')); + + await request(app).get('/').expect(403, { error: customErrorMessage }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + `OPA middleware sending input to OPA: ${JSON.stringify({ + method: 'GET', + path: '/', + headers: 'testHeaders', + user: 'testUser', + })}`, + ); + expect(mockLogger.debug).toHaveBeenCalledWith( + `OPA middleware response: ${JSON.stringify({ + decision_id: 'test-decision-id', + result: { allow: false }, + })}`, + ); + }); + + it('should return a 500 status code if an error occurs during policy evaluation', async () => { + mockOpaAuthzClient.evaluatePolicy.mockRejectedValueOnce( + new Error('OPA Error'), + ); + + app.use( + opaAuthzMiddleware(mockOpaAuthzClient, entryPoint, setInput, mockLogger), + ); + app.use((_req, res) => res.status(200).send('OK')); + + await request(app).get('/').expect(500, { error: 'Internal Server Error' }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + `OPA middleware sending input to OPA: ${JSON.stringify({ + method: 'GET', + path: '/', + headers: 'testHeaders', + user: 'testUser', + })}`, + ); + expect(mockLogger.error).toHaveBeenCalledWith( + `An error occurred while sending the policy input to the OPA server: Error: OPA Error`, + ); + }); +}); diff --git a/packages/opa-authz/src/middleware/opaMiddleware.ts b/packages/opa-authz/src/middleware/opaMiddleware.ts new file mode 100644 index 00000000..9109530f --- /dev/null +++ b/packages/opa-authz/src/middleware/opaMiddleware.ts @@ -0,0 +1,74 @@ +import { Request, Response, NextFunction } from 'express'; +import { PolicyInput } from '../types'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { OpaAuthzClient } from '../api'; + +/** + * Middleware function for handling authorization using OPA (Open Policy Agent). + * + * @param opaClient - An instance of OpaAuthzClient used to evaluate policies. + * @param entryPoint - The entry point for the OPA policy evaluation. + * @param setInput - A function that sets the policy input based on the request. + * @param logger - (Optional) An instance of LoggerService for logging debug information. + * @param customErrorMessage - (Optional) Custom error message to return when access is forbidden. + * + * @returns An Express middleware function that evaluates the policy and either allows the request to proceed or responds with an error. + * + * @example + * + * // Create an instance of OpaAuthzClient + * const opaAuthzClient = new OpaAuthzClient(config, logger); + * + * // Set an entry point for the OPA policy + * const entryPoint = 'authz'; + * + * // Construct your policy input + * const setInput = (req: Request): PolicyInput => { + * return { + * method: req.method, + * path: req.path, + * headers: req.headers, + * someFoo: 'bar', + * }; + * }; + * + * router.get('/some-url', opaAuthzMiddleware(opaClient, entryPoint, setInput, logger), async (req, res, next) => { + * // Route handler logic + * }); + * + */ + +export const opaAuthzMiddleware = ( + opaClient: OpaAuthzClient, + entryPoint: string, + setInput: (req: Request) => PolicyInput, + logger?: LoggerService, + customErrorMessage?: string, +) => { + return async (req: Request, res: Response, next: NextFunction) => { + const input = setInput(req); + try { + if (logger) { + logger.debug( + `OPA middleware sending input to OPA: ${JSON.stringify(input)}`, + ); + } + const opaResponse = await opaClient.evaluatePolicy(input, entryPoint); + if (logger) { + logger.debug(`OPA middleware response: ${JSON.stringify(opaResponse)}`); + } + if (opaResponse.result.allow) { + next(); + } else { + res.status(403).json({ error: customErrorMessage ?? 'Forbidden' }); + } + } catch (error: unknown) { + if (logger) { + logger.error( + `An error occurred while sending the policy input to the OPA server: ${error}`, + ); + } + res.status(500).json({ error: 'Internal Server Error' }); + } + }; +}; diff --git a/packages/opa-authz/src/setupTests.ts b/packages/opa-authz/src/setupTests.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/packages/opa-authz/src/setupTests.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/opa-authz/src/types.ts b/packages/opa-authz/src/types.ts new file mode 100644 index 00000000..0bfe15d2 --- /dev/null +++ b/packages/opa-authz/src/types.ts @@ -0,0 +1,42 @@ +/** + * Represents the input to a policy evaluation. + * + * This type is a record where the keys are strings and the values can be of any type. + * It allows for flexible input structures to be passed to the policy evaluation function. + * + * @example + * const input: PolicyInput = { + * user: { + * id: "123", + * role: "admin" + * }, + * resource: { + * type: "document", + * id: "456" + * }, + * action: "read" + * }; + */ +export type PolicyInput = Record; + +/** + * Represents the result of a policy evaluation. + * + * @property {string} decision_id - A unique identifier for the decision, useful for tracking and auditing purposes. + * @property {object} result - The outcome of the policy evaluation. + * @property {boolean} result.allow - Indicates whether the action is allowed based on the policy evaluation. + * + * @example + * const result: PolicyResult = { + * decision_id: "abc-123-def-456", + * result: { + * allow: true + * } + * }; + */ +export type PolicyResult = { + decision_id: string; + result: { + allow: boolean; + }; +}; diff --git a/plugins/backstage-opa-backend/README.md b/plugins/backstage-opa-backend/README.md index fe6b6216..d2ec4f46 100644 --- a/plugins/backstage-opa-backend/README.md +++ b/plugins/backstage-opa-backend/README.md @@ -1,15 +1,25 @@ ![NPM Version](https://img.shields.io/npm/v/%40parsifal-m%2Fplugin-opa-backend?logo=npm) -# backstage-opa-backend +# Backstage OPA Backend Plugin -This serves as the OPA Backend Plugin, eventually to route all your OPA needs through! +A backend plugin for Backstage, this plugin integrates with the Open Policy Agent (OPA) to facilitate policy evaluation. -> There may be breaking changes as this plugin is still in development! Please be aware of this! +It's a dependency of the following plugins: + +- [OPA Entity Checker](../opa-entity-checker/introduction.md#keep-your-entity-data-in-check-with-opa-entity-checker) +- [OPA Policies](../opa-policies/introduction.md) +- [OPA Authz React](../opa-authz-react/introduction.md) + +By itself, this plugin does not provide any user-facing features. + +> This plugin is **NOT** required for the [OPA Permissions Wrapper Module](../opa-permissions-wrapper-module/introduction.md). # Pre-requisites The only pre-requisites to use this plugin is that you have set up an OPA server. You can find more information on how to do that [here](https://www.openpolicyagent.org/docs/latest/deployments/). And you have a Backstage instance running. More info on how to do that [here](https://backstage.io/docs/getting-started). +Or, you can check [these docs](../../docs/deploying-opa/deploying-opa.md#deploying-opa) for a quick guide on how to deploy OPA as a sidecar to your Backstage instance and add policies to it. + ## Installation This plugin is currently used by the [backstage-opa-entity-checker](../backstage-opa-entity-checker/README.md), and the [backstage-opa-policies](../backstage-opa-policies/README.md) plugins. You can install it by running the following command: @@ -52,7 +62,11 @@ The `entrypoint` name in the `app-config.yaml` file should be the entrypoint to ## Contributing -I am happy to accept contributions to this plugin. Please fork the repository and open a PR with your changes. If you have any questions, please feel free to reach out to me on [Mastodon](https://hachyderm.io/@parcifal) +I am happy to accept contributions and suggestions for these plugins, if you are looking to make significant changes, please open an issue first to discuss the changes you would like to make! + +Please fork the repository and open a PR with your changes. If you have any questions, please feel free to reach out to me on [Mastodon](https://hachyderm.io/@parcifal). + +Please remember to sign your commits with `git commit -s` so that your commits are signed! ## License diff --git a/plugins/backstage-opa-backend/config.d.ts b/plugins/backstage-opa-backend/config.d.ts index 96da6c24..87cb3b75 100644 --- a/plugins/backstage-opa-backend/config.d.ts +++ b/plugins/backstage-opa-backend/config.d.ts @@ -4,7 +4,8 @@ export interface Config { */ opaClient?: { /** - * The base url of the OPA server used for the plugin + * The base url of the OPA server used for all OPA plugins. + * This is used across all the OPA plugins. */ baseUrl?: string; diff --git a/plugins/backstage-opa-backend/package.json b/plugins/backstage-opa-backend/package.json index d53335e6..238cd017 100644 --- a/plugins/backstage-opa-backend/package.json +++ b/plugins/backstage-opa-backend/package.json @@ -42,11 +42,11 @@ "@backstage/config": "^1.2.0", "@backstage/errors": "^1.2.4", "@backstage/integration": "^1.15.1", - "@types/express": "*", + "@backstage/plugin-catalog-node": "^1.13.1", + "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "node-fetch": "^2.6.7", - "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { diff --git a/plugins/backstage-opa-backend/src/lib/read.ts b/plugins/backstage-opa-backend/src/lib/read.ts index e50533b9..c7b6ec11 100644 --- a/plugins/backstage-opa-backend/src/lib/read.ts +++ b/plugins/backstage-opa-backend/src/lib/read.ts @@ -1,17 +1,16 @@ import { UrlReaderService } from '@backstage/backend-plugin-api'; -import { NotFoundError } from '@backstage/errors'; export async function readPolicyFile( - reader: UrlReaderService, + urlReader: UrlReaderService, policyFilePath: string, ): Promise { const url = `${policyFilePath}`; try { - const data = await reader.readUrl(url); - const buffer = await data.buffer(); + const response = await urlReader.readUrl(url); + const buffer = await response.buffer(); return buffer.toString(); } catch (error) { - if (error instanceof NotFoundError) { + if (error.name === 'NotFoundError') { return undefined; } throw error; diff --git a/plugins/backstage-opa-backend/src/plugin.ts b/plugins/backstage-opa-backend/src/plugin.ts index bc40e996..df94743d 100644 --- a/plugins/backstage-opa-backend/src/plugin.ts +++ b/plugins/backstage-opa-backend/src/plugin.ts @@ -3,12 +3,14 @@ import { createBackendPlugin, } from '@backstage/backend-plugin-api'; import { createRouter } from './service/router'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; export const opaPlugin = createBackendPlugin({ pluginId: 'opa', register(env) { env.registerInit({ deps: { + catalogApi: catalogServiceRef, config: coreServices.rootConfig, logger: coreServices.logger, httpRouter: coreServices.httpRouter, @@ -16,24 +18,25 @@ export const opaPlugin = createBackendPlugin({ auth: coreServices.auth, httpAuth: coreServices.httpAuth, urlReader: coreServices.urlReader, + userInfo: coreServices.userInfo, }, async init({ config, logger, httpRouter, - auth, httpAuth, discovery, urlReader, + userInfo, }) { httpRouter.use( await createRouter({ config, logger, - auth, httpAuth, discovery, urlReader, + userInfo, }), ); diff --git a/plugins/backstage-opa-backend/src/service/router.test.ts b/plugins/backstage-opa-backend/src/service/router.test.ts index 1966f0fe..a63500ce 100644 --- a/plugins/backstage-opa-backend/src/service/router.test.ts +++ b/plugins/backstage-opa-backend/src/service/router.test.ts @@ -1,241 +1,42 @@ +import { mockServices } from '@backstage/backend-test-utils'; import express from 'express'; import request from 'supertest'; import { createRouter } from './router'; -import { ConfigReader } from '@backstage/config'; -import fetch from 'node-fetch'; -import { mockServices } from '@backstage/backend-test-utils'; -import { UrlReaderService } from '@backstage/backend-plugin-api'; - -jest.mock('node-fetch'); - -const { Response: FetchResponse } = jest.requireActual('node-fetch'); - -const mockUrlReader: UrlReaderService = { - readUrl: url => - Promise.resolve({ - buffer: async () => Buffer.from(url), - etag: 'buffer', - stream: jest.fn(), - }), - readTree: jest.fn(), - search: jest.fn(), -}; describe('createRouter', () => { let app: express.Express; - const config = new ConfigReader({ - opaClient: { - baseUrl: 'http://localhost', - policies: { - entityChecker: { - entrypoint: 'entitymeta_policy/somepoint', + beforeAll(async () => { + const mockConfig = mockServices.rootConfig({ + data: { + data: { + opaClient: { + baseUrl: 'http://localhost:8181', + }, }, }, - }, - }); - - beforeAll(async () => { + }); const router = await createRouter({ + config: mockConfig, logger: mockServices.logger.mock(), - config: config, - discovery: mockServices.discovery(), - urlReader: mockUrlReader, + discovery: mockServices.discovery.mock(), + urlReader: mockServices.urlReader.mock(), + httpAuth: mockServices.httpAuth.mock(), + userInfo: mockServices.userInfo.mock(), }); - app = express().use(router); + }); + + beforeEach(() => { jest.resetAllMocks(); }); describe('GET /health', () => { it('returns ok', async () => { - const res = await request(app).get('/health'); - - expect(res.status).toEqual(200); - expect(res.body).toEqual({ status: 'ok' }); - }); - }); - - describe('/entity-checker routes', () => { - const mockedPayload = { - input: { - metadata: { - namespace: 'default', - annotations: { - 'backstage.io/managed-by-location': - 'file:/brewed-backstage/examples/entities.yaml', - 'backstage.io/managed-by-origin-location': - 'file:/brewed-backstage/examples/entities.yaml', - }, - name: 'example-website', - uid: '762d5d68-7418-4b65-baa4-43d5e6cd591d', - etag: '46e9e22027eb7c502df70e8c34a0285123bc8e01', - }, - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - spec: { - type: 'website', - lifecycle: 'experimental', - owner: 'guests', - system: 'examples', - providesApis: ['example-grpc-api'], - }, - relations: [ - { - type: 'ownedBy', - targetRef: 'group:default/guests', - target: { - kind: 'group', - namespace: 'default', - name: 'guests', - }, - }, - { - type: 'partOf', - targetRef: 'system:default/examples', - target: { - kind: 'system', - namespace: 'default', - name: 'examples', - }, - }, - { - type: 'providesApi', - targetRef: 'api:default/example-grpc-api', - target: { - kind: 'api', - namespace: 'default', - name: 'example-grpc-api', - }, - }, - ], - }, - }; - - const mockedEntityResponse = { - allow: true, - is_system_present: true, - violation: [ - { - level: 'warning', - message: 'You do not have any tags set!', - }, - ], - }; - - it('POSTS and returns a response from OPA as expected', async () => { - (fetch as jest.MockedFunction).mockImplementation(() => { - return Promise.resolve( - new FetchResponse(JSON.stringify(mockedEntityResponse), { - headers: { 'Content-Type': 'application/json' }, - }), - ); - }); - - const res = await request(app) - .post('/entity-checker') - .send(mockedPayload) - .expect('Content-Type', /json/); - - expect(res.status).toEqual(200); - expect(res.body).toEqual(mockedEntityResponse); - }); - - it('returns 500 if OPA URL is not set', async () => { - const router = await createRouter({ - logger: mockServices.logger.mock(), - config: new ConfigReader({ - opaClient: { - baseUrl: undefined, - policies: { - entityChecker: { - entrypoint: 'entitymeta_policy/somepoint', - }, - }, - }, - }), - discovery: mockServices.discovery(), - urlReader: mockUrlReader, - }); - - app = express().use(router); - - const res = await request(app) - .post('/entity-checker') - .send(mockedPayload) - .expect('Content-Type', /json/); - - expect(res.status).toEqual(500); - }); - - it('complains if no entrypoint is set', async () => { - const router = await createRouter({ - logger: mockServices.logger.mock(), - config: new ConfigReader({ - opaClient: { - baseUrl: 'http://localhost', - policies: { - entityChecker: { - entrypoint: undefined, - }, - }, - }, - }), - discovery: mockServices.discovery(), - urlReader: mockUrlReader, - }); - - app = express().use(router); - - const res = await request(app) - .post('/entity-checker') - .send(mockedPayload) - .expect('Content-Type', /json/); - - expect(res.status).toEqual(500); - }); - }); - - describe('/get-policy route', () => { - it('returns policy content when valid opaPolicy is provided', async () => { - const opaPolicy = 'https://github.com/some/opa/repo/rbac.rego'; - const policyContent = 'policy content'; - - // Mock the readPolicyFile function to return a predefined policy content - mockUrlReader.readUrl = jest.fn().mockResolvedValue({ - buffer: async () => Buffer.from(policyContent), - etag: 'buffer', - stream: jest.fn(), - }); - - const res = await request(app).get( - `/get-policy?opaPolicy=${encodeURIComponent(opaPolicy)}`, - ); - - expect(res.status).toEqual(200); - expect(res.body).toEqual({ policyContent }); - }); - - it('returns 500 when no opaPolicy is provided', async () => { - const res = await request(app).get('/get-policy'); - - expect(res.status).toEqual(500); - }); - - it('returns 500 when an error occurs while fetching the policy file', async () => { - const opaPolicy = - 'https://github.com/Parsifal-M/backstage-testing-grounds/blob/main/rbac.rego'; - - // Mock the readPolicyFile function to throw an error - mockUrlReader.readUrl = jest - .fn() - .mockRejectedValue(new Error('An error occurred')); - - const res = await request(app).get( - `/get-policy?opaPolicy=${encodeURIComponent(opaPolicy)}`, - ); + const response = await request(app).get('/health'); - expect(res.status).toEqual(500); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); }); }); }); diff --git a/plugins/backstage-opa-backend/src/service/router.ts b/plugins/backstage-opa-backend/src/service/router.ts index 698ba3c4..05e99830 100644 --- a/plugins/backstage-opa-backend/src/service/router.ts +++ b/plugins/backstage-opa-backend/src/service/router.ts @@ -1,15 +1,16 @@ import express from 'express'; import Router from 'express-promise-router'; import { - AuthService, DiscoveryService, HttpAuthService, LoggerService, UrlReaderService, + UserInfoService, } from '@backstage/backend-plugin-api'; -import fetch from 'node-fetch'; +import { entityCheckerRouter } from './routers/entityChecker'; +import { policyContentRouter } from './routers/policyContent'; +import { authzRouter } from './routers/authz'; import { Config } from '@backstage/config'; -import { readPolicyFile } from '../lib/read'; import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter'; export type RouterOptions = { @@ -17,94 +18,26 @@ export type RouterOptions = { config: Config; discovery: DiscoveryService; urlReader: UrlReaderService; - auth?: AuthService; - httpAuth?: HttpAuthService; + httpAuth: HttpAuthService; + userInfo: UserInfoService; }; export async function createRouter( options: RouterOptions, ): Promise { - const { logger, config, urlReader } = options; + const { logger, config, urlReader, httpAuth, userInfo } = options; const router = Router(); router.use(express.json()); - // Get the config options for the OPA plugin - const opaBaseUrl = config.getOptionalString('opaClient.baseUrl'); - - // This is the Entity Checker package - const entityCheckerEntrypoint = config.getOptionalString( - 'opaClient.policies.entityChecker.entrypoint', - ); - - router.get('/health', (_, resp) => { - resp.json({ status: 'ok' }); + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.json({ status: 'ok' }); }); - router.post('/entity-checker', async (req, res, next) => { - const entityMetadata = req.body.input; - - if (!opaBaseUrl) { - logger.error('OPA URL not set or missing!'); - throw new Error('OPA URL not set or missing!'); - } - - const opaUrl = `${opaBaseUrl}/v1/data/${entityCheckerEntrypoint}`; - - if (!entityCheckerEntrypoint) { - logger.error('OPA package not set or missing!'); - throw new Error('OPA package not set or missing!'); - } - - if (!entityMetadata) { - logger.error('Entity metadata is missing!'); - throw new Error('Entity metadata is missing!'); - } - - try { - logger.debug(`Sending entity metadata to OPA: ${entityMetadata}`); - const opaResponse = await fetch(opaUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ input: entityMetadata }), - }); - const opaEntityCheckerResponse = await opaResponse.json(); - logger.debug(`Received response from OPA: ${opaEntityCheckerResponse}`); - return res.json(opaEntityCheckerResponse); - } catch (error) { - logger.error( - 'An error occurred trying to send entity metadata to OPA:', - error, - ); - return next(error); - } - }); - - router.get('/get-policy', async (req, res, next) => { - const opaPolicy = req.query.opaPolicy as string; - - if (!opaPolicy) { - logger.error( - 'No OPA policy provided!, please check the open-policy-agent/policy annotation and provide a URL to the policy file', - ); - throw new Error( - 'No OPA policy provided!, please check the open-policy-agent/policy annotation and provide a URL to the policy file', - ); - } - - try { - // Fetch the content of the policy file - logger.debug(`Fetching policy file from ${opaPolicy}`); - const policyContent = await readPolicyFile(urlReader, opaPolicy); - - return res.json({ policyContent }); - } catch (error) { - logger.error('An error occurred trying to fetch the policy file:', error); - return next(error); - } - }); + router.use(entityCheckerRouter(logger, config)); + router.use(authzRouter(logger, config, httpAuth, userInfo)); + router.use(policyContentRouter(logger, urlReader)); const middleware = MiddlewareFactory.create({ logger, config }); diff --git a/plugins/backstage-opa-backend/src/service/routers/authz.test.ts b/plugins/backstage-opa-backend/src/service/routers/authz.test.ts new file mode 100644 index 00000000..4d3a161f --- /dev/null +++ b/plugins/backstage-opa-backend/src/service/routers/authz.test.ts @@ -0,0 +1,106 @@ +import { mockServices } from '@backstage/backend-test-utils'; +import express from 'express'; +import request from 'supertest'; +import { authzRouter } from './authz'; +import fetch from 'node-fetch'; +import { BackstageUserInfo } from '@backstage/backend-plugin-api'; + +jest.mock('node-fetch'); +const { Response } = jest.requireActual('node-fetch'); + +describe('authzRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const mockConfig = mockServices.rootConfig({ + data: { + opaClient: { + baseUrl: 'http://localhost:8181', + }, + }, + }); + + const mockLogger = mockServices.logger.mock(); + const mockHttpAuth = mockServices.httpAuth.mock(); + const mockUserInfo = mockServices.userInfo.mock(); + const mockUserInfoData = { user: 'testUser', email: 'test@example.com' }; + + mockUserInfo.getUserInfo.mockResolvedValue( + mockUserInfoData as unknown as BackstageUserInfo, + ); + + const router = authzRouter( + mockLogger, + mockConfig, + mockHttpAuth, + mockUserInfo, + ); + app = express().use(express.json()).use(router); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('POST /opa-authz', () => { + it('returns 400 if input or entryPoint is missing', async () => { + const res = await request(app).post('/opa-authz').send({}); + + expect(res.status).toEqual(400); + expect(res.body).toEqual({ + error: 'Missing input or entryPoint in request body', + }); + }); + + it('returns the policy evaluation result', async () => { + const mockResult = { + decision_id: 'test-decision-id', + result: { allow: true }, + }; + + (fetch as jest.MockedFunction).mockResolvedValueOnce( + new Response(JSON.stringify(mockResult), { status: 200 }), + ); + + const res = await request(app) + .post('/opa-authz') + .send({ input: { user: 'testUser' }, entryPoint: 'testEntryPoint' }); + + expect(res.status).toEqual(200); + expect(res.body).toEqual(mockResult); + expect(fetch).toHaveBeenCalledWith( + 'http://localhost:8181/v1/data/testEntryPoint', + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + input: { user: 'testUser', email: 'test@example.com' }, + }), + }), + ); + }); + + it('returns 500 if an error occurs during policy evaluation', async () => { + (fetch as jest.MockedFunction).mockRejectedValueOnce( + new Error('OPA Error'), + ); + + const res = await request(app) + .post('/opa-authz') + .send({ input: { user: 'testUser' }, entryPoint: 'testEntryPoint' }); + + expect(res.status).toEqual(500); + expect(res.body).toEqual({ error: 'Error evaluating policy' }); + expect(fetch).toHaveBeenCalledWith( + 'http://localhost:8181/v1/data/testEntryPoint', + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + input: { user: 'testUser', email: 'test@example.com' }, + }), + }), + ); + }); + }); +}); diff --git a/plugins/backstage-opa-backend/src/service/routers/authz.ts b/plugins/backstage-opa-backend/src/service/routers/authz.ts new file mode 100644 index 00000000..1f1a3ea1 --- /dev/null +++ b/plugins/backstage-opa-backend/src/service/routers/authz.ts @@ -0,0 +1,74 @@ +import { Router } from 'express'; +import { Config } from '@backstage/config'; +import { + HttpAuthService, + LoggerService, + UserInfoService, +} from '@backstage/backend-plugin-api'; +import fetch from 'node-fetch'; + +export function authzRouter( + logger: LoggerService, + config: Config, + httpAuth: HttpAuthService, + userInfo: UserInfoService, +): Router { + const router = Router(); + const baseUrl = + config.getOptionalString('opaClient.baseUrl') ?? 'http://localhost:8181'; + + router.post('/opa-authz', async (req, res) => { + const { input, entryPoint } = req.body; + const credentials = await httpAuth.credentials(req, { allow: ['user'] }); + const info = await userInfo.getUserInfo(credentials); + + const inputWithCredentials = { ...input, ...info }; + + logger.debug( + `OPA Backend received request with input: ${JSON.stringify( + input, + )} and entryPoint: ${entryPoint}`, + ); + + if (!input || !entryPoint) { + return res + .status(400) + .json({ error: 'Missing input or entryPoint in request body' }); + } + + try { + const url = `${baseUrl}/v1/data/${entryPoint}`; + logger.debug( + `Sending data to OPA: ${JSON.stringify(inputWithCredentials)}`, + ); + + const opaResponse = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ input: inputWithCredentials }), + }); + + if (!opaResponse.ok) { + const message = `An error response was returned after sending the policy input to the OPA server: ${opaResponse.status} - ${opaResponse.statusText}`; + logger.error(message); + return res.status(opaResponse.status).json({ error: message }); + } + + const opaPermissionsResponse = await opaResponse.json(); + logger.debug( + `Received data from OPA: ${JSON.stringify(opaPermissionsResponse)}`, + ); + + return res.json(opaPermissionsResponse); + } catch (error: unknown) { + logger.error( + `An error occurred while sending the policy input to the OPA server: ${error}`, + ); + return res.status(500).json({ error: 'Error evaluating policy' }); + } + }); + + return router; +} diff --git a/plugins/backstage-opa-backend/src/service/routers/entityChecker.test.ts b/plugins/backstage-opa-backend/src/service/routers/entityChecker.test.ts new file mode 100644 index 00000000..32e3fe12 --- /dev/null +++ b/plugins/backstage-opa-backend/src/service/routers/entityChecker.test.ts @@ -0,0 +1,123 @@ +import { mockServices } from '@backstage/backend-test-utils'; +import express from 'express'; +import request from 'supertest'; +import { entityCheckerRouter } from './entityChecker'; +import fetch from 'node-fetch'; + +jest.mock('node-fetch'); +const { Response } = jest.requireActual('node-fetch'); + +describe('entityCheckerRouter', () => { + let app: express.Express; + let mockFetch: jest.Mock; + + beforeAll(async () => { + const mockConfig = mockServices.rootConfig({ + data: { + opaClient: { + baseUrl: 'http://localhost:8181', + policies: { + entityChecker: { + entrypoint: 'entityCheckerEntrypoint', + }, + }, + }, + }, + }); + + const mockLogger = mockServices.logger.mock(); + mockFetch = fetch as unknown as jest.Mock; + + const router = entityCheckerRouter(mockLogger, mockConfig); + app = express().use(express.json()).use(router); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('POST /entity-checker', () => { + it('returns the OPA response', async () => { + const mockOpaResponse = { result: { allow: true } }; + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify(mockOpaResponse)), + ); + + const res = await request(app) + .post('/entity-checker') + .send({ input: { metadata: 'test' } }); + + expect(res.status).toEqual(200); + expect(res.body).toEqual(mockOpaResponse); + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost:8181/v1/data/entityCheckerEntrypoint', + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ input: { metadata: 'test' } }), + }), + ); + }); + + it('returns 500 if OPA package is not set', async () => { + const mockConfigNoPackage = mockServices.rootConfig({ + data: { + opaClient: { + baseUrl: 'http://localhost:8181', + }, + }, + }); + + const mockLogger = mockServices.logger.mock(); + const router = entityCheckerRouter(mockLogger, mockConfigNoPackage); + const appNoPackage = express().use(express.json()).use(router); + + const res = await request(appNoPackage) + .post('/entity-checker') + .send({ input: {} }); + + expect(res.status).toEqual(500); + expect(mockLogger.error).toHaveBeenCalledWith( + 'OPA package not set or missing!', + ); + }); + + it('returns 500 if entity metadata is missing', async () => { + const mockConfigWithPackage = mockServices.rootConfig({ + data: { + opaClient: { + baseUrl: 'http://localhost:8181', + policies: { + entityChecker: { + entrypoint: 'entityCheckerEntrypoint', + }, + }, + }, + }, + }); + + const mockLogger = mockServices.logger.mock(); + const router = entityCheckerRouter(mockLogger, mockConfigWithPackage); + const appWithPackage = express().use(express.json()).use(router); + + const res = await request(appWithPackage) + .post('/entity-checker') + .send({}); + + expect(res.status).toEqual(500); + expect(mockLogger.error).toHaveBeenCalledWith( + 'Entity metadata is missing!', + ); + }); + + it('returns 500 if an error occurs during the fetch', async () => { + mockFetch.mockRejectedValueOnce(new Error('Fetch error')); + + const res = await request(app) + .post('/entity-checker') + .send({ input: { metadata: 'test' } }); + + expect(res.status).toEqual(500); + }); + }); +}); diff --git a/plugins/backstage-opa-backend/src/service/routers/entityChecker.ts b/plugins/backstage-opa-backend/src/service/routers/entityChecker.ts new file mode 100644 index 00000000..c6c0de21 --- /dev/null +++ b/plugins/backstage-opa-backend/src/service/routers/entityChecker.ts @@ -0,0 +1,56 @@ +import express from 'express'; +import fetch from 'node-fetch'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; + +export const entityCheckerRouter = ( + logger: LoggerService, + config: Config, +): express.Router => { + const router = express.Router(); + + // Get the config options for the OPA plugin + const opaBaseUrl = + config.getOptionalString('opaClient.baseUrl') || 'http://localhost:8181'; + + // This is the Entity Checker package + const entityCheckerEntrypoint = config.getOptionalString( + 'opaClient.policies.entityChecker.entrypoint', + ); + + router.post('/entity-checker', async (req, res, next) => { + const entityMetadata = req.body.input; + + const opaUrl = `${opaBaseUrl}/v1/data/${entityCheckerEntrypoint}`; + + if (!entityCheckerEntrypoint) { + logger.error('OPA package not set or missing!'); + } + + if (!entityMetadata) { + logger.error('Entity metadata is missing!'); + } + + try { + logger.debug(`Sending entity metadata to OPA: ${entityMetadata}`); + const opaResponse = await fetch(opaUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ input: entityMetadata }), + }); + const opaEntityCheckerResponse = await opaResponse.json(); + logger.debug(`Received response from OPA: ${opaEntityCheckerResponse}`); + return res.json(opaEntityCheckerResponse); + } catch (error) { + logger.error( + 'An error occurred trying to send entity metadata to OPA:', + error, + ); + return next(error); + } + }); + + return router; +}; diff --git a/plugins/backstage-opa-backend/src/service/routers/policyContent.test.ts b/plugins/backstage-opa-backend/src/service/routers/policyContent.test.ts new file mode 100644 index 00000000..f6b96232 --- /dev/null +++ b/plugins/backstage-opa-backend/src/service/routers/policyContent.test.ts @@ -0,0 +1,39 @@ +import { mockServices } from '@backstage/backend-test-utils'; +import express from 'express'; +import request from 'supertest'; +import { policyContentRouter } from './policyContent'; +import { readPolicyFile } from '../../lib/read'; + +jest.mock('node-fetch'); +jest.mock('../../lib/read'); + +describe('policyContentRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const mockUrlReader = mockServices.urlReader.mock(); + const mockLogger = mockServices.logger.mock(); + + const router = policyContentRouter(mockLogger, mockUrlReader); + app = express().use(express.json()).use(router); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('GET /get-policy', () => { + it('should return the content of the policy file', async () => { + (readPolicyFile as jest.Mock).mockResolvedValue('test-policy-content'); + + const response = await request(app).get( + '/get-policy?opaPolicy=test-policy-url', + ); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + opaPolicyContent: 'test-policy-content', + }); + }); + }); +}); diff --git a/plugins/backstage-opa-backend/src/service/routers/policyContent.ts b/plugins/backstage-opa-backend/src/service/routers/policyContent.ts new file mode 100644 index 00000000..d4d79875 --- /dev/null +++ b/plugins/backstage-opa-backend/src/service/routers/policyContent.ts @@ -0,0 +1,33 @@ +import express from 'express'; +import { LoggerService, UrlReaderService } from '@backstage/backend-plugin-api'; +import { readPolicyFile } from '../../lib/read'; + +export const policyContentRouter = ( + logger: LoggerService, + urlReader: UrlReaderService, +): express.Router => { + const router = express.Router(); + + router.get('/get-policy', async (req, res, next) => { + const opaPolicy = req.query.opaPolicy as string; + + if (!opaPolicy) { + logger.error( + 'No OPA policy provided! Please check the open-policy-agent/policy annotation and provide a URL to the policy file.', + ); + } + + try { + // Fetch the content of the policy file + logger.debug(`Fetching policy file from ${opaPolicy}`); + const opaPolicyContent = await readPolicyFile(urlReader, opaPolicy); + + return res.json({ opaPolicyContent }); + } catch (error) { + logger.error('An error occurred trying to fetch the policy file:', error); + return next(error); + } + }); + + return router; +}; diff --git a/plugins/backstage-opa-entity-checker/README.md b/plugins/backstage-opa-entity-checker/README.md index 1a0c08bb..d0402b19 100644 --- a/plugins/backstage-opa-entity-checker/README.md +++ b/plugins/backstage-opa-entity-checker/README.md @@ -64,37 +64,37 @@ import future.keywords.in default good_entity := false good_entity if { - count({v | some v in violation; v.level == "error"}) == 0 + count({v | some v in violation; v.level == "error"}) == 0 } violation contains {"check_title": entity_check, "message": msg, "level": "warning"} if { - not input.metadata.tags - entity_check := "Tags" - msg := "You do not have any tags set!" + not input.metadata.tags + entity_check := "Tags" + msg := "You do not have any tags set!" } violation contains {"check_title": entity_check, "message": msg, "level": "error"} if { - valid_lifecycles = {"production", "development"} - not valid_lifecycles[input.spec.lifecycle] - entity_check := "Lifecycle" - msg := "Incorrect lifecycle, should be one of production or development" + valid_lifecycles = {"production", "development"} + not valid_lifecycles[input.spec.lifecycle] + entity_check := "Lifecycle" + msg := "Incorrect lifecycle, should be one of production or development" } violation contains {"check_title": entity_check, "message": msg, "level": "error"} if { - not is_system_present - entity_check := "System" - msg := "System is missing!" + not is_system_present + entity_check := "System" + msg := "System is missing!" } violation contains {"check_title": entity_check, "message": msg, "level": "error"} if { - valid_types = {"website", "library", "service"} - not valid_types[input.spec.type] - entity_check := "Type" - msg := "Incorrect component type!" + valid_types = {"website", "library", "service"} + not valid_types[input.spec.type] + entity_check := "Type" + msg := "Incorrect component type!" } is_system_present if { - input.spec.system + input.spec.system } ``` @@ -155,7 +155,11 @@ Please see the [Docs Site](https://parsifal-m.github.io/backstage-opa-plugins/#/ ## Contributing -I am happy to accept contributions to this plugin. Please fork the repository and open a PR with your changes. If you have any questions, please feel free to reach out to me on [Mastodon](https://hachyderm.io/@parcifal) or [Twitter](https://twitter.com/_PeterM_) (I am not as active on Twitter) +I am happy to accept contributions and suggestions for these plugins, if you are looking to make significant changes, please open an issue first to discuss the changes you would like to make! + +Please fork the repository and open a PR with your changes. If you have any questions, please feel free to reach out to me on [Mastodon](https://hachyderm.io/@parcifal). + +Please remember to sign your commits with `git commit -s` so that your commits are signed! ## License diff --git a/plugins/backstage-opa-entity-checker/src/components/OpaMetadataAnalysisCard/OpaMetadataAnalysisCard.test.tsx b/plugins/backstage-opa-entity-checker/src/components/OpaMetadataAnalysisCard/OpaMetadataAnalysisCard.test.tsx index 5be374f6..22e20937 100644 --- a/plugins/backstage-opa-entity-checker/src/components/OpaMetadataAnalysisCard/OpaMetadataAnalysisCard.test.tsx +++ b/plugins/backstage-opa-entity-checker/src/components/OpaMetadataAnalysisCard/OpaMetadataAnalysisCard.test.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { render, waitFor, screen, act } from '@testing-library/react'; +import React, { act } from 'react'; +import { render, waitFor, screen } from '@testing-library/react'; import { OpaMetadataAnalysisCard } from './OpaMetadataAnalysisCard'; import { alertApiRef } from '@backstage/core-plugin-api'; import { TestApiProvider } from '@backstage/test-utils'; diff --git a/plugins/backstage-opa-policies/README.md b/plugins/backstage-opa-policies/README.md index 167e6907..d14ebd53 100644 --- a/plugins/backstage-opa-policies/README.md +++ b/plugins/backstage-opa-policies/README.md @@ -70,3 +70,15 @@ const websiteEntityPage = ( ``` Note that using `isOpaPoliciesEnabled` will then only display the OPA Policy tab if the entity has the annotation set. + +## Contributing + +I am happy to accept contributions and suggestions for these plugins, if you are looking to make significant changes, please open an issue first to discuss the changes you would like to make! + +Please fork the repository and open a PR with your changes. If you have any questions, please feel free to reach out to me on [Mastodon](https://hachyderm.io/@parcifal). + +Please remember to sign your commits with `git commit -s` so that your commits are signed! + +## License + +This project is released under the Apache 2.0 License. diff --git a/plugins/backstage-opa-policies/package.json b/plugins/backstage-opa-policies/package.json index d91190a4..2ae57ef5 100644 --- a/plugins/backstage-opa-policies/package.json +++ b/plugins/backstage-opa-policies/package.json @@ -50,7 +50,7 @@ "react-use": "^17.2.4" }, "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + "react": "^17.0.0 || ^18.0.0" }, "devDependencies": { "@backstage/cli": "^0.28.0", diff --git a/plugins/backstage-opa-policies/src/api/types.ts b/plugins/backstage-opa-policies/src/api/types.ts index 2b53056d..fe8cc4da 100644 --- a/plugins/backstage-opa-policies/src/api/types.ts +++ b/plugins/backstage-opa-policies/src/api/types.ts @@ -1,7 +1,7 @@ import { createApiRef } from '@backstage/core-plugin-api'; export type OpaPolicy = { - policyContent: string; + opaPolicyContent: string; }; export interface OpaPolicyBackendApi { diff --git a/plugins/backstage-opa-policies/src/components/OpaPolicyComponent/OpaPolicyComponent.test.tsx b/plugins/backstage-opa-policies/src/components/OpaPolicyComponent/OpaPolicyComponent.test.tsx index 7448890b..8a030e5e 100644 --- a/plugins/backstage-opa-policies/src/components/OpaPolicyComponent/OpaPolicyComponent.test.tsx +++ b/plugins/backstage-opa-policies/src/components/OpaPolicyComponent/OpaPolicyComponent.test.tsx @@ -1,8 +1,8 @@ -import { act, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { OpaPolicyPage } from './OpaPolicyComponent'; import { opaPolicyBackendApiRef } from '../../api/types'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import React from 'react'; +import React, { act } from 'react'; import { alertApiRef } from '@backstage/core-plugin-api'; jest.mock('@backstage/plugin-catalog-react', () => ({ @@ -26,13 +26,13 @@ const mockAlertApi = { const mockOpaBackendApi = { getPolicyFromRepo: jest .fn() - .mockResolvedValue({ policyContent: 'test-policy-content' }), + .mockResolvedValue({ opaPolicyContent: 'test-policy-content' }), }; describe('OpaPolicyPage', () => { it('renders without crashing', async () => { mockOpaBackendApi.getPolicyFromRepo.mockResolvedValueOnce({ - policyContent: 'test-policy-content', + opaPolicyContent: 'test-policy-content', }); await act(async () => { renderInTestApp( diff --git a/plugins/backstage-opa-policies/src/components/OpaPolicyComponent/OpaPolicyComponent.tsx b/plugins/backstage-opa-policies/src/components/OpaPolicyComponent/OpaPolicyComponent.tsx index 7b3b4274..b3deecd9 100644 --- a/plugins/backstage-opa-policies/src/components/OpaPolicyComponent/OpaPolicyComponent.tsx +++ b/plugins/backstage-opa-policies/src/components/OpaPolicyComponent/OpaPolicyComponent.tsx @@ -22,7 +22,7 @@ export const OpaPolicyPage = () => { if (opaPolicy) { try { const response = await opaApi.getPolicyFromRepo(opaPolicy); - if (response.policyContent) { + if (response.opaPolicyContent) { setPolicy(response); setLoading(false); } @@ -49,7 +49,7 @@ export const OpaPolicyPage = () => { data-testid="opa-policy-card" > ScmIntegrationsApi.fromConfig(configApi), + }), + // Add the OPA Authz API + createApiFactory({ + api: opaAuthzBackendApiRef, + deps: { + fetchApi: fetchApiRef, + }, + factory: ({ fetchApi }) => new OpaAuthzClientReact({ fetchApi }), + }), + ScmAuth.createDefaultApiFactory(), +]; +``` + +### Using the `RequireOpaAuthz` component + +To control and hide a component based on the result of a policy evaluation, you can use the `RequireOpaAuthz` component. + +Install the library first to your Backstage plugin: + +```bash +yarn add --cwd @parsifal-m/backstage-plugin-opa-authz-react +``` + +```tsx +import { RequireOpaAuthz } from '@parsifal-m/backstage-plugin-opa-authz-react'; + +// Some code... + +return ( + + + +); +``` + +The above will render `MyComponent` only if the policy evaluation `allow` is `true`. It will send to OPA the input `{ action: 'read-policy' }` and the entry point `authz`. + +### Using the `useOpaAuthz` hook directly (optional) + +If you want to use the `useOpaAuthz` hook directly, you can do so: + +```tsx +import React from 'react'; +import { useOpaAuthz } from '@parsifal-m/backstage-plugin-opa-authz-react'; + +const MyComponent = () => { + const { loading, data, error } = useOpaAuthz( + { action: 'read-policy' }, + 'authz', + ); + + if (loading) { + return
Loading...
; + } + + if (error || !data?.result.allow) { + return
Access Denied
; + } + + return
Content
; +}; +``` + +## Example Demo Plugin(s) + +To help visualize how this library can be used, we have created a demo plugin that demonstrates how to use the `RequireOpaAuthz` component in the frontend, you can find the demo code [here](../../plugins/opa-frontend-demo/README.md). + +## Contributing + +I am happy to accept contributions and suggestions for these plugins, if you are looking to make significant changes, please open an issue first to discuss the changes you would like to make! + +Please fork the repository and open a PR with your changes. If you have any questions, please feel free to reach out to me on [Mastodon](https://hachyderm.io/@parcifal). + +Please remember to sign your commits with `git commit -s` so that your commits are signed! + +## License + +This project is released under the Apache 2.0 License. diff --git a/plugins/opa-authz-react/package.json b/plugins/opa-authz-react/package.json new file mode 100644 index 00000000..e9bf8617 --- /dev/null +++ b/plugins/opa-authz-react/package.json @@ -0,0 +1,63 @@ +{ + "name": "@parsifal-m/backstage-plugin-opa-authz-react", + "description": "A Backstage frontend plugin that allows you to use OPA for authorization in the Backstage frontend", + "version": "1.0.0-beta", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library", + "pluginId": "opa-authz", + "pluginPackages": [ + "@parsifal-m/backstage-plugin-opa-authz-react" + ] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Parsifal-M/backstage-opa-plugins.git" + }, + "keywords": [ + "backstage", + "OPA", + "Open Policy Agent", + "permissions", + "RBAC", + "frontend", + "plugin" + ], + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "fix": "backstage-cli repo fix --publish" + }, + "dependencies": { + "@backstage/core-plugin-api": "^1.9.3", + "@material-ui/core": "^4.9.13", + "swr": "^2.2.5" + }, + "peerDependencies": { + "react": "^18.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.26.10", + "@backstage/test-utils": "^1.5.7", + "@testing-library/dom": "^9.0.0", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^14.0.0", + "msw": "^1.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/opa-authz-react/src/api/api.test.ts b/plugins/opa-authz-react/src/api/api.test.ts new file mode 100644 index 00000000..d9db2b23 --- /dev/null +++ b/plugins/opa-authz-react/src/api/api.test.ts @@ -0,0 +1,90 @@ +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; +import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; +import { OpaAuthzClientReact } from './api'; +import { PolicyInput, PolicyResult } from './types'; + +const mockBaseUrl = 'http://mock'; +const server = setupServer(); + +describe('OpaAuthzClientReact', () => { + registerMswTestHooks(server); + const fetchApi = new MockFetchApi({ + resolvePluginProtocol: { + discoveryApi: { + getBaseUrl: async () => mockBaseUrl, + }, + }, + }); + + let client: OpaAuthzClientReact; + + beforeEach(() => { + client = new OpaAuthzClientReact({ fetchApi }); + }); + + describe('evalPolicy', () => { + it('should call the correct endpoint', async () => { + const input: PolicyInput = { + user: 'test-user', + action: 'read', + resource: 'document', + }; + const entryPoint = 'example/allow'; + const mockResponse: PolicyResult = { + decision_id: '12345', + result: { allow: true }, + }; + + server.use( + rest.post(`${mockBaseUrl}/opa-authz`, async (req, res, ctx) => { + const requestBody = await req.json(); + expect(requestBody).toEqual({ input, entryPoint }); + return res(ctx.json(mockResponse)); + }), + ); + + const response = await client.evalPolicy(input, entryPoint); + expect(response).toEqual(mockResponse); + }); + + it('should throw an error on unsuccessful response with details', async () => { + const input: PolicyInput = { + user: 'test-user', + action: 'read', + resource: 'document', + }; + const entryPoint = 'example/allow'; + const mockErrorResponse = { error: 'Some error details' }; + + server.use( + rest.post(`${mockBaseUrl}/opa-authz`, async (_req, res, ctx) => { + return res(ctx.status(400), ctx.json(mockErrorResponse)); + }), + ); + + await expect(client.evalPolicy(input, entryPoint)).rejects.toThrow( + 'Error 400: Bad Request.', + ); + }); + + it('should throw an error on unsuccessful response without details', async () => { + const input: PolicyInput = { + user: 'test-user', + action: 'read', + resource: 'document', + }; + const entryPoint = 'example/allow'; + + server.use( + rest.post(`${mockBaseUrl}/opa-authz`, async (_req, res, ctx) => { + return res(ctx.status(500), ctx.text('Internal Server Error')); + }), + ); + + await expect(client.evalPolicy(input, entryPoint)).rejects.toThrow( + 'Error 500: Internal Server Error.', + ); + }); + }); +}); diff --git a/plugins/opa-authz-react/src/api/api.ts b/plugins/opa-authz-react/src/api/api.ts new file mode 100644 index 00000000..938ec772 --- /dev/null +++ b/plugins/opa-authz-react/src/api/api.ts @@ -0,0 +1,36 @@ +import { FetchApi } from '@backstage/core-plugin-api'; +import { OpaAuthzApi, PolicyInput, PolicyResult } from './types'; + +export class OpaAuthzClientReact implements OpaAuthzApi { + private readonly fetchApi: FetchApi; + constructor(options: { fetchApi: FetchApi }) { + this.fetchApi = options.fetchApi; + } + + async evalPolicy( + input: PolicyInput, + entryPoint: string, + ): Promise { + const url = `plugin://opa/opa-authz`; + + const response = await this.fetchApi.fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ input, entryPoint }), + }); + + if (!response.ok) { + const message = `Error ${response.status}: ${response.statusText}.`; + + try { + const responseBody = await response.json(); + throw new Error(`${message} ${responseBody.error}`); + } catch (error) { + throw new Error(message); + } + } + return response.json(); + } +} diff --git a/plugins/opa-authz-react/src/api/index.ts b/plugins/opa-authz-react/src/api/index.ts new file mode 100644 index 00000000..4d4b4e29 --- /dev/null +++ b/plugins/opa-authz-react/src/api/index.ts @@ -0,0 +1,2 @@ +export * from './api'; +export * from './types'; diff --git a/plugins/opa-authz-react/src/api/types.ts b/plugins/opa-authz-react/src/api/types.ts new file mode 100644 index 00000000..6105e0ce --- /dev/null +++ b/plugins/opa-authz-react/src/api/types.ts @@ -0,0 +1,19 @@ +import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; + +export type PolicyInput = Record; + +export type PolicyResult = { + decision_id?: string; + result: { + allow: boolean; + }; +}; + +export type OpaAuthzApi = { + evalPolicy(input: PolicyInput, entryPoint: string): Promise; +}; + +export const opaAuthzBackendApiRef: ApiRef = + createApiRef({ + id: 'plugin.opa-authz.api', + }); diff --git a/plugins/opa-authz-react/src/components/OpaAuthzComponent/RequireOpaAuthz.test.tsx b/plugins/opa-authz-react/src/components/OpaAuthzComponent/RequireOpaAuthz.test.tsx new file mode 100644 index 00000000..e130e46d --- /dev/null +++ b/plugins/opa-authz-react/src/components/OpaAuthzComponent/RequireOpaAuthz.test.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import { RequireOpaAuthz } from './RequireOpaAuthz'; +import { useOpaAuthz } from '../../hooks/useOpaAuthz/useOpaAuthz'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { opaAuthzBackendApiRef } from '../../api'; + +// Mock the useOpaAuthz hook +jest.mock('../../hooks/useOpaAuthz/useOpaAuthz'); + +const mockOpaBackendApi = { + evalPolicy: jest.fn().mockResolvedValue({ result: { allow: true } }), +}; + +describe('RequireOpaAuthz', () => { + const mockInput = { user: 'test-user', action: 'read', resource: 'document' }; + const mockEntryPoint = 'example/allow'; + + it('renders null when loading', async () => { + (useOpaAuthz as jest.Mock).mockReturnValue({ loading: true, data: null }); + + renderInTestApp( + + +
Protected Content
+
+
, + ); + + await waitFor(() => { + expect(screen.queryByText('Protected Content')).toBeNull(); + }); + }); + + it('renders null when there is an error', async () => { + (useOpaAuthz as jest.Mock).mockReturnValue({ + loading: false, + data: null, + error: new Error('Error'), + }); + + renderInTestApp( + + +
Protected Content
+
+
, + ); + + await waitFor(() => { + expect(screen.queryByText('Protected Content')).toBeNull(); + }); + }); + + it('renders null when access is not allowed', async () => { + (useOpaAuthz as jest.Mock).mockReturnValue({ + loading: false, + data: { result: { allow: false } }, + }); + + renderInTestApp( + + +
Protected Content
+
+
, + ); + + await waitFor(() => { + expect(screen.queryByText('Protected Content')).toBeNull(); + }); + }); + + it('renders children when access is allowed', async () => { + (useOpaAuthz as jest.Mock).mockReturnValue({ + loading: false, + data: { result: { allow: true } }, + }); + + renderInTestApp( + + +
Protected Content
+
+
, + ); + + await waitFor(() => { + expect(screen.getByText('Protected Content')).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/opa-authz-react/src/components/OpaAuthzComponent/RequireOpaAuthz.tsx b/plugins/opa-authz-react/src/components/OpaAuthzComponent/RequireOpaAuthz.tsx new file mode 100644 index 00000000..50b2a204 --- /dev/null +++ b/plugins/opa-authz-react/src/components/OpaAuthzComponent/RequireOpaAuthz.tsx @@ -0,0 +1,24 @@ +import React, { ReactNode } from 'react'; +import { PolicyInput } from '../../api/types'; +import { useOpaAuthz } from '../../hooks/useOpaAuthz/useOpaAuthz'; + +interface RequireOpaAuthzProps { + input: PolicyInput; + entryPoint: string; + errorPage?: ReactNode; + children: ReactNode; +} + +export function RequireOpaAuthz( + props: Readonly, +): React.JSX.Element | null { + const { input, entryPoint } = props; + + const { loading, data, error } = useOpaAuthz(input, entryPoint); + + if (loading || error || !data?.result.allow) { + return null; + } + + return <>{props.children}; +} diff --git a/plugins/opa-authz-react/src/components/OpaAuthzComponent/index.ts b/plugins/opa-authz-react/src/components/OpaAuthzComponent/index.ts new file mode 100644 index 00000000..3b589937 --- /dev/null +++ b/plugins/opa-authz-react/src/components/OpaAuthzComponent/index.ts @@ -0,0 +1 @@ +export { RequireOpaAuthz } from './RequireOpaAuthz'; diff --git a/plugins/opa-authz-react/src/hooks/useOpaAuthz/index.ts b/plugins/opa-authz-react/src/hooks/useOpaAuthz/index.ts new file mode 100644 index 00000000..3e44ea11 --- /dev/null +++ b/plugins/opa-authz-react/src/hooks/useOpaAuthz/index.ts @@ -0,0 +1 @@ +export { useOpaAuthz } from './useOpaAuthz'; diff --git a/plugins/opa-authz-react/src/hooks/useOpaAuthz/useOpaAuthz.test.ts b/plugins/opa-authz-react/src/hooks/useOpaAuthz/useOpaAuthz.test.ts new file mode 100644 index 00000000..80fa7f11 --- /dev/null +++ b/plugins/opa-authz-react/src/hooks/useOpaAuthz/useOpaAuthz.test.ts @@ -0,0 +1,31 @@ +import { waitFor, renderHook } from '@testing-library/react'; +import { useOpaAuthz } from './useOpaAuthz'; +import { useApi } from '@backstage/core-plugin-api'; +import { OpaAuthzApi } from '../../api'; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest.fn(), +})); + +describe('useOpaAuthz', () => { + const mockEvalPolicy = jest.fn(); + + beforeEach(() => { + (useApi as jest.Mock).mockReturnValue({ + evalPolicy: mockEvalPolicy, + } as unknown as OpaAuthzApi); + }); + + it('should return the policy result', async () => { + mockEvalPolicy.mockResolvedValue({ result: { allow: true } }); + + const { result } = renderHook(() => + useOpaAuthz({ entity: 'test' }, 'test'), + ); + + await waitFor(() => result.current.data !== null); + + expect(result.current.data).toEqual({ result: { allow: true } }); + }); +}); diff --git a/plugins/opa-authz-react/src/hooks/useOpaAuthz/useOpaAuthz.ts b/plugins/opa-authz-react/src/hooks/useOpaAuthz/useOpaAuthz.ts new file mode 100644 index 00000000..163b1dd7 --- /dev/null +++ b/plugins/opa-authz-react/src/hooks/useOpaAuthz/useOpaAuthz.ts @@ -0,0 +1,34 @@ +import { useApi } from '@backstage/core-plugin-api'; +import { + opaAuthzBackendApiRef, + PolicyInput, + PolicyResult, +} from '../../api/types'; +import useSWR from 'swr'; + +export type AsyncOpaAuthzResult = { + loading: boolean; + data: PolicyResult | null; + error?: Error; +}; + +export function useOpaAuthz( + input: PolicyInput, + entryPoint: string, +): AsyncOpaAuthzResult { + const opaAuthzBackendApi = useApi(opaAuthzBackendApiRef); + + const { data, error } = useSWR(input, async (authzInput: PolicyInput) => { + return await opaAuthzBackendApi.evalPolicy(authzInput, entryPoint); + }); + + if (error) { + return { error, loading: false, data: null }; + } + + if (!data?.result) { + return { loading: true, data: null }; + } + + return { loading: false, data: data }; +} diff --git a/plugins/opa-authz-react/src/index.ts b/plugins/opa-authz-react/src/index.ts new file mode 100644 index 00000000..db9d7bfc --- /dev/null +++ b/plugins/opa-authz-react/src/index.ts @@ -0,0 +1,3 @@ +export * from './components/OpaAuthzComponent'; +export * from './hooks/useOpaAuthz'; +export * from './api'; diff --git a/plugins/opa-authz-react/src/setupTests.ts b/plugins/opa-authz-react/src/setupTests.ts new file mode 100644 index 00000000..7b0828bf --- /dev/null +++ b/plugins/opa-authz-react/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/plugins/opa-demo-backend/.eslintrc.js b/plugins/opa-demo-backend/.eslintrc.js new file mode 100644 index 00000000..e2a53a6a --- /dev/null +++ b/plugins/opa-demo-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/opa-demo-backend/README.md b/plugins/opa-demo-backend/README.md new file mode 100644 index 00000000..83c39f85 --- /dev/null +++ b/plugins/opa-demo-backend/README.md @@ -0,0 +1,9 @@ +# opa-backend-demo + +This plugin serves as a demo for: + +- [opa-authz](../../packages/opa-authz/README.md) - A node-library plugin that allows you to use OPA to make authorization decisions for your backend services and APIs! + +It is using the `OpaAuthzClient` and `opaAuthzMiddleware` to protect the plugin endpoints. + +The policy can be found here: [opa_demo](../../policies/opa_demo.rego) diff --git a/plugins/opa-demo-backend/dev/index.ts b/plugins/opa-demo-backend/dev/index.ts new file mode 100644 index 00000000..8f5e1d33 --- /dev/null +++ b/plugins/opa-demo-backend/dev/index.ts @@ -0,0 +1,60 @@ +import { createBackend } from '@backstage/backend-defaults'; +import { mockServices } from '@backstage/backend-test-utils'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; + +// TEMPLATE NOTE: +// This is the development setup for your plugin that wires up a +// minimal backend that can use both real and mocked plugins and services. +// +// Start up the backend by running `yarn start` in the package directory. +// Once it's up and running, try out the following requests: +// +// Create a new todo item, standalone or for the sample component: +// +// curl http://localhost:7007/api/opa-demo/todos -H 'Content-Type: application/json' -d '{"title": "My Todo"}' +// curl http://localhost:7007/api/opa-demo/todos -H 'Content-Type: application/json' -d '{"title": "My Todo", "entityRef": "component:default/sample"}' +// +// List TODOs: +// +// curl http://localhost:7007/api/opa-demo/todos +// +// Explicitly make an unauthenticated request, or with service auth: +// +// curl http://localhost:7007/api/opa-demo/todos -H 'Authorization: Bearer mock-none-token' +// curl http://localhost:7007/api/opa-demo/todos -H 'Authorization: Bearer mock-service-token' + +const backend = createBackend(); + +// TEMPLATE NOTE: +// Mocking the auth and httpAuth service allows you to call your plugin API without +// having to authenticate. +// +// If you want to use real auth, you can install the following instead: +// backend.add(import('@backstage/plugin-auth-backend')); +// backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); +backend.add(mockServices.auth.factory()); +backend.add(mockServices.httpAuth.factory()); + +// TEMPLATE NOTE: +// Rather than using a real catalog you can use a mock with a fixed set of entities. +backend.add( + catalogServiceMock.factory({ + entities: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'sample', + title: 'Sample Component', + }, + spec: { + type: 'service', + }, + }, + ], + }), +); + +backend.add(import('../src')); + +backend.start(); diff --git a/plugins/opa-demo-backend/package.json b/plugins/opa-demo-backend/package.json new file mode 100644 index 00000000..703b5871 --- /dev/null +++ b/plugins/opa-demo-backend/package.json @@ -0,0 +1,47 @@ +{ + "name": "@internal/backstage-plugin-opa-demo-backend", + "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": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-defaults": "^0.5.1", + "@backstage/backend-plugin-api": "^1.0.1", + "@backstage/catalog-client": "^1.7.1", + "@backstage/config": "^1.2.0", + "@backstage/errors": "^1.2.4", + "@backstage/plugin-catalog-node": "^1.13.1", + "@parsifal-m/backstage-opa-authz": "workspace:^", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^1.0.1", + "@backstage/cli": "^0.28.0", + "@types/express": "*", + "@types/supertest": "^2.0.12", + "supertest": "^6.2.4" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/opa-demo-backend/src/index.ts b/plugins/opa-demo-backend/src/index.ts new file mode 100644 index 00000000..e0b6ed04 --- /dev/null +++ b/plugins/opa-demo-backend/src/index.ts @@ -0,0 +1 @@ +export { opaDemoPlugin as default } from './plugin'; diff --git a/plugins/opa-demo-backend/src/plugin.ts b/plugins/opa-demo-backend/src/plugin.ts new file mode 100644 index 00000000..f1c23ae7 --- /dev/null +++ b/plugins/opa-demo-backend/src/plugin.ts @@ -0,0 +1,44 @@ +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './router'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { createTodoListService } from './services/TodoListService'; + +/** + * opaDemoPlugin backend plugin + * + * @public + */ +export const opaDemoPlugin = createBackendPlugin({ + pluginId: 'opa-demo', + register(env) { + env.registerInit({ + deps: { + logger: coreServices.logger, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + httpRouter: coreServices.httpRouter, + catalog: catalogServiceRef, + config: coreServices.rootConfig, + }, + async init({ logger, auth, httpAuth, httpRouter, catalog, config }) { + const todoListService = await createTodoListService({ + logger, + auth, + catalog, + }); + + httpRouter.use( + await createRouter({ + httpAuth, + todoListService, + logger, + config, + }), + ); + }, + }); + }, +}); diff --git a/plugins/opa-demo-backend/src/router.ts b/plugins/opa-demo-backend/src/router.ts new file mode 100644 index 00000000..9ca18212 --- /dev/null +++ b/plugins/opa-demo-backend/src/router.ts @@ -0,0 +1,83 @@ +import { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api'; +import { InputError } from '@backstage/errors'; +import { z } from 'zod'; +import express from 'express'; +import Router from 'express-promise-router'; +import { TodoListService } from './services/TodoListService/types'; +import { Config } from '@backstage/config'; +import { + OpaAuthzClient, + opaAuthzMiddleware, +} from '@parsifal-m/backstage-opa-authz'; + +export async function createRouter({ + todoListService, + config, + logger, +}: { + httpAuth: HttpAuthService; + config: Config; + logger: LoggerService; + todoListService: TodoListService; +}): Promise { + const router = Router(); + router.use(express.json()); + + // TEMPLATE NOTE: + // Zod is a powerful library for data validation and recommended in particular + // for user-defined schemas. In this case we use it for input validation too. + // + // If you want to define a schema for your API we recommend using Backstage's + // OpenAPI tooling: https://backstage.io/docs/next/openapi/01-getting-started + const todoSchema = z.object({ + title: z.string(), + entityRef: z.string().optional(), + }); + + const opaAuthzClient = new OpaAuthzClient(logger, config); + + const entryPoint = 'opa_demo'; + + const setInput = (req: express.Request) => { + return { + method: req.method, + path: req.path, + permission: { name: 'read' }, + plugin: 'opa-demo-backend-todo', + dateTime: new Date().toISOString(), + }; + }; + + router.post( + '/todos', + opaAuthzMiddleware(opaAuthzClient, entryPoint, setInput), + async (req, res) => { + const parsed = todoSchema.safeParse(req.body); + if (!parsed.success) { + throw new InputError(parsed.error.toString()); + } + + const result = await todoListService.createTodo(parsed.data); + + res.status(201).json(result); + }, + ); + + router.get( + '/todos', + opaAuthzMiddleware(opaAuthzClient, entryPoint, setInput), + async (_req, res) => { + res.json(await todoListService.listTodos()); + }, + ); + + router.get( + '/todos/:id', + opaAuthzMiddleware(opaAuthzClient, entryPoint, setInput), + async (req, res) => { + res.json(await todoListService.getTodo({ id: req.params.id })); + }, + ); + + return router; +} diff --git a/plugins/opa-demo-backend/src/services/TodoListService/createTodoListService.ts b/plugins/opa-demo-backend/src/services/TodoListService/createTodoListService.ts new file mode 100644 index 00000000..5c702660 --- /dev/null +++ b/plugins/opa-demo-backend/src/services/TodoListService/createTodoListService.ts @@ -0,0 +1,94 @@ +import { AuthService, LoggerService } from '@backstage/backend-plugin-api'; +import { NotFoundError } from '@backstage/errors'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import crypto from 'node:crypto'; +import { TodoItem, TodoListService } from './types'; + +// TEMPLATE NOTE: +// This is a simple in-memory todo list store. It is recommended to use a +// database to store data in a real application. See the database service +// documentation for more information on how to do this: +// https://backstage.io/docs/backend-system/core-services/database +export async function createTodoListService({ + logger, + catalog, +}: { + auth: AuthService; + logger: LoggerService; + catalog: typeof catalogServiceRef.T; +}): Promise { + logger.info('Initializing TodoListService'); + + const storedTodos = new Array(); + + return { + async createTodo(input) { + let title = input.title; + + // TEMPLATE NOTE: + // A common pattern for Backstage plugins is to pass an entity reference + // from the frontend to then fetch the entire entity from the catalog in the + // backend plugin. + if (input.entityRef) { + // TEMPLATE NOTE: + // Cross-plugin communication uses service-to-service authentication. The + // `AuthService` lets you generate a token that is valid for communication + // with the target plugin only. You must also provide credentials for the + // identity that you are making the request on behalf of. + // + // If you want to make a request using the plugin backend's own identity, + // you can access it via the `auth.getOwnServiceCredentials()` method. + // Beware that this bypasses any user permission checks. + + const entity = await catalog.getEntityByRef(input.entityRef); + if (!entity) { + throw new NotFoundError( + `No entity found for ref '${input.entityRef}'`, + ); + } + + // TEMPLATE NOTE: + // Here you could read any form of data from the entity. A common use case + // is to read the value of a custom annotation for your plugin. You can + // read more about how to add custom annotations here: + // https://backstage.io/docs/features/software-catalog/extending-the-model#adding-a-new-annotation + // + // In this example we just use the entity title to decorate the todo item. + + const entityDisplay = entity.metadata.title ?? input.entityRef; + title = `[${entityDisplay}] ${input.title}`; + } + + const id = crypto.randomUUID(); + const createdBy = 'opa-guy'; + const newTodo = { + title, + id, + createdBy, + createdAt: new Date().toISOString(), + }; + + storedTodos.push(newTodo); + + // TEMPLATE NOTE: + // The second argument of the logger methods can be used to pass + // structured metadata. You can read more about the logger service here: + // https://backstage.io/docs/backend-system/core-services/logger + logger.info('Created new todo item', { id, title, createdBy }); + + return newTodo; + }, + + async listTodos() { + return { items: Array.from(storedTodos) }; + }, + + async getTodo(request: { id: string }) { + const todo = storedTodos.find(item => item.id === request.id); + if (!todo) { + throw new NotFoundError(`No todo found with id '${request.id}'`); + } + return todo; + }, + }; +} diff --git a/plugins/opa-demo-backend/src/services/TodoListService/index.ts b/plugins/opa-demo-backend/src/services/TodoListService/index.ts new file mode 100644 index 00000000..1bc52001 --- /dev/null +++ b/plugins/opa-demo-backend/src/services/TodoListService/index.ts @@ -0,0 +1 @@ +export { createTodoListService } from './createTodoListService'; diff --git a/plugins/opa-demo-backend/src/services/TodoListService/types.ts b/plugins/opa-demo-backend/src/services/TodoListService/types.ts new file mode 100644 index 00000000..66f1dc3b --- /dev/null +++ b/plugins/opa-demo-backend/src/services/TodoListService/types.ts @@ -0,0 +1,19 @@ +import { + BackstageCredentials, + BackstageUserPrincipal, +} from '@backstage/backend-plugin-api'; + +export interface TodoItem { + title: string; + id: string; + createdBy: string; + createdAt: string; +} + +export interface TodoListService { + createTodo(input: { title: string; entityRef?: string }): Promise; + + listTodos(): Promise<{ items: TodoItem[] }>; + + getTodo(request: { id: string }): Promise; +} diff --git a/plugins/opa-demo-backend/src/setupTests.ts b/plugins/opa-demo-backend/src/setupTests.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/plugins/opa-demo-backend/src/setupTests.ts @@ -0,0 +1 @@ +export {}; diff --git a/plugins/opa-frontend-demo/.eslintrc.js b/plugins/opa-frontend-demo/.eslintrc.js new file mode 100644 index 00000000..e2a53a6a --- /dev/null +++ b/plugins/opa-frontend-demo/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/opa-frontend-demo/README.md b/plugins/opa-frontend-demo/README.md new file mode 100644 index 00000000..66c2492a --- /dev/null +++ b/plugins/opa-frontend-demo/README.md @@ -0,0 +1,9 @@ +# opa-frontend-demo + +This plugin serves as a demo for: + +- [backstage-opa-authz](./plugins/opa-authz-react/README.md) - A frontend plugin that allows you to make authorization decisions in the frontend using OPA! (Think controlling the visibility of components based on the user's permissions) + +It has no real functionality other than to show how the `RequireOpaAuthz` component works. + +The policy can be found here: [opa_demo](../../policies/opa_demo.rego) diff --git a/plugins/opa-frontend-demo/dev/index.tsx b/plugins/opa-frontend-demo/dev/index.tsx new file mode 100644 index 00000000..8da9ae20 --- /dev/null +++ b/plugins/opa-frontend-demo/dev/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { opaFrontendDemoPlugin, OpaFrontendDemoPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(opaFrontendDemoPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/opa-frontend-demo', + }) + .render(); diff --git a/plugins/opa-frontend-demo/package.json b/plugins/opa-frontend-demo/package.json new file mode 100644 index 00000000..80261c92 --- /dev/null +++ b/plugins/opa-frontend-demo/package.json @@ -0,0 +1,53 @@ +{ + "name": "@internal/backstage-plugin-opa-frontend-demo", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "^0.15.1", + "@backstage/core-plugin-api": "^1.9.3", + "@backstage/theme": "^0.6.0", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.61", + "@parsifal-m/backstage-plugin-opa-authz-react": "workspace:^", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.28.0", + "@backstage/core-app-api": "^1.15.1", + "@backstage/dev-utils": "^1.1.2", + "@backstage/test-utils": "^1.7.0", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.0.0", + "msw": "^1.0.0", + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/opa-frontend-demo/src/components/ExampleComponent/ExampleComponent.test.tsx b/plugins/opa-frontend-demo/src/components/ExampleComponent/ExampleComponent.test.tsx new file mode 100644 index 00000000..60debd86 --- /dev/null +++ b/plugins/opa-frontend-demo/src/components/ExampleComponent/ExampleComponent.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { ExampleComponent } from './ExampleComponent'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { screen } from '@testing-library/react'; +import { registerMswTestHooks, renderInTestApp } from '@backstage/test-utils'; + +describe('ExampleComponent', () => { + const server = setupServer(); + // Enable sane handlers for network requests + registerMswTestHooks(server); + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + }); + + it('should render', async () => { + await renderInTestApp(); + expect( + screen.getByText('Welcome to opa-frontend-demo!'), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/opa-frontend-demo/src/components/ExampleComponent/ExampleComponent.tsx b/plugins/opa-frontend-demo/src/components/ExampleComponent/ExampleComponent.tsx new file mode 100644 index 00000000..f5d0c1fe --- /dev/null +++ b/plugins/opa-frontend-demo/src/components/ExampleComponent/ExampleComponent.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { Typography, Grid } from '@material-ui/core'; +import { + InfoCard, + Header, + Page, + Content, + ContentHeader, + HeaderLabel, + SupportButton, +} from '@backstage/core-components'; +import { ExampleFetchComponent } from '../ExampleFetchComponent'; +import { RequireOpaAuthz } from '@parsifal-m/backstage-plugin-opa-authz-react'; + +// We can set permissions based on the day of the week to display the table +const daysOfWeek = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', +]; + +const currentDate = new Date(); +const dayOfWeek = daysOfWeek[currentDate.getDay()]; + +const opaInput = { + day: dayOfWeek, +}; + +export const ExampleComponent = () => ( + + + +
+ + +
+
+ + + + Help! + + + + + + + + This card is conditionally rendered based on the OPA policy! + + + + + {/* Table is rendered based on the day of the week! */} + + + + + + + +
+
+); diff --git a/plugins/opa-frontend-demo/src/components/ExampleComponent/index.ts b/plugins/opa-frontend-demo/src/components/ExampleComponent/index.ts new file mode 100644 index 00000000..8b843752 --- /dev/null +++ b/plugins/opa-frontend-demo/src/components/ExampleComponent/index.ts @@ -0,0 +1 @@ +export { ExampleComponent } from './ExampleComponent'; diff --git a/plugins/opa-frontend-demo/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx b/plugins/opa-frontend-demo/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx new file mode 100644 index 00000000..1e746ff3 --- /dev/null +++ b/plugins/opa-frontend-demo/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { ExampleFetchComponent } from './ExampleFetchComponent'; + +describe('ExampleFetchComponent', () => { + it('renders the user table', async () => { + render(); + + // Wait for the table to render + const table = await screen.findByRole('table'); + const nationality = screen.getAllByText('GB'); + // Assert that the table contains the expected user data + expect(table).toBeInTheDocument(); + expect(screen.getByAltText('Carolyn')).toBeInTheDocument(); + expect(screen.getByText('Carolyn Moore')).toBeInTheDocument(); + expect(screen.getByText('carolyn.moore@example.com')).toBeInTheDocument(); + expect(nationality[0]).toBeInTheDocument(); + }); +}); diff --git a/plugins/opa-frontend-demo/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx b/plugins/opa-frontend-demo/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx new file mode 100644 index 00000000..fff5e731 --- /dev/null +++ b/plugins/opa-frontend-demo/src/components/ExampleFetchComponent/ExampleFetchComponent.tsx @@ -0,0 +1,308 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { + Table, + TableColumn, + Progress, + ResponseErrorPanel, +} from '@backstage/core-components'; +import useAsync from 'react-use/lib/useAsync'; + +export const exampleUsers = { + results: [ + { + gender: 'female', + name: { + title: 'Miss', + first: 'Carolyn', + last: 'Moore', + }, + email: 'carolyn.moore@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Carolyn', + nat: 'GB', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Esma', + last: 'Berberoğlu', + }, + email: 'esma.berberoglu@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Esma', + nat: 'TR', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Isabella', + last: 'Rhodes', + }, + email: 'isabella.rhodes@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Isabella', + nat: 'GB', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Derrick', + last: 'Carter', + }, + email: 'derrick.carter@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Derrick', + nat: 'IE', + }, + { + gender: 'female', + name: { + title: 'Miss', + first: 'Mattie', + last: 'Lambert', + }, + email: 'mattie.lambert@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mattie', + nat: 'AU', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Mijat', + last: 'Rakić', + }, + email: 'mijat.rakic@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mijat', + nat: 'RS', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Javier', + last: 'Reid', + }, + email: 'javier.reid@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Javier', + nat: 'US', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Isabella', + last: 'Li', + }, + email: 'isabella.li@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Isabella', + nat: 'CA', + }, + { + gender: 'female', + name: { + title: 'Mrs', + first: 'Stephanie', + last: 'Garrett', + }, + email: 'stephanie.garrett@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Stephanie', + nat: 'AU', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Antonia', + last: 'Núñez', + }, + email: 'antonia.nunez@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Antonia', + nat: 'ES', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Donald', + last: 'Young', + }, + email: 'donald.young@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Donald', + nat: 'US', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Iegor', + last: 'Holodovskiy', + }, + email: 'iegor.holodovskiy@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Iegor', + nat: 'UA', + }, + { + gender: 'female', + name: { + title: 'Madame', + first: 'Jessica', + last: 'David', + }, + email: 'jessica.david@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Jessica', + nat: 'CH', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Eve', + last: 'Martinez', + }, + email: 'eve.martinez@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Eve', + nat: 'FR', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Caleb', + last: 'Silva', + }, + email: 'caleb.silva@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Caleb', + nat: 'US', + }, + { + gender: 'female', + name: { + title: 'Miss', + first: 'Marcia', + last: 'Jenkins', + }, + email: 'marcia.jenkins@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Marcia', + nat: 'US', + }, + { + gender: 'female', + name: { + title: 'Mrs', + first: 'Mackenzie', + last: 'Jones', + }, + email: 'mackenzie.jones@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Mackenzie', + nat: 'NZ', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Jeremiah', + last: 'Gutierrez', + }, + email: 'jeremiah.gutierrez@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Jeremiah', + nat: 'AU', + }, + { + gender: 'female', + name: { + title: 'Ms', + first: 'Luciara', + last: 'Souza', + }, + email: 'luciara.souza@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Luciara', + nat: 'BR', + }, + { + gender: 'male', + name: { + title: 'Mr', + first: 'Valgi', + last: 'da Cunha', + }, + email: 'valgi.dacunha@example.com', + picture: 'https://api.dicebear.com/6.x/open-peeps/svg?seed=Valgi', + nat: 'BR', + }, + ], +}; + +const useStyles = makeStyles({ + avatar: { + height: 32, + width: 32, + borderRadius: '50%', + }, +}); + +type User = { + gender: string; // "male" + name: { + title: string; // "Mr", + first: string; // "Duane", + last: string; // "Reed" + }; + email: string; // "duane.reed@example.com" + picture: string; // "https://api.dicebear.com/6.x/open-peeps/svg?seed=Duane" + nat: string; // "AU" +}; + +type DenseTableProps = { + users: User[]; +}; + +export const DenseTable = ({ users }: DenseTableProps) => { + const classes = useStyles(); + + const columns: TableColumn[] = [ + { title: 'Avatar', field: 'avatar' }, + { title: 'Name', field: 'name' }, + { title: 'Email', field: 'email' }, + { title: 'Nationality', field: 'nationality' }, + ]; + + const data = users.map(user => { + return { + avatar: ( + {user.name.first} + ), + name: `${user.name.first} ${user.name.last}`, + email: user.email, + nationality: user.nat, + }; + }); + + return ( + + ); +}; + +export const ExampleFetchComponent = () => { + const { value, loading, error } = useAsync(async (): Promise => { + // Would use fetch in a real world example + return exampleUsers.results; + }, []); + + if (loading) { + return ; + } else if (error) { + return ; + } + + return ; +}; diff --git a/plugins/opa-frontend-demo/src/components/ExampleFetchComponent/index.ts b/plugins/opa-frontend-demo/src/components/ExampleFetchComponent/index.ts new file mode 100644 index 00000000..41a43e84 --- /dev/null +++ b/plugins/opa-frontend-demo/src/components/ExampleFetchComponent/index.ts @@ -0,0 +1 @@ +export { ExampleFetchComponent } from './ExampleFetchComponent'; diff --git a/plugins/opa-frontend-demo/src/index.ts b/plugins/opa-frontend-demo/src/index.ts new file mode 100644 index 00000000..16e9a8ad --- /dev/null +++ b/plugins/opa-frontend-demo/src/index.ts @@ -0,0 +1 @@ +export { opaFrontendDemoPlugin, OpaFrontendDemoPage } from './plugin'; diff --git a/plugins/opa-frontend-demo/src/plugin.test.ts b/plugins/opa-frontend-demo/src/plugin.test.ts new file mode 100644 index 00000000..428ea8ab --- /dev/null +++ b/plugins/opa-frontend-demo/src/plugin.test.ts @@ -0,0 +1,7 @@ +import { opaFrontendDemoPlugin } from './plugin'; + +describe('opa-frontend-demo', () => { + it('should export plugin', () => { + expect(opaFrontendDemoPlugin).toBeDefined(); + }); +}); diff --git a/plugins/opa-frontend-demo/src/plugin.ts b/plugins/opa-frontend-demo/src/plugin.ts new file mode 100644 index 00000000..8c42e1bb --- /dev/null +++ b/plugins/opa-frontend-demo/src/plugin.ts @@ -0,0 +1,22 @@ +import { + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +export const opaFrontendDemoPlugin = createPlugin({ + id: 'opa-frontend-demo', + routes: { + root: rootRouteRef, + }, +}); + +export const OpaFrontendDemoPage = opaFrontendDemoPlugin.provide( + createRoutableExtension({ + name: 'OpaFrontendDemoPage', + component: () => + import('./components/ExampleComponent').then(m => m.ExampleComponent), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/opa-frontend-demo/src/routes.ts b/plugins/opa-frontend-demo/src/routes.ts new file mode 100644 index 00000000..715e75f7 --- /dev/null +++ b/plugins/opa-frontend-demo/src/routes.ts @@ -0,0 +1,5 @@ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'opa-frontend-demo', +}); diff --git a/plugins/opa-frontend-demo/src/setupTests.ts b/plugins/opa-frontend-demo/src/setupTests.ts new file mode 100644 index 00000000..7b0828bf --- /dev/null +++ b/plugins/opa-frontend-demo/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/plugins/permission-backend-module-opa-wrapper/config.d.ts b/plugins/permission-backend-module-opa-wrapper/config.d.ts new file mode 100644 index 00000000..9eb9b2bf --- /dev/null +++ b/plugins/permission-backend-module-opa-wrapper/config.d.ts @@ -0,0 +1,32 @@ +export interface Config { + /** + * Configuration options for the OpaClient plugin + */ + opaClient?: { + /** + * The base url of the OPA server used for all OPA plugins. + * This is used across all the OPA plugins. + */ + baseUrl?: string; + + /** + * Configuration options for the OPA policies + */ + policies?: { + /** + * Configuration options for the OPA Permissions Wrapper + */ + permissions?: { + /** + * The entrypoint to the OPA Permissions Wrapper + */ + entrypoint?: string; + + /** + * The fallback policy to use when the OPA server is unavailable + */ + policyFallback?: string; + }; + }; + }; +} diff --git a/plugins/permission-backend-module-opa-wrapper/package.json b/plugins/permission-backend-module-opa-wrapper/package.json index 63db298e..e8aaf5ed 100644 --- a/plugins/permission-backend-module-opa-wrapper/package.json +++ b/plugins/permission-backend-module-opa-wrapper/package.json @@ -50,6 +50,7 @@ "@backstage/cli": "^0.28.0" }, "files": [ - "dist" + "dist", + "config.d.ts" ] } diff --git a/policies/opa_demo.rego b/policies/opa_demo.rego new file mode 100644 index 00000000..8febfcdd --- /dev/null +++ b/policies/opa_demo.rego @@ -0,0 +1,42 @@ +package opa_demo + +import rego.v1 + +default allow := false + +# OPA Frontend Demo Rules +allow if { + input.day == "Tuesday" + "user:default/parsifal-m" in input.ownershipEntityRefs +} + +allow if { + input.action == "see-header" + "user:default/parsifal-m" in input.ownershipEntityRefs +} + +allow if { + input.action == "see-support-button" + "user:default/parsifal-m" in input.ownershipEntityRefs +} + +allow if { + input.action == "see-plugin" + "user:default/parsifal-m" in input.ownershipEntityRefs +} + +allow if { + input.action == "see-info-card" + "user:default/parsifal-m" in input.ownershipEntityRefs +} + + +# OPA Backend Demo Rules +allow if { + input.method == "GET" + input.params.id == "23768468-6ec5-4c52-bb34-bbe18b9703c5" +} + +allow if { + input.method == "POST" +} diff --git a/policies/rbac_policy.rego b/policies/rbac_policy.rego index c5291bc9..1ae14356 100644 --- a/policies/rbac_policy.rego +++ b/policies/rbac_policy.rego @@ -19,7 +19,7 @@ claims := input.identity.claims is_admin if "group:twocodersbrewing/maintainers" in claims # decision := {"result": "DENY"} if { -# permission == "catalog.entity.read" +# permission == "catalog.entity.create" # not is_admin # } diff --git a/yarn.lock b/yarn.lock index 4ab79089..133e13a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,10 +5,10 @@ __metadata: version: 6 cacheKey: 8 -"@adobe/css-tools@npm:^4.3.2": - version: 4.3.3 - resolution: "@adobe/css-tools@npm:4.3.3" - checksum: d21f3786b84911fee59c995a146644a85c98692979097b26484ffa9e442fb1a92ccd68ce984e3e7cf8d5933c3560fbc0ad3e3cd1de50b9a723d1c012e793bbcb +"@adobe/css-tools@npm:^4.4.0": + version: 4.4.0 + resolution: "@adobe/css-tools@npm:4.4.0" + checksum: 1f08fb49bf17fc7f2d1a86d3e739f29ca80063d28168307f1b0a962ef37501c5667271f6771966578897f2e94e43c4770fd802728a6e6495b812da54112d506a languageName: node linkType: hard @@ -2803,6 +2803,95 @@ __metadata: languageName: node linkType: hard +"@backstage/backend-app-api@npm:^0.8.0": + version: 0.8.0 + resolution: "@backstage/backend-app-api@npm:0.8.0" + dependencies: + "@backstage/backend-common": ^0.23.3 + "@backstage/backend-plugin-api": ^0.7.0 + "@backstage/backend-tasks": ^0.5.27 + "@backstage/cli-common": ^0.1.14 + "@backstage/cli-node": ^0.2.7 + "@backstage/config": ^1.2.0 + "@backstage/config-loader": ^1.8.1 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-auth-node": ^0.4.17 + "@backstage/plugin-permission-node": ^0.8.0 + "@backstage/types": ^1.1.1 + "@manypkg/get-packages": ^1.1.3 + "@types/cors": ^2.8.6 + "@types/express": ^4.17.6 + compression: ^1.7.4 + cookie: ^0.6.0 + cors: ^2.8.5 + express: ^4.17.1 + express-promise-router: ^4.1.0 + fs-extra: ^11.2.0 + helmet: ^6.0.0 + jose: ^5.0.0 + knex: ^3.0.0 + lodash: ^4.17.21 + logform: ^2.3.2 + luxon: ^3.0.0 + minimatch: ^9.0.0 + minimist: ^1.2.5 + morgan: ^1.10.0 + node-fetch: ^2.6.7 + node-forge: ^1.3.1 + path-to-regexp: ^6.2.1 + selfsigned: ^2.0.0 + stoppable: ^1.1.0 + triple-beam: ^1.4.1 + uuid: ^9.0.0 + winston: ^3.2.1 + winston-transport: ^4.5.0 + checksum: 663b0517e7d4c948d005c2120a83f7720ea02e68ad9600b5e5a3b22441a23b70523ebaf725b0598c7a1916c6a36261367d8316cfa1686a09955bdfdf457497d6 + languageName: node + linkType: hard + +"@backstage/backend-app-api@npm:^0.9.3": + version: 0.9.3 + resolution: "@backstage/backend-app-api@npm:0.9.3" + dependencies: + "@backstage/backend-common": ^0.24.1 + "@backstage/backend-plugin-api": ^0.8.1 + "@backstage/cli-common": ^0.1.14 + "@backstage/cli-node": ^0.2.7 + "@backstage/config": ^1.2.0 + "@backstage/config-loader": ^1.9.0 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-auth-node": ^0.5.1 + "@backstage/plugin-permission-node": ^0.8.2 + "@backstage/types": ^1.1.1 + "@manypkg/get-packages": ^1.1.3 + compression: ^1.7.4 + cookie: ^0.6.0 + cors: ^2.8.5 + express: ^4.17.1 + express-promise-router: ^4.1.0 + fs-extra: ^11.2.0 + helmet: ^6.0.0 + jose: ^5.0.0 + knex: ^3.0.0 + lodash: ^4.17.21 + logform: ^2.3.2 + luxon: ^3.0.0 + minimatch: ^9.0.0 + minimist: ^1.2.5 + morgan: ^1.10.0 + node-fetch: ^2.7.0 + node-forge: ^1.3.1 + path-to-regexp: ^6.2.1 + selfsigned: ^2.0.0 + stoppable: ^1.1.0 + triple-beam: ^1.4.1 + uuid: ^9.0.0 + winston: ^3.2.1 + winston-transport: ^4.5.0 + checksum: 42c41bf9c71bcd5a34675e9116879949693f388d74147a6f1c8ad4b483799ee03be999b5d431547f0ab3f3e24cc9a7a813315fc627b2ba4af961f99b237e9dd5 + languageName: node + linkType: hard + "@backstage/backend-app-api@npm:^1.0.1": version: 1.0.1 resolution: "@backstage/backend-app-api@npm:1.0.1" @@ -2844,6 +2933,82 @@ __metadata: languageName: node linkType: hard +"@backstage/backend-common@npm:^0.23.3": + version: 0.23.3 + resolution: "@backstage/backend-common@npm:0.23.3" + dependencies: + "@aws-sdk/abort-controller": ^3.347.0 + "@aws-sdk/client-codecommit": ^3.350.0 + "@aws-sdk/client-s3": ^3.350.0 + "@aws-sdk/credential-providers": ^3.350.0 + "@aws-sdk/types": ^3.347.0 + "@backstage/backend-dev-utils": ^0.1.4 + "@backstage/backend-plugin-api": ^0.7.0 + "@backstage/cli-common": ^0.1.14 + "@backstage/config": ^1.2.0 + "@backstage/config-loader": ^1.8.1 + "@backstage/errors": ^1.2.4 + "@backstage/integration": ^1.13.0 + "@backstage/integration-aws-node": ^0.1.12 + "@backstage/plugin-auth-node": ^0.4.17 + "@backstage/types": ^1.1.1 + "@google-cloud/storage": ^7.0.0 + "@keyv/memcache": ^1.3.5 + "@keyv/redis": ^2.5.3 + "@kubernetes/client-node": 0.20.0 + "@manypkg/get-packages": ^1.1.3 + "@octokit/rest": ^19.0.3 + "@types/cors": ^2.8.6 + "@types/dockerode": ^3.3.0 + "@types/express": ^4.17.6 + "@types/luxon": ^3.0.0 + "@types/webpack-env": ^1.15.2 + archiver: ^6.0.0 + base64-stream: ^1.0.0 + compression: ^1.7.4 + concat-stream: ^2.0.0 + cors: ^2.8.5 + dockerode: ^4.0.0 + express: ^4.17.1 + express-promise-router: ^4.1.0 + fs-extra: ^11.2.0 + git-url-parse: ^14.0.0 + helmet: ^6.0.0 + isomorphic-git: ^1.23.0 + jose: ^5.0.0 + keyv: ^4.5.2 + knex: ^3.0.0 + lodash: ^4.17.21 + logform: ^2.3.2 + luxon: ^3.0.0 + minimatch: ^9.0.0 + minimist: ^1.2.5 + morgan: ^1.10.0 + mysql2: ^3.0.0 + node-fetch: ^2.6.7 + node-forge: ^1.3.1 + p-limit: ^3.1.0 + path-to-regexp: ^6.2.1 + pg: ^8.11.3 + raw-body: ^2.4.1 + selfsigned: ^2.0.0 + stoppable: ^1.1.0 + tar: ^6.1.12 + triple-beam: ^1.4.1 + uuid: ^9.0.0 + winston: ^3.2.1 + winston-transport: ^4.5.0 + yauzl: ^3.0.0 + yn: ^4.0.0 + peerDependencies: + pg-connection-string: ^2.3.0 + peerDependenciesMeta: + pg-connection-string: + optional: true + checksum: 3cd96e153a5537e95c783fb7f5783c7ba15700375248f102b89aae1144962e64382caec2fec5b27d5ed08ae988c0fc6b3bc34921e9355d12bdbf8ce78aa99acb + languageName: node + linkType: hard + "@backstage/backend-common@npm:^0.24.1": version: 0.24.1 resolution: "@backstage/backend-common@npm:0.24.1" @@ -2998,6 +3163,83 @@ __metadata: languageName: node linkType: hard +"@backstage/backend-defaults@npm:^0.4.0": + version: 0.4.4 + resolution: "@backstage/backend-defaults@npm:0.4.4" + dependencies: + "@aws-sdk/abort-controller": ^3.347.0 + "@aws-sdk/client-codecommit": ^3.350.0 + "@aws-sdk/client-s3": ^3.350.0 + "@aws-sdk/credential-providers": ^3.350.0 + "@aws-sdk/types": ^3.347.0 + "@backstage/backend-app-api": ^0.9.3 + "@backstage/backend-common": ^0.24.1 + "@backstage/backend-dev-utils": ^0.1.5 + "@backstage/backend-plugin-api": ^0.8.1 + "@backstage/cli-common": ^0.1.14 + "@backstage/config": ^1.2.0 + "@backstage/config-loader": ^1.9.0 + "@backstage/errors": ^1.2.4 + "@backstage/integration": ^1.14.0 + "@backstage/integration-aws-node": ^0.1.12 + "@backstage/plugin-auth-node": ^0.5.1 + "@backstage/plugin-events-node": ^0.3.10 + "@backstage/plugin-permission-node": ^0.8.2 + "@backstage/types": ^1.1.1 + "@google-cloud/storage": ^7.0.0 + "@keyv/memcache": ^1.3.5 + "@keyv/redis": ^2.5.3 + "@manypkg/get-packages": ^1.1.3 + "@octokit/rest": ^19.0.3 + "@opentelemetry/api": ^1.3.0 + "@types/cors": ^2.8.6 + "@types/express": ^4.17.6 + archiver: ^6.0.0 + base64-stream: ^1.0.0 + better-sqlite3: ^11.0.0 + compression: ^1.7.4 + concat-stream: ^2.0.0 + cookie: ^0.6.0 + cors: ^2.8.5 + cron: ^3.0.0 + express: ^4.17.1 + express-promise-router: ^4.1.0 + fs-extra: ^11.2.0 + git-url-parse: ^14.0.0 + helmet: ^6.0.0 + isomorphic-git: ^1.23.0 + jose: ^5.0.0 + keyv: ^4.5.2 + knex: ^3.0.0 + lodash: ^4.17.21 + logform: ^2.3.2 + luxon: ^3.0.0 + minimatch: ^9.0.0 + minimist: ^1.2.5 + morgan: ^1.10.0 + mysql2: ^3.0.0 + node-fetch: ^2.7.0 + node-forge: ^1.3.1 + p-limit: ^3.1.0 + path-to-regexp: ^6.2.1 + pg: ^8.11.3 + pg-connection-string: ^2.3.0 + pg-format: ^1.0.4 + raw-body: ^2.4.1 + selfsigned: ^2.0.0 + stoppable: ^1.1.0 + tar: ^6.1.12 + triple-beam: ^1.4.1 + uuid: ^9.0.0 + winston: ^3.2.1 + winston-transport: ^4.5.0 + yauzl: ^3.0.0 + yn: ^4.0.0 + zod: ^3.22.4 + checksum: 07f02b0eed51b5741b8e32f3026b8a2a29c8598a60bd20c4c4ae98dcdf849321f55672266ae50de9f28fb6f1e9c65409d8a76760ca405af175739eb00a01be21 + languageName: node + linkType: hard + "@backstage/backend-defaults@npm:^0.5.1": version: 0.5.1 resolution: "@backstage/backend-defaults@npm:0.5.1" @@ -3076,7 +3318,7 @@ __metadata: languageName: node linkType: hard -"@backstage/backend-dev-utils@npm:^0.1.5": +"@backstage/backend-dev-utils@npm:^0.1.4, @backstage/backend-dev-utils@npm:^0.1.5": version: 0.1.5 resolution: "@backstage/backend-dev-utils@npm:0.1.5" checksum: 7c7eced8cc6fe88b6b54d7b9f04953dbfd07846772368a0b269d4e75da30133b61e4fe29782c0dc0aa547234d75ff60a985f378f92911680a9172fa8f2820e5b @@ -3108,33 +3350,52 @@ __metadata: languageName: node linkType: hard -"@backstage/backend-plugin-api@npm:^0.8.1": - version: 0.8.1 - resolution: "@backstage/backend-plugin-api@npm:0.8.1" +"@backstage/backend-plugin-api@npm:^0.6.21": + version: 0.6.21 + resolution: "@backstage/backend-plugin-api@npm:0.6.21" dependencies: "@backstage/cli-common": ^0.1.14 "@backstage/config": ^1.2.0 "@backstage/errors": ^1.2.4 - "@backstage/plugin-auth-node": ^0.5.1 - "@backstage/plugin-permission-common": ^0.8.1 + "@backstage/plugin-auth-node": ^0.4.16 + "@backstage/plugin-permission-common": ^0.7.14 "@backstage/types": ^1.1.1 "@types/express": ^4.17.6 "@types/luxon": ^3.0.0 express: ^4.17.1 knex: ^3.0.0 luxon: ^3.0.0 - checksum: 4a6614ceec13ff5ace3e04e8a1bad40567ce6a66afc19c02935161d12bdd7edbf4863d1d0203539e8a353dd78184078eb873fd14da6cbd093d1d9e4ced44c0fb + checksum: d6b81036579108835cbf63fcc2c3e5a9ac684e3797d415d1ac4e26a32db72c0b0b182c098fb91e7a3219eaed2362a85d717327f69f6d2b566c3f5c6a8963c9d1 languageName: node linkType: hard -"@backstage/backend-plugin-api@npm:^1.0.0": - version: 1.0.0 - resolution: "@backstage/backend-plugin-api@npm:1.0.0" +"@backstage/backend-plugin-api@npm:^0.7.0": + version: 0.7.0 + resolution: "@backstage/backend-plugin-api@npm:0.7.0" dependencies: "@backstage/cli-common": ^0.1.14 "@backstage/config": ^1.2.0 "@backstage/errors": ^1.2.4 - "@backstage/plugin-auth-node": ^0.5.2 + "@backstage/plugin-auth-node": ^0.4.17 + "@backstage/plugin-permission-common": ^0.8.0 + "@backstage/types": ^1.1.1 + "@types/express": ^4.17.6 + "@types/luxon": ^3.0.0 + express: ^4.17.1 + knex: ^3.0.0 + luxon: ^3.0.0 + checksum: ea3f8a97750b8f9afae5ee45e0afdb4b04f46c889108b32fe0a86447d1578f4d5e1bca37c4fccdd6270593b6db0729d2e281349d8e11e2528e76ab18ab649c33 + languageName: node + linkType: hard + +"@backstage/backend-plugin-api@npm:^0.8.1": + version: 0.8.1 + resolution: "@backstage/backend-plugin-api@npm:0.8.1" + dependencies: + "@backstage/cli-common": ^0.1.14 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-auth-node": ^0.5.1 "@backstage/plugin-permission-common": ^0.8.1 "@backstage/types": ^1.1.1 "@types/express": ^4.17.6 @@ -3142,11 +3403,11 @@ __metadata: express: ^4.17.1 knex: ^3.0.0 luxon: ^3.0.0 - checksum: b18b93fb631a81826ef9049567c5a151285d643aea820954f03e3a293d75f00ae679b2d3054fab2cf9dff476a65fad5b8841ca608b2f182f394b22dca026664a + checksum: 4a6614ceec13ff5ace3e04e8a1bad40567ce6a66afc19c02935161d12bdd7edbf4863d1d0203539e8a353dd78184078eb873fd14da6cbd093d1d9e4ced44c0fb languageName: node linkType: hard -"@backstage/backend-plugin-api@npm:^1.0.1": +"@backstage/backend-plugin-api@npm:^1.0.0, @backstage/backend-plugin-api@npm:^1.0.1": version: 1.0.1 resolution: "@backstage/backend-plugin-api@npm:1.0.1" dependencies: @@ -3165,6 +3426,27 @@ __metadata: languageName: node linkType: hard +"@backstage/backend-tasks@npm:^0.5.27": + version: 0.5.27 + resolution: "@backstage/backend-tasks@npm:0.5.27" + dependencies: + "@backstage/backend-common": ^0.23.3 + "@backstage/backend-plugin-api": ^0.7.0 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/types": ^1.1.1 + "@opentelemetry/api": ^1.3.0 + "@types/luxon": ^3.0.0 + cron: ^3.0.0 + knex: ^3.0.0 + lodash: ^4.17.21 + luxon: ^3.0.0 + uuid: ^9.0.0 + zod: ^3.22.4 + checksum: 69afa09bb380cdc93d52bf4e93b94a4aa8b3c9ef74f3e4350a6beedbbc623095805c0613f691a42a3995795fe0c9f9ccce689ce8c2f3a11277534d13ac4aa2a6 + languageName: node + linkType: hard + "@backstage/backend-tasks@npm:^0.6.1": version: 0.6.1 resolution: "@backstage/backend-tasks@npm:0.6.1" @@ -3186,6 +3468,41 @@ __metadata: languageName: node linkType: hard +"@backstage/backend-test-utils@npm:^0.4.3": + version: 0.4.4 + resolution: "@backstage/backend-test-utils@npm:0.4.4" + dependencies: + "@backstage/backend-app-api": ^0.8.0 + "@backstage/backend-defaults": ^0.4.0 + "@backstage/backend-plugin-api": ^0.7.0 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-auth-node": ^0.4.17 + "@backstage/plugin-events-node": ^0.3.8 + "@backstage/types": ^1.1.1 + "@keyv/memcache": ^1.3.5 + "@keyv/redis": ^2.5.3 + "@types/keyv": ^4.2.0 + better-sqlite3: ^11.0.0 + cookie: ^0.6.0 + express: ^4.17.1 + fs-extra: ^11.0.0 + keyv: ^4.5.2 + knex: ^3.0.0 + msw: ^1.0.0 + mysql2: ^3.0.0 + pg: ^8.11.3 + pg-connection-string: ^2.3.0 + testcontainers: ^10.0.0 + textextensions: ^5.16.0 + uuid: ^9.0.0 + yn: ^4.0.0 + peerDependencies: + "@types/jest": "*" + checksum: 89b551093b4dc90b169646c0238b885bd31f4c2cafaf251aa6b4c2f6c4783e52b00068e43c5e6fcd0ac648a76b8502d43b0b133a1f36bbe7f4e3c1e1db17d417 + languageName: node + linkType: hard + "@backstage/backend-test-utils@npm:^1.0.1": version: 1.0.1 resolution: "@backstage/backend-test-utils@npm:1.0.1" @@ -3224,31 +3541,7 @@ __metadata: languageName: node linkType: hard -"@backstage/catalog-client@npm:^1.6.5, @backstage/catalog-client@npm:^1.6.6": - version: 1.6.6 - resolution: "@backstage/catalog-client@npm:1.6.6" - dependencies: - "@backstage/catalog-model": ^1.6.0 - "@backstage/errors": ^1.2.4 - cross-fetch: ^4.0.0 - uri-template: ^2.0.0 - checksum: 10a859979a6ec3d9bcca519ace01ca371517e56bd54a90e0d667b4ef90bee54b09495238974b4040d7dc75f8cacf0d212900b40fe6738d484ff57a739e3eec4b - languageName: node - linkType: hard - -"@backstage/catalog-client@npm:^1.7.0": - version: 1.7.0 - resolution: "@backstage/catalog-client@npm:1.7.0" - dependencies: - "@backstage/catalog-model": ^1.7.0 - "@backstage/errors": ^1.2.4 - cross-fetch: ^4.0.0 - uri-template: ^2.0.0 - checksum: 66a0570c57281fbf7b59786ebf2dd97efbb5c7c7393025e14a605d38f1bf2317974c319da138146e21d31ac83c3214223631211ebbee36fc96c84bd803acd913 - languageName: node - linkType: hard - -"@backstage/catalog-client@npm:^1.7.1": +"@backstage/catalog-client@npm:^1.6.5, @backstage/catalog-client@npm:^1.7.1": version: 1.7.1 resolution: "@backstage/catalog-client@npm:1.7.1" dependencies: @@ -3260,19 +3553,7 @@ __metadata: languageName: node linkType: hard -"@backstage/catalog-model@npm:^1.4.5, @backstage/catalog-model@npm:^1.5.0, @backstage/catalog-model@npm:^1.6.0": - version: 1.6.0 - resolution: "@backstage/catalog-model@npm:1.6.0" - dependencies: - "@backstage/errors": ^1.2.4 - "@backstage/types": ^1.1.1 - ajv: ^8.10.0 - lodash: ^4.17.21 - checksum: b3bac3578a43b4d3f9e2fcd90396b5043f8c1e46f5308c5204b7973c79e76e465b3804edc985355cb4b3f669d065e1522b25bc6ee38567b5c74341b2d115ecf0 - languageName: node - linkType: hard - -"@backstage/catalog-model@npm:^1.7.0": +"@backstage/catalog-model@npm:^1.4.5, @backstage/catalog-model@npm:^1.5.0, @backstage/catalog-model@npm:^1.7.0": version: 1.7.0 resolution: "@backstage/catalog-model@npm:1.7.0" dependencies: @@ -3291,7 +3572,7 @@ __metadata: languageName: node linkType: hard -"@backstage/cli-node@npm:^0.2.9": +"@backstage/cli-node@npm:^0.2.7, @backstage/cli-node@npm:^0.2.9": version: 0.2.9 resolution: "@backstage/cli-node@npm:0.2.9" dependencies: @@ -3307,6 +3588,142 @@ __metadata: languageName: node linkType: hard +"@backstage/cli@npm:^0.26.10": + version: 0.26.11 + resolution: "@backstage/cli@npm:0.26.11" + dependencies: + "@backstage/catalog-model": ^1.5.0 + "@backstage/cli-common": ^0.1.14 + "@backstage/cli-node": ^0.2.7 + "@backstage/config": ^1.2.0 + "@backstage/config-loader": ^1.8.1 + "@backstage/errors": ^1.2.4 + "@backstage/eslint-plugin": ^0.1.8 + "@backstage/integration": ^1.13.0 + "@backstage/release-manifests": ^0.0.11 + "@backstage/types": ^1.1.1 + "@manypkg/get-packages": ^1.1.3 + "@module-federation/enhanced": ^0.1.19 + "@octokit/graphql": ^5.0.0 + "@octokit/graphql-schema": ^13.7.0 + "@octokit/oauth-app": ^4.2.0 + "@octokit/request": ^6.0.0 + "@pmmmwh/react-refresh-webpack-plugin": ^0.5.7 + "@rollup/plugin-commonjs": ^25.0.0 + "@rollup/plugin-json": ^6.0.0 + "@rollup/plugin-node-resolve": ^15.0.0 + "@rollup/plugin-yaml": ^4.0.0 + "@spotify/eslint-config-base": ^15.0.0 + "@spotify/eslint-config-react": ^15.0.0 + "@spotify/eslint-config-typescript": ^15.0.0 + "@sucrase/webpack-loader": ^2.0.0 + "@svgr/core": 6.5.x + "@svgr/plugin-jsx": 6.5.x + "@svgr/plugin-svgo": 6.5.x + "@svgr/rollup": 6.5.x + "@svgr/webpack": 6.5.x + "@swc/core": ^1.3.46 + "@swc/helpers": ^0.5.0 + "@swc/jest": ^0.2.22 + "@types/jest": ^29.5.11 + "@types/webpack-env": ^1.15.2 + "@typescript-eslint/eslint-plugin": ^6.12.0 + "@typescript-eslint/parser": ^6.7.2 + "@yarnpkg/lockfile": ^1.1.0 + "@yarnpkg/parsers": ^3.0.0 + bfj: ^8.0.0 + buffer: ^6.0.3 + chalk: ^4.0.0 + chokidar: ^3.3.1 + commander: ^12.0.0 + cross-fetch: ^4.0.0 + cross-spawn: ^7.0.3 + css-loader: ^6.5.1 + ctrlc-windows: ^2.1.0 + diff: ^5.0.0 + esbuild: ^0.21.0 + esbuild-loader: ^4.0.0 + eslint: ^8.6.0 + eslint-config-prettier: ^9.0.0 + eslint-formatter-friendly: ^7.0.0 + eslint-plugin-deprecation: ^2.0.0 + eslint-plugin-import: ^2.25.4 + eslint-plugin-jest: ^27.0.0 + eslint-plugin-jsx-a11y: ^6.5.1 + eslint-plugin-react: ^7.28.0 + eslint-plugin-react-hooks: ^4.3.0 + eslint-plugin-unused-imports: ^3.0.0 + eslint-webpack-plugin: ^4.0.0 + express: ^4.17.1 + fork-ts-checker-webpack-plugin: ^9.0.0 + fs-extra: ^11.2.0 + git-url-parse: ^14.0.0 + glob: ^7.1.7 + global-agent: ^3.0.0 + handlebars: ^4.7.3 + html-webpack-plugin: ^5.3.1 + inquirer: ^8.2.0 + jest: ^29.7.0 + jest-css-modules: ^2.1.0 + jest-environment-jsdom: ^29.0.2 + jest-runtime: ^29.0.2 + json-schema: ^0.4.0 + lodash: ^4.17.21 + mini-css-extract-plugin: ^2.4.2 + minimatch: ^9.0.0 + node-fetch: ^2.6.7 + node-libs-browser: ^2.2.1 + npm-packlist: ^5.0.0 + ora: ^5.3.0 + p-limit: ^3.1.0 + p-queue: ^6.6.2 + pirates: ^4.0.6 + postcss: ^8.1.0 + process: ^0.11.10 + react-dev-utils: ^12.0.0-next.60 + react-refresh: ^0.14.0 + recursive-readdir: ^2.2.2 + replace-in-file: ^7.1.0 + rollup: ^4.0.0 + rollup-plugin-dts: ^6.1.0 + rollup-plugin-esbuild: ^6.1.1 + rollup-plugin-postcss: ^4.0.0 + rollup-pluginutils: ^2.8.2 + run-script-webpack-plugin: ^0.2.0 + semver: ^7.5.3 + style-loader: ^3.3.1 + sucrase: ^3.20.2 + swc-loader: ^0.2.3 + tar: ^6.1.12 + terser-webpack-plugin: ^5.1.3 + util: ^0.12.3 + webpack: ^5.70.0 + webpack-dev-server: ^5.0.0 + webpack-node-externals: ^3.0.0 + yaml: ^2.0.0 + yml-loader: ^2.1.0 + yn: ^4.0.0 + zod: ^3.22.4 + peerDependencies: + "@vitejs/plugin-react": ^4.0.4 + vite: ^4.4.9 + vite-plugin-html: ^3.2.0 + vite-plugin-node-polyfills: ^0.22.0 + peerDependenciesMeta: + "@vitejs/plugin-react": + optional: true + vite: + optional: true + vite-plugin-html: + optional: true + vite-plugin-node-polyfills: + optional: true + bin: + backstage-cli: bin/backstage-cli + checksum: 328525101cfa824722e7bdace20dbdb8f7ef55a1eb3b7084f03394177db5f13fe3e49a4746d07ae5d7fbb9ee45f01f239bb19cf4f59f28bf5c8093c395caea0f + languageName: node + linkType: hard + "@backstage/cli@npm:^0.28.0": version: 0.28.0 resolution: "@backstage/cli@npm:0.28.0" @@ -3459,31 +3876,7 @@ __metadata: languageName: node linkType: hard -"@backstage/config-loader@npm:^1.9.0": - version: 1.9.0 - resolution: "@backstage/config-loader@npm:1.9.0" - dependencies: - "@backstage/cli-common": ^0.1.14 - "@backstage/config": ^1.2.0 - "@backstage/errors": ^1.2.4 - "@backstage/types": ^1.1.1 - "@types/json-schema": ^7.0.6 - ajv: ^8.10.0 - chokidar: ^3.5.2 - fs-extra: ^11.2.0 - json-schema: ^0.4.0 - json-schema-merge-allof: ^0.8.1 - json-schema-traverse: ^1.0.0 - lodash: ^4.17.21 - minimist: ^1.2.5 - node-fetch: ^2.7.0 - typescript-json-schema: ^0.63.0 - yaml: ^2.0.0 - checksum: a7881957eed96b2fd707415914800fc72de8e0a067259d0e25ae9d89008abc405c8c9bece528a07e1c33ac18efd919f4bf10d7624b0a454c8a491785a539bc7f - languageName: node - linkType: hard - -"@backstage/config-loader@npm:^1.9.1": +"@backstage/config-loader@npm:^1.8.1, @backstage/config-loader@npm:^1.9.0, @backstage/config-loader@npm:^1.9.1": version: 1.9.1 resolution: "@backstage/config-loader@npm:1.9.1" dependencies: @@ -3681,7 +4074,7 @@ __metadata: languageName: node linkType: hard -"@backstage/core-plugin-api@npm:^1.10.0": +"@backstage/core-plugin-api@npm:^1.10.0, @backstage/core-plugin-api@npm:^1.9.1, @backstage/core-plugin-api@npm:^1.9.2, @backstage/core-plugin-api@npm:^1.9.3": version: 1.10.0 resolution: "@backstage/core-plugin-api@npm:1.10.0" dependencies: @@ -3702,24 +4095,6 @@ __metadata: languageName: node linkType: hard -"@backstage/core-plugin-api@npm:^1.9.1, @backstage/core-plugin-api@npm:^1.9.2, @backstage/core-plugin-api@npm:^1.9.3": - version: 1.9.3 - resolution: "@backstage/core-plugin-api@npm:1.9.3" - dependencies: - "@backstage/config": ^1.2.0 - "@backstage/errors": ^1.2.4 - "@backstage/types": ^1.1.1 - "@backstage/version-bridge": ^1.0.8 - "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 - history: ^5.0.0 - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: 490295c126eff7f7f3107565097bc3dbdda5b72e52301a6c47fab91429961aff5757ba91208cbe12c37f90d429703919ab6067a30c394249ec792d900308d309 - languageName: node - linkType: hard - "@backstage/dev-utils@npm:^1.1.2": version: 1.1.2 resolution: "@backstage/dev-utils@npm:1.1.2" @@ -3757,7 +4132,7 @@ __metadata: languageName: node linkType: hard -"@backstage/eslint-plugin@npm:^0.1.10": +"@backstage/eslint-plugin@npm:^0.1.10, @backstage/eslint-plugin@npm:^0.1.8": version: 0.1.10 resolution: "@backstage/eslint-plugin@npm:0.1.10" dependencies: @@ -3899,25 +4274,7 @@ __metadata: languageName: node linkType: hard -"@backstage/integration-react@npm:^1.1.26, @backstage/integration-react@npm:^1.1.28": - version: 1.1.28 - resolution: "@backstage/integration-react@npm:1.1.28" - dependencies: - "@backstage/config": ^1.2.0 - "@backstage/core-plugin-api": ^1.9.3 - "@backstage/integration": ^1.12.0 - "@material-ui/core": ^4.12.2 - "@material-ui/icons": ^4.9.1 - "@types/react": ^16.13.1 || ^17.0.0 - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: 15443a7ebc457114715651e652a40472085ca18ec10da078bad1e71e3aa9cac33d10704b164d09108f6195a9cb4d2eae11751d853598ef0dd0e4c160858f782d - languageName: node - linkType: hard - -"@backstage/integration-react@npm:^1.2.0": +"@backstage/integration-react@npm:^1.1.26, @backstage/integration-react@npm:^1.2.0": version: 1.2.0 resolution: "@backstage/integration-react@npm:1.2.0" dependencies: @@ -3938,41 +4295,7 @@ __metadata: languageName: node linkType: hard -"@backstage/integration@npm:^1.10.0, @backstage/integration@npm:^1.12.0, @backstage/integration@npm:^1.14.0": - version: 1.14.0 - resolution: "@backstage/integration@npm:1.14.0" - dependencies: - "@azure/identity": ^4.0.0 - "@backstage/config": ^1.2.0 - "@backstage/errors": ^1.2.4 - "@octokit/auth-app": ^4.0.0 - "@octokit/rest": ^19.0.3 - cross-fetch: ^4.0.0 - git-url-parse: ^14.0.0 - lodash: ^4.17.21 - luxon: ^3.0.0 - checksum: 3dc2272eda0205e880469aa2a68f390ba9f7a36b5ca41bc2bcb693c46fa7f99e057748c9aab1f88637dda3d8e2dec6b82ffab2aea6d3f12781c563502f0ed4d1 - languageName: node - linkType: hard - -"@backstage/integration@npm:^1.15.0": - version: 1.15.0 - resolution: "@backstage/integration@npm:1.15.0" - dependencies: - "@azure/identity": ^4.0.0 - "@backstage/config": ^1.2.0 - "@backstage/errors": ^1.2.4 - "@octokit/auth-app": ^4.0.0 - "@octokit/rest": ^19.0.3 - cross-fetch: ^4.0.0 - git-url-parse: ^14.0.0 - lodash: ^4.17.21 - luxon: ^3.0.0 - checksum: a2c5b51b1403341f56fe91bd53a1105875855642927b95277c3e8ea29d604b718c39984fd2b8cd298d6206d1f23718da10dbbb53f4ec6eb74b296e6621fc4b7e - languageName: node - linkType: hard - -"@backstage/integration@npm:^1.15.1": +"@backstage/integration@npm:^1.10.0, @backstage/integration@npm:^1.13.0, @backstage/integration@npm:^1.14.0, @backstage/integration@npm:^1.15.0, @backstage/integration@npm:^1.15.1": version: 1.15.1 resolution: "@backstage/integration@npm:1.15.1" dependencies: @@ -4397,39 +4720,14 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-auth-node@npm:^0.5.1": - version: 0.5.1 - resolution: "@backstage/plugin-auth-node@npm:0.5.1" +"@backstage/plugin-auth-node@npm:^0.4.16, @backstage/plugin-auth-node@npm:^0.4.17": + version: 0.4.17 + resolution: "@backstage/plugin-auth-node@npm:0.4.17" dependencies: - "@backstage/backend-common": ^0.24.1 - "@backstage/backend-plugin-api": ^0.8.1 - "@backstage/catalog-client": ^1.6.6 - "@backstage/catalog-model": ^1.6.0 - "@backstage/config": ^1.2.0 - "@backstage/errors": ^1.2.4 - "@backstage/types": ^1.1.1 - "@types/express": "*" - "@types/passport": ^1.0.3 - express: ^4.17.1 - jose: ^5.0.0 - lodash: ^4.17.21 - node-fetch: ^2.7.0 - passport: ^0.7.0 - winston: ^3.2.1 - zod: ^3.22.4 - zod-to-json-schema: ^3.21.4 - checksum: 42b991b81569dd0258eeac949abf887e63202da0d2374e161d5fb9de96cf4a482fb0db64fd612d401e0088e73360a23a1023923805fd1b18cddfd9ab3ff801e8 - languageName: node - linkType: hard - -"@backstage/plugin-auth-node@npm:^0.5.2": - version: 0.5.2 - resolution: "@backstage/plugin-auth-node@npm:0.5.2" - dependencies: - "@backstage/backend-common": ^0.25.0 - "@backstage/backend-plugin-api": ^1.0.0 - "@backstage/catalog-client": ^1.7.0 - "@backstage/catalog-model": ^1.7.0 + "@backstage/backend-common": ^0.23.3 + "@backstage/backend-plugin-api": ^0.7.0 + "@backstage/catalog-client": ^1.6.5 + "@backstage/catalog-model": ^1.5.0 "@backstage/config": ^1.2.0 "@backstage/errors": ^1.2.4 "@backstage/types": ^1.1.1 @@ -4438,16 +4736,16 @@ __metadata: express: ^4.17.1 jose: ^5.0.0 lodash: ^4.17.21 - node-fetch: ^2.7.0 + node-fetch: ^2.6.7 passport: ^0.7.0 winston: ^3.2.1 zod: ^3.22.4 zod-to-json-schema: ^3.21.4 - checksum: 4a461ac73368f673bbe4d11a8f74807429f58e5295809e80d63365df57238735f0df8c9a7b33b096b371ddddbc79f02b879c7b42600dc1a22fe85f166095ccea + checksum: 2506045877e9f76f70d4d5541725a0c6cf9ba0f1604bade22ec92852e887e7b844bc815cdace74c5053ef23c4306e18b6b1b4bdefe6dfab62dd9e03bd66e2d08 languageName: node linkType: hard -"@backstage/plugin-auth-node@npm:^0.5.3": +"@backstage/plugin-auth-node@npm:^0.5.1, @backstage/plugin-auth-node@npm:^0.5.2, @backstage/plugin-auth-node@npm:^0.5.3": version: 0.5.3 resolution: "@backstage/plugin-auth-node@npm:0.5.3" dependencies: @@ -4598,17 +4896,6 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-catalog-common@npm:^1.0.24": - version: 1.0.24 - resolution: "@backstage/plugin-catalog-common@npm:1.0.24" - dependencies: - "@backstage/catalog-model": ^1.5.0 - "@backstage/plugin-permission-common": ^0.7.14 - "@backstage/plugin-search-common": ^1.2.12 - checksum: 57f23ce5a5f12f47062c6796c576ae11d982bd27644abe7895892870ff533757afccb9c051e70f1bc61c779005a1d0ca22126ce022f16bf832edde08c49052d0 - languageName: node - linkType: hard - "@backstage/plugin-catalog-common@npm:^1.1.0": version: 1.1.0 resolution: "@backstage/plugin-catalog-common@npm:1.1.0" @@ -4706,43 +4993,7 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-catalog-react@npm:^1.11.3": - version: 1.12.1 - resolution: "@backstage/plugin-catalog-react@npm:1.12.1" - dependencies: - "@backstage/catalog-client": ^1.6.5 - "@backstage/catalog-model": ^1.5.0 - "@backstage/core-components": ^0.14.8 - "@backstage/core-plugin-api": ^1.9.3 - "@backstage/errors": ^1.2.4 - "@backstage/frontend-plugin-api": ^0.6.6 - "@backstage/integration-react": ^1.1.28 - "@backstage/plugin-catalog-common": ^1.0.24 - "@backstage/plugin-permission-common": ^0.7.14 - "@backstage/plugin-permission-react": ^0.4.23 - "@backstage/types": ^1.1.1 - "@backstage/version-bridge": ^1.0.8 - "@material-ui/core": ^4.12.2 - "@material-ui/icons": ^4.9.1 - "@material-ui/lab": 4.0.0-alpha.61 - "@react-hookz/web": ^24.0.0 - "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 - classnames: ^2.2.6 - lodash: ^4.17.21 - material-ui-popup-state: ^1.9.3 - qs: ^6.9.4 - react-use: ^17.2.4 - yaml: ^2.0.0 - zen-observable: ^0.10.0 - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: adbf967e978fa6bb4798d0620853e41f64cc08c39dd0056fc01b2146ca427d1bfab401b8972d7b551d4188b90f80f69db9254799bca095608b7079cec73da9c5 - languageName: node - linkType: hard - -"@backstage/plugin-catalog-react@npm:^1.14.0": +"@backstage/plugin-catalog-react@npm:^1.11.3, @backstage/plugin-catalog-react@npm:^1.14.0": version: 1.14.0 resolution: "@backstage/plugin-catalog-react@npm:1.14.0" dependencies: @@ -4825,6 +5076,15 @@ __metadata: languageName: node linkType: hard +"@backstage/plugin-events-node@npm:^0.3.10, @backstage/plugin-events-node@npm:^0.3.8": + version: 0.3.10 + resolution: "@backstage/plugin-events-node@npm:0.3.10" + dependencies: + "@backstage/backend-plugin-api": ^0.8.1 + checksum: 19a6cb5541e08e905d6015c92c3e3f3c8707b0ec31a9638832106b55b0e9999156ff7a4360c424f12b105f4d1b710fa14f1435fb58812a64601e4890d575287b + languageName: node + linkType: hard + "@backstage/plugin-events-node@npm:^0.4.1": version: 0.4.1 resolution: "@backstage/plugin-events-node@npm:0.4.1" @@ -4906,7 +5166,7 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-permission-common@npm:^0.8.1": +"@backstage/plugin-permission-common@npm:^0.8.0, @backstage/plugin-permission-common@npm:^0.8.1": version: 0.8.1 resolution: "@backstage/plugin-permission-common@npm:0.8.1" dependencies: @@ -4921,7 +5181,7 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-permission-node@npm:^0.8.4": +"@backstage/plugin-permission-node@npm:^0.8.0, @backstage/plugin-permission-node@npm:^0.8.2, @backstage/plugin-permission-node@npm:^0.8.4": version: 0.8.4 resolution: "@backstage/plugin-permission-node@npm:0.8.4" dependencies: @@ -4940,23 +5200,6 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-permission-react@npm:^0.4.23": - version: 0.4.23 - resolution: "@backstage/plugin-permission-react@npm:0.4.23" - dependencies: - "@backstage/config": ^1.2.0 - "@backstage/core-plugin-api": ^1.9.3 - "@backstage/plugin-permission-common": ^0.7.14 - "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 - swr: ^2.0.0 - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: 812a5d3618e08e7b9c3438d3613ce3c250d012a7a3c4c366f84fa408b0d68ac9c3372bdf81aa0af5126a41eb2dd33cdb9a5842903dab882af94a4a36421a9cc8 - languageName: node - linkType: hard - "@backstage/plugin-permission-react@npm:^0.4.27": version: 0.4.27 resolution: "@backstage/plugin-permission-react@npm:0.4.27" @@ -5446,16 +5689,6 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-search-common@npm:^1.2.12": - version: 1.2.12 - resolution: "@backstage/plugin-search-common@npm:1.2.12" - dependencies: - "@backstage/plugin-permission-common": ^0.7.14 - "@backstage/types": ^1.1.1 - checksum: 2c1b77e74b88353abbc1addf274431cd315d3ec181ee4e93d11ded8a78279de269d9ba418fbefa8fe159e277eaf90a8072f3c8a3de02f8bc1ad01691355c46a1 - languageName: node - linkType: hard - "@backstage/plugin-search-common@npm:^1.2.14": version: 1.2.14 resolution: "@backstage/plugin-search-common@npm:1.2.14" @@ -5763,7 +5996,7 @@ __metadata: languageName: node linkType: hard -"@backstage/test-utils@npm:^1.7.0": +"@backstage/test-utils@npm:^1.5.7, @backstage/test-utils@npm:^1.7.0": version: 1.7.0 resolution: "@backstage/test-utils@npm:1.7.0" dependencies: @@ -5838,7 +6071,7 @@ __metadata: languageName: node linkType: hard -"@backstage/version-bridge@npm:^1.0.10": +"@backstage/version-bridge@npm:^1.0.10, @backstage/version-bridge@npm:^1.0.8": version: 1.0.10 resolution: "@backstage/version-bridge@npm:1.0.10" peerDependencies: @@ -5853,19 +6086,6 @@ __metadata: languageName: node linkType: hard -"@backstage/version-bridge@npm:^1.0.8": - version: 1.0.8 - resolution: "@backstage/version-bridge@npm:1.0.8" - dependencies: - "@types/react": ^16.13.1 || ^17.0.0 - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: bf74cd70af7c23558d26637a90ed1ffe52449396a9759cbbb0f87f3517c6a2a760140c2723c8aabeb2e94b436e02110e78763e262293a88b37e15e622753f23a - languageName: node - linkType: hard - "@balena/dockerignore@npm:^1.0.2": version: 1.0.2 resolution: "@balena/dockerignore@npm:1.0.2" @@ -6502,6 +6722,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/aix-ppc64@npm:0.21.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/aix-ppc64@npm:0.24.0" @@ -6516,6 +6743,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm64@npm:0.21.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/android-arm64@npm:0.24.0" @@ -6530,6 +6764,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm@npm:0.21.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/android-arm@npm:0.24.0" @@ -6544,6 +6785,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-x64@npm:0.21.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/android-x64@npm:0.24.0" @@ -6558,6 +6806,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-arm64@npm:0.21.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/darwin-arm64@npm:0.24.0" @@ -6572,6 +6827,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-x64@npm:0.21.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/darwin-x64@npm:0.24.0" @@ -6586,6 +6848,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-arm64@npm:0.21.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/freebsd-arm64@npm:0.24.0" @@ -6600,6 +6869,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-x64@npm:0.21.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/freebsd-x64@npm:0.24.0" @@ -6614,6 +6890,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm64@npm:0.21.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/linux-arm64@npm:0.24.0" @@ -6628,6 +6911,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm@npm:0.21.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/linux-arm@npm:0.24.0" @@ -6642,6 +6932,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ia32@npm:0.21.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/linux-ia32@npm:0.24.0" @@ -6656,6 +6953,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-loong64@npm:0.21.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/linux-loong64@npm:0.24.0" @@ -6670,6 +6974,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-mips64el@npm:0.21.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/linux-mips64el@npm:0.24.0" @@ -6684,6 +6995,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ppc64@npm:0.21.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/linux-ppc64@npm:0.24.0" @@ -6698,6 +7016,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-riscv64@npm:0.21.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/linux-riscv64@npm:0.24.0" @@ -6712,6 +7037,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-s390x@npm:0.21.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/linux-s390x@npm:0.24.0" @@ -6726,6 +7058,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-x64@npm:0.21.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/linux-x64@npm:0.24.0" @@ -6740,6 +7079,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/netbsd-x64@npm:0.21.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/netbsd-x64@npm:0.24.0" @@ -6761,6 +7107,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/openbsd-x64@npm:0.21.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/openbsd-x64@npm:0.24.0" @@ -6775,6 +7128,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/sunos-x64@npm:0.21.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/sunos-x64@npm:0.24.0" @@ -6789,6 +7149,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-arm64@npm:0.21.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/win32-arm64@npm:0.24.0" @@ -6803,6 +7170,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-ia32@npm:0.21.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/win32-ia32@npm:0.24.0" @@ -6817,6 +7191,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-x64@npm:0.21.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.24.0": version: 0.24.0 resolution: "@esbuild/win32-x64@npm:0.24.0" @@ -7088,23 +7469,7 @@ __metadata: languageName: node linkType: hard -"@graphiql/toolkit@npm:^0.9.1": - version: 0.9.1 - resolution: "@graphiql/toolkit@npm:0.9.1" - dependencies: - "@n1ru4l/push-pull-async-iterable-iterator": ^3.1.0 - meros: ^1.1.4 - peerDependencies: - graphql: ^15.5.0 || ^16.0.0 - graphql-ws: ">= 4.5.0" - peerDependenciesMeta: - graphql-ws: - optional: true - checksum: 5328426051b7f9a9ffbd569c950d1a103ce0e2ee7b5d7a57f3d899488ad43d1a5101e8aeced7416e106c7687d67bb7981aa7e87dea5b0f17b77569aa738bf3b5 - languageName: node - linkType: hard - -"@graphiql/toolkit@npm:^0.9.2": +"@graphiql/toolkit@npm:^0.9.1, @graphiql/toolkit@npm:^0.9.2": version: 0.9.2 resolution: "@graphiql/toolkit@npm:0.9.2" dependencies: @@ -7522,6 +7887,54 @@ __metadata: languageName: node linkType: hard +"@internal/backstage-plugin-opa-demo-backend@^0.1.0, @internal/backstage-plugin-opa-demo-backend@workspace:plugins/opa-demo-backend": + version: 0.0.0-use.local + resolution: "@internal/backstage-plugin-opa-demo-backend@workspace:plugins/opa-demo-backend" + dependencies: + "@backstage/backend-defaults": ^0.5.1 + "@backstage/backend-plugin-api": ^1.0.1 + "@backstage/backend-test-utils": ^1.0.1 + "@backstage/catalog-client": ^1.7.1 + "@backstage/cli": ^0.28.0 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-catalog-node": ^1.13.1 + "@parsifal-m/backstage-opa-authz": "workspace:^" + "@types/express": "*" + "@types/supertest": ^2.0.12 + express: ^4.17.1 + express-promise-router: ^4.1.0 + supertest: ^6.2.4 + zod: ^3.22.4 + languageName: unknown + linkType: soft + +"@internal/backstage-plugin-opa-frontend-demo@^0.1.0, @internal/backstage-plugin-opa-frontend-demo@workspace:plugins/opa-frontend-demo": + version: 0.0.0-use.local + resolution: "@internal/backstage-plugin-opa-frontend-demo@workspace:plugins/opa-frontend-demo" + dependencies: + "@backstage/cli": ^0.28.0 + "@backstage/core-app-api": ^1.15.1 + "@backstage/core-components": ^0.15.1 + "@backstage/core-plugin-api": ^1.9.3 + "@backstage/dev-utils": ^1.1.2 + "@backstage/test-utils": ^1.7.0 + "@backstage/theme": ^0.6.0 + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.61 + "@parsifal-m/backstage-plugin-opa-authz-react": "workspace:^" + "@testing-library/jest-dom": ^6.0.0 + "@testing-library/react": ^14.0.0 + "@testing-library/user-event": ^14.0.0 + msw: ^1.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + languageName: unknown + linkType: soft + "@ioredis/commands@npm:^1.1.1": version: 1.2.0 resolution: "@ioredis/commands@npm:1.2.0" @@ -8275,6 +8688,35 @@ __metadata: languageName: node linkType: hard +"@module-federation/dts-plugin@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/dts-plugin@npm:0.1.21" + dependencies: + "@module-federation/managers": 0.1.21 + "@module-federation/sdk": 0.1.21 + "@module-federation/third-party-dts-extractor": 0.1.21 + adm-zip: ^0.5.10 + ansi-colors: ^4.1.3 + axios: ^1.6.7 + chalk: 3.0.0 + fs-extra: 9.1.0 + isomorphic-ws: 5.0.0 + koa: 2.11.0 + lodash.clonedeepwith: 4.5.0 + log4js: 6.9.1 + node-schedule: 2.1.1 + rambda: ^9.1.0 + ws: 8.17.0 + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: ^1.0.24 + peerDependenciesMeta: + vue-tsc: + optional: true + checksum: ec4cd030a25617698754cbac2da5463f8942cdd0a64bdc95f6ff5fd29fff6b88cf3db90e53e6b260cd3593893fe6ee2d6e149d0a46698eb7f9cc19a9df26193d + languageName: node + linkType: hard + "@module-federation/dts-plugin@npm:0.6.6": version: 0.6.6 resolution: "@module-federation/dts-plugin@npm:0.6.6" @@ -8304,6 +8746,32 @@ __metadata: languageName: node linkType: hard +"@module-federation/enhanced@npm:^0.1.19": + version: 0.1.21 + resolution: "@module-federation/enhanced@npm:0.1.21" + dependencies: + "@module-federation/dts-plugin": 0.1.21 + "@module-federation/managers": 0.1.21 + "@module-federation/manifest": 0.1.21 + "@module-federation/rspack": 0.1.21 + "@module-federation/runtime-tools": 0.1.21 + "@module-federation/sdk": 0.1.21 + upath: 2.0.1 + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: ^1.0.24 + webpack: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + webpack: + optional: true + checksum: bc0ff541db1066b290b3ad9ab868437dc3d0754b9d06ab263f8fb7f54e08238eae9232a482a681dd6152e4a175578d65df9f27fe181fc8fb602d7cc1ae34807d + languageName: node + linkType: hard + "@module-federation/enhanced@npm:^0.6.0": version: 0.6.6 resolution: "@module-federation/enhanced@npm:0.6.6" @@ -8333,6 +8801,17 @@ __metadata: languageName: node linkType: hard +"@module-federation/managers@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/managers@npm:0.1.21" + dependencies: + "@module-federation/sdk": 0.1.21 + find-pkg: 2.0.0 + fs-extra: 9.1.0 + checksum: 5f230d5795d86dfd68c404ee2b7a1264950c283a4b1c6f4ee9cc9579fabb413718dfbc1ff726b9c213f9d3223d944dd38dd9d04b700962e6398c3c3728d6323e + languageName: node + linkType: hard + "@module-federation/managers@npm:0.6.6": version: 0.6.6 resolution: "@module-federation/managers@npm:0.6.6" @@ -8344,6 +8823,19 @@ __metadata: languageName: node linkType: hard +"@module-federation/manifest@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/manifest@npm:0.1.21" + dependencies: + "@module-federation/dts-plugin": 0.1.21 + "@module-federation/managers": 0.1.21 + "@module-federation/sdk": 0.1.21 + chalk: 3.0.0 + find-pkg: 2.0.0 + checksum: cef2011875f14e853a355626ae1dbc8ae3b0714d31140e329b5dd71525782b08c2e1d6ca45276a563bb3c3b7f7c4e64a31f0698ef12606f05aa6da46e759f345 + languageName: node + linkType: hard + "@module-federation/manifest@npm:0.6.6": version: 0.6.6 resolution: "@module-federation/manifest@npm:0.6.6" @@ -8357,6 +8849,19 @@ __metadata: languageName: node linkType: hard +"@module-federation/rspack@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/rspack@npm:0.1.21" + dependencies: + "@module-federation/dts-plugin": 0.1.21 + "@module-federation/managers": 0.1.21 + "@module-federation/manifest": 0.1.21 + "@module-federation/runtime-tools": 0.1.21 + "@module-federation/sdk": 0.1.21 + checksum: 55516285e23f4ca7127afafb14af667defbe46dc3224f85d7e07edbc8937d7fac909dfebc2f9dd73120b99bbe5135372cf0fbbe282990d80e6953a60dfa4c93e + languageName: node + linkType: hard + "@module-federation/rspack@npm:0.6.6": version: 0.6.6 resolution: "@module-federation/rspack@npm:0.6.6" @@ -8379,6 +8884,16 @@ __metadata: languageName: node linkType: hard +"@module-federation/runtime-tools@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/runtime-tools@npm:0.1.21" + dependencies: + "@module-federation/runtime": 0.1.21 + "@module-federation/webpack-bundler-runtime": 0.1.21 + checksum: 628c0c4834093520f9c71481d587c9e18163f82e481b05b1900f04e2d5da4abb69af6d814ac5cd1951057b28d73f3adeb1cee7cd83628305b10cc7988405fbc5 + languageName: node + linkType: hard + "@module-federation/runtime-tools@npm:0.6.6": version: 0.6.6 resolution: "@module-federation/runtime-tools@npm:0.6.6" @@ -8389,6 +8904,15 @@ __metadata: languageName: node linkType: hard +"@module-federation/runtime@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/runtime@npm:0.1.21" + dependencies: + "@module-federation/sdk": 0.1.21 + checksum: ce4de8515b54f1cd07a3c7c4cbd35fea163294b9fb24be10827872f3ebb62cd5c289f3602efe4149d963282739f79b51947afa039ee6f36be7f66dea83d590fc + languageName: node + linkType: hard + "@module-federation/runtime@npm:0.6.6": version: 0.6.6 resolution: "@module-federation/runtime@npm:0.6.6" @@ -8398,6 +8922,13 @@ __metadata: languageName: node linkType: hard +"@module-federation/sdk@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/sdk@npm:0.1.21" + checksum: 6856dcfe2ef5ae939890b82010aaad911fa6c4330a05f290ae054c316c9b532d3691456a1f9e176fe05f1df2d6f2d8c7e0c842ca5648a0fd7abf270e44ed9ecb + languageName: node + linkType: hard + "@module-federation/sdk@npm:0.6.6": version: 0.6.6 resolution: "@module-federation/sdk@npm:0.6.6" @@ -8405,6 +8936,17 @@ __metadata: languageName: node linkType: hard +"@module-federation/third-party-dts-extractor@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/third-party-dts-extractor@npm:0.1.21" + dependencies: + find-pkg: 2.0.0 + fs-extra: 9.1.0 + resolve: 1.22.8 + checksum: e394fd7c2e6dbdf8df6937628680e7356ac897ee6f1309d7fbc38c00bcf4be9c0363f8bc1a75c29f7987a5a2f11f7855481813889b18e8b444ee9006aeb4a299 + languageName: node + linkType: hard + "@module-federation/third-party-dts-extractor@npm:0.6.6": version: 0.6.6 resolution: "@module-federation/third-party-dts-extractor@npm:0.6.6" @@ -8416,6 +8958,16 @@ __metadata: languageName: node linkType: hard +"@module-federation/webpack-bundler-runtime@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/webpack-bundler-runtime@npm:0.1.21" + dependencies: + "@module-federation/runtime": 0.1.21 + "@module-federation/sdk": 0.1.21 + checksum: 7d96002066e63bdb503964fd5fb2798be25f4135a599d87721f4d26ebe1de1affbf447c56b082f7ee850ae7798d0ac637f6a486f58591269065e114051b466e5 + languageName: node + linkType: hard + "@module-federation/webpack-bundler-runtime@npm:0.6.6": version: 0.6.6 resolution: "@module-federation/webpack-bundler-runtime@npm:0.6.6" @@ -9416,6 +9968,38 @@ __metadata: languageName: node linkType: hard +"@parsifal-m/backstage-opa-authz@workspace:^, @parsifal-m/backstage-opa-authz@workspace:packages/opa-authz": + version: 0.0.0-use.local + resolution: "@parsifal-m/backstage-opa-authz@workspace:packages/opa-authz" + dependencies: + "@backstage/backend-plugin-api": ^0.6.21 + "@backstage/backend-test-utils": ^0.4.3 + "@backstage/cli": ^0.26.10 + "@backstage/config": ^1.2.0 + express: ^4.17.1 + node-fetch: ^2.6.7 + supertest: ^7.0.0 + languageName: unknown + linkType: soft + +"@parsifal-m/backstage-plugin-opa-authz-react@workspace:^, @parsifal-m/backstage-plugin-opa-authz-react@workspace:plugins/opa-authz-react": + version: 0.0.0-use.local + resolution: "@parsifal-m/backstage-plugin-opa-authz-react@workspace:plugins/opa-authz-react" + dependencies: + "@backstage/cli": ^0.26.10 + "@backstage/core-plugin-api": ^1.9.3 + "@backstage/test-utils": ^1.5.7 + "@material-ui/core": ^4.9.13 + "@testing-library/dom": ^9.0.0 + "@testing-library/jest-dom": ^6.0.0 + "@testing-library/react": ^14.0.0 + msw: ^1.0.0 + swr: ^2.2.5 + peerDependencies: + react: ^18.0.0 + languageName: unknown + linkType: soft + "@parsifal-m/plugin-dev-quotes-homepage@npm:^3.0.3": version: 3.0.4 resolution: "@parsifal-m/plugin-dev-quotes-homepage@npm:3.0.4" @@ -9439,14 +10023,14 @@ __metadata: "@backstage/config": ^1.2.0 "@backstage/errors": ^1.2.4 "@backstage/integration": ^1.15.1 - "@types/express": "*" + "@backstage/plugin-catalog-node": ^1.13.1 + "@types/express": ^4.17.6 "@types/supertest": ^2.0.12 express: ^4.17.1 express-promise-router: ^4.1.0 msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.2.4 - winston: ^3.2.1 yn: ^4.0.0 languageName: unknown linkType: soft @@ -9503,7 +10087,7 @@ __metadata: react-syntax-highlighter: ^15.5.0 react-use: ^17.2.4 peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 languageName: unknown linkType: soft @@ -10301,6 +10885,25 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-commonjs@npm:^25.0.0": + version: 25.0.8 + resolution: "@rollup/plugin-commonjs@npm:25.0.8" + dependencies: + "@rollup/pluginutils": ^5.0.1 + commondir: ^1.0.1 + estree-walker: ^2.0.2 + glob: ^8.0.3 + is-reference: 1.2.1 + magic-string: ^0.30.3 + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: dd105ee5625fbcaf832c0cf80be0aaf6a86bbd8fe99ff911f9ac4b78c79f26e9e99442b5aa0cc1136b5ddf89ec0b6c5728e5341ac04d687aef1b53063670b395 + languageName: node + linkType: hard + "@rollup/plugin-commonjs@npm:^26.0.0": version: 26.0.3 resolution: "@rollup/plugin-commonjs@npm:26.0.3" @@ -12368,36 +12971,34 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:^6.1.5": - version: 6.4.5 - resolution: "@testing-library/jest-dom@npm:6.4.5" +"@testing-library/dom@npm:^9.0.0": + version: 9.3.4 + resolution: "@testing-library/dom@npm:9.3.4" dependencies: - "@adobe/css-tools": ^4.3.2 - "@babel/runtime": ^7.9.2 + "@babel/code-frame": ^7.10.4 + "@babel/runtime": ^7.12.5 + "@types/aria-query": ^5.0.1 + aria-query: 5.1.3 + chalk: ^4.1.0 + dom-accessibility-api: ^0.5.9 + lz-string: ^1.5.0 + pretty-format: ^27.0.2 + checksum: dfd6fb0d6c7b4dd716ba3c47309bc9541b4a55772cb61758b4f396b3785efe2dbc75dc63423545c039078c7ffcc5e4b8c67c2db1b6af4799580466036f70026f + languageName: node + linkType: hard + +"@testing-library/jest-dom@npm:^6.0.0, @testing-library/jest-dom@npm:^6.1.5": + version: 6.5.0 + resolution: "@testing-library/jest-dom@npm:6.5.0" + dependencies: + "@adobe/css-tools": ^4.4.0 aria-query: ^5.0.0 chalk: ^3.0.0 css.escape: ^1.5.1 dom-accessibility-api: ^0.6.3 lodash: ^4.17.21 redent: ^3.0.0 - peerDependencies: - "@jest/globals": ">= 28" - "@types/bun": "*" - "@types/jest": ">= 28" - jest: ">= 28" - vitest: ">= 0.32" - peerDependenciesMeta: - "@jest/globals": - optional: true - "@types/bun": - optional: true - "@types/jest": - optional: true - jest: - optional: true - vitest: - optional: true - checksum: 95bd94f1f4ba2110eaaa15174207f74d46481f0f168a4d58c30f92a9285f797f9949c166aa8401bcb46e16edbf14a097013204d65801e9d1361892757e525bd6 + checksum: c2d14103ebe3358852ec527ff7512f64207a39932b2f7b6dff7e73ba91296b01a71bad9a9584b6ee010681380a906c1740af50470adc6db660e1c7585d012ebf languageName: node linkType: hard @@ -12415,6 +13016,20 @@ __metadata: languageName: node linkType: hard +"@testing-library/react@npm:^14.0.0": + version: 14.3.1 + resolution: "@testing-library/react@npm:14.3.1" + dependencies: + "@babel/runtime": ^7.12.5 + "@testing-library/dom": ^9.0.0 + "@types/react-dom": ^18.0.0 + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: b057d4c9db5a523acfc24d7bc4665a924ab8d6f252c7f51eecf7dd30f1239413e1134925fd5cc9cbdef80496af64c04e6719b2081f89fe05ba87e8c6305bcc16 + languageName: node + linkType: hard + "@testing-library/user-event@npm:^14.0.0": version: 14.5.2 resolution: "@testing-library/user-event@npm:14.5.2" @@ -12895,7 +13510,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.11, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.11, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98 @@ -13045,23 +13660,14 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^16.11.26, @types/node@npm:^16.9.2": +"@types/node@npm:^16.11.26": version: 16.18.97 resolution: "@types/node@npm:16.18.97" checksum: 54f44aaeaa523d4c728177d070aeb20b8011e12ac45aff0d992e350e10cac4d899ac6429cd0f06a6c3a001c8a6cd204429b1a16628d82f1b1e4cc1cbdeca780f languageName: node linkType: hard -"@types/node@npm:^18.11.18": - version: 18.19.33 - resolution: "@types/node@npm:18.19.33" - dependencies: - undici-types: ~5.26.4 - checksum: b6db87d095bc541d64a410fa323a35c22c6113220b71b608bbe810b2397932d0f0a51c3c0f3ef90c20d8180a1502d950a7c5314b907e182d9cc10b36efd2a44e - languageName: node - linkType: hard - -"@types/node@npm:^18.11.9": +"@types/node@npm:^18.11.18, @types/node@npm:^18.11.9": version: 18.19.54 resolution: "@types/node@npm:18.19.54" dependencies: @@ -13139,14 +13745,7 @@ __metadata: languageName: node linkType: hard -"@types/qs@npm:*": - version: 6.9.15 - resolution: "@types/qs@npm:6.9.15" - checksum: 97d8208c2b82013b618e7a9fc14df6bd40a73e1385ac479b6896bafc7949a46201c15f42afd06e86a05e914f146f495f606b6fb65610cc60cf2e0ff743ec38a2 - languageName: node - linkType: hard - -"@types/qs@npm:^6.9.11, @types/qs@npm:^6.9.6": +"@types/qs@npm:*, @types/qs@npm:^6.9.11, @types/qs@npm:^6.9.6": version: 6.9.16 resolution: "@types/qs@npm:6.9.16" checksum: 2e8918150c12735630f7ee16b770c72949274938c30306025f68aaf977227f41fe0c698ed93db1099e04916d582ac5a1faf7e3c7061c8d885d9169f59a184b6c @@ -13169,12 +13768,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^17": - version: 17.0.25 - resolution: "@types/react-dom@npm:17.0.25" +"@types/react-dom@npm:^18": + version: 18.3.1 + resolution: "@types/react-dom@npm:18.3.1" dependencies: - "@types/react": ^17 - checksum: d1e582682478e0848c8d54ea3e89d02047bac6d916266b85ce63731b06987575919653ea7159d98fda47ade3362b8c4d5796831549564b83088e7aa9ce8b60ed + "@types/react": "*" + checksum: ad28ecce3915d30dc76adc2a1373fda1745ba429cea290e16c6628df9a05fd80b6403c8e87d78b45e6c60e51df7a67add389ab62b90070fbfdc9bda8307d9953 languageName: node linkType: hard @@ -13217,14 +13816,13 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:^17": - version: 17.0.80 - resolution: "@types/react@npm:17.0.80" +"@types/react@npm:^18": + version: 18.3.11 + resolution: "@types/react@npm:18.3.11" dependencies: "@types/prop-types": "*" - "@types/scheduler": ^0.16 csstype: ^3.0.2 - checksum: 1c27bfc42305d77ef0da55f8f6d4c4a3471aa02b294fcf29ea0f2cfb0bf02892e5a0a3bc7559fa4a5ba50697b2e31076cb5aa5987f69cfc2e880f6426edb8bdf + checksum: 6cbf36673b64e758dd61b16c24139d015f58530e0d476777de26ba83f24b55e142fbf64e3b8f6b3c7b05ed9ba548551b2a62d9ffb0f95743d0a368646a619163 languageName: node linkType: hard @@ -13263,14 +13861,7 @@ __metadata: languageName: node linkType: hard -"@types/scheduler@npm:^0.16": - version: 0.16.8 - resolution: "@types/scheduler@npm:0.16.8" - checksum: 6c091b096daa490093bf30dd7947cd28e5b2cd612ec93448432b33f724b162587fed9309a0acc104d97b69b1d49a0f3fc755a62282054d62975d53d7fd13472d - languageName: node - linkType: hard - -"@types/semver@npm:7.5.8, @types/semver@npm:^7.3.12, @types/semver@npm:^7.5.0, @types/semver@npm:^7.5.8": +"@types/semver@npm:7.5.8, @types/semver@npm:^7.3.12, @types/semver@npm:^7.5.0": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" checksum: ea6f5276f5b84c55921785a3a27a3cd37afee0111dfe2bcb3e03c31819c197c782598f17f0b150a69d453c9584cd14c4c4d7b9a55d2c5e6cacd4d66fdb3b3663 @@ -13470,7 +14061,7 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:*": +"@types/ws@npm:*, @types/ws@npm:^8.0.0, @types/ws@npm:^8.5.10, @types/ws@npm:^8.5.3": version: 8.5.12 resolution: "@types/ws@npm:8.5.12" dependencies: @@ -13479,15 +14070,6 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.0.0, @types/ws@npm:^8.5.10, @types/ws@npm:^8.5.3": - version: 8.5.10 - resolution: "@types/ws@npm:8.5.10" - dependencies: - "@types/node": "*" - checksum: 3ec416ea2be24042ebd677932a462cf16d2080393d8d7d0b1b3f5d6eaa4a7387aaf0eefb99193c0bfd29444857cf2e0c3ac89899e130550dc6c14ada8a46d25e - languageName: node - linkType: hard - "@types/xml-encryption@npm:^1.2.4": version: 1.2.4 resolution: "@types/xml-encryption@npm:1.2.4" @@ -13594,16 +14176,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:7.8.0": - version: 7.8.0 - resolution: "@typescript-eslint/scope-manager@npm:7.8.0" - dependencies: - "@typescript-eslint/types": 7.8.0 - "@typescript-eslint/visitor-keys": 7.8.0 - checksum: 2ab9158f2d055f0917b7004568e50fec112d4a7abcc36a04bdded4fbb32f5ac3bb2ed57e12aec9cc1f41a9322dcd97d7bc1529e3a90640a6c431887e75099527 - languageName: node - linkType: hard - "@typescript-eslint/scope-manager@npm:8.7.0": version: 8.7.0 resolution: "@typescript-eslint/scope-manager@npm:8.7.0" @@ -13645,13 +14217,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:7.8.0": - version: 7.8.0 - resolution: "@typescript-eslint/types@npm:7.8.0" - checksum: fb4b0e09cae2cf66e4699f0f978a39e7aa82aab1112858ca40265c1aeb628cdecd95856beaf727b8479b1abeac181601241348f5d387fcd1f51293eb65b18a54 - languageName: node - linkType: hard - "@typescript-eslint/types@npm:8.7.0": version: 8.7.0 resolution: "@typescript-eslint/types@npm:8.7.0" @@ -13696,25 +14261,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:7.8.0": - version: 7.8.0 - resolution: "@typescript-eslint/typescript-estree@npm:7.8.0" - dependencies: - "@typescript-eslint/types": 7.8.0 - "@typescript-eslint/visitor-keys": 7.8.0 - debug: ^4.3.4 - globby: ^11.1.0 - is-glob: ^4.0.3 - minimatch: ^9.0.4 - semver: ^7.6.0 - ts-api-utils: ^1.3.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 278ac7f988bde27ac5bf8400ad141125783895be53ba2cd1ad2faaa30b01dbcbc026a6aa2db4a877f9453c8c2811465cb7b91c30f15ebd9450415c9b27250a1d - languageName: node - linkType: hard - "@typescript-eslint/typescript-estree@npm:8.7.0": version: 8.7.0 resolution: "@typescript-eslint/typescript-estree@npm:8.7.0" @@ -13751,7 +14297,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^5.58.0": +"@typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.58.0": version: 5.62.0 resolution: "@typescript-eslint/utils@npm:5.62.0" dependencies: @@ -13769,23 +14315,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0": - version: 7.8.0 - resolution: "@typescript-eslint/utils@npm:7.8.0" - dependencies: - "@eslint-community/eslint-utils": ^4.4.0 - "@types/json-schema": ^7.0.15 - "@types/semver": ^7.5.8 - "@typescript-eslint/scope-manager": 7.8.0 - "@typescript-eslint/types": 7.8.0 - "@typescript-eslint/typescript-estree": 7.8.0 - semver: ^7.6.0 - peerDependencies: - eslint: ^8.56.0 - checksum: 770c4742acf3a1845dcc7c280d6af3d338b02187c333f7df4a5f974ba69f56d6be84b888b1d951674f5aab2317b32d3f29a96292d992a87d1a9238d34b15c943 - languageName: node - linkType: hard - "@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0": version: 8.7.0 resolution: "@typescript-eslint/utils@npm:8.7.0" @@ -13820,16 +14349,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:7.8.0": - version: 7.8.0 - resolution: "@typescript-eslint/visitor-keys@npm:7.8.0" - dependencies: - "@typescript-eslint/types": 7.8.0 - eslint-visitor-keys: ^3.4.3 - checksum: 9e635f783188733b41fd6b34053f9a06a85f24c24734882e341116c496e04561fa3ad93c951d4bd4d25a76c2a31219f4329b16ade85bf03222a492dc77a3418f - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:8.7.0": version: 8.7.0 resolution: "@typescript-eslint/visitor-keys@npm:8.7.0" @@ -14360,19 +14879,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.11.2, ajv@npm:^8.12.0, ajv@npm:^8.6.0, ajv@npm:^8.6.3, ajv@npm:^8.9.0": - version: 8.13.0 - resolution: "ajv@npm:8.13.0" - dependencies: - fast-deep-equal: ^3.1.3 - json-schema-traverse: ^1.0.0 - require-from-string: ^2.0.2 - uri-js: ^4.4.1 - checksum: 6de82d0b2073e645ca3300561356ddda0234f39b35d2125a8700b650509b296f41c00ab69f53178bbe25ad688bd6ac3747ab44101f2f4bd245952e8fd6ccc3c1 - languageName: node - linkType: hard - -"ajv@npm:^8.16.0": +"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.11.2, ajv@npm:^8.12.0, ajv@npm:^8.16.0, ajv@npm:^8.6.0, ajv@npm:^8.6.3, ajv@npm:^8.9.0": version: 8.17.1 resolution: "ajv@npm:8.17.1" dependencies: @@ -14469,7 +14976,7 @@ __metadata: languageName: node linkType: hard -"any-promise@npm:^1.0.0": +"any-promise@npm:^1.0.0, any-promise@npm:^1.1.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" checksum: 0ee8a9bdbe882c90464d75d1f55cf027f5458650c4bd1f0467e65aec38ccccda07ca5844969ee77ed46d04e7dded3eaceb027e8d32f385688523fe305fa7e1de @@ -14529,8 +15036,10 @@ __metadata: "@backstage/plugin-user-settings": ^0.8.14 "@backstage/test-utils": ^1.7.0 "@backstage/theme": ^0.6.0 + "@internal/backstage-plugin-opa-frontend-demo": ^0.1.0 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 + "@parsifal-m/backstage-plugin-opa-authz-react": "workspace:^" "@parsifal-m/plugin-dev-quotes-homepage": ^3.0.3 "@parsifal-m/plugin-opa-entity-checker": "workspace:*" "@parsifal-m/plugin-opa-policies": "workspace:*" @@ -14543,8 +15052,8 @@ __metadata: cypress: ^9.7.0 eslint-plugin-cypress: ^2.10.3 history: ^5.0.0 - react: ^17.0.2 - react-dom: ^17.0.2 + react: ^18.0.0 + react-dom: ^18.0.0 react-router-dom: ^6.3.0 react-use: ^17.2.4 start-server-and-test: ^1.10.11 @@ -15107,18 +15616,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.0.0, axios@npm:^1.4.0, axios@npm:^1.6.0": - version: 1.6.8 - resolution: "axios@npm:1.6.8" - dependencies: - follow-redirects: ^1.15.6 - form-data: ^4.0.0 - proxy-from-env: ^1.1.0 - checksum: bf007fa4b207d102459300698620b3b0873503c6d47bf5a8f6e43c0c64c90035a4f698b55027ca1958f61ab43723df2781c38a99711848d232cad7accbcdfcdd - languageName: node - linkType: hard - -"axios@npm:^1.7.4": +"axios@npm:^1.0.0, axios@npm:^1.4.0, axios@npm:^1.6.0, axios@npm:^1.6.7, axios@npm:^1.7.4": version: 1.7.7 resolution: "axios@npm:1.7.7" dependencies: @@ -15331,6 +15829,7 @@ __metadata: "@backstage/plugin-search-backend-module-techdocs": ^0.3.0 "@backstage/plugin-search-backend-node": ^1.3.3 "@backstage/plugin-techdocs-backend": ^1.11.0 + "@internal/backstage-plugin-opa-demo-backend": ^0.1.0 "@parsifal-m/plugin-opa-backend": "workspace:*" "@parsifal-m/plugin-permission-backend-module-opa-wrapper": "workspace:*" "@types/dockerode": ^3.3.0 @@ -15598,26 +16097,6 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:1.20.2": - version: 1.20.2 - resolution: "body-parser@npm:1.20.2" - dependencies: - bytes: 3.1.2 - content-type: ~1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.11.0 - raw-body: 2.5.2 - type-is: ~1.6.18 - unpipe: 1.0.0 - checksum: 14d37ec638ab5c93f6099ecaed7f28f890d222c650c69306872e00b9efa081ff6c596cd9afb9930656aae4d6c4e1c17537bea12bb73c87a217cb3cfea8896737 - languageName: node - linkType: hard - "body-parser@npm:1.20.3, body-parser@npm:^1.15.2": version: 1.20.3 resolution: "body-parser@npm:1.20.3" @@ -16502,21 +16981,7 @@ __metadata: languageName: node linkType: hard -"codemirror-graphql@npm:^2.0.11": - version: 2.0.11 - resolution: "codemirror-graphql@npm:2.0.11" - dependencies: - "@types/codemirror": ^0.0.90 - graphql-language-service: 5.2.0 - peerDependencies: - "@codemirror/language": 6.0.0 - codemirror: ^5.65.3 - graphql: ^15.5.0 || ^16.0.0 - checksum: cdbeb713af63a069c5526f864edf4f71dd811437e44dba7967164ea2d380c52dfe51d3ea3fde06c94ffeb596b93af737767923d6fc434c628fa4241621684950 - languageName: node - linkType: hard - -"codemirror-graphql@npm:^2.0.13": +"codemirror-graphql@npm:^2.0.11, codemirror-graphql@npm:^2.0.13": version: 2.1.1 resolution: "codemirror-graphql@npm:2.1.1" dependencies: @@ -17012,7 +17477,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.6.0, cookie@npm:~0.6.0": +"cookie@npm:0.6.0, cookie@npm:^0.6.0, cookie@npm:~0.6.0": version: 0.6.0 resolution: "cookie@npm:0.6.0" checksum: f56a7d32a07db5458e79c726b77e3c2eff655c36792f2b6c58d351fb5f61531e5b1ab7f46987150136e366c65213cbe31729e02a3eaed630c3bf7334635fb410 @@ -17047,6 +17512,16 @@ __metadata: languageName: node linkType: hard +"cookies@npm:~0.8.0": + version: 0.8.0 + resolution: "cookies@npm:0.8.0" + dependencies: + depd: ~2.0.0 + keygrip: ~1.1.0 + checksum: 806055a44f128705265b1bc6a853058da18bf80dea3654ad99be20985b1fa1b14f86c1eef73644aab8071241f8a78acd57202b54c4c5c70769fc694fbb9c4edc + languageName: node + linkType: hard + "cookies@npm:~0.9.0": version: 0.9.1 resolution: "cookies@npm:0.9.1" @@ -17985,6 +18460,15 @@ __metadata: languageName: node linkType: hard +"debug@npm:~3.1.0": + version: 3.1.0 + resolution: "debug@npm:3.1.0" + dependencies: + ms: 2.0.0 + checksum: 0b52718ab957254a5b3ca07fc34543bc778f358620c206a08452251eb7fc193c3ea3505072acbf4350219c14e2d71ceb7bdaa0d3370aa630b50da790458d08b3 + languageName: node + linkType: hard + "decamelize-keys@npm:^1.1.0": version: 1.1.1 resolution: "decamelize-keys@npm:1.1.1" @@ -18224,7 +18708,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:~1.1.2": +"depd@npm:^1.1.2, depd@npm:~1.1.2": version: 1.1.2 resolution: "depd@npm:1.1.2" checksum: 6b406620d269619852885ce15965272b829df6f409724415e0002c8632ab6a8c0a08ec1f0bd2add05dc7bd7507606f7e2cc034fa24224ab829580040b835ecd9 @@ -18847,6 +19331,13 @@ __metadata: languageName: node linkType: hard +"error-inject@npm:^1.0.0": + version: 1.0.0 + resolution: "error-inject@npm:1.0.0" + checksum: 258cb26c7c7e04d9b730d074926ff5e18755b6945781540fdd124cafc5015610d97e4b971eb3226469f407fd34fa899a60fbcf9ade8923ab42fa2a3c61e246cf + languageName: node + linkType: hard + "error-stack-parser@npm:^2.0.6": version: 2.1.4 resolution: "error-stack-parser@npm:2.1.4" @@ -19129,6 +19620,86 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.21.0": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": 0.21.5 + "@esbuild/android-arm": 0.21.5 + "@esbuild/android-arm64": 0.21.5 + "@esbuild/android-x64": 0.21.5 + "@esbuild/darwin-arm64": 0.21.5 + "@esbuild/darwin-x64": 0.21.5 + "@esbuild/freebsd-arm64": 0.21.5 + "@esbuild/freebsd-x64": 0.21.5 + "@esbuild/linux-arm": 0.21.5 + "@esbuild/linux-arm64": 0.21.5 + "@esbuild/linux-ia32": 0.21.5 + "@esbuild/linux-loong64": 0.21.5 + "@esbuild/linux-mips64el": 0.21.5 + "@esbuild/linux-ppc64": 0.21.5 + "@esbuild/linux-riscv64": 0.21.5 + "@esbuild/linux-s390x": 0.21.5 + "@esbuild/linux-x64": 0.21.5 + "@esbuild/netbsd-x64": 0.21.5 + "@esbuild/openbsd-x64": 0.21.5 + "@esbuild/sunos-x64": 0.21.5 + "@esbuild/win32-arm64": 0.21.5 + "@esbuild/win32-ia32": 0.21.5 + "@esbuild/win32-x64": 0.21.5 + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 2911c7b50b23a9df59a7d6d4cdd3a4f85855787f374dce751148dbb13305e0ce7e880dde1608c2ab7a927fc6cec3587b80995f7fc87a64b455f8b70b55fd8ec1 + languageName: node + linkType: hard + "esbuild@npm:^0.24.0": version: 0.24.0 resolution: "esbuild@npm:0.24.0" @@ -19390,13 +19961,13 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jest@npm:*": - version: 28.5.0 - resolution: "eslint-plugin-jest@npm:28.5.0" +"eslint-plugin-jest@npm:*, eslint-plugin-jest@npm:^28.0.0": + version: 28.8.3 + resolution: "eslint-plugin-jest@npm:28.8.3" dependencies: - "@typescript-eslint/utils": ^6.0.0 || ^7.0.0 + "@typescript-eslint/utils": ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependencies: - "@typescript-eslint/eslint-plugin": ^6.0.0 || ^7.0.0 + "@typescript-eslint/eslint-plugin": ^6.0.0 || ^7.0.0 || ^8.0.0 eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 jest: "*" peerDependenciesMeta: @@ -19404,25 +19975,25 @@ __metadata: optional: true jest: optional: true - checksum: 73ba168fb028db0765027c9e7844fe37cb6b660125929e72c5cf5c8e0fd4e67e7136409583256cf6267e066e1aa0be498c0ee729d1f3babaef4a3e7bdac428a8 + checksum: e371fcbe2127a403824b6c23b66f6b2e2cc54074c3c70a9965d48bdcdfb461670965a7d7cdddab68f09e703d3a09a281d05591b1cb4315f5246d27fd8baa84ac languageName: node linkType: hard -"eslint-plugin-jest@npm:^28.0.0": - version: 28.8.3 - resolution: "eslint-plugin-jest@npm:28.8.3" +"eslint-plugin-jest@npm:^27.0.0": + version: 27.9.0 + resolution: "eslint-plugin-jest@npm:27.9.0" dependencies: - "@typescript-eslint/utils": ^6.0.0 || ^7.0.0 || ^8.0.0 + "@typescript-eslint/utils": ^5.10.0 peerDependencies: - "@typescript-eslint/eslint-plugin": ^6.0.0 || ^7.0.0 || ^8.0.0 - eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + "@typescript-eslint/eslint-plugin": ^5.0.0 || ^6.0.0 || ^7.0.0 + eslint: ^7.0.0 || ^8.0.0 jest: "*" peerDependenciesMeta: "@typescript-eslint/eslint-plugin": optional: true jest: optional: true - checksum: e371fcbe2127a403824b6c23b66f6b2e2cc54074c3c70a9965d48bdcdfb461670965a7d7cdddab68f09e703d3a09a281d05591b1cb4315f5246d27fd8baa84ac + checksum: e2a4b415105408de28ad146818fcc6f4e122f6a39c6b2216ec5c24a80393f1390298b20231b0467bc5fd730f6e24b05b89e1a6a3ce651fc159aa4174ecc233d0 languageName: node linkType: hard @@ -19958,7 +20529,7 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.14.0": +"express@npm:^4.14.0, express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.2, express@npm:^4.19.2": version: 4.21.1 resolution: "express@npm:4.21.1" dependencies: @@ -19997,45 +20568,6 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.17.1, express@npm:^4.17.3, express@npm:^4.18.2, express@npm:^4.19.2": - version: 4.19.2 - resolution: "express@npm:4.19.2" - dependencies: - accepts: ~1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.2 - content-disposition: 0.5.4 - content-type: ~1.0.4 - cookie: 0.6.0 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: ~1.0.2 - escape-html: ~1.0.3 - etag: ~1.8.1 - finalhandler: 1.2.0 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.1 - methods: ~1.1.2 - on-finished: 2.4.1 - parseurl: ~1.3.3 - path-to-regexp: 0.1.7 - proxy-addr: ~2.0.7 - qs: 6.11.0 - range-parser: ~1.2.1 - safe-buffer: 5.2.1 - send: 0.18.0 - serve-static: 1.15.0 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: ~1.6.18 - utils-merge: 1.0.1 - vary: ~1.1.2 - checksum: 212dbd6c2c222a96a61bc927639c95970a53b06257080bb9e2838adb3bffdb966856551fdad1ab5dd654a217c35db94f987d0aa88d48fb04d306340f5f34dca5 - languageName: node - linkType: hard - "extend@npm:3.0.2, extend@npm:^3.0.0, extend@npm:^3.0.2, extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -20374,21 +20906,6 @@ __metadata: languageName: node linkType: hard -"finalhandler@npm:1.2.0": - version: 1.2.0 - resolution: "finalhandler@npm:1.2.0" - dependencies: - debug: 2.6.9 - encodeurl: ~1.0.2 - escape-html: ~1.0.3 - on-finished: 2.4.1 - parseurl: ~1.3.3 - statuses: 2.0.1 - unpipe: ~1.0.0 - checksum: 92effbfd32e22a7dff2994acedbd9bcc3aa646a3e919ea6a53238090e87097f8ef07cced90aa2cc421abdf993aefbdd5b00104d55c7c5479a8d00ed105b45716 - languageName: node - linkType: hard - "finalhandler@npm:1.3.1": version: 1.3.1 resolution: "finalhandler@npm:1.3.1" @@ -20669,6 +21186,17 @@ __metadata: languageName: node linkType: hard +"formidable@npm:^3.5.1": + version: 3.5.1 + resolution: "formidable@npm:3.5.1" + dependencies: + dezalgo: ^1.0.4 + hexoid: ^1.0.0 + once: ^1.4.0 + checksum: 46b21496f9f985161cf7636163147b6728f9997c7e1d59433680d92619758bf6862330e6d105b5816bafcd1ab32f27ef183455991f93ef836ea731c68db62af9 + languageName: node + linkType: hard + "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -21110,7 +21638,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.0.0, glob@npm:^10.4.1": +"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7, glob@npm:^10.4.1": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -21126,21 +21654,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7": - version: 10.3.14 - resolution: "glob@npm:10.3.14" - dependencies: - foreground-child: ^3.1.0 - jackspeak: ^2.3.6 - minimatch: ^9.0.1 - minipass: ^7.0.4 - path-scurry: ^1.11.0 - bin: - glob: dist/esm/bin.mjs - checksum: 6fb26013e6ee1cc0fe099c746f5870783612082342eeeca80031446ca8839acc13243015056e4f3158d7c4260d32f37ff1c2aad9c273672ed0911c8262316041 - languageName: node - linkType: hard - "glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7, glob@npm:^7.2.3": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -21155,7 +21668,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.0, glob@npm:^8.0.1, glob@npm:^8.1.0": +"glob@npm:^8.0.0, glob@npm:^8.0.1, glob@npm:^8.0.3, glob@npm:^8.1.0": version: 8.1.0 resolution: "glob@npm:8.1.0" dependencies: @@ -21417,21 +21930,7 @@ __metadata: languageName: node linkType: hard -"graphql-language-service@npm:5.2.0, graphql-language-service@npm:^5.2.0": - version: 5.2.0 - resolution: "graphql-language-service@npm:5.2.0" - dependencies: - nullthrows: ^1.0.0 - vscode-languageserver-types: ^3.17.1 - peerDependencies: - graphql: ^15.5.0 || ^16.0.0 - bin: - graphql: dist/temp-bin.js - checksum: b053c6b7158d0ee7a3e55391bfd8be956fc5380211ca586b3a252007845e119540fb40efcc438975eaebc5ef25f46973f7ff4d9543c66e14ebd992957e0299b7 - languageName: node - linkType: hard - -"graphql-language-service@npm:5.3.0, graphql-language-service@npm:^5.2.2": +"graphql-language-service@npm:5.3.0, graphql-language-service@npm:^5.2.0, graphql-language-service@npm:^5.2.2": version: 5.3.0 resolution: "graphql-language-service@npm:5.3.0" dependencies: @@ -22083,17 +22582,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.0, https-proxy-agent@npm:^7.0.1": - version: 7.0.4 - resolution: "https-proxy-agent@npm:7.0.4" - dependencies: - agent-base: ^7.0.2 - debug: 4 - checksum: daaab857a967a2519ddc724f91edbbd388d766ff141b9025b629f92b9408fc83cee8a27e11a907aede392938e9c398e240d643e178408a59e4073539cde8cfe9 - languageName: node - linkType: hard - -"https-proxy-agent@npm:^7.0.5": +"https-proxy-agent@npm:^7.0.0, https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.5": version: 7.0.5 resolution: "https-proxy-agent@npm:7.0.5" dependencies: @@ -23252,19 +23741,6 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^2.3.6": - version: 2.3.6 - resolution: "jackspeak@npm:2.3.6" - dependencies: - "@isaacs/cliui": ^8.0.2 - "@pkgjs/parseargs": ^0.11.0 - dependenciesMeta: - "@pkgjs/parseargs": - optional: true - checksum: 57d43ad11eadc98cdfe7496612f6bbb5255ea69fe51ea431162db302c2a11011642f50cfad57288bd0aea78384a0612b16e131944ad8ecd09d619041c8531b54 - languageName: node - linkType: hard - "jackspeak@npm:^3.1.2": version: 3.4.3 resolution: "jackspeak@npm:3.4.3" @@ -24501,6 +24977,15 @@ __metadata: languageName: node linkType: hard +"koa-compose@npm:^3.0.0": + version: 3.2.1 + resolution: "koa-compose@npm:3.2.1" + dependencies: + any-promise: ^1.1.0 + checksum: ff8e5fc0348455acf751179c6c613eb030a5fac6406d3b49ae9e00460b7ee8770db3ef62633fd3db0306cd4a6d2a0b5152399ebd5bb5e684418f9eeeb251c2de + languageName: node + linkType: hard + "koa-compose@npm:^4.1.0": version: 4.1.0 resolution: "koa-compose@npm:4.1.0" @@ -24508,6 +24993,16 @@ __metadata: languageName: node linkType: hard +"koa-convert@npm:^1.2.0": + version: 1.2.0 + resolution: "koa-convert@npm:1.2.0" + dependencies: + co: ^4.6.0 + koa-compose: ^3.0.0 + checksum: a33944dbda4ed87565985f5b37ba1122a012db872724b216b6fd8f9176d4bba42c4a9bf3c129330e45f6474d28f50ca0ed28d41b9bccd2ab5d36d6436cf0d676 + languageName: node + linkType: hard + "koa-convert@npm:^2.0.0": version: 2.0.0 resolution: "koa-convert@npm:2.0.0" @@ -24518,6 +25013,38 @@ __metadata: languageName: node linkType: hard +"koa@npm:2.11.0": + version: 2.11.0 + resolution: "koa@npm:2.11.0" + dependencies: + accepts: ^1.3.5 + cache-content-type: ^1.0.0 + content-disposition: ~0.5.2 + content-type: ^1.0.4 + cookies: ~0.8.0 + debug: ~3.1.0 + delegates: ^1.0.0 + depd: ^1.1.2 + destroy: ^1.0.4 + encodeurl: ^1.0.2 + error-inject: ^1.0.0 + escape-html: ^1.0.3 + fresh: ~0.5.2 + http-assert: ^1.3.0 + http-errors: ^1.6.3 + is-generator-function: ^1.0.7 + koa-compose: ^4.1.0 + koa-convert: ^1.2.0 + on-finished: ^2.3.0 + only: ~0.0.2 + parseurl: ^1.3.2 + statuses: ^1.5.0 + type-is: ^1.6.16 + vary: ^1.1.2 + checksum: b08e1aea03e70fe4ff6e35dee9f9e979e8608461ee1002f6e8dd72f45fc49404873888ea9a3aab2904e24bf43522df7c601033522f4151189e4055e87f94a979 + languageName: node + linkType: hard + "koa@npm:2.15.3": version: 2.15.3 resolution: "koa@npm:2.15.3" @@ -25209,20 +25736,20 @@ __metadata: languageName: node linkType: hard -"luxon@npm:^3.0.0, luxon@npm:~3.4.0": - version: 3.4.4 - resolution: "luxon@npm:3.4.4" - checksum: 36c1f99c4796ee4bfddf7dc94fa87815add43ebc44c8934c924946260a58512f0fd2743a629302885df7f35ccbd2d13f178c15df046d0e3b6eb71db178f1c60c - languageName: node - linkType: hard - -"luxon@npm:^3.2.1": +"luxon@npm:^3.0.0, luxon@npm:^3.2.1": version: 3.5.0 resolution: "luxon@npm:3.5.0" checksum: f290fe5788c8e51e748744f05092160d4be12150dca70f9fadc0d233e53d60ce86acd82e7d909a114730a136a77e56f0d3ebac6141bbb82fd310969a4704825b languageName: node linkType: hard +"luxon@npm:~3.4.0": + version: 3.4.4 + resolution: "luxon@npm:3.4.4" + checksum: 36c1f99c4796ee4bfddf7dc94fa87815add43ebc44c8934c924946260a58512f0fd2743a629302885df7f35ccbd2d13f178c15df046d0e3b6eb71db178f1c60c + languageName: node + linkType: hard + "lz-string@npm:^1.5.0": version: 1.5.0 resolution: "lz-string@npm:1.5.0" @@ -25660,13 +26187,6 @@ __metadata: languageName: node linkType: hard -"merge-descriptors@npm:1.0.1": - version: 1.0.1 - resolution: "merge-descriptors@npm:1.0.1" - checksum: 5abc259d2ae25bb06d19ce2b94a21632583c74e2a9109ee1ba7fd147aa7362b380d971e0251069f8b3eb7d48c21ac839e21fa177b335e82c76ec172e30c31a26 - languageName: node - linkType: hard - "merge-descriptors@npm:1.0.3": version: 1.0.3 resolution: "merge-descriptors@npm:1.0.3" @@ -26223,7 +26743,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:^9.0.4": +"minimatch@npm:^9.0.0, minimatch@npm:^9.0.4": version: 9.0.4 resolution: "minimatch@npm:9.0.4" dependencies: @@ -26326,14 +26846,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4": - version: 7.1.1 - resolution: "minipass@npm:7.1.1" - checksum: d2c461947a7530f93de4162aa3ca0a1bed1f121626906f6ec63a5ba05fd7b1d9bee4fe89a37a43db7241c2416be98a799c1796abae583c7180be37be5c392ef6 - languageName: node - linkType: hard - -"minipass@npm:^7.1.2": +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.1.2": version: 7.1.2 resolution: "minipass@npm:7.1.2" checksum: 2bfd325b95c555f2b4d2814d49325691c7bee937d753814861b0b49d5edcda55cbbf22b6b6a60bb91eddac8668771f03c5ff647dcd9d0f798e9548b9cdc46ee3 @@ -27957,16 +28470,6 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.11.0": - version: 1.11.0 - resolution: "path-scurry@npm:1.11.0" - dependencies: - lru-cache: ^10.2.0 - minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 - checksum: 79732c6a4d989846632d3ff8a441fabb8ea197ecdc7f328ea0f1694d611ed9bd3f0c940582d91c7dd108a29898866212a359d1a9b92ca69c4dbb16ebeae86b73 - languageName: node - linkType: hard - "path-scurry@npm:^1.11.1": version: 1.11.1 resolution: "path-scurry@npm:1.11.1" @@ -27984,13 +28487,6 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:0.1.7": - version: 0.1.7 - resolution: "path-to-regexp@npm:0.1.7" - checksum: 69a14ea24db543e8b0f4353305c5eac6907917031340e5a8b37df688e52accd09e3cebfe1660b70d76b6bd89152f52183f28c74813dbf454ba1a01c82a38abce - languageName: node - linkType: hard - "path-to-regexp@npm:^6.2.0, path-to-regexp@npm:^6.2.1": version: 6.2.2 resolution: "path-to-regexp@npm:6.2.2" @@ -29183,16 +29679,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.11.0": - version: 6.11.0 - resolution: "qs@npm:6.11.0" - dependencies: - side-channel: ^1.0.4 - checksum: 6e1f29dd5385f7488ec74ac7b6c92f4d09a90408882d0c208414a34dd33badc1a621019d4c799a3df15ab9b1d0292f97c1dd71dc7c045e69f81a8064e5af7297 - languageName: node - linkType: hard - -"qs@npm:6.13.0, qs@npm:^6.10.3": +"qs@npm:6.13.0, qs@npm:^6.10.1, qs@npm:^6.10.2, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.9.4": version: 6.13.0 resolution: "qs@npm:6.13.0" dependencies: @@ -29201,15 +29688,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.1, qs@npm:^6.10.2, qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.9.4": - version: 6.12.1 - resolution: "qs@npm:6.12.1" - dependencies: - side-channel: ^1.0.6 - checksum: aa761d99e65b6936ba2dd2187f2d9976afbcda38deb3ff1b3fe331d09b0c578ed79ca2abdde1271164b5be619c521ec7db9b34c23f49a074e5921372d16242d5 - languageName: node - linkType: hard - "qs@npm:~6.10.3": version: 6.10.4 resolution: "qs@npm:6.10.4" @@ -29523,16 +30001,15 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^17.0.2": - version: 17.0.2 - resolution: "react-dom@npm:17.0.2" +"react-dom@npm:^18.0.0": + version: 18.3.1 + resolution: "react-dom@npm:18.3.1" dependencies: loose-envify: ^1.1.0 - object-assign: ^4.1.1 - scheduler: ^0.20.2 + scheduler: ^0.23.2 peerDependencies: - react: 17.0.2 - checksum: 1c1eaa3bca7c7228d24b70932e3d7c99e70d1d04e13bb0843bbf321582bc25d7961d6b8a6978a58a598af2af496d1cedcfb1bf65f6b0960a0a8161cb8dab743c + react: ^18.3.1 + checksum: 298954ecd8f78288dcaece05e88b570014d8f6dce5db6f66e6ee91448debeb59dcd31561dddb354eee47e6c1bb234669459060deb238ed0213497146e555a0b9 languageName: node linkType: hard @@ -29940,13 +30417,12 @@ __metadata: languageName: node linkType: hard -"react@npm:^17.0.2": - version: 17.0.2 - resolution: "react@npm:17.0.2" +"react@npm:^16.13.1 || ^17.0.0 || ^18.0.0, react@npm:^18.0.0": + version: 18.3.1 + resolution: "react@npm:18.3.1" dependencies: loose-envify: ^1.1.0 - object-assign: ^4.1.1 - checksum: b254cc17ce3011788330f7bbf383ab653c6848902d7936a87b09d835d091e3f295f7e9dd1597c6daac5dc80f90e778c8230218ba8ad599f74adcc11e33b9d61b + checksum: a27bcfa8ff7c15a1e50244ad0d0c1cb2ad4375eeffefd266a64889beea6f6b64c4966c9b37d14ee32d6c9fcd5aa6ba183b6988167ab4d127d13e7cb5b386a376 languageName: node linkType: hard @@ -30961,13 +31437,12 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.20.2": - version: 0.20.2 - resolution: "scheduler@npm:0.20.2" +"scheduler@npm:^0.23.2": + version: 0.23.2 + resolution: "scheduler@npm:0.23.2" dependencies: loose-envify: ^1.1.0 - object-assign: ^4.1.1 - checksum: c4b35cf967c8f0d3e65753252d0f260271f81a81e427241295c5a7b783abf4ea9e905f22f815ab66676f5313be0a25f47be582254db8f9241b259213e999b8fc + checksum: 3e82d1f419e240ef6219d794ff29c7ee415fbdc19e038f680a10c067108e06284f1847450a210b29bbaf97b9d8a97ced5f624c31c681248ac84c80d56ad5a2c4 languageName: node linkType: hard @@ -31045,7 +31520,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.6.3": +"semver@npm:7.6.3, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -31063,36 +31538,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": - version: 7.6.2 - resolution: "semver@npm:7.6.2" - bin: - semver: bin/semver.js - checksum: 40f6a95101e8d854357a644da1b8dd9d93ce786d5c6a77227bc69dbb17bea83d0d1d1d7c4cd5920a6df909f48e8bd8a5909869535007f90278289f2451d0292d - languageName: node - linkType: hard - -"send@npm:0.18.0": - version: 0.18.0 - resolution: "send@npm:0.18.0" - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: ~1.0.2 - escape-html: ~1.0.3 - etag: ~1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: ~1.2.1 - statuses: 2.0.1 - checksum: 74fc07ebb58566b87b078ec63e5a3e41ecd987e4272ba67b7467e86c6ad51bc6b0b0154133b6d8b08a2ddda360464f71382f7ef864700f34844a76c8027817a8 - languageName: node - linkType: hard - "send@npm:0.19.0": version: 0.19.0 resolution: "send@npm:0.19.0" @@ -31163,18 +31608,6 @@ __metadata: languageName: node linkType: hard -"serve-static@npm:1.15.0": - version: 1.15.0 - resolution: "serve-static@npm:1.15.0" - dependencies: - encodeurl: ~1.0.2 - escape-html: ~1.0.3 - parseurl: ~1.3.3 - send: 0.18.0 - checksum: af57fc13be40d90a12562e98c0b7855cf6e8bd4c107fe9a45c212bf023058d54a1871b1c89511c3958f70626fff47faeb795f5d83f8cf88514dbaeb2b724464d - languageName: node - linkType: hard - "serve-static@npm:1.16.2": version: 1.16.2 resolution: "serve-static@npm:1.16.2" @@ -31505,18 +31938,7 @@ __metadata: languageName: node linkType: hard -"socks-proxy-agent@npm:^8.0.3": - version: 8.0.3 - resolution: "socks-proxy-agent@npm:8.0.3" - dependencies: - agent-base: ^7.1.1 - debug: ^4.3.4 - socks: ^2.7.1 - checksum: 8fab38821c327c190c28f1658087bc520eb065d55bc07b4a0fdf8d1e0e7ad5d115abbb22a95f94f944723ea969dd771ad6416b1e3cde9060c4c71f705c8b85c5 - languageName: node - linkType: hard - -"socks-proxy-agent@npm:^8.0.4": +"socks-proxy-agent@npm:^8.0.3, socks-proxy-agent@npm:^8.0.4": version: 8.0.4 resolution: "socks-proxy-agent@npm:8.0.4" dependencies: @@ -31527,7 +31949,7 @@ __metadata: languageName: node linkType: hard -"socks@npm:^2.6.2, socks@npm:^2.7.1, socks@npm:^2.8.3": +"socks@npm:^2.6.2, socks@npm:^2.8.3": version: 2.8.3 resolution: "socks@npm:2.8.3" dependencies: @@ -32440,6 +32862,23 @@ __metadata: languageName: node linkType: hard +"superagent@npm:^9.0.1": + version: 9.0.2 + resolution: "superagent@npm:9.0.2" + dependencies: + component-emitter: ^1.3.0 + cookiejar: ^2.1.4 + debug: ^4.3.4 + fast-safe-stringify: ^2.1.1 + form-data: ^4.0.0 + formidable: ^3.5.1 + methods: ^1.1.2 + mime: 2.6.0 + qs: ^6.11.0 + checksum: f471461b21f034d844fd0aca332128d61e3afb75c2ee5950f3339f2a3b5ca8b23e2861224f19ad9b43f21c9184d28b7d9384af5a4fde64fdef479efdb15036db + languageName: node + linkType: hard + "supertest@npm:^6.2.4": version: 6.3.4 resolution: "supertest@npm:6.3.4" @@ -32450,6 +32889,16 @@ __metadata: languageName: node linkType: hard +"supertest@npm:^7.0.0": + version: 7.0.0 + resolution: "supertest@npm:7.0.0" + dependencies: + methods: ^1.1.2 + superagent: ^9.0.1 + checksum: 974743aa511ec0f387135dfca05e378f6202366c81f0850dfbcc2c3d6fc690e856dda27e175c70db38510e21d87f331c0f62e1a942afea4c447953c647c26c8b + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -32590,7 +33039,7 @@ __metadata: languageName: node linkType: hard -"swr@npm:^2.0.0": +"swr@npm:^2.0.0, swr@npm:^2.2.5": version: 2.2.5 resolution: "swr@npm:2.2.5" dependencies: @@ -33277,14 +33726,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:^2.5.0, tslib@npm:^2.6.0, tslib@npm:^2.6.2": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad - languageName: node - linkType: hard - -"tslib@npm:^2.0.1": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:^2.5.0, tslib@npm:^2.6.0, tslib@npm:^2.6.2": version: 2.7.0 resolution: "tslib@npm:2.7.0" checksum: 1606d5c89f88d466889def78653f3aab0f88692e80bb2066d090ca6112ae250ec1cfa9dbfaab0d17b60da15a4186e8ec4d893801c67896b277c17374e36e1d28 @@ -33521,24 +33963,6 @@ __metadata: languageName: node linkType: hard -"typescript-json-schema@npm:^0.63.0": - version: 0.63.0 - resolution: "typescript-json-schema@npm:0.63.0" - dependencies: - "@types/json-schema": ^7.0.9 - "@types/node": ^16.9.2 - glob: ^7.1.7 - path-equal: ^1.2.5 - safe-stable-stringify: ^2.2.0 - ts-node: ^10.9.1 - typescript: ~5.1.0 - yargs: ^17.1.1 - bin: - typescript-json-schema: bin/typescript-json-schema - checksum: 619ab7aece08e140ba9542c6378c335751dbff3994a23343d0af67786a0c1e682d532a436c1674ddb10bca3f34972ecac7ba529b66d0e9b3e00ca81defb3aa77 - languageName: node - linkType: hard - "typescript-json-schema@npm:^0.65.0": version: 0.65.1 resolution: "typescript-json-schema@npm:0.65.1" @@ -33557,16 +33981,6 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~5.1.0": - version: 5.1.6 - resolution: "typescript@npm:5.1.6" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: b2f2c35096035fe1f5facd1e38922ccb8558996331405eb00a5111cc948b2e733163cc22fab5db46992aba7dd520fff637f2c1df4996ff0e134e77d3249a7350 - languageName: node - linkType: hard - "typescript@npm:~5.3.3": version: 5.3.3 resolution: "typescript@npm:5.3.3" @@ -33587,16 +34001,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@~5.1.0#~builtin": - version: 5.1.6 - resolution: "typescript@patch:typescript@npm%3A5.1.6#~builtin::version=5.1.6&hash=5da071" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: f53bfe97f7c8b2b6d23cf572750d4e7d1e0c5fff1c36d859d0ec84556a827b8785077bc27676bf7e71fae538e517c3ecc0f37e7f593be913d884805d931bc8be - languageName: node - linkType: hard - "typescript@patch:typescript@~5.3.3#~builtin": version: 5.3.3 resolution: "typescript@patch:typescript@npm%3A5.3.3#~builtin::version=5.3.3&hash=29ae49" @@ -33911,7 +34315,7 @@ __metadata: languageName: node linkType: hard -"uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": +"uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" dependencies: @@ -34846,7 +35250,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:*, ws@npm:^8.8.0": +"ws@npm:*, ws@npm:^8.11.0, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.15.0, ws@npm:^8.16.0, ws@npm:^8.8.0": version: 8.18.0 resolution: "ws@npm:8.18.0" peerDependencies: @@ -34861,9 +35265,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.17.1": - version: 8.17.1 - resolution: "ws@npm:8.17.1" +"ws@npm:8.17.0": + version: 8.17.0 + resolution: "ws@npm:8.17.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -34872,37 +35276,37 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 442badcce1f1178ec87a0b5372ae2e9771e07c4929a3180321901f226127f252441e8689d765aa5cfba5f50ac60dd830954afc5aeae81609aefa11d3ddf5cecf + checksum: 147ef9eab0251364e1d2c55338ad0efb15e6913923ccbfdf20f7a8a6cb8f88432bcd7f4d8f66977135bfad35575644f9983201c1a361019594a4e53977bf6d4e languageName: node linkType: hard -"ws@npm:^7.4.6": - version: 7.5.9 - resolution: "ws@npm:7.5.9" +"ws@npm:8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: ">=5.0.2" peerDependenciesMeta: bufferutil: optional: true utf-8-validate: optional: true - checksum: c3c100a181b731f40b7f2fddf004aa023f79d64f489706a28bc23ff88e87f6a64b3c6651fbec3a84a53960b75159574d7a7385709847a62ddb7ad6af76f49138 + checksum: 442badcce1f1178ec87a0b5372ae2e9771e07c4929a3180321901f226127f252441e8689d765aa5cfba5f50ac60dd830954afc5aeae81609aefa11d3ddf5cecf languageName: node linkType: hard -"ws@npm:^8.11.0, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.15.0, ws@npm:^8.16.0": - version: 8.17.0 - resolution: "ws@npm:8.17.0" +"ws@npm:^7.4.6": + version: 7.5.9 + resolution: "ws@npm:7.5.9" peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" + utf-8-validate: ^5.0.2 peerDependenciesMeta: bufferutil: optional: true utf-8-validate: optional: true - checksum: 147ef9eab0251364e1d2c55338ad0efb15e6913923ccbfdf20f7a8a6cb8f88432bcd7f4d8f66977135bfad35575644f9983201c1a361019594a4e53977bf6d4e + checksum: c3c100a181b731f40b7f2fddf004aa023f79d64f489706a28bc23ff88e87f6a64b3c6651fbec3a84a53960b75159574d7a7385709847a62ddb7ad6af76f49138 languageName: node linkType: hard