From 59276dfb82990fd37378832d2f9cf0960a1feda4 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 10 May 2024 15:31:16 +0000 Subject: [PATCH 01/17] chore(release): 3.0.0 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## @janus-idp/backstage-plugin-rbac-backend [3.0.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-rbac-backend@2.8.2...@janus-idp/backstage-plugin-rbac-backend@3.0.0) (2024-05-10) ### ⚠ BREAKING CHANGES * **rbac:** remove token manager for auth service (#1632) ### Bug Fixes * **rbac:** remove token manager for auth service ([#1632](https://github.com/janus-idp/backstage-plugins/issues/1632)) ([2f19655](https://github.com/janus-idp/backstage-plugins/commit/2f196556cffc61c83239721b1cd51d6a2c64eee7)) --- plugins/rbac-backend/CHANGELOG.md | 11 +++++++++++ plugins/rbac-backend/package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/plugins/rbac-backend/CHANGELOG.md b/plugins/rbac-backend/CHANGELOG.md index 5cc9e37ceae..355f6419c96 100644 --- a/plugins/rbac-backend/CHANGELOG.md +++ b/plugins/rbac-backend/CHANGELOG.md @@ -1,3 +1,14 @@ +## @janus-idp/backstage-plugin-rbac-backend [3.0.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-rbac-backend@2.8.2...@janus-idp/backstage-plugin-rbac-backend@3.0.0) (2024-05-10) + + +### ⚠ BREAKING CHANGES + +* **rbac:** remove token manager for auth service (#1632) + +### Bug Fixes + +* **rbac:** remove token manager for auth service ([#1632](https://github.com/janus-idp/backstage-plugins/issues/1632)) ([2f19655](https://github.com/janus-idp/backstage-plugins/commit/2f196556cffc61c83239721b1cd51d6a2c64eee7)) + ## @janus-idp/backstage-plugin-rbac-backend [2.8.2](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-rbac-backend@2.8.1...@janus-idp/backstage-plugin-rbac-backend@2.8.2) (2024-05-09) diff --git a/plugins/rbac-backend/package.json b/plugins/rbac-backend/package.json index a1a86c4d595..93dd21b917b 100644 --- a/plugins/rbac-backend/package.json +++ b/plugins/rbac-backend/package.json @@ -1,6 +1,6 @@ { "name": "@janus-idp/backstage-plugin-rbac-backend", - "version": "2.8.2", + "version": "3.0.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 7b1c3ed0ff5cc97c4f67969f17d72096158af3fb Mon Sep 17 00:00:00 2001 From: Kim Tsao <84398375+kim-tsao@users.noreply.github.com> Date: Fri, 10 May 2024 16:09:05 -0400 Subject: [PATCH 02/17] chore(owners): remove OWNERS files from kiali, update CODEOWNERS (#1634) Signed-off-by: Kim Tsao --- .github/CODEOWNERS | 1 - plugins/kiali-backend/OWNERS | 8 -------- plugins/kiali/OWNERS | 8 -------- 3 files changed, 17 deletions(-) delete mode 100644 plugins/kiali-backend/OWNERS delete mode 100644 plugins/kiali/OWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0cbc220fff9..70f097a4d53 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -38,7 +38,6 @@ yarn.lock @janus-idp/maintainers-plugins /plugins/analytics-module-matomo @janus-idp/maintainers-plugins @janus-idp/devex-uxe /plugins/kiali @janus-idp/maintainers-plugins @janus-idp/kiali /plugins/kiali-backend @janus-idp/maintainers-plugins @janus-idp/kiali -/plugins/kiali-common @janus-idp/maintainers-plugins @janus-idp/kiali /plugins/quay @janus-idp/rhtap /plugins/tekton @janus-idp/rhtap /plugins/argocd @janus-idp/rhtap diff --git a/plugins/kiali-backend/OWNERS b/plugins/kiali-backend/OWNERS deleted file mode 100644 index c059a5035c7..00000000000 --- a/plugins/kiali-backend/OWNERS +++ /dev/null @@ -1,8 +0,0 @@ -approvers: - - aljesusg - - josunect - - leandroberetta -reviewers: - - aljesusg - - josunect - - leandroberetta \ No newline at end of file diff --git a/plugins/kiali/OWNERS b/plugins/kiali/OWNERS deleted file mode 100644 index c059a5035c7..00000000000 --- a/plugins/kiali/OWNERS +++ /dev/null @@ -1,8 +0,0 @@ -approvers: - - aljesusg - - josunect - - leandroberetta -reviewers: - - aljesusg - - josunect - - leandroberetta \ No newline at end of file From b89ca1deff93a088a6a101091350204c3ee0a117 Mon Sep 17 00:00:00 2001 From: Kim Tsao <84398375+kim-tsao@users.noreply.github.com> Date: Sun, 12 May 2024 12:34:18 -0400 Subject: [PATCH 03/17] chore(owners): remove OWNERS files from devex and rhtap plugins (#1636) Signed-off-by: Kim Tsao --- plugins/analytics-module-matomo/OWNERS | 8 -------- plugins/argocd/OWNERS | 6 ------ plugins/feedback-backend/OWNERS | 8 -------- plugins/feedback/OWNERS | 8 -------- plugins/quay/OWNERS | 8 -------- plugins/tekton/OWNERS | 8 -------- 6 files changed, 46 deletions(-) delete mode 100644 plugins/analytics-module-matomo/OWNERS delete mode 100644 plugins/argocd/OWNERS delete mode 100644 plugins/feedback-backend/OWNERS delete mode 100644 plugins/feedback/OWNERS delete mode 100644 plugins/quay/OWNERS delete mode 100644 plugins/tekton/OWNERS diff --git a/plugins/analytics-module-matomo/OWNERS b/plugins/analytics-module-matomo/OWNERS deleted file mode 100644 index 20e33094710..00000000000 --- a/plugins/analytics-module-matomo/OWNERS +++ /dev/null @@ -1,8 +0,0 @@ -approvers: - - deshmukhmayur - - riginoommen - - yashoswalyo -reviewers: - - deshmukhmayur - - riginoommen - - yashoswalyo diff --git a/plugins/argocd/OWNERS b/plugins/argocd/OWNERS deleted file mode 100644 index 9f4ccefff9f..00000000000 --- a/plugins/argocd/OWNERS +++ /dev/null @@ -1,6 +0,0 @@ -approvers: - - karthikjeeyar - - rohitkrai03 -reviewers: - - karthikjeeyar - - rohitkrai03 \ No newline at end of file diff --git a/plugins/feedback-backend/OWNERS b/plugins/feedback-backend/OWNERS deleted file mode 100644 index ac83ec42036..00000000000 --- a/plugins/feedback-backend/OWNERS +++ /dev/null @@ -1,8 +0,0 @@ -approvers: - - yashoswalyo - - riginoommen - - deshmukhmayur -reviewers: - - yashoswalyo - - riginoommen - - deshmukhmayur diff --git a/plugins/feedback/OWNERS b/plugins/feedback/OWNERS deleted file mode 100644 index ac83ec42036..00000000000 --- a/plugins/feedback/OWNERS +++ /dev/null @@ -1,8 +0,0 @@ -approvers: - - yashoswalyo - - riginoommen - - deshmukhmayur -reviewers: - - yashoswalyo - - riginoommen - - deshmukhmayur diff --git a/plugins/quay/OWNERS b/plugins/quay/OWNERS deleted file mode 100644 index 303b62a594a..00000000000 --- a/plugins/quay/OWNERS +++ /dev/null @@ -1,8 +0,0 @@ -approvers: - - karthikjeeyar - - jeff-phillips-18 - - rohitkrai03 -reviewers: - - karthikjeeyar - - jeff-phillips-18 - - rohitkrai03 \ No newline at end of file diff --git a/plugins/tekton/OWNERS b/plugins/tekton/OWNERS deleted file mode 100644 index 303b62a594a..00000000000 --- a/plugins/tekton/OWNERS +++ /dev/null @@ -1,8 +0,0 @@ -approvers: - - karthikjeeyar - - jeff-phillips-18 - - rohitkrai03 -reviewers: - - karthikjeeyar - - jeff-phillips-18 - - rohitkrai03 \ No newline at end of file From 7ff4c754f73681e1a596d56721972af8872f3211 Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Mon, 13 May 2024 17:47:27 +0200 Subject: [PATCH 04/17] fix(orchestrator): typos mentioning OpenShift (#1639) Fix typos mentioning OpenShift Signed-off-by: Jordi Gil --- plugins/orchestrator/README.md | 2 +- plugins/orchestrator/docs/quickstart.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/orchestrator/README.md b/plugins/orchestrator/README.md index 0148a6d7366..f6ab9f659a2 100644 --- a/plugins/orchestrator/README.md +++ b/plugins/orchestrator/README.md @@ -28,7 +28,7 @@ The orchestrator controls the flow orchestrating operations/tasks that may be ex - Timer/timeout control - Built-in powerful expression evaluation with JQ - Low Code/No code -- Cloud-native architecture Kubernetes/Openshit with Operator support +- Cloud-native architecture Kubernetes/OpenShift with Operator support - OpenAPI / REST built-in integration etc. **Client-side tooling** diff --git a/plugins/orchestrator/docs/quickstart.md b/plugins/orchestrator/docs/quickstart.md index 7c33bb93e0f..064cb238a52 100644 --- a/plugins/orchestrator/docs/quickstart.md +++ b/plugins/orchestrator/docs/quickstart.md @@ -9,7 +9,7 @@ This quickstart guide will help you install the Orchestrator using the helm char Follow the [installation instructions for the greetings workflow](https://github.com/parodos-dev/serverless-workflows-config/blob/gh-pages/docs/greeting/README.md). 3. **Access Red Hat Developer Hub**: - Open your web browser and navigate to the Red Hat Developer Hub application. Retrieve the URL using the following openshift CLI command. + Open your web browser and navigate to the Red Hat Developer Hub application. Retrieve the URL using the following OpenShift CLI command. ```bash oc get route backstage-backstage -n rhdh-operator -o jsonpath='{.spec.host}' @@ -24,7 +24,7 @@ This quickstart guide will help you install the Orchestrator using the helm char Navigate to the Orchestrator page by clicking on the Orchestrator icon in the left navigation menu. ![orchestratorIcon](https://raw.githubusercontent.com/janus-idp/backstage-plugins/main/plugins/orchestrator/docs/orchestratorIcon.png) -6. **Execute Greeting Workflow**: +6. **Execute Greeting Workflow**: Click on the 'Execute' button in the ACTIONS column of the Greeting workflow. ![workflowsPage](https://raw.githubusercontent.com/janus-idp/backstage-plugins/main/plugins/orchestrator/docs/workflowsPage.png) The 'Run workflow' page will open. Click 'Next step' and then 'Run' From c29309cc7b3cb18cd824bb07e302a88604add2d5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 13 May 2024 16:12:48 +0000 Subject: [PATCH 05/17] chore(release): 1.11.2 [skip ci] ## @janus-idp/backstage-plugin-orchestrator [1.11.2](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-orchestrator@1.11.1...@janus-idp/backstage-plugin-orchestrator@1.11.2) (2024-05-13) ### Bug Fixes * **orchestrator:** typos mentioning OpenShift ([#1639](https://github.com/janus-idp/backstage-plugins/issues/1639)) ([7ff4c75](https://github.com/janus-idp/backstage-plugins/commit/7ff4c754f73681e1a596d56721972af8872f3211)) --- plugins/orchestrator/CHANGELOG.md | 7 +++++++ plugins/orchestrator/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/orchestrator/CHANGELOG.md b/plugins/orchestrator/CHANGELOG.md index 4fa657236cc..6dda5705526 100644 --- a/plugins/orchestrator/CHANGELOG.md +++ b/plugins/orchestrator/CHANGELOG.md @@ -1,3 +1,10 @@ +## @janus-idp/backstage-plugin-orchestrator [1.11.2](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-orchestrator@1.11.1...@janus-idp/backstage-plugin-orchestrator@1.11.2) (2024-05-13) + + +### Bug Fixes + +* **orchestrator:** typos mentioning OpenShift ([#1639](https://github.com/janus-idp/backstage-plugins/issues/1639)) ([7ff4c75](https://github.com/janus-idp/backstage-plugins/commit/7ff4c754f73681e1a596d56721972af8872f3211)) + ## @janus-idp/backstage-plugin-orchestrator [1.11.1](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-orchestrator@1.11.0...@janus-idp/backstage-plugin-orchestrator@1.11.1) (2024-05-09) diff --git a/plugins/orchestrator/package.json b/plugins/orchestrator/package.json index 97158dd54b4..288e532fe7f 100644 --- a/plugins/orchestrator/package.json +++ b/plugins/orchestrator/package.json @@ -1,6 +1,6 @@ { "name": "@janus-idp/backstage-plugin-orchestrator", - "version": "1.11.1", + "version": "1.11.2", "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", From a3146073bebb17b6f990891a277323a19e3731d6 Mon Sep 17 00:00:00 2001 From: Pavel Marek <86114326+ScriptingShrimp@users.noreply.github.com> Date: Mon, 13 May 2024 19:22:56 +0200 Subject: [PATCH 06/17] fix(kiali): removing unnecessary afterAll hook (#1642) removing unnecessary afterAll hook --- plugins/kiali/tests/kiali.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/kiali/tests/kiali.spec.ts b/plugins/kiali/tests/kiali.spec.ts index 0a96a94ffef..03ab55ddb4d 100644 --- a/plugins/kiali/tests/kiali.spec.ts +++ b/plugins/kiali/tests/kiali.spec.ts @@ -16,10 +16,6 @@ test.describe('Kiali plugin', () => { await page.locator('[data-test="Kiali Errors"]'); }); - test.afterAll(async ({ browser }) => { - await browser.close(); - }); - test('Networking error', async () => { await expect( page.locator('[data-test="Warning: Error reaching Kiali"]'), From a2c42498504a358b15ccc46410faa94e75447d85 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 13 May 2024 17:48:55 +0000 Subject: [PATCH 07/17] chore(release): 1.17.3 [skip ci] ## @janus-idp/backstage-plugin-kiali [1.17.3](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-kiali@1.17.2...@janus-idp/backstage-plugin-kiali@1.17.3) (2024-05-13) ### Bug Fixes * **kiali:** removing unnecessary afterAll hook ([#1642](https://github.com/janus-idp/backstage-plugins/issues/1642)) ([a314607](https://github.com/janus-idp/backstage-plugins/commit/a3146073bebb17b6f990891a277323a19e3731d6)) --- plugins/kiali/CHANGELOG.md | 7 +++++++ plugins/kiali/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/kiali/CHANGELOG.md b/plugins/kiali/CHANGELOG.md index a97288ea7dd..63f2e4db601 100644 --- a/plugins/kiali/CHANGELOG.md +++ b/plugins/kiali/CHANGELOG.md @@ -1,3 +1,10 @@ +## @janus-idp/backstage-plugin-kiali [1.17.3](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-kiali@1.17.2...@janus-idp/backstage-plugin-kiali@1.17.3) (2024-05-13) + + +### Bug Fixes + +* **kiali:** removing unnecessary afterAll hook ([#1642](https://github.com/janus-idp/backstage-plugins/issues/1642)) ([a314607](https://github.com/janus-idp/backstage-plugins/commit/a3146073bebb17b6f990891a277323a19e3731d6)) + ## @janus-idp/backstage-plugin-kiali [1.17.2](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-kiali@1.17.1...@janus-idp/backstage-plugin-kiali@1.17.2) (2024-05-09) diff --git a/plugins/kiali/package.json b/plugins/kiali/package.json index b893ef70c99..b463cb0d463 100644 --- a/plugins/kiali/package.json +++ b/plugins/kiali/package.json @@ -1,6 +1,6 @@ { "name": "@janus-idp/backstage-plugin-kiali", - "version": "1.17.2", + "version": "1.17.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 2c24d35f050801801c597967e890b6d2e647fb06 Mon Sep 17 00:00:00 2001 From: Karthik Jeeyar Date: Tue, 14 May 2024 14:59:07 +0530 Subject: [PATCH 08/17] fix(argocd): make refreshInterval configuration as optional (#1647) --- plugins/argocd/config.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/argocd/config.d.ts b/plugins/argocd/config.d.ts index 47d5bc3da90..35dd94bbb94 100644 --- a/plugins/argocd/config.d.ts +++ b/plugins/argocd/config.d.ts @@ -20,7 +20,7 @@ export interface Config { * Polling interval timeout * @visibility frontend */ - refreshInterval: number; + refreshInterval?: number; /** * The base url of the ArgoCD instance. * @visibility frontend From 7cb7cb2872d12af165ba2fd7dd6c195154866678 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 14 May 2024 09:56:00 +0000 Subject: [PATCH 09/17] chore(release): 1.0.4 [skip ci] ## @janus-idp/backstage-plugin-argocd [1.0.4](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.0.3...@janus-idp/backstage-plugin-argocd@1.0.4) (2024-05-14) ### Bug Fixes * **argocd:** make refreshInterval configuration as optional ([#1647](https://github.com/janus-idp/backstage-plugins/issues/1647)) ([2c24d35](https://github.com/janus-idp/backstage-plugins/commit/2c24d35f050801801c597967e890b6d2e647fb06)) --- plugins/argocd/CHANGELOG.md | 7 +++++++ plugins/argocd/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/argocd/CHANGELOG.md b/plugins/argocd/CHANGELOG.md index 0332c10d142..338a10521e8 100644 --- a/plugins/argocd/CHANGELOG.md +++ b/plugins/argocd/CHANGELOG.md @@ -1,3 +1,10 @@ +## @janus-idp/backstage-plugin-argocd [1.0.4](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.0.3...@janus-idp/backstage-plugin-argocd@1.0.4) (2024-05-14) + + +### Bug Fixes + +* **argocd:** make refreshInterval configuration as optional ([#1647](https://github.com/janus-idp/backstage-plugins/issues/1647)) ([2c24d35](https://github.com/janus-idp/backstage-plugins/commit/2c24d35f050801801c597967e890b6d2e647fb06)) + ## @janus-idp/backstage-plugin-argocd [1.0.3](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-argocd@1.0.2...@janus-idp/backstage-plugin-argocd@1.0.3) (2024-05-09) diff --git a/plugins/argocd/package.json b/plugins/argocd/package.json index c8bb322aca4..edb9aa6443a 100644 --- a/plugins/argocd/package.json +++ b/plugins/argocd/package.json @@ -1,6 +1,6 @@ { "name": "@janus-idp/backstage-plugin-argocd", - "version": "1.0.3", + "version": "1.0.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 7d9ee11d1ed0dbc1ff026de3b1e50f2888c3f542 Mon Sep 17 00:00:00 2001 From: Yash Oswal Date: Tue, 14 May 2024 17:30:22 +0530 Subject: [PATCH 10/17] feat(feedback): use backstage auth service in backend plugin (#1646) - use backstage LoggerSevice - use backstage FetchApi in frontend plugin (fixes #1626) - update tests Signed-off-by: Yash Oswal --- plugins/feedback-backend/README.md | 28 ++++---- .../dist-dynamic/package.json | 3 +- .../feedback-backend/dist-dynamic/yarn.lock | 6 +- plugins/feedback-backend/package.json | 1 + .../src/database/feedbackStore.ts | 6 +- plugins/feedback-backend/src/index.ts | 2 +- plugins/feedback-backend/src/plugin.ts | 7 +- .../feedback-backend/src/service/emails.ts | 4 +- .../src/service/router.test.ts | 13 ++-- .../feedback-backend/src/service/router.ts | 25 +++++-- .../src/service/standaloneServer.ts | 15 ++-- plugins/feedback/app-config.janus-idp.yaml | 19 +++-- plugins/feedback/src/api/index.ts | 17 ++--- plugins/feedback/src/plugin.ts | 11 ++- yarn.lock | 72 +++++++------------ 15 files changed, 125 insertions(+), 104 deletions(-) diff --git a/plugins/feedback-backend/README.md b/plugins/feedback-backend/README.md index 1ae9256e096..3d2037edf7e 100644 --- a/plugins/feedback-backend/README.md +++ b/plugins/feedback-backend/README.md @@ -15,7 +15,20 @@ Install the NPM Package yarn workspace backend add @janus-idp/backstage-plugin-feedback-backend ``` -#### Adding the plugin to the legacy backend +#### Adding the plugin to the new backend + +Add the following to your `packages/backend/src/index.ts` file: + +```ts title="packages/backend/src/index.ts" +const backend = createBackend(); + +// Add the following line +backend.add(import('@janus-idp/backstage-plugin-feedback-backend')); + +backend.start(); +``` + +#### Adding the plugin to the legacy backend (`@janus-idp/backstage-plugin-feedback-backend@1.2.6` and lower) 1. Create a new file `packages/backend/src/plugins/feedback.ts` and add the following: @@ -50,19 +63,6 @@ yarn workspace backend add @janus-idp/backstage-plugin-feedback-backend } ``` -#### Adding the plugin to the new backend - -Add the following to your `packages/backend/src/index.ts` file: - -```ts title="packages/backend/src/index.ts" -const backend = createBackend(); - -// Add the following line -backend.add(import('@janus-idp/backstage-plugin-feedback-backend/alpha')); - -backend.start(); -``` - ### Configurations Add the following config in your `app-config.yaml` file. diff --git a/plugins/feedback-backend/dist-dynamic/package.json b/plugins/feedback-backend/dist-dynamic/package.json index 89653ad1b82..502497b14a4 100644 --- a/plugins/feedback-backend/dist-dynamic/package.json +++ b/plugins/feedback-backend/dist-dynamic/package.json @@ -1,6 +1,6 @@ { "name": "@janus-idp/backstage-plugin-feedback-backend-dynamic", - "version": "1.2.5", + "version": "1.2.6", "main": "dist/index.cjs.js", "types": "dist/index.d.ts", "license": "Apache-2.0", @@ -57,6 +57,7 @@ "peerDependencies": { "@backstage/backend-common": "^0.21.7", "@backstage/backend-plugin-api": "^0.6.17", + "@backstage/backend-test-utils": "^0.3.7", "@backstage/catalog-client": "^1.6.4", "@backstage/catalog-model": "^1.4.5", "@backstage/config": "^1.2.0" diff --git a/plugins/feedback-backend/dist-dynamic/yarn.lock b/plugins/feedback-backend/dist-dynamic/yarn.lock index 130772f918b..beadc8646be 100644 --- a/plugins/feedback-backend/dist-dynamic/yarn.lock +++ b/plugins/feedback-backend/dist-dynamic/yarn.lock @@ -85,9 +85,9 @@ integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== "@types/node@*": - version "20.12.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.11.tgz#c4ef00d3507000d17690643278a60dc55a9dc9be" - integrity sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw== + version "20.12.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.12.tgz#7cbecdf902085cec634fdb362172dfe12b8f2050" + integrity sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw== dependencies: undici-types "~5.26.4" diff --git a/plugins/feedback-backend/package.json b/plugins/feedback-backend/package.json index 3264a332b01..e4d63bc9bb7 100644 --- a/plugins/feedback-backend/package.json +++ b/plugins/feedback-backend/package.json @@ -41,6 +41,7 @@ "dependencies": { "@backstage/backend-common": "^0.21.7", "@backstage/backend-plugin-api": "^0.6.17", + "@backstage/backend-test-utils": "^0.3.7", "@backstage/catalog-client": "^1.6.4", "@backstage/catalog-model": "^1.4.5", "@backstage/config": "^1.2.0", diff --git a/plugins/feedback-backend/src/database/feedbackStore.ts b/plugins/feedback-backend/src/database/feedbackStore.ts index a5db9d4d995..e0b399973ca 100644 --- a/plugins/feedback-backend/src/database/feedbackStore.ts +++ b/plugins/feedback-backend/src/database/feedbackStore.ts @@ -2,10 +2,10 @@ import { PluginDatabaseManager, resolvePackagePath, } from '@backstage/backend-common'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { Knex } from 'knex'; import short from 'short-uuid'; -import { Logger } from 'winston'; import { FeedbackModel } from '../model/feedback.model'; @@ -36,7 +36,7 @@ const migrationsDir = resolvePackagePath( export class DatabaseFeedbackStore implements FeedbackStore { private constructor( private readonly db: Knex, - private readonly logger: Logger, + private readonly logger: LoggerService, ) {} static async create({ @@ -46,7 +46,7 @@ export class DatabaseFeedbackStore implements FeedbackStore { }: { database: PluginDatabaseManager; skipMigrations: boolean; - logger: Logger; + logger: LoggerService; }): Promise { const client = await database.getClient(); diff --git a/plugins/feedback-backend/src/index.ts b/plugins/feedback-backend/src/index.ts index 5d5bfdd3f83..e5aa17d7e56 100644 --- a/plugins/feedback-backend/src/index.ts +++ b/plugins/feedback-backend/src/index.ts @@ -1,2 +1,2 @@ export { createRouter } from './service/router'; -export { feedbackPlugin } from './plugin'; +export { feedbackPlugin as default } from './plugin'; diff --git a/plugins/feedback-backend/src/plugin.ts b/plugins/feedback-backend/src/plugin.ts index e36c37d250b..c451ce4244e 100644 --- a/plugins/feedback-backend/src/plugin.ts +++ b/plugins/feedback-backend/src/plugin.ts @@ -1,4 +1,3 @@ -import { loggerToWinstonLogger } from '@backstage/backend-common'; import { coreServices, createBackendPlugin, @@ -15,13 +14,15 @@ export const feedbackPlugin = createBackendPlugin({ httpRouter: coreServices.httpRouter, config: coreServices.rootConfig, discovery: coreServices.discovery, + auth: coreServices.auth, }, - async init({ logger, httpRouter, config, discovery }) { + async init({ logger, httpRouter, config, discovery, auth }) { httpRouter.use( await createRouter({ - logger: loggerToWinstonLogger(logger), + logger: logger, config: config, discovery: discovery, + auth: auth, }), ); }, diff --git a/plugins/feedback-backend/src/service/emails.ts b/plugins/feedback-backend/src/service/emails.ts index cc89ad6ab85..1cf39b047c2 100644 --- a/plugins/feedback-backend/src/service/emails.ts +++ b/plugins/feedback-backend/src/service/emails.ts @@ -1,7 +1,7 @@ +import { LoggerService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { createTransport, Transporter } from 'nodemailer'; -import { Logger } from 'winston'; import { readFileSync } from 'fs'; @@ -11,7 +11,7 @@ export class NodeMailer { constructor( config: Config, - private logger: Logger, + private logger: LoggerService, ) { const useSecure: boolean = config.getBoolean( 'feedback.integrations.email.secure', diff --git a/plugins/feedback-backend/src/service/router.test.ts b/plugins/feedback-backend/src/service/router.test.ts index 5b3b8d7b558..0f5dafa38fa 100644 --- a/plugins/feedback-backend/src/service/router.test.ts +++ b/plugins/feedback-backend/src/service/router.test.ts @@ -1,12 +1,13 @@ -import { HostDiscovery } from '@backstage/backend-common'; -import { DiscoveryService } from '@backstage/backend-plugin-api'; +import { getRootLogger, HostDiscovery } from '@backstage/backend-common'; +import { AuthService, DiscoveryService } from '@backstage/backend-plugin-api'; +import { mockServices } from '@backstage/backend-test-utils'; import { Config, ConfigReader } from '@backstage/config'; import express from 'express'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import request from 'supertest'; -import { createLogger, Logger, transports } from 'winston'; +import { Logger } from 'winston'; import { mockConfig, @@ -67,9 +68,8 @@ describe('Router', () => { mswMockServer.listen({ onUnhandledRequest: 'bypass' }); const config: Config = new ConfigReader(mockConfig); const discovery: DiscoveryService = HostDiscovery.fromConfig(config); - const logger: Logger = createLogger({ - transports: [new transports.Console({ silent: true })], - }); + const logger: Logger = getRootLogger().child({ service: 'feedback-backend' }); + const auth: AuthService = mockServices.auth(); let app: express.Express; beforeAll(async () => { @@ -77,6 +77,7 @@ describe('Router', () => { logger: logger, config: config, discovery: discovery, + auth: auth, }); app = express().use(router); }); diff --git a/plugins/feedback-backend/src/service/router.ts b/plugins/feedback-backend/src/service/router.ts index f281503e4bc..3ba241c33f6 100644 --- a/plugins/feedback-backend/src/service/router.ts +++ b/plugins/feedback-backend/src/service/router.ts @@ -3,13 +3,13 @@ import { errorHandler, PluginEndpointDiscovery, } from '@backstage/backend-common'; +import { AuthService, LoggerService } from '@backstage/backend-plugin-api'; import { CatalogClient } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import express from 'express'; import Router from 'express-promise-router'; -import { Logger } from 'winston'; import { JiraApiService } from '../api'; import { DatabaseFeedbackStore } from '../database/feedbackStore'; @@ -17,16 +17,16 @@ import { FeedbackCategory, FeedbackModel } from '../model/feedback.model'; import { NodeMailer } from './emails'; export interface RouterOptions { - logger: Logger; + logger: LoggerService; config: Config; discovery: PluginEndpointDiscovery; + auth: AuthService; } export async function createRouter( options: RouterOptions, ): Promise { - const { logger, config, discovery } = options; - + const { logger, config, discovery, auth } = options; const router = Router(); const feedbackDB = await DatabaseFeedbackStore.create({ database: DatabaseManager.fromConfig(config).forPlugin('feedback'), @@ -58,8 +58,15 @@ export async function createRouter( error: `The value of feedbackType should be either 'FEEDBACK'/'BUG'`, }); + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); const entityRef: Entity | undefined = await catalogClient.getEntityByRef( reqData.projectId!, + { + token, + }, ); if (!entityRef) { return res @@ -101,8 +108,8 @@ export async function createRouter( const type = annotations['feedback/type']; const replyTo = annotations['feedback/email-to']; const reporterEmail = ( - (await catalogClient.getEntityByRef(reqData.createdBy!))?.spec - ?.profile as { email: string } + (await catalogClient.getEntityByRef(reqData.createdBy!, { token })) + ?.spec?.profile as { email: string } ).email; const appTitle = config.getString('app.title'); @@ -251,8 +258,12 @@ export async function createRouter( : null; if (ticketId && projectId) { + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); const entityRef: Entity | undefined = - await catalogClient.getEntityByRef(projectId); + await catalogClient.getEntityByRef(projectId, { token }); if (!entityRef) { return res .status(404) diff --git a/plugins/feedback-backend/src/service/standaloneServer.ts b/plugins/feedback-backend/src/service/standaloneServer.ts index 5b16f09eeb8..2b86aac439c 100644 --- a/plugins/feedback-backend/src/service/standaloneServer.ts +++ b/plugins/feedback-backend/src/service/standaloneServer.ts @@ -1,8 +1,13 @@ import { createServiceBuilder, HostDiscovery } from '@backstage/backend-common'; -import { DiscoveryService } from '@backstage/backend-plugin-api'; +import { + AuthService, + DiscoveryService, + LoggerService, +} from '@backstage/backend-plugin-api'; +import { mockServices } from '@backstage/backend-test-utils'; import { Config, ConfigReader } from '@backstage/config'; -import { createLogger, Logger, transports } from 'winston'; +import { Logger } from 'winston'; import { Server } from 'http'; @@ -19,14 +24,16 @@ export async function startStandaloneServer( ): Promise { const config: Config = new ConfigReader({}); const discovery: DiscoveryService = HostDiscovery.fromConfig(config); - const logger: Logger = createLogger({ - transports: [new transports.Console({ silent: true })], + const auth: AuthService = mockServices.auth({ pluginId: 'feedback' }); + const logger: LoggerService = options.logger.child({ + service: 'feedback-backend', }); logger.debug('Starting application server...'); const router = await createRouter({ logger: logger, config: config, discovery: discovery, + auth: auth, }); let service = createServiceBuilder(module) diff --git a/plugins/feedback/app-config.janus-idp.yaml b/plugins/feedback/app-config.janus-idp.yaml index c546c14c4d6..2e22b59e737 100644 --- a/plugins/feedback/app-config.janus-idp.yaml +++ b/plugins/feedback/app-config.janus-idp.yaml @@ -7,7 +7,7 @@ feedback: # should be between 1-255 summaryLimit: ${FEEDBACK_PLUGIN_SUMMARY_LIMIT} -# Add dynamic plugin configuration +# Add dynamic plugin configuration (copy this to dynamic-plugins.yaml file) dynamicPlugins: frontend: janus-idp.backstage-plugin-feedback: @@ -20,12 +20,23 @@ dynamicPlugins: menuItem: icon: feedbackIcon text: Feedback - - # The below configuration doesn't work as of now. + routeBindings: + targets: + - importName: feedbackPlugin + bindings: + - bindTarget: feedbackPlugin.externalRoutes + bindMap: + viewDocs: techdocsPlugin.routes.root entityTabs: - path: '/feedback' title: Feedback mountPoint: entity.page.feedback mountPoints: - - mountPoint: entity.page.feedback + - mountPoint: entity.page.feedback/cards importName: EntityFeedbackPage + config: + layout: + gridColumn: '1 / -1' + if: + anyOf: + - hasAnnotation: feedback/type diff --git a/plugins/feedback/src/api/index.ts b/plugins/feedback/src/api/index.ts index 8926423dfe0..442c5fcb301 100644 --- a/plugins/feedback/src/api/index.ts +++ b/plugins/feedback/src/api/index.ts @@ -2,6 +2,7 @@ import { ConfigApi, createApiRef, DiscoveryApi, + FetchApi, IdentityApi, } from '@backstage/core-plugin-api'; @@ -15,6 +16,7 @@ type Options = { discoveryApi: DiscoveryApi; configApi: ConfigApi; identityApi: IdentityApi; + fetchApi: FetchApi; }; type feedbackResp = { @@ -32,9 +34,11 @@ type feedbacksResp = { export class FeedbackAPI { private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; constructor(options: Options) { this.discoveryApi = options.discoveryApi; + this.fetchApi = options.fetchApi; } async getAllFeedbacks( @@ -46,7 +50,7 @@ export class FeedbackAPI { const baseUrl = await this.discoveryApi.getBaseUrl('feedback'); const offset = (page - 1) * pageSize; try { - const resp = await fetch( + const resp = await this.fetchApi.fetch( `${baseUrl}?query=${searchText}&offset=${offset}&limit=${pageSize}&projectId=${projectId}`, ); const respData: feedbacksResp = await resp.json(); @@ -58,17 +62,17 @@ export class FeedbackAPI { async getFeedbackById(feedbackId: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('feedback'); - const resp = await fetch(`${baseUrl}/${feedbackId}`); + const resp = await this.fetchApi.fetch(`${baseUrl}/${feedbackId}`); const respData: feedbackResp = await resp.json(); return respData; } async createFeedback( - data: any, + data: Partial, ): Promise<{ data?: {}; message?: string; error?: string }> { try { const baseUrl = await this.discoveryApi.getBaseUrl('feedback'); - const resp = await fetch(`${baseUrl}`, { + const resp = await this.fetchApi.fetch(`${baseUrl}`, { method: 'POST', body: JSON.stringify(data), headers: { @@ -89,13 +93,10 @@ export class FeedbackAPI { ): Promise<{ status: string; assignee: string; avatarUrls: any }> { const baseUrl = await this.discoveryApi.getBaseUrl('feedback'); const ticketId = ticketUrl.split('/').at(-1); - const resp = await fetch( + const resp = await this.fetchApi.fetch( `${baseUrl}/${feedbackId}/ticket?ticketId=${ticketId}&projectId=${projectId}`, { method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, }, ); const data = (await resp.json()).data; diff --git a/plugins/feedback/src/plugin.ts b/plugins/feedback/src/plugin.ts index 74c606d47ff..ca6fa5ef586 100644 --- a/plugins/feedback/src/plugin.ts +++ b/plugins/feedback/src/plugin.ts @@ -5,6 +5,7 @@ import { createPlugin, createRoutableExtension, discoveryApiRef, + fetchApiRef, identityApiRef, } from '@backstage/core-plugin-api'; @@ -27,9 +28,15 @@ export const feedbackPlugin = createPlugin({ discoveryApi: discoveryApiRef, configApi: configApiRef, identityApi: identityApiRef, + fetchApi: fetchApiRef, }, - factory: ({ discoveryApi, configApi, identityApi }) => { - return new FeedbackAPI({ discoveryApi, configApi, identityApi }); + factory: ({ discoveryApi, configApi, identityApi, fetchApi }) => { + return new FeedbackAPI({ + discoveryApi, + configApi, + identityApi, + fetchApi, + }); }, }), ], diff --git a/yarn.lock b/yarn.lock index 187c87ad074..86846a609d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14591,9 +14591,9 @@ "@types/react" "*" "@types/react@*", "@types/react@18.2.48", "@types/react@18.2.79", "@types/react@>=16", "@types/react@^16.13.1 || ^17.0.0", "@types/react@^16.13.1 || ^17.0.0 || ^18.0.0", "@types/react@^18": - version "18.3.1" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.1.tgz#fed43985caa834a2084d002e4771e15dfcbdbe8e" - integrity sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw== + version "18.3.2" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.2.tgz#462ae4904973bc212fa910424d901e3d137dbfcd" + integrity sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -14951,18 +14951,18 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== -"@typescript-eslint/types@7.8.0": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.8.0.tgz#1fd2577b3ad883b769546e2d1ef379f929a7091d" - integrity sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw== +"@typescript-eslint/types@7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.9.0.tgz#b58e485e4bfba055659c7e683ad4f5f0821ae2ec" + integrity sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w== "@typescript-eslint/typescript-estree@5.62.0", "@typescript-eslint/typescript-estree@6.21.0", "@typescript-eslint/typescript-estree@^7.3.1": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz#b028a9226860b66e623c1ee55cc2464b95d2987c" - integrity sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg== + version "7.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz#3395e27656060dc313a6b406c3a298b729685e07" + integrity sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg== dependencies: - "@typescript-eslint/types" "7.8.0" - "@typescript-eslint/visitor-keys" "7.8.0" + "@typescript-eslint/types" "7.9.0" + "@typescript-eslint/visitor-keys" "7.9.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -15013,12 +15013,12 @@ "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" -"@typescript-eslint/visitor-keys@7.8.0": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz#7285aab991da8bee411a42edbd5db760d22fdd91" - integrity sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA== +"@typescript-eslint/visitor-keys@7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz#82162656e339c3def02895f5c8546f6888d9b9ea" + integrity sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ== dependencies: - "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/types" "7.9.0" eslint-visitor-keys "^3.4.3" "@uiw/codemirror-extensions-basic-setup@4.21.25": @@ -32084,13 +32084,18 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: +semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== dependencies: lru-cache "^6.0.0" +semver@^7.6.0: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + semver@~7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" @@ -33044,16 +33049,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -33131,7 +33127,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -33145,13 +33141,6 @@ strip-ansi@5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@6.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -36006,7 +35995,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -36024,15 +36013,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 62fcafcdb3ab3cb308b16b8fab0a14916b921b82 Mon Sep 17 00:00:00 2001 From: Patrick Knight Date: Tue, 14 May 2024 08:25:05 -0400 Subject: [PATCH 11/17] feat(rbac): implement a file watcher for csv reloads (#1587) * feat(rbac): implement a file watcher for csv reloads * feat(rbac): minor fixes * feat(rbac): minor fixes * feat(rbac): update to use new auth backend service * fix(rbac): fix the error handling for the RBAC file watcher Signed-off-by: Oleksandr Andriienko --------- Signed-off-by: Oleksandr Andriienko Co-authored-by: Oleksandr Andriienko --- plugins/rbac-backend/package.json | 2 + .../duplicate-policies-actions.csv | 3 - .../data/invalid-csv/duplicate-policy.csv | 3 + .../data/invalid-csv/entityref-policy.csv | 1 - .../data/invalid-csv/error-policy.csv | 4 + .../data/invalid-csv/permission-policy.csv | 1 - .../invalid-csv/role-entityref-policy.csv | 1 - .../data/valid-csv/simple-policy.csv | 2 + .../data/valid-csv/updated-rbac-policy.csv | 10 - .../file-permissions/csv-file-watcher.test.ts | 619 ++++++++++ .../src/file-permissions/csv-file-watcher.ts | 397 +++++++ .../src/file-permissions/csv.test.ts | 1021 ----------------- .../rbac-backend/src/file-permissions/csv.ts | 397 ------- .../src/role-manager/role-manager.ts | 1 - .../src/service/enforcer-delegate.test.ts | 2 +- .../src/service/permission-policy.test.ts | 18 - .../src/service/permission-policy.ts | 52 +- .../src/service/policies-rest-api.test.ts | 9 - .../src/service/policies-validation.ts | 22 +- .../src/service/policy-builder.ts | 1 - yarn.lock | 5 + 21 files changed, 1063 insertions(+), 1508 deletions(-) delete mode 100644 plugins/rbac-backend/src/__fixtures__/data/invalid-csv/duplicate-policies-actions.csv delete mode 100644 plugins/rbac-backend/src/__fixtures__/data/invalid-csv/entityref-policy.csv create mode 100644 plugins/rbac-backend/src/__fixtures__/data/invalid-csv/error-policy.csv delete mode 100644 plugins/rbac-backend/src/__fixtures__/data/invalid-csv/permission-policy.csv delete mode 100644 plugins/rbac-backend/src/__fixtures__/data/invalid-csv/role-entityref-policy.csv create mode 100644 plugins/rbac-backend/src/__fixtures__/data/valid-csv/simple-policy.csv delete mode 100644 plugins/rbac-backend/src/__fixtures__/data/valid-csv/updated-rbac-policy.csv create mode 100644 plugins/rbac-backend/src/file-permissions/csv-file-watcher.test.ts create mode 100644 plugins/rbac-backend/src/file-permissions/csv-file-watcher.ts delete mode 100644 plugins/rbac-backend/src/file-permissions/csv.test.ts delete mode 100644 plugins/rbac-backend/src/file-permissions/csv.ts diff --git a/plugins/rbac-backend/package.json b/plugins/rbac-backend/package.json index 93dd21b917b..53807a37ee0 100644 --- a/plugins/rbac-backend/package.json +++ b/plugins/rbac-backend/package.json @@ -39,6 +39,8 @@ "@janus-idp/backstage-plugin-rbac-common": "1.4.2", "@janus-idp/backstage-plugin-rbac-node": "1.1.1", "casbin": "^5.27.1", + "chokidar": "^3.6.0", + "csv-parse": "^5.5.5", "express": "^4.18.2", "express-promise-router": "^4.1.1", "knex": "^3.0.0", diff --git a/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/duplicate-policies-actions.csv b/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/duplicate-policies-actions.csv deleted file mode 100644 index 6f7291622b3..00000000000 --- a/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/duplicate-policies-actions.csv +++ /dev/null @@ -1,3 +0,0 @@ -p, role:default/catalog-writer, catalog.entity.create, use, allow - -p, role:default/catalog-writer, catalog.entity.create, use, deny diff --git a/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/duplicate-policy.csv b/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/duplicate-policy.csv index 90a0f92f09a..181fb0868b4 100644 --- a/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/duplicate-policy.csv +++ b/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/duplicate-policy.csv @@ -7,3 +7,6 @@ p, role:default/catalog-writer, catalog.entity.create, use, allow p, role:default/catalog-writer, catalog.entity.create, use, allow p, role:default/catalog-writer, catalog-entity, delete, allow + +p, role:default/duplication-effect, catalog-entity, update, allow +p, role:default/duplication-effect, catalog-entity, update, deny diff --git a/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/entityref-policy.csv b/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/entityref-policy.csv deleted file mode 100644 index 77040c9af43..00000000000 --- a/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/entityref-policy.csv +++ /dev/null @@ -1 +0,0 @@ -g, user:default/, role:default/catalog-deleter diff --git a/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/error-policy.csv b/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/error-policy.csv new file mode 100644 index 00000000000..556bbfc6a63 --- /dev/null +++ b/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/error-policy.csv @@ -0,0 +1,4 @@ +g, user:default/, role:default/catalog-deleter +g, user:default/test, role:default/ +p, role:default/, catalog.entity.create, use, allow +p, role:default/test, catalog.entity.create, delete, temp diff --git a/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/permission-policy.csv b/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/permission-policy.csv deleted file mode 100644 index ab2d78e0651..00000000000 --- a/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/permission-policy.csv +++ /dev/null @@ -1 +0,0 @@ -p, role:default/, catalog.entity.create, use, allow diff --git a/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/role-entityref-policy.csv b/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/role-entityref-policy.csv deleted file mode 100644 index cb1153fddbd..00000000000 --- a/plugins/rbac-backend/src/__fixtures__/data/invalid-csv/role-entityref-policy.csv +++ /dev/null @@ -1 +0,0 @@ -g, user:default/test, role:default/ diff --git a/plugins/rbac-backend/src/__fixtures__/data/valid-csv/simple-policy.csv b/plugins/rbac-backend/src/__fixtures__/data/valid-csv/simple-policy.csv new file mode 100644 index 00000000000..15c68d545a6 --- /dev/null +++ b/plugins/rbac-backend/src/__fixtures__/data/valid-csv/simple-policy.csv @@ -0,0 +1,2 @@ +g, user:default/guest, role:default/catalog-writer +p, role:default/catalog-writer, catalog-entity, update, allow diff --git a/plugins/rbac-backend/src/__fixtures__/data/valid-csv/updated-rbac-policy.csv b/plugins/rbac-backend/src/__fixtures__/data/valid-csv/updated-rbac-policy.csv deleted file mode 100644 index 7d02f304ac2..00000000000 --- a/plugins/rbac-backend/src/__fixtures__/data/valid-csv/updated-rbac-policy.csv +++ /dev/null @@ -1,10 +0,0 @@ -g, user:default/guest, role:default/catalog-writer -g, user:default/guest, role:default/catalog-updater - -g, user:default/guest, role:default/catalog-tester - -p, role:default/catalog-writer, catalog-entity, update, allow -p, role:default/catalog-writer, catalog.entity.create, use, deny -p, role:default/catalog-deleter, catalog-entity, delete, allow - -p, role:default/catalog-writer, catalog.entity.delete, delete, allow diff --git a/plugins/rbac-backend/src/file-permissions/csv-file-watcher.test.ts b/plugins/rbac-backend/src/file-permissions/csv-file-watcher.test.ts new file mode 100644 index 00000000000..3883d243e99 --- /dev/null +++ b/plugins/rbac-backend/src/file-permissions/csv-file-watcher.test.ts @@ -0,0 +1,619 @@ +import { DatabaseService } from '@backstage/backend-plugin-api'; +import { mockServices } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; + +import { + Adapter, + Enforcer, + Model, + newEnforcer, + newModelFromString, +} from 'casbin'; +import * as Knex from 'knex'; +import { MockClient } from 'knex-mock-client'; +import { Logger } from 'winston'; + +import { + PermissionPolicyMetadata, + RoleMetadata, + Source, +} from '@janus-idp/backstage-plugin-rbac-common'; + +import { resolve } from 'path'; + +import { CasbinDBAdapterFactory } from '../database/casbin-adapter-factory'; +import { + PermissionPolicyMetadataDao, + PolicyMetadataStorage, +} from '../database/policy-metadata-storage'; +import { RoleMetadataStorage } from '../database/role-metadata'; +import { policyToString } from '../helper'; +import { BackstageRoleManager } from '../role-manager/role-manager'; +import { EnforcerDelegate } from '../service/enforcer-delegate'; +import { MODEL } from '../service/permission-model'; +import { CSVFileWatcher } from './csv-file-watcher'; + +const catalogApi = { + getEntityAncestors: jest.fn().mockImplementation(), + getLocationById: jest.fn().mockImplementation(), + getEntities: jest.fn().mockImplementation(), + getEntitiesByRefs: jest.fn().mockImplementation(), + queryEntities: jest.fn().mockImplementation(), + getEntityByRef: jest.fn().mockImplementation(), + refreshEntity: jest.fn().mockImplementation(), + getEntityFacets: jest.fn().mockImplementation(), + addLocation: jest.fn().mockImplementation(), + getLocationByRef: jest.fn().mockImplementation(), + removeLocationById: jest.fn().mockImplementation(), + removeEntityByUid: jest.fn().mockImplementation(), + validateEntity: jest.fn().mockImplementation(), + getLocationByEntity: jest.fn().mockImplementation(), +}; + +const loggerMock: any = { + warn: jest.fn().mockImplementation(), + debug: jest.fn().mockImplementation(), +}; + +const roleMetadataStorageMock: RoleMetadataStorage = { + findRoleMetadata: jest + .fn() + .mockImplementation( + async ( + _roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + return { source: 'csv-file' }; + }, + ), + createRoleMetadata: jest.fn().mockImplementation(), + updateRoleMetadata: jest.fn().mockImplementation(), + removeRoleMetadata: jest.fn().mockImplementation(), +}; + +const policyMetadataStorageMock: PolicyMetadataStorage = { + findPolicyMetadataBySource: jest + .fn() + .mockImplementation( + async (_source: Source): Promise => { + return []; + }, + ), + findPolicyMetadata: jest + .fn() + .mockImplementation( + async ( + _policy: string[], + _trx: Knex.Knex.Transaction, + ): Promise => { + return { source: 'csv-file' }; + }, + ), + createPolicyMetadata: jest.fn().mockImplementation(), + removePolicyMetadata: jest.fn().mockImplementation(), +}; + +const dbManagerMock: DatabaseService = { + getClient: jest.fn().mockImplementation(), +}; + +const mockAuthService = mockServices.auth(); + +const currentPermissionPolicies = [ + ['role:default/catalog-writer', 'catalog-entity', 'update', 'allow'], + ['role:default/catalog-writer', 'catalog-entity', 'read', 'allow'], + ['role:default/catalog-writer', 'catalog.entity.create', 'use', 'allow'], + ['role:default/catalog-deleter', 'catalog-entity', 'delete', 'deny'], + ['role:default/known_role', 'test.resource.deny', 'use', 'allow'], +]; + +const currentRoles = [ + ['user:default/guest', 'role:default/catalog-writer'], + ['user:default/guest', 'role:default/catalog-reader'], + ['user:default/guest', 'role:default/catalog-deleter'], + ['user:default/known_user', 'role:default/known_role'], +]; + +const legacyPermission = [ + 'role:default/catalog-writer', + 'catalog-entity', + 'update', + 'allow', +]; + +const legacyRole = ['user:default/guest', 'role:default/catalog-writer']; + +describe('CSVFileWatcher', () => { + let csvFileWatcher: CSVFileWatcher; + let enforcerDelegate: EnforcerDelegate; + let csvFileName: string; + + let enfAddPolicySpy: jest.SpyInstance, string[], any>; + let enfRemovePolicySpy: jest.SpyInstance, string[], any>; + let enfAddGroupingSpy: jest.SpyInstance, string[], any>; + let enfRemoveGroupingSpy: jest.SpyInstance, string[], any>; + + beforeEach(async () => { + csvFileName = resolve( + __dirname, + './../__fixtures__/data/valid-csv/rbac-policy.csv', + ); + + const config = newConfigReader(); + + const adapter = await new CasbinDBAdapterFactory( + config, + dbManagerMock, + ).createAdapter(); + + const stringModel = newModelFromString(MODEL); + const enf = await createEnforcer(stringModel, adapter, loggerMock); + + const knex = Knex.knex({ client: MockClient }); + + enforcerDelegate = new EnforcerDelegate( + enf, + policyMetadataStorageMock, + roleMetadataStorageMock, + knex, + ); + + enfAddPolicySpy = jest.spyOn(enf, 'addPolicy'); + enfRemovePolicySpy = jest.spyOn(enf, 'removePolicy'); + enfAddGroupingSpy = jest.spyOn(enf, 'addGroupingPolicy'); + enfRemoveGroupingSpy = jest.spyOn(enf, 'removeGroupingPolicy'); + + csvFileWatcher = new CSVFileWatcher( + enforcerDelegate, + loggerMock, + roleMetadataStorageMock, + ); + }); + + afterEach(() => { + (loggerMock.warn as jest.Mock).mockReset(); + }); + + describe('initialize', () => { + beforeEach(() => { + policyMetadataStorageMock.findPolicyMetadataBySource = jest + .fn() + .mockImplementation( + async (_source: Source): Promise => { + return []; + }, + ); + + policyMetadataStorageMock.findPolicyMetadata = jest + .fn() + .mockImplementation( + async ( + _policy: string[], + _trx: Knex.Knex.Transaction, + ): Promise => { + return { source: 'csv-file' }; + }, + ); + }); + + it('should be able to add permission policies during initialization', async () => { + await csvFileWatcher.initialize(csvFileName, false); + + const enfPolicies = await enforcerDelegate.getPolicy(); + + expect(enfPolicies).toStrictEqual(currentPermissionPolicies); + }); + + it('should be able to add roles during initialization', async () => { + await csvFileWatcher.initialize(csvFileName, false); + + const enfRoles = await enforcerDelegate.getGroupingPolicy(); + + expect(enfRoles).toStrictEqual(currentRoles); + }); + + it('should be able to update legacy permission policies during initialization', async () => { + policyMetadataStorageMock.findPolicyMetadataBySource = jest + .fn() + .mockImplementation( + async (source: Source): Promise => { + if (source === 'legacy') { + return [ + { + id: 0, + policy: + '[role:default/catalog-writer, catalog-entity, update, allow]', + source: 'legacy', + }, + ]; + } + return []; + }, + ); + + policyMetadataStorageMock.findPolicyMetadata = jest + .fn() + .mockImplementation( + async ( + policy: string[], + _trx: Knex.Knex.Transaction, + ): Promise => { + if (policyToString(policy) === policyToString(legacyPermission)) { + return { source: 'legacy' }; + } + return { source: 'csv-file' }; + }, + ); + + const permissionPolicies = [ + ['role:default/catalog-writer', 'catalog-entity', 'read', 'allow'], + [ + 'role:default/catalog-writer', + 'catalog.entity.create', + 'use', + 'allow', + ], + ['role:default/catalog-deleter', 'catalog-entity', 'delete', 'deny'], + ['role:default/known_role', 'test.resource.deny', 'use', 'allow'], + ['role:default/catalog-writer', 'catalog-entity', 'update', 'allow'], + ]; + + await enforcerDelegate.addPolicy(legacyPermission, 'legacy'); + + await csvFileWatcher.initialize(csvFileName, false); + + const enfPolicies = await enforcerDelegate.getPolicy(); + + expect(enfRemovePolicySpy).toHaveBeenCalledWith(...legacyPermission); + + expect(enfAddPolicySpy).toHaveBeenCalledWith(...legacyPermission); + + expect(enfPolicies).toStrictEqual(permissionPolicies); + }); + + it('should be able to update legacy roles during initialization', async () => { + policyMetadataStorageMock.findPolicyMetadataBySource = jest + .fn() + .mockImplementation( + async (source: Source): Promise => { + if (source === 'legacy') { + return [ + { + id: 0, + policy: '[user:default/guest, role:default/catalog-writer]', + source: 'legacy', + }, + ]; + } + return []; + }, + ); + + policyMetadataStorageMock.findPolicyMetadata = jest + .fn() + .mockImplementation( + async ( + policy: string[], + _trx: Knex.Knex.Transaction, + ): Promise => { + if (policyToString(policy) === policyToString(legacyRole)) { + return { source: 'legacy' }; + } + return { source: 'csv-file' }; + }, + ); + + const roles = [ + ['user:default/guest', 'role:default/catalog-reader'], + ['user:default/guest', 'role:default/catalog-deleter'], + ['user:default/known_user', 'role:default/known_role'], + ['user:default/guest', 'role:default/catalog-writer'], + ]; + + await enforcerDelegate.addGroupingPolicy(legacyRole, { + roleEntityRef: legacyRole[1], + source: 'legacy', + }); + + await csvFileWatcher.initialize(csvFileName, false); + + const enfPolicies = await enforcerDelegate.getGroupingPolicy(); + + expect(enfRemoveGroupingSpy).toHaveBeenCalledWith(...legacyRole); + + expect(enfAddGroupingSpy).toHaveBeenCalledWith(...legacyRole); + + expect(enfPolicies).toStrictEqual(roles); + }); + + // Failing tests + it('should fail to add duplicate policies', async () => { + csvFileName = resolve( + __dirname, + './../__fixtures__/data/invalid-csv/duplicate-policy.csv', + ); + + const duplicatePolicy = [ + 'role:default/catalog-writer', + 'catalog.entity.create', + 'use', + 'allow', + ]; + const duplicateRole = [ + 'user:default/guest', + 'role:default/catalog-deleter', + ]; + + const duplicatePolicyWithDifferentEffect = [ + 'role:default/duplication-effect', + 'catalog-entity', + 'update', + ]; + + await csvFileWatcher.initialize(csvFileName, false); + + expect(loggerMock.warn).toHaveBeenNthCalledWith( + 1, + `Duplicate policy: ${duplicatePolicy} found in the file ${csvFileName}`, + ); + expect(loggerMock.warn).toHaveBeenNthCalledWith( + 2, + `Duplicate policy: ${duplicatePolicy} found in the file ${csvFileName}`, + ); + expect(loggerMock.warn).toHaveBeenNthCalledWith( + 3, + `Duplicate policy: ${duplicatePolicyWithDifferentEffect[0]}, ${duplicatePolicyWithDifferentEffect[1]}, ${duplicatePolicyWithDifferentEffect[2]} with different effect found in the file ${csvFileName}`, + ); + expect(loggerMock.warn).toHaveBeenNthCalledWith( + 4, + `Duplicate policy: ${duplicatePolicyWithDifferentEffect[0]}, ${duplicatePolicyWithDifferentEffect[1]}, ${duplicatePolicyWithDifferentEffect[2]} with different effect found in the file ${csvFileName}`, + ); + expect(loggerMock.warn).toHaveBeenNthCalledWith( + 5, + `Duplicate role: ${duplicateRole} found in the file ${csvFileName}`, + ); + expect(loggerMock.warn).toHaveBeenNthCalledWith( + 6, + `Duplicate role: ${duplicateRole} found in the file ${csvFileName}`, + ); + }); + + it('should fail to add policies with errors', async () => { + csvFileName = resolve( + __dirname, + './../__fixtures__/data/invalid-csv/error-policy.csv', + ); + + const entityRoleError = ['user:default/', 'role:default/catalog-deleter']; + const roleError = ['user:default/test', 'role:default/']; + + const roleErrorPolicy = [ + 'role:default/', + 'catalog.entity.create', + 'use', + 'allow', + ]; + const allowErrorPolicy = [ + 'role:default/test', + 'catalog.entity.create', + 'delete', + 'temp', + ]; + + await csvFileWatcher.initialize(csvFileName, false); + + expect(loggerMock.warn).toHaveBeenNthCalledWith( + 1, + `Failed to validate policy from file ${csvFileName}. Cause: Entity reference "${roleErrorPolicy[0]}" was not on the form [:][/]`, + ); + expect(loggerMock.warn).toHaveBeenNthCalledWith( + 2, + `Failed to validate policy from file ${csvFileName}. Cause: 'effect' has invalid value: '${allowErrorPolicy[3]}'. It should be: 'allow' or 'deny'`, + ); + expect(loggerMock.warn).toHaveBeenNthCalledWith( + 3, + `Failed to validate group policy ${entityRoleError} from file ${csvFileName}. Cause: Entity reference "${entityRoleError[0]}" was not on the form [:][/]`, + ); + expect(loggerMock.warn).toHaveBeenNthCalledWith( + 4, + `Failed to validate group policy ${roleError} from file ${csvFileName}. Cause: Entity reference "${roleError[1]}" was not on the form [:][/]`, + ); + }); + }); + + describe('onChange', () => { + beforeEach(async () => { + csvFileName = resolve( + __dirname, + './../__fixtures__/data/valid-csv/simple-policy.csv', + ); + await csvFileWatcher.initialize(csvFileName, false); + }); + + it('should add new permission policies on change', async () => { + const addContents = [ + ['g', 'user:default/guest', 'role:default/catalog-writer'], + [ + 'p', + 'role:default/catalog-writer', + 'catalog-entity', + 'update', + 'allow', + ], + [ + 'p', + 'role:default/catalog-writer', + 'catalog-entity', + 'delete', + 'allow', + ], + ]; + + const policies = [ + ['role:default/catalog-writer', 'catalog-entity', 'update', 'allow'], + ['role:default/catalog-writer', 'catalog-entity', 'delete', 'allow'], + ]; + + csvFileWatcher.parse = jest.fn().mockImplementation(() => { + return addContents; + }); + + await csvFileWatcher.onChange(); + + const enfPolicies = await enforcerDelegate.getPolicy(); + + expect(enfPolicies).toStrictEqual(policies); + }); + + it('should add new roles on change', async () => { + const addContents = [ + ['g', 'user:default/guest', 'role:default/catalog-writer'], + [ + 'p', + 'role:default/catalog-writer', + 'catalog-entity', + 'update', + 'allow', + ], + ['g', 'user:default/test', 'role:default/catalog-writer'], + ]; + + const roles = [ + ['user:default/guest', 'role:default/catalog-writer'], + ['user:default/test', 'role:default/catalog-writer'], + ]; + + csvFileWatcher.parse = jest.fn().mockImplementation(() => { + return addContents; + }); + + await csvFileWatcher.onChange(); + + const enfRoles = await enforcerDelegate.getGroupingPolicy(); + + expect(enfRoles).toStrictEqual(roles); + }); + + it('should remove old permission policies on change', async () => { + const addContents = [ + ['g', 'user:default/guest', 'role:default/catalog-writer'], + ]; + + csvFileWatcher.parse = jest.fn().mockImplementation(() => { + return addContents; + }); + + await csvFileWatcher.onChange(); + + const enfPolicies = await enforcerDelegate.getPolicy(); + + expect(enfPolicies).toStrictEqual([]); + }); + + it('should remove old roles on change', async () => { + const addContents = [ + [ + 'p', + 'role:default/catalog-writer', + 'catalog-entity', + 'update', + 'allow', + ], + ]; + + csvFileWatcher.parse = jest.fn().mockImplementation(() => { + return addContents; + }); + + await csvFileWatcher.onChange(); + + const enfRoles = await enforcerDelegate.getGroupingPolicy(); + + expect(enfRoles).toStrictEqual([]); + }); + + it('should do nothing if there is no change', async () => { + const addContents = [ + ['g', 'user:default/guest', 'role:default/catalog-writer'], + [ + 'p', + 'role:default/catalog-writer', + 'catalog-entity', + 'update', + 'allow', + ], + ]; + + csvFileWatcher.parse = jest.fn().mockImplementation(() => { + return addContents; + }); + + await csvFileWatcher.onChange(); + + const enfRoles = await enforcerDelegate.getGroupingPolicy(); + const enfPolicies = await enforcerDelegate.getPolicy(); + + expect(enfRoles).toStrictEqual([ + ['user:default/guest', 'role:default/catalog-writer'], + ]); + expect(enfPolicies).toStrictEqual([ + ['role:default/catalog-writer', 'catalog-entity', 'update', 'allow'], + ]); + }); + }); +}); + +async function createEnforcer( + theModel: Model, + adapter: Adapter, + log: Logger, +): Promise { + const catalogDBClient = Knex.knex({ client: MockClient }); + const enf = await newEnforcer(theModel, adapter); + + const config = newConfigReader(); + + const rm = new BackstageRoleManager( + catalogApi, + log, + catalogDBClient, + config, + mockAuthService, + ); + enf.setRoleManager(rm); + enf.enableAutoBuildRoleLinks(false); + await enf.buildRoleLinks(); + + return enf; +} + +function newConfigReader( + users?: Array<{ name: string }>, + superUsers?: Array<{ name: string }>, +): ConfigReader { + const testUsers = [ + { + name: 'user:default/guest', + }, + { + name: 'group:default/guests', + }, + ]; + + return new ConfigReader({ + permission: { + rbac: { + admin: { + users: users || testUsers, + superUsers: superUsers, + }, + }, + }, + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + }); +} diff --git a/plugins/rbac-backend/src/file-permissions/csv-file-watcher.ts b/plugins/rbac-backend/src/file-permissions/csv-file-watcher.ts new file mode 100644 index 00000000000..1890c93421d --- /dev/null +++ b/plugins/rbac-backend/src/file-permissions/csv-file-watcher.ts @@ -0,0 +1,397 @@ +import { Enforcer, FileAdapter, newEnforcer, newModelFromString } from 'casbin'; +import chokidar from 'chokidar'; +import { parse } from 'csv-parse/sync'; +import { difference } from 'lodash'; +import { Logger } from 'winston'; + +import fs from 'fs'; + +import { RoleMetadataStorage } from '../database/role-metadata'; +import { + metadataStringToPolicy, + policyToString, + transformArrayToPolicy, +} from '../helper'; +import { EnforcerDelegate } from '../service/enforcer-delegate'; +import { MODEL } from '../service/permission-model'; +import { + checkForDuplicateGroupPolicies, + checkForDuplicatePolicies, + validateGroupingPolicy, + validatePolicy, +} from '../service/policies-validation'; + +export const CSV_PERMISSION_POLICY_FILE_AUTHOR = 'csv permission policy file'; + +type CSVFilePolicies = { + addedPolicies: string[][]; + addedGroupPolicies: string[][]; + removedPolicies: string[][]; + removedGroupPolicies: string[][]; +}; + +export class CSVFileWatcher { + private currentContent: string[][]; + private csvFilePolicies: CSVFilePolicies; + private csvFileName: string; + constructor( + private readonly enforcer: EnforcerDelegate, + private readonly logger: Logger, + private readonly roleMetadataStorage: RoleMetadataStorage, + ) { + this.csvFileName = ''; + this.currentContent = []; + this.csvFilePolicies = { + addedPolicies: [], + addedGroupPolicies: [], + removedPolicies: [], + removedGroupPolicies: [], + }; + } + + /** + * getCurrentContents reads the current contents of the CSV file. + * @returns The current contents of the CSV file. + */ + getCurrentContents(): string { + return fs.readFileSync(this.csvFileName, 'utf-8'); + } + + /** + * parse is used to parse the current contents of the CSV file. + * @returns The CSV file parsed into a string[][]. + */ + parse(): string[][] { + const content = this.getCurrentContents(); + const parser = parse(content, { + skip_empty_lines: true, + relax_column_count: true, + trim: true, + }); + + return parser; + } + + /** + * watchFile initializes the file watcher and sets it to begin watching for changes. + */ + watchFile(): void { + const watcher = chokidar.watch(this.csvFileName); + watcher.on('change', async path => { + this.logger.info(`file ${path} has changed`); + await this.onChange(); + }); + } + + /** + * initialize will initialize the CSV file by loading all of the permission policies and roles into + * the enforcer. + * First, we will remove all roles and permission policies if they do not exist in the temporary file enforcer. + * Next, we will add all roles and permission polices if they are new to the CSV file + * Finally, we will set the file to be watched if allow reload is set + * @param csvFileName The name of the csvFile + * @param allowReload Whether or not we will allow reloads of the CSV file + */ + async initialize( + csvFileName: string | undefined, + allowReload: boolean, + ): Promise { + let content: string[][] = []; + // If the file is set load the file contents + if (csvFileName) { + this.csvFileName = csvFileName; + content = this.parse(); + } + + const tempEnforcer = await newEnforcer( + newModelFromString(MODEL), + new FileAdapter(this.csvFileName), + ); + + // Check for any old policies that will need to be removed by checking if + // the policy no longer exists in the temp enforcer (csv file) + const policiesToRemove = + await this.enforcer.getFilteredPolicyMetadata('csv-file'); + + for (const policy of policiesToRemove) { + const convertedPolicy = metadataStringToPolicy(policy.policy); + if ( + convertedPolicy.length === 2 && + !(await tempEnforcer.hasGroupingPolicy(...convertedPolicy)) + ) { + this.csvFilePolicies.removedGroupPolicies.push(convertedPolicy); + } else if ( + convertedPolicy.length > 2 && + !(await tempEnforcer.hasPolicy(...convertedPolicy)) + ) { + this.csvFilePolicies.removedPolicies.push(convertedPolicy); + } + } + + // Check for any new policies that need to be added by checking if + // the policy does not currently exist in the enforcer + const policiesToAdd = await tempEnforcer.getPolicy(); + const groupPoliciesToAdd = await tempEnforcer.getGroupingPolicy(); + + for (const policy of policiesToAdd) { + if (!(await this.enforcer.hasPolicy(...policy))) { + this.csvFilePolicies.addedPolicies.push(policy); + } + } + + for (const groupPolicy of groupPoliciesToAdd) { + if (!(await this.enforcer.hasGroupingPolicy(...groupPolicy))) { + this.csvFilePolicies.addedGroupPolicies.push(groupPolicy); + } + } + + // Check for policies that might need to be updated + // This will involve removing legacy policies if they exist in both the + // temp enforcer (csv file) and the enforcer + // We will then add them back with the new source + const policiesToUpdate = + await this.enforcer.getFilteredPolicyMetadata('legacy'); + + for (const policy of policiesToUpdate) { + const convertedPolicy = metadataStringToPolicy(policy.policy); + if ( + convertedPolicy.length === 2 && + (await tempEnforcer.hasGroupingPolicy(...convertedPolicy)) && + (await this.enforcer.hasGroupingPolicy(...convertedPolicy)) + ) { + this.csvFilePolicies.addedGroupPolicies.push(convertedPolicy); + } else if ( + convertedPolicy.length > 2 && + (await tempEnforcer.hasPolicy(...convertedPolicy)) && + (await this.enforcer.hasPolicy(...convertedPolicy)) + ) { + this.csvFilePolicies.addedPolicies.push(convertedPolicy); + } + } + + // We pass current here because this is during initialization and it has not changed yet + await this.updatePolicies(content, tempEnforcer); + + if (allowReload && csvFileName) { + this.watchFile(); + } + } + + /** + * onChange is called whenever there is a change to the CSV file. + * It will parse the current and new contents of the CSV file and process the roles and permission policies present. + * Afterwards, it will find the difference between the current and new contents of the CSV file + * and sort them into added / removed, permission policies / roles. + * It will finally call updatePolicies with the new content. + */ + async onChange(): Promise { + const tempEnforcer = await newEnforcer( + newModelFromString(MODEL), + new FileAdapter(this.csvFileName), + ); + + const newContent = this.parse(); + const currentFlatContent = this.currentContent.flatMap(data => { + return policyToString(data); + }); + const newFlatContent = newContent.flatMap(data => { + return policyToString(data); + }); + + const diffRemoved = difference(currentFlatContent, newFlatContent); // policy was removed + const diffAdded = difference(newFlatContent, currentFlatContent); // policy was added + + if (diffRemoved.length === 0 && diffAdded.length === 0) { + return; + } + + diffRemoved.forEach(policy => { + const convertedPolicy = metadataStringToPolicy(policy); + if (convertedPolicy[0] === 'p') { + convertedPolicy.splice(0, 1); + this.csvFilePolicies.removedPolicies.push(convertedPolicy); + } else if (convertedPolicy[0] === 'g') { + convertedPolicy.splice(0, 1); + this.csvFilePolicies.removedGroupPolicies.push(convertedPolicy); + } + }); + + diffAdded.forEach(policy => { + const convertedPolicy = metadataStringToPolicy(policy); + if (convertedPolicy[0] === 'p') { + convertedPolicy.splice(0, 1); + this.csvFilePolicies.addedPolicies.push(convertedPolicy); + } else if (convertedPolicy[0] === 'g') { + convertedPolicy.splice(0, 1); + this.csvFilePolicies.addedGroupPolicies.push(convertedPolicy); + } + }); + + await this.updatePolicies(newContent, tempEnforcer); + } + + /** + * updatePolicies is used to update all of the permission policies and roles within a CSV file. + * It will check the number of added and removed permissions policies and roles and call the appropriate + * methods for these. + * It will also update the current contents of the CSV file to the most recent + * @param newContent The new content present in the CSV file + * @param tempEnforcer Temporary enforcer for checking for duplicates when adding policies + */ + async updatePolicies( + newContent: string[][], + tempEnforcer: Enforcer, + ): Promise { + this.currentContent = newContent; + + if (this.csvFilePolicies.addedPolicies.length > 0) + await this.addPermissionPolicies(tempEnforcer); + if (this.csvFilePolicies.removedPolicies.length > 0) + await this.removePermissionPolicies(); + if (this.csvFilePolicies.addedGroupPolicies.length > 0) + await this.addRoles(tempEnforcer); + if (this.csvFilePolicies.removedGroupPolicies.length > 0) + await this.removeRoles(); + } + + /** + * addPermissionPolicies will add the new permission policies that are present in the CSV file. + * We will attempt to validate the permission policy and log any warnings that are encountered. + * If a warning is encountered, we will skip adding the permission policy to the enforcer. + * @param tempEnforcer Temporary enforcer for checking for duplicates when adding policies + */ + async addPermissionPolicies(tempEnforcer: Enforcer): Promise { + for (const policy of this.csvFilePolicies.addedPolicies) { + let err = validatePolicy(transformArrayToPolicy(policy)); + if (err) { + this.logger.warn( + `Failed to validate policy from file ${this.csvFileName}. Cause: ${err.message}`, + ); + continue; + } + + err = await checkForDuplicatePolicies( + tempEnforcer, + policy, + this.csvFileName, + ); + if (err) { + this.logger.warn(err.message); + continue; + } + try { + await this.enforcer.addOrUpdatePolicy(policy, 'csv-file', true); + } catch (e) { + this.logger.warn( + `Failed to add or update policy ${policy} after modification ${this.csvFileName}. Cause: ${e}`, + ); + } + } + + this.csvFilePolicies.addedPolicies = []; + } + + /** + * removePermissionPolicies will remove the permission policies that are no longer present in the CSV file. + */ + async removePermissionPolicies(): Promise { + try { + await this.enforcer.removePolicies( + this.csvFilePolicies.removedPolicies, + 'csv-file', + true, + ); + } catch (e) { + this.logger.warn( + `Failed to remove policies ${JSON.stringify( + this.csvFilePolicies.removedPolicies, + )} after modification ${this.csvFileName}. Cause: ${e}`, + ); + } + this.csvFilePolicies.removedPolicies = []; + } + + /** + * addRoles will add the new roles that are present in the CSV file. + * We will attempt to validate the role and log any warnings that are encountered. + * If a warning is encountered, we will skip adding the role to the enforcer. + * @param tempEnforcer Temporary enforcer for checking for duplicates when adding policies + */ + async addRoles(tempEnforcer: Enforcer): Promise { + for (const groupPolicy of this.csvFilePolicies.addedGroupPolicies) { + let err = await validateGroupingPolicy( + groupPolicy, + this.csvFileName, + this.roleMetadataStorage, + 'csv-file', + ); + if (err) { + this.logger.warn(err.message); + continue; + } + + err = await checkForDuplicateGroupPolicies( + tempEnforcer, + groupPolicy, + this.csvFileName, + ); + if (err) { + this.logger.warn(err.message); + continue; + } + + try { + await this.enforcer.addOrUpdateGroupingPolicy( + groupPolicy, + { + source: 'csv-file', + roleEntityRef: groupPolicy[1], + author: CSV_PERMISSION_POLICY_FILE_AUTHOR, + modifiedBy: CSV_PERMISSION_POLICY_FILE_AUTHOR, + }, + true, + ); + } catch (e) { + this.logger.warn( + `Failed to add or update group policy ${groupPolicy} after modification ${this.csvFileName}. Cause: ${e}`, + ); + } + } + this.csvFilePolicies.addedGroupPolicies = []; + } + + /** + * removeRoles will remove the roles that are no longer present in the CSV file. + * If the role exists with multiple groups and or users, we will update it role information. + * Otherwise, we will remove the role completely. + */ + async removeRoles(): Promise { + for (const groupPolicy of this.csvFilePolicies.removedGroupPolicies) { + // this requires knowledge of whether or not it is an update + const isUpdate = await this.enforcer.getFilteredGroupingPolicy( + 1, + groupPolicy[1], + ); + + // Need to update the time + try { + await this.enforcer.removeGroupingPolicy( + groupPolicy, + { + source: 'csv-file', + roleEntityRef: groupPolicy[1], + author: CSV_PERMISSION_POLICY_FILE_AUTHOR, + modifiedBy: CSV_PERMISSION_POLICY_FILE_AUTHOR, + }, + isUpdate.length > 1, + true, + ); + } catch (e) { + this.logger.warn( + `Failed to remove group policy ${groupPolicy} after modification ${this.csvFileName}. Cause: ${e}`, + ); + } + } + this.csvFilePolicies.removedGroupPolicies = []; + } +} diff --git a/plugins/rbac-backend/src/file-permissions/csv.test.ts b/plugins/rbac-backend/src/file-permissions/csv.test.ts deleted file mode 100644 index 21fdb30d197..00000000000 --- a/plugins/rbac-backend/src/file-permissions/csv.test.ts +++ /dev/null @@ -1,1021 +0,0 @@ -import { mockServices } from '@backstage/backend-test-utils'; -import { ConfigReader } from '@backstage/config'; - -import { - Adapter, - Enforcer, - FileAdapter, - Model, - newEnforcer, - newModelFromString, - StringAdapter, -} from 'casbin'; -import * as Knex from 'knex'; -import { MockClient } from 'knex-mock-client'; -import { isEqual } from 'lodash'; -import { Logger } from 'winston'; - -import { - PermissionPolicyMetadata, - RoleMetadata, - Source, -} from '@janus-idp/backstage-plugin-rbac-common'; - -import { resolve } from 'path'; - -import { - PermissionPolicyMetadataDao, - PolicyMetadataStorage, -} from '../database/policy-metadata-storage'; -import { RoleMetadataStorage } from '../database/role-metadata'; -import { BackstageRoleManager } from '../role-manager/role-manager'; -import { EnforcerDelegate } from '../service/enforcer-delegate'; -import { MODEL } from '../service/permission-model'; -import { - addPermissionPoliciesFileData, - loadFilteredGroupingPoliciesFromCSV, - loadFilteredPoliciesFromCSV, -} from './csv'; - -const catalogApi = { - getEntityAncestors: jest.fn().mockImplementation(), - getLocationById: jest.fn().mockImplementation(), - getEntities: jest.fn().mockImplementation(), - getEntitiesByRefs: jest.fn().mockImplementation(), - queryEntities: jest.fn().mockImplementation(), - getEntityByRef: jest.fn().mockImplementation(), - refreshEntity: jest.fn().mockImplementation(), - getEntityFacets: jest.fn().mockImplementation(), - addLocation: jest.fn().mockImplementation(), - getLocationByRef: jest.fn().mockImplementation(), - removeLocationById: jest.fn().mockImplementation(), - removeEntityByUid: jest.fn().mockImplementation(), - validateEntity: jest.fn().mockImplementation(), - getLocationByEntity: jest.fn().mockImplementation(), -}; - -const roleMetadataStorageMock: RoleMetadataStorage = { - findRoleMetadata: jest - .fn() - .mockImplementation( - async ( - _roleEntityRef: string, - _trx: Knex.Knex.Transaction, - ): Promise => { - return { source: 'csv-file' }; - }, - ), - createRoleMetadata: jest.fn().mockImplementation(), - updateRoleMetadata: jest.fn().mockImplementation(), - removeRoleMetadata: jest.fn().mockImplementation(), -}; - -const policyMetadataStorageMock: PolicyMetadataStorage = { - findPolicyMetadataBySource: jest - .fn() - .mockImplementation( - async (_source: Source): Promise => { - return []; - }, - ), - findPolicyMetadata: jest - .fn() - .mockImplementation( - async ( - _policy: string[], - _trx: Knex.Knex.Transaction, - ): Promise => { - const test: PermissionPolicyMetadata = { - source: 'csv-file', - }; - return test; - }, - ), - createPolicyMetadata: jest.fn().mockImplementation(), - removePolicyMetadata: jest.fn().mockImplementation(), -}; - -const loggerMock: any = { - warn: jest.fn().mockImplementation(), - debug: jest.fn().mockImplementation(), -}; - -const mockAuthService = mockServices.auth(); - -async function createEnforcer( - theModel: Model, - adapter: Adapter, - log: Logger, -): Promise { - const catalogDBClient = Knex.knex({ client: MockClient }); - const enf = await newEnforcer(theModel, adapter); - - const config = newConfigReader(); - - const rm = new BackstageRoleManager( - catalogApi, - log, - catalogDBClient, - config, - mockAuthService, - ); - enf.setRoleManager(rm); - enf.enableAutoBuildRoleLinks(false); - await enf.buildRoleLinks(); - - return enf; -} - -describe('CSV file', () => { - let enfAddPolicySpy: jest.SpyInstance, string[], any>; - let enfRemovePolicySpy: jest.SpyInstance, string[], any>; - let enfAddGroupingSpy: jest.SpyInstance, string[], any>; - let enfRemoveGroupingSpy: jest.SpyInstance, string[], any>; - - const policyFilter: string[] = [ - 'user:default/guest', - 'catalog.entity.create', - 'use', - ]; - - describe('Loading filtered policies from a CSV file', () => { - let csvPermFile: string; - let enf: Enforcer; - let enfDelegate: EnforcerDelegate; - let knex: Knex.Knex; - beforeEach(async () => { - loggerMock.warn = jest.fn().mockImplementation(); - policyMetadataStorageMock.findPolicyMetadata = jest - .fn() - .mockImplementation( - async ( - _policy: string[], - _trx: Knex.Knex.Transaction, - ): Promise => { - const test: PermissionPolicyMetadata = { - source: 'csv-file', - }; - return test; - }, - ); - - csvPermFile = resolve( - __dirname, - './../__fixtures__/data/valid-csv/rbac-policy.csv', - ); - const adapter = new FileAdapter(csvPermFile); - - const stringModel = newModelFromString(MODEL); - enf = await createEnforcer(stringModel, adapter, loggerMock); - - knex = Knex.knex({ client: MockClient }); - - enfDelegate = new EnforcerDelegate( - enf, - policyMetadataStorageMock, - roleMetadataStorageMock, - knex, - ); - - enfAddPolicySpy = jest.spyOn(enf, 'addPolicy'); - enfRemovePolicySpy = jest.spyOn(enf, 'removePolicy'); - enfAddGroupingSpy = jest.spyOn(enf, 'addGroupingPolicy'); - enfRemoveGroupingSpy = jest.spyOn(enf, 'removeGroupingPolicy'); - }); - - afterEach(() => { - (loggerMock.warn as jest.Mock).mockReset(); - (policyMetadataStorageMock.findPolicyMetadata as jest.Mock).mockReset(); - }); - - it('should update a policy that has changed in the file (allow -> deny)', async () => { - const originalPolicy = [ - 'role:default/catalog-writer', - 'catalog.entity.create', - 'use', - 'allow', - ]; - const updatedPolicy = [ - 'role:default/catalog-writer', - 'catalog.entity.create', - 'use', - 'deny', - ]; - - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/valid-csv/updated-rbac-policy.csv', - ); - - expect(await enfDelegate.hasPolicy(...originalPolicy)).toBe(true); - expect(await enfDelegate.hasPolicy(...updatedPolicy)).toBe(false); - - await loadFilteredPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - policyFilter, - loggerMock, - policyMetadataStorageMock, - ); - - expect(enfAddPolicySpy).toHaveBeenCalledWith(...updatedPolicy); - expect(enfRemovePolicySpy).toHaveBeenCalledWith(...originalPolicy); - - expect(await enfDelegate.hasPolicy(...originalPolicy)).toBe(false); - expect(await enfDelegate.hasPolicy(...updatedPolicy)).toBe(true); - }); - - it('should update a policy that has changed in the file (deny -> allow)', async () => { - const tempFilter: string[] = [ - policyFilter[0], - 'catalog-entity', - 'delete', - ]; - const originalPolicy = [ - 'role:default/catalog-deleter', - 'catalog-entity', - 'delete', - 'deny', - ]; - const updatedPolicy = [ - 'role:default/catalog-deleter', - 'catalog-entity', - 'delete', - 'allow', - ]; - - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/valid-csv/updated-rbac-policy.csv', - ); - - expect(await enfDelegate.hasPolicy(...originalPolicy)).toBe(true); - expect(await enfDelegate.hasPolicy(...updatedPolicy)).toBe(false); - - await loadFilteredPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - tempFilter, - loggerMock, - policyMetadataStorageMock, - ); - - expect(enfAddPolicySpy).toHaveBeenCalledWith(...updatedPolicy); - expect(enfRemovePolicySpy).toHaveBeenCalledWith(...originalPolicy); - - expect(await enfDelegate.hasPolicy(...originalPolicy)).toBe(false); - expect(await enfDelegate.hasPolicy(...updatedPolicy)).toBe(true); - }); - - it('should add a policy that is new in the file', async () => { - const tempFilter: string[] = [ - policyFilter[0], - 'catalog.entity.delete', - 'delete', - ]; - const newPolicy = [ - 'role:default/catalog-writer', - 'catalog.entity.delete', - 'delete', - 'allow', - ]; - - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/valid-csv/updated-rbac-policy.csv', - ); - - expect(await enfDelegate.hasPolicy(...newPolicy)).toBe(false); - - await loadFilteredPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - tempFilter, - loggerMock, - policyMetadataStorageMock, - ); - - expect(enfAddPolicySpy).toHaveBeenCalledWith(...newPolicy); - - expect(await enfDelegate.hasPolicy(...newPolicy)).toBe(true); - }); - - it('should remove a policy that is no longer in the file', async () => { - const tempFilter: string[] = [policyFilter[0], 'catalog-entity', 'read']; - const originalPolicy = [ - 'role:default/catalog-writer', - 'catalog-entity', - 'read', - 'allow', - ]; - - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/valid-csv/updated-rbac-policy.csv', - ); - - expect(await enfDelegate.hasPolicy(...originalPolicy)).toBe(true); - - await loadFilteredPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - tempFilter, - loggerMock, - policyMetadataStorageMock, - ); - - expect(enfRemovePolicySpy).toHaveBeenCalledWith(...originalPolicy); - - expect(await enfDelegate.hasPolicy(...originalPolicy)).toBe(false); - }); - - it('should do nothing if there is no change', async () => { - const originalPolicy = [ - 'role:default/catalog-writer', - 'catalog.entity.create', - 'use', - 'allow', - ]; - const originalPolicyFile = resolve( - __dirname, - './../__fixtures__/data/valid-csv/rbac-policy.csv', - ); - - expect(await enfDelegate.hasPolicy(...originalPolicy)).toBe(true); - - await loadFilteredPoliciesFromCSV( - originalPolicyFile, - enfDelegate, - policyFilter, - loggerMock, - policyMetadataStorageMock, - ); - - expect(enfAddPolicySpy).toHaveBeenCalledTimes(0); - expect(enfRemovePolicySpy).toHaveBeenCalledTimes(0); - - expect(await enfDelegate.hasPolicy(...originalPolicy)).toBe(true); - }); - - // Validation tests - it('should fail to update a policy that has changed in the file, entityRef error', async () => { - const originalPolicy = [ - 'role:default/catalog-writer', - 'catalog.entity.create', - 'use', - 'allow', - ]; - const updatedPolicy = [ - 'role:default/catalog-writer', - 'catalog.entity.create', - 'use', - 'deny', - ]; - - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/invalid-csv/permission-policy.csv', - ); - - expect(await enfDelegate.hasPolicy(...originalPolicy)).toBe(true); - expect(await enfDelegate.hasPolicy(...updatedPolicy)).toBe(false); - - await loadFilteredPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - policyFilter, - loggerMock, - policyMetadataStorageMock, - ); - expect(loggerMock.warn).toHaveBeenCalledWith( - `Failed to validate policy from file ${updatedPolicyFile}. Cause: Entity reference "role:default/" was not on the form [:][/]`, - ); - }); - - it('should fail to update a policy that has changed in the file, duplicate error', async () => { - const originalPolicy = [ - 'role:default/catalog-writer', - 'catalog.entity.create', - 'use', - 'allow', - ]; - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/invalid-csv/duplicate-policy.csv', - ); - - expect(await enfDelegate.hasPolicy(...originalPolicy)).toBe(true); - - await loadFilteredPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - policyFilter, - loggerMock, - policyMetadataStorageMock, - ); - - expect(loggerMock.warn).toHaveBeenNthCalledWith( - 1, - `Duplicate policy: ${originalPolicy} found in the file ${updatedPolicyFile}`, - ); - expect(loggerMock.warn).toHaveBeenNthCalledWith( - 2, - `Duplicate policy: ${originalPolicy} found in the file ${updatedPolicyFile}`, - ); - }); - - it('should fail to update a policy that has changed in the file, duplicate error different actions', async () => { - const originalPolicy = [ - 'role:default/catalog-writer', - 'catalog.entity.create', - 'use', - 'allow', - ]; - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/invalid-csv/duplicate-policies-actions.csv', - ); - - expect(await enfDelegate.hasPolicy(...originalPolicy)).toBe(true); - - await loadFilteredPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - policyFilter, - loggerMock, - policyMetadataStorageMock, - ); - - expect(loggerMock.warn).toHaveBeenNthCalledWith( - 1, - `Duplicate policy: ${originalPolicy.at(0)}, ${originalPolicy.at( - 1, - )}, ${originalPolicy.at( - 2, - )} with different actions found with the source csv-file`, - ); - expect(loggerMock.warn).toHaveBeenNthCalledWith( - 2, - `Duplicate policy: ${originalPolicy.at(0)}, ${originalPolicy.at( - 1, - )}, ${originalPolicy.at( - 2, - )} with different actions found with the source csv-file`, - ); - }); - - it('should fail to update a policy that has changed in the file, duplicate error different source', async () => { - const tempFilter: string[] = [ - policyFilter[0], - 'catalog-entity', - 'delete', - ]; - const originalPolicy = [ - 'role:default/catalog-writer', - 'catalog-entity', - 'delete', - 'allow', - ]; - - await enfDelegate.addPolicy(originalPolicy, 'rest'); - - policyMetadataStorageMock.findPolicyMetadata = jest - .fn() - .mockImplementation( - async ( - policy: string[], - _trx: Knex.Knex.Transaction, - ): Promise => { - if (isEqual(policy, originalPolicy)) { - return { source: 'rest' }; - } - return { source: 'csv-file' }; - }, - ); - - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/invalid-csv/duplicate-policy.csv', - ); - - expect(await enfDelegate.hasPolicy(...originalPolicy)).toBe(true); - - await loadFilteredPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - tempFilter, - loggerMock, - policyMetadataStorageMock, - ); - - expect(loggerMock.warn).toHaveBeenCalledWith( - `Duplicate policy: ${originalPolicy.at(0)}, ${originalPolicy.at( - 1, - )}, ${originalPolicy.at(2)} found with the source rest`, - ); - }); - }); - - describe('Loading filtered grouping policies from a CSV file', () => { - let csvPermFile: string; - let enf: Enforcer; - let enfDelegate: EnforcerDelegate; - beforeEach(async () => { - policyMetadataStorageMock.findPolicyMetadata = jest - .fn() - .mockImplementation( - async ( - _policy: string[], - _trx: Knex.Knex.Transaction, - ): Promise => { - const test: PermissionPolicyMetadata = { - source: 'csv-file', - }; - return test; - }, - ); - - csvPermFile = resolve( - __dirname, - './../__fixtures__/data/valid-csv/rbac-policy.csv', - ); - const adapter = new FileAdapter(csvPermFile); - - const stringModel = newModelFromString(MODEL); - enf = await createEnforcer(stringModel, adapter, loggerMock); - - const knex = Knex.knex({ client: MockClient }); - - enfDelegate = new EnforcerDelegate( - enf, - policyMetadataStorageMock, - roleMetadataStorageMock, - knex, - ); - - enfAddPolicySpy = jest.spyOn(enf, 'addPolicy'); - enfRemovePolicySpy = jest.spyOn(enf, 'removePolicy'); - enfAddGroupingSpy = jest.spyOn(enf, 'addGroupingPolicy'); - enfRemoveGroupingSpy = jest.spyOn(enf, 'removeGroupingPolicy'); - }); - - afterEach(() => { - (loggerMock.warn as jest.Mock).mockReset(); - (policyMetadataStorageMock.findPolicyMetadata as jest.Mock).mockReset(); - }); - - it('should update a policy that has changed in the file', async () => { - const originalPolicy = [ - 'user:default/guest', - 'role:default/catalog-reader', - ]; - const updatedPolicy = [ - 'user:default/guest', - 'role:default/catalog-updater', - ]; - - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/valid-csv/updated-rbac-policy.csv', - ); - - expect(await enfDelegate.hasGroupingPolicy(...originalPolicy)).toBe(true); - expect(await enfDelegate.hasGroupingPolicy(...updatedPolicy)).toBe(false); - - await loadFilteredGroupingPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - policyFilter[0], - loggerMock, - policyMetadataStorageMock, - ); - - expect(enfAddGroupingSpy).toHaveBeenCalledWith(...updatedPolicy); - expect(enfRemoveGroupingSpy).toHaveBeenCalledWith(...originalPolicy); - - expect(await enfDelegate.hasGroupingPolicy(...originalPolicy)).toBe( - false, - ); - expect(await enfDelegate.hasGroupingPolicy(...updatedPolicy)).toBe(true); - }); - - it('should add a role that is new in the file', async () => { - const newPolicy = ['user:default/guest', 'role:default/catalog-tester']; - - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/valid-csv/updated-rbac-policy.csv', - ); - - expect(await enfDelegate.hasGroupingPolicy(...newPolicy)).toBe(false); - - await loadFilteredGroupingPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - policyFilter[0], - loggerMock, - policyMetadataStorageMock, - ); - - expect(enfAddGroupingSpy).toHaveBeenCalledWith(...newPolicy); - - expect(await enfDelegate.hasGroupingPolicy(...newPolicy)).toBe(true); - }); - - it('should remove a policy that is no longer in the file', async () => { - const originalPolicy = [ - 'user:default/guest', - 'role:default/catalog-deleter', - ]; - - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/valid-csv/updated-rbac-policy.csv', - ); - - expect(await enfDelegate.hasGroupingPolicy(...originalPolicy)).toBe(true); - - await loadFilteredGroupingPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - policyFilter[0], - loggerMock, - policyMetadataStorageMock, - ); - - expect(enfRemoveGroupingSpy).toHaveBeenCalledWith(...originalPolicy); - - expect(await enfDelegate.hasGroupingPolicy(...originalPolicy)).toBe( - false, - ); - }); - - it('should do nothing if there is no change', async () => { - const originalPolicy = [ - 'user:default/guest', - 'role:default/catalog-writer', - ]; - const originalPolicyFile = resolve( - __dirname, - './../__fixtures__/data/valid-csv/rbac-policy.csv', - ); - - expect(await enfDelegate.hasGroupingPolicy(...originalPolicy)).toBe(true); - - await loadFilteredGroupingPoliciesFromCSV( - originalPolicyFile, - enfDelegate, - policyFilter[0], - loggerMock, - policyMetadataStorageMock, - ); - - expect(enfAddGroupingSpy).toHaveBeenCalledTimes(0); - expect(enfRemoveGroupingSpy).toHaveBeenCalledTimes(0); - - expect(await enfDelegate.hasGroupingPolicy(...originalPolicy)).toBe(true); - }); - - // Validation tests - it('should fail to update a policy that has changed in the file, user entityRef error', async () => { - const originalPolicy = [ - 'user:default/guest', - 'role:default/catalog-deleter', - ]; - const updatedPolicy = [ - 'user:default/test', - 'role:default/catalog-deleter', - ]; - - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/invalid-csv/entityref-policy.csv', - ); - - expect(await enfDelegate.hasGroupingPolicy(...originalPolicy)).toBe(true); - expect(await enfDelegate.hasGroupingPolicy(...updatedPolicy)).toBe(false); - - await loadFilteredGroupingPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - policyFilter[0], - loggerMock, - policyMetadataStorageMock, - ); - - expect(loggerMock.warn).toHaveBeenCalledWith( - `Failed to validate role from file ${updatedPolicyFile}. Cause: Entity reference "user:default/" was not on the form [:][/]`, - ); - }); - - it('should fail to update a policy that has changed in the file, role entityRef error', async () => { - const newUser = 'user:default/test'; - const newPolicy = ['user:default/test', 'role:default/catalog-reader']; - - const updatedPolicyFile = resolve( - __dirname, - './../__fixtures__/data/invalid-csv/role-entityref-policy.csv', - ); - - expect(await enfDelegate.hasGroupingPolicy(...newPolicy)).toBe(false); - - await loadFilteredGroupingPoliciesFromCSV( - updatedPolicyFile, - enfDelegate, - newUser, - loggerMock, - policyMetadataStorageMock, - ); - - expect(loggerMock.warn).toHaveBeenCalledWith( - `Failed to validate role from file ${updatedPolicyFile}. Cause: Entity reference "role:default/" was not on the form [:][/]`, - ); - }); - - it('should fail to update a policy that has changed in the file, duplicate error with and without different sources', async () => { - const duplicateCSV = [ - 'user:default/guest', - 'role:default/catalog-deleter', - ]; - const duplicateRest = [ - 'user:default/guest', - 'role:default/catalog-updater', - ]; - - policyMetadataStorageMock.findPolicyMetadata = jest - .fn() - .mockImplementation( - async ( - policy: string[], - _trx: Knex.Knex.Transaction, - ): Promise => { - if (isEqual(policy, duplicateRest)) { - return { source: 'rest' }; - } - return { source: 'csv-file' }; - }, - ); - - await enfDelegate.addGroupingPolicy(duplicateRest, { - source: 'rest', - roleEntityRef: duplicateRest[1], - }); - - const errorPolicyFile = resolve( - __dirname, - './../__fixtures__/data/invalid-csv/duplicate-policy.csv', - ); - - await loadFilteredGroupingPoliciesFromCSV( - errorPolicyFile, - enfDelegate, - policyFilter[0], - loggerMock, - policyMetadataStorageMock, - ); - - expect(loggerMock.warn).toHaveBeenNthCalledWith( - 1, - `Duplicate role: ${duplicateCSV} found in the file ${errorPolicyFile}`, - ); - expect(loggerMock.warn).toHaveBeenNthCalledWith( - 2, - `Duplicate role: ${duplicateCSV} found in the file ${errorPolicyFile}`, - ); - expect(loggerMock.warn).toHaveBeenNthCalledWith( - 3, - `Duplicate role: ${duplicateRest[0]}, ${duplicateRest[1]} found with the source rest`, - ); - }); - }); - - describe('Loading policies from a CSV file', () => { - let csvPermFile: string; - let enf: Enforcer; - let enfDelegate: EnforcerDelegate; - - beforeEach(async () => { - policyMetadataStorageMock.findPolicyMetadata = jest - .fn() - .mockImplementation( - async ( - _policy: string[], - _trx: Knex.Knex.Transaction, - ): Promise => { - return { source: 'csv-file' }; - }, - ); - - const adapter = new StringAdapter( - ` - p, user:default/known_user, test.resource.deny, use, allow - `, - ); - csvPermFile = resolve( - __dirname, - './../__fixtures__/data/valid-csv/rbac-policy.csv', - ); - - const stringModel = newModelFromString(MODEL); - enf = await createEnforcer(stringModel, adapter, loggerMock); - - const knex = Knex.knex({ client: MockClient }); - - enfDelegate = new EnforcerDelegate( - enf, - policyMetadataStorageMock, - roleMetadataStorageMock, - knex, - ); - }); - - afterEach(() => { - (loggerMock.warn as jest.Mock).mockReset(); - (policyMetadataStorageMock.findPolicyMetadata as jest.Mock).mockReset(); - }); - - it('should add policies from the CSV file', async () => { - const test = [ - 'role:default/catalog-writer', - 'catalog-entity', - 'update', - 'allow', - ]; - await addPermissionPoliciesFileData( - csvPermFile, - enfDelegate, - roleMetadataStorageMock, - loggerMock, - ); - - expect(await enfDelegate.hasPolicy(...test)).toBe(true); - }); - - // Validation tests - it('should fail to add policies from the CSV file, user entityRef group error', async () => { - const errorPolicyFile = resolve( - __dirname, - './../__fixtures__/data/invalid-csv/entityref-policy.csv', - ); - - await addPermissionPoliciesFileData( - errorPolicyFile, - enfDelegate, - roleMetadataStorageMock, - loggerMock, - ); - - expect(loggerMock.warn).toHaveBeenCalledWith( - `Failed to validate group policy user:default/,role:default/catalog-deleter from file ${errorPolicyFile}. Cause: Entity reference "user:default/" was not on the form [:][/]`, - ); - }); - - it('should fail to add policies from the CSV file, role entityRef group error', async () => { - const errorPolicyFile = resolve( - __dirname, - './../__fixtures__/data/invalid-csv/role-entityref-policy.csv', - ); - - await addPermissionPoliciesFileData( - errorPolicyFile, - enfDelegate, - roleMetadataStorageMock, - loggerMock, - ); - - expect(loggerMock.warn).toHaveBeenCalledWith( - `Failed to validate group policy user:default/test,role:default/ from file ${errorPolicyFile}. Cause: Entity reference "role:default/" was not on the form [:][/]`, - ); - }); - - it('should fail to add policies from the CSV file, role entityRef permission policy error', async () => { - const errorPolicyFile = resolve( - __dirname, - './../__fixtures__/data/invalid-csv/permission-policy.csv', - ); - - await addPermissionPoliciesFileData( - errorPolicyFile, - enfDelegate, - roleMetadataStorageMock, - loggerMock, - ); - expect(loggerMock.warn).toHaveBeenCalledWith( - `Failed to validate policy from file ${errorPolicyFile}. Cause: Entity reference "role:default/" was not on the form [:][/]`, - ); - }); - - it('should fail to add policies from the CSV file, duplicate permission policies in CSV and in enforcer', async () => { - const duplicatePolicyCSV = [ - 'role:default/catalog-writer', - 'catalog.entity.create', - 'use', - 'allow', - ]; - - const duplicateRoleCSV = [ - 'user:default/guest', - 'role:default/catalog-deleter', - ]; - - const duplicatePolicyEnforcer = [ - 'role:default/catalog-writer', - 'catalog-entity', - 'delete', - 'allow', - ]; - - const duplicateRoleEnforcer = [ - 'user:default/guest', - 'role:default/catalog-updater', - ]; - - policyMetadataStorageMock.findPolicyMetadata = jest - .fn() - .mockImplementation( - async ( - policy: string[], - _trx: Knex.Knex.Transaction, - ): Promise => { - if ( - isEqual(policy, duplicatePolicyEnforcer) || - isEqual(policy, duplicateRoleEnforcer) - ) { - return { source: 'rest' }; - } - return { source: 'csv-file' }; - }, - ); - - await enfDelegate.addPolicy(duplicatePolicyEnforcer, 'rest'); - await enfDelegate.addGroupingPolicy(duplicateRoleEnforcer, { - source: 'rest', - roleEntityRef: duplicateRoleEnforcer[1], - }); - - const errorPolicyFile = resolve( - __dirname, - './../__fixtures__/data/invalid-csv/duplicate-policy.csv', - ); - - await addPermissionPoliciesFileData( - errorPolicyFile, - enfDelegate, - roleMetadataStorageMock, - loggerMock, - ); - - expect(loggerMock.warn).toHaveBeenNthCalledWith( - 1, - `Duplicate policy: ${duplicatePolicyEnforcer} found in the file ${errorPolicyFile}, originates from source: rest`, - ); - expect(loggerMock.warn).toHaveBeenNthCalledWith( - 2, - `Duplicate policy: ${duplicatePolicyCSV} found in the file ${errorPolicyFile}`, - ); - expect(loggerMock.warn).toHaveBeenNthCalledWith( - 3, - `Duplicate policy: ${duplicatePolicyCSV} found in the file ${errorPolicyFile}`, - ); - expect(loggerMock.warn).toHaveBeenNthCalledWith( - 4, - `Duplicate role: ${duplicateRoleCSV} found in the file ${errorPolicyFile}`, - ); - expect(loggerMock.warn).toHaveBeenNthCalledWith( - 5, - `Duplicate role: ${duplicateRoleCSV} found in the file ${errorPolicyFile}`, - ); - }); - }); -}); - -function newConfigReader( - users?: Array<{ name: string }>, - superUsers?: Array<{ name: string }>, -): ConfigReader { - const testUsers = [ - { - name: 'user:default/guest', - }, - { - name: 'group:default/guests', - }, - ]; - - return new ConfigReader({ - permission: { - rbac: { - admin: { - users: users || testUsers, - superUsers: superUsers, - }, - }, - }, - backend: { - database: { - client: 'better-sqlite3', - connection: ':memory:', - }, - }, - }); -} diff --git a/plugins/rbac-backend/src/file-permissions/csv.ts b/plugins/rbac-backend/src/file-permissions/csv.ts deleted file mode 100644 index c5bbec6c690..00000000000 --- a/plugins/rbac-backend/src/file-permissions/csv.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { Enforcer, FileAdapter, newEnforcer, newModelFromString } from 'casbin'; -import { isEqual } from 'lodash'; -import { Logger } from 'winston'; - -import { PolicyMetadataStorage } from '../database/policy-metadata-storage'; -import { - RoleMetadataDao, - RoleMetadataStorage, -} from '../database/role-metadata'; -import { metadataStringToPolicy, transformArrayToPolicy } from '../helper'; -import { EnforcerDelegate } from '../service/enforcer-delegate'; -import { MODEL } from '../service/permission-model'; -import { - checkForDuplicateGroupPolicies, - checkForDuplicatePolicies, - validateAllPredefinedPolicies, - validateEntityReference, - validatePolicy, -} from '../service/policies-validation'; - -export const CSV_PERMISSION_POLICY_FILE_AUTHOR = 'csv permission policy file'; - -const addPolicy = async ( - policy: string[], - enf: EnforcerDelegate, - policyMetadataStorage: PolicyMetadataStorage, - logger: Logger, -): Promise => { - const source = await policyMetadataStorage?.findPolicyMetadata(policy); - - if (!(await enf.hasPolicy(...policy))) { - await enf.addPolicy(policy, 'csv-file'); - } else if (source?.source !== 'csv-file') { - logger.warn( - `Duplicate policy: ${policy[0]}, ${policy[1]}, ${policy[2]} found with the source ${source?.source}`, - ); - } -}; - -const removeEnforcerPolicies = async ( - enforcerPolicies: string[][], - tempEnf: Enforcer, - enf: EnforcerDelegate, - policyMetadataStorage: PolicyMetadataStorage, -): Promise => { - for (const policy of enforcerPolicies) { - const enfPolicySource = - await policyMetadataStorage?.findPolicyMetadata(policy); - if ( - !(await tempEnf.hasPolicy(...policy)) && - enfPolicySource?.source === 'csv-file' - ) { - await enf.removePolicy(policy, 'csv-file', true); - } - } -}; - -const catchRoleIssues = ( - roleIssues: string[][], - policyFile: string, - logger: Logger, -): void => { - for (const role of roleIssues) { - const err = validateEntityReference(role[0]); - if (err) { - logger.warn( - `Failed to validate role from file ${policyFile}. Cause: ${err.message}`, - ); - } - } -}; - -export const loadFilteredPoliciesFromCSV = async ( - policyFile: string, - enf: EnforcerDelegate, - policyFilter: string[], - logger: Logger, - policyMetadataStorage: PolicyMetadataStorage, - fileEnf?: Enforcer, -) => { - const tempEnforcer = - fileEnf ?? - (await newEnforcer(newModelFromString(MODEL), new FileAdapter(policyFile))); - - const roles = await enf.getFilteredGroupingPolicy(0, policyFilter[0]); - - const policies: string[][] = []; - let enforcerPolicies: string[][] = []; - - for (const role of roles) { - const policyByRole = await tempEnforcer.getFilteredPolicy( - 0, - role[1], - policyFilter[1], - policyFilter[2], - ); - policies.push(...policyByRole); - const enforcerPolicy = await enf.getFilteredPolicy( - 0, - role[1], - policyFilter[1], - policyFilter[2], - ); - enforcerPolicies.push(...enforcerPolicy); - } - - if (isEqual(policies, enforcerPolicies)) { - return; - } - - if (policies.length === 0) { - policies.push( - ...(await tempEnforcer.getFilteredPolicy( - 1, - policyFilter[1], - policyFilter[2], - )), - ); - } - - await removeEnforcerPolicies( - enforcerPolicies, - tempEnforcer, - enf, - policyMetadataStorage, - ); - - for (const policy of policies) { - const err = validatePolicy(transformArrayToPolicy(policy)); - if (err) { - logger.warn( - `Failed to validate policy from file ${policyFile}. Cause: ${err.message}`, - ); - continue; - } - - const duplicateError = await checkForDuplicatePolicies( - tempEnforcer, - policy, - policyFile, - ); - if (duplicateError) { - logger.warn(duplicateError.message); - } - - const effectFlipPolicy = [ - policy[0], - policy[1], - policy[2], - policy[3] === 'deny' ? 'allow' : 'deny', - ]; - - const flipSource = - await policyMetadataStorage?.findPolicyMetadata(effectFlipPolicy); - const isDupFlipPolicy = await enf.hasPolicy(...effectFlipPolicy); - const isFileFlipPolicy = await tempEnforcer.hasPolicy(...effectFlipPolicy); - const isCSVSource = flipSource?.source === 'csv-file'; - - if ( - (isDupFlipPolicy && !isCSVSource) || - (isFileFlipPolicy && isCSVSource) - ) { - logger.warn( - `Duplicate policy: ${policy[0]}, ${policy[1]}, ${policy[2]} with different actions found with the source ${flipSource?.source}`, - ); - continue; - } - - if (isDupFlipPolicy && isCSVSource) { - await enf.removePolicy(effectFlipPolicy, 'csv-file', true); - - enforcerPolicies = enforcerPolicies.filter( - policyCheck => !isEqual(policyCheck, effectFlipPolicy), - ); - } - - await addPolicy(policy, enf, policyMetadataStorage, logger); - } -}; - -export const loadFilteredGroupingPoliciesFromCSV = async ( - policyFile: string, - enf: EnforcerDelegate, - entityRef: string, - logger: Logger, - policyMetadataStorage: PolicyMetadataStorage, - fileEnf?: Enforcer, -) => { - const tempEnforcer = - fileEnf ?? - (await newEnforcer(newModelFromString(MODEL), new FileAdapter(policyFile))); - const roleIssues: string[][] = []; - - const roles = await tempEnforcer.getFilteredGroupingPolicy(0, entityRef); - const enforcerRoles = await enf.getFilteredGroupingPolicy(0, entityRef); - - if (isEqual(roles, enforcerRoles)) { - return; - } - - for (const role of roles) { - const duplicateError = await checkForDuplicateGroupPolicies( - tempEnforcer, - role, - policyFile, - ); - - if (duplicateError) { - logger.warn(duplicateError.message); - } - - const roleEntityRef = role[1]; - const err = validateEntityReference(roleEntityRef); - if (err) { - logger.warn( - `Failed to validate role from file ${policyFile}. Cause: ${err.message}`, - ); - continue; - } - - const roleSource = await policyMetadataStorage?.findPolicyMetadata(role); - - // Role exists in the file but not the enforcer - if (!(await enf.hasGroupingPolicy(...role))) { - await enf.addOrUpdateGroupingPolicy(role, { - roleEntityRef, - source: 'csv-file', - author: CSV_PERMISSION_POLICY_FILE_AUTHOR, - modifiedBy: CSV_PERMISSION_POLICY_FILE_AUTHOR, - }); - } else if (roleSource?.source !== 'csv-file') { - logger.warn( - `Duplicate role: ${role[0]}, ${role[1]} found with the source ${roleSource?.source}`, - ); - continue; - } - } - - for (const role of enforcerRoles) { - // This is to catch stray issues with roles that have problems with their users - roleIssues.push( - ...(await tempEnforcer.getFilteredGroupingPolicy(1, role[1])), - ); - - // Role exists in the enforcer but not the file - const enfRoleSource = await policyMetadataStorage?.findPolicyMetadata(role); - - if ( - !(await tempEnforcer.hasGroupingPolicy(...role)) && - enfRoleSource?.source === 'csv-file' - ) { - await enf.removeGroupingPolicy( - role, - { - roleEntityRef: role[1], - source: 'csv-file', - author: CSV_PERMISSION_POLICY_FILE_AUTHOR, - modifiedBy: CSV_PERMISSION_POLICY_FILE_AUTHOR, - }, - true, - true, - ); - } - } - - // Role Issues was meant to catch things with messed up users, - catchRoleIssues(roleIssues, policyFile, logger); -}; - -export const loadFilteredCSV = async ( - policyFile: string, - enf: EnforcerDelegate, - policyFilter: string[], - logger: Logger, - policyMetadataStorage: PolicyMetadataStorage, -) => { - const fileEnf = await newEnforcer( - newModelFromString(MODEL), - new FileAdapter(policyFile), - ); - - await loadFilteredPoliciesFromCSV( - policyFile, - enf, - policyFilter, - logger, - policyMetadataStorage, - fileEnf, - ); - await loadFilteredGroupingPoliciesFromCSV( - policyFile, - enf, - policyFilter[0], - logger, - policyMetadataStorage, - fileEnf, - ); -}; - -export const removedOldPermissionPoliciesFileData = async ( - enf: EnforcerDelegate, - fileEnf?: Enforcer, -) => { - const tempEnforcer = - fileEnf ?? (await newEnforcer(newModelFromString(MODEL))); - const oldFilePolicies = new Set(); - const policiesMetadata = await enf.getFilteredPolicyMetadata('csv-file'); - for (const policyMetadata of policiesMetadata) { - oldFilePolicies.add(metadataStringToPolicy(policyMetadata.policy)); - } - - const policiesToDelete: string[][] = []; - const groupPoliciesToDelete: string[][] = []; - for (const oldFilePolicy of oldFilePolicies) { - if ( - oldFilePolicy.length === 2 && - !(await tempEnforcer.hasGroupingPolicy(...oldFilePolicy)) - ) { - groupPoliciesToDelete.push(oldFilePolicy); - } else if ( - oldFilePolicy.length > 2 && - !(await tempEnforcer.hasPolicy(...oldFilePolicy)) - ) { - policiesToDelete.push(oldFilePolicy); - } - } - - if (groupPoliciesToDelete.length > 0) { - await enf.removeGroupingPolicies( - groupPoliciesToDelete, - 'csv-file', - CSV_PERMISSION_POLICY_FILE_AUTHOR, - true, - ); - } - if (policiesToDelete.length > 0) { - await enf.removePolicies(policiesToDelete, 'csv-file', true); - } -}; - -export const addPermissionPoliciesFileData = async ( - preDefinedPoliciesFile: string, - enf: EnforcerDelegate, - roleMetadataStorage: RoleMetadataStorage, - logger: Logger, -) => { - const fileEnf = await newEnforcer( - newModelFromString(MODEL), - new FileAdapter(preDefinedPoliciesFile), - ); - const policies = await fileEnf.getPolicy(); - const groupPolicies = await fileEnf.getGroupingPolicy(); - - const validationError = await validateAllPredefinedPolicies( - policies, - groupPolicies, - preDefinedPoliciesFile, - roleMetadataStorage, - enf, - ); - if (validationError) { - logger.warn(validationError.message); - } - - await removedOldPermissionPoliciesFileData(enf, fileEnf); - - for (const policy of policies) { - const duplicateError = await checkForDuplicatePolicies( - fileEnf, - policy, - preDefinedPoliciesFile, - ); - if (duplicateError) { - logger.warn(duplicateError.message); - } - await enf.addOrUpdatePolicy(policy, 'csv-file', true); - } - - for (const groupPolicy of groupPolicies) { - const duplicateError = await checkForDuplicateGroupPolicies( - fileEnf, - groupPolicy, - preDefinedPoliciesFile, - ); - - if (duplicateError) { - logger.warn(duplicateError.message); - } - const metadata: RoleMetadataDao = { - roleEntityRef: groupPolicy[1], - source: 'csv-file', - author: CSV_PERMISSION_POLICY_FILE_AUTHOR, - modifiedBy: CSV_PERMISSION_POLICY_FILE_AUTHOR, - }; - await enf.addOrUpdateGroupingPolicy(groupPolicy, metadata, false); - } -}; diff --git a/plugins/rbac-backend/src/role-manager/role-manager.ts b/plugins/rbac-backend/src/role-manager/role-manager.ts index f1fea91f58f..147e016b0b1 100644 --- a/plugins/rbac-backend/src/role-manager/role-manager.ts +++ b/plugins/rbac-backend/src/role-manager/role-manager.ts @@ -188,7 +188,6 @@ export class BackstageRoleManager implements RoleManager { this.auth, this.maxDepth, ); - await memo.getAllGroups(); await memo.buildUserGraph(memo); memo.debugNodesAndEdges(this.log, name); const userAndParentGroups = memo.getNodes(); diff --git a/plugins/rbac-backend/src/service/enforcer-delegate.test.ts b/plugins/rbac-backend/src/service/enforcer-delegate.test.ts index 2a187673d83..101f98d910d 100644 --- a/plugins/rbac-backend/src/service/enforcer-delegate.test.ts +++ b/plugins/rbac-backend/src/service/enforcer-delegate.test.ts @@ -21,7 +21,7 @@ import { RoleMetadataDao, RoleMetadataStorage, } from '../database/role-metadata'; -import { CSV_PERMISSION_POLICY_FILE_AUTHOR } from '../file-permissions/csv'; +import { CSV_PERMISSION_POLICY_FILE_AUTHOR } from '../file-permissions/csv-file-watcher'; import { policyToString } from '../helper'; import { BackstageRoleManager } from '../role-manager/role-manager'; import { EnforcerDelegate } from './enforcer-delegate'; diff --git a/plugins/rbac-backend/src/service/permission-policy.test.ts b/plugins/rbac-backend/src/service/permission-policy.test.ts index db054b12000..b5a4ec31e75 100644 --- a/plugins/rbac-backend/src/service/permission-policy.test.ts +++ b/plugins/rbac-backend/src/service/permission-policy.test.ts @@ -850,7 +850,6 @@ describe('RBACPermissionPolicy Tests', () => { config, enfDelegate, roleMetadataStorageTest, - policyMetadataStorageTest, ); catalogApi.getEntities.mockReturnValue({ items: [] }); @@ -1123,7 +1122,6 @@ describe('RBACPermissionPolicy Tests', () => { config, enfDelegate, roleMetadataStorageTest, - policyMetadataStorageTest, ); }); @@ -1214,18 +1212,6 @@ describe('Policy checks for resourced permissions defined by name', () => { updateRoleMetadata: jest.fn().mockImplementation(), removeRoleMetadata: jest.fn().mockImplementation(), }; - const policyMetadataStorageTest: PolicyMetadataStorage = { - findPolicyMetadataBySource: jest - .fn() - .mockImplementation( - async (_source: Source): Promise => { - return []; - }, - ), - findPolicyMetadata: jest.fn().mockImplementation(), - createPolicyMetadata: jest.fn().mockImplementation(), - removePolicyMetadata: jest.fn().mockImplementation(), - }; let enfDelegate: EnforcerDelegate; let policy: RBACPermissionPolicy; @@ -1237,7 +1223,6 @@ describe('Policy checks for resourced permissions defined by name', () => { config, enfDelegate, roleMetadataStorageTest, - policyMetadataStorageTest, ); }); @@ -1813,7 +1798,6 @@ describe('Policy checks for conditional policies', () => { conditionalStorage, enfDelegate, roleMetadataStorageMock, - policyMetadataStorageMock, knex, ); @@ -2170,7 +2154,6 @@ async function newPermissionPolicy( config: ConfigReader, enfDelegate: EnforcerDelegate, roleMock?: RoleMetadataStorage, - policyMock?: PolicyMetadataStorage, ): Promise { const logger = getVoidLogger(); return await RBACPermissionPolicy.build( @@ -2179,7 +2162,6 @@ async function newPermissionPolicy( conditionalStorage, enfDelegate, roleMock || roleMetadataStorageMock, - policyMock || policyMetadataStorageMock, knex, ); } diff --git a/plugins/rbac-backend/src/service/permission-policy.ts b/plugins/rbac-backend/src/service/permission-policy.ts index 449f2675272..cbe66557946 100644 --- a/plugins/rbac-backend/src/service/permission-policy.ts +++ b/plugins/rbac-backend/src/service/permission-policy.ts @@ -23,16 +23,11 @@ import { } from '@janus-idp/backstage-plugin-rbac-common'; import { ConditionalStorage } from '../database/conditional-storage'; -import { PolicyMetadataStorage } from '../database/policy-metadata-storage'; import { RoleMetadataDao, RoleMetadataStorage, } from '../database/role-metadata'; -import { - addPermissionPoliciesFileData, - loadFilteredCSV, - removedOldPermissionPoliciesFileData, -} from '../file-permissions/csv'; +import { CSVFileWatcher } from '../file-permissions/csv-file-watcher'; import { metadataStringToPolicy, removeTheDifference } from '../helper'; import { EnforcerDelegate } from './enforcer-delegate'; import { validateEntityReference } from './policies-validation'; @@ -169,9 +164,6 @@ export class RBACPermissionPolicy implements PermissionPolicy { private readonly enforcer: EnforcerDelegate; private readonly logger: Logger; private readonly conditionStorage: ConditionalStorage; - private readonly policyMetadataStorage: PolicyMetadataStorage; - private readonly policiesFile?: string; - private readonly allowReload?: boolean; private readonly superUserList?: string[]; public static async build( @@ -180,7 +172,6 @@ export class RBACPermissionPolicy implements PermissionPolicy { conditionalStorage: ConditionalStorage, enforcerDelegate: EnforcerDelegate, roleMetadataStorage: RoleMetadataStorage, - policyMetaDataStorage: PolicyMetadataStorage, knex: Knex, ): Promise { const superUserList: string[] = []; @@ -196,9 +187,8 @@ export class RBACPermissionPolicy implements PermissionPolicy { 'permission.rbac.policies-csv-file', ); - const allowReload = configApi.getOptionalBoolean( - 'permission.rbac.policyFileReload', - ); + const allowReload = + configApi.getOptionalBoolean('permission.rbac.policyFileReload') || false; if (superUsers && superUsers.length > 0) { for (const user of superUsers) { @@ -224,24 +214,17 @@ export class RBACPermissionPolicy implements PermissionPolicy { ); } - if (policiesFile) { - await addPermissionPoliciesFileData( - policiesFile, - enforcerDelegate, - roleMetadataStorage, - logger, - ); - } else { - await removedOldPermissionPoliciesFileData(enforcerDelegate); - } + const csvFile = new CSVFileWatcher( + enforcerDelegate, + logger, + roleMetadataStorage, + ); + await csvFile.initialize(policiesFile, allowReload); return new RBACPermissionPolicy( enforcerDelegate, logger, conditionalStorage, - policyMetaDataStorage, - policiesFile, - allowReload, superUserList, ); } @@ -250,17 +233,11 @@ export class RBACPermissionPolicy implements PermissionPolicy { enforcer: EnforcerDelegate, logger: Logger, conditionStorage: ConditionalStorage, - policyMetadataStorage: PolicyMetadataStorage, - policiesFile?: string, - allowReload?: boolean, superUserList?: string[], ) { this.enforcer = enforcer; this.logger = logger; this.conditionStorage = conditionStorage; - this.policyMetadataStorage = policyMetadataStorage; - this.policiesFile = policiesFile; - this.allowReload = allowReload; this.superUserList = superUserList; } @@ -370,17 +347,6 @@ export class RBACPermissionPolicy implements PermissionPolicy { return true; } - const filter: string[] = [userIdentity, permission, action]; - if (this.policiesFile && this.allowReload) { - await loadFilteredCSV( - this.policiesFile, - this.enforcer, - filter, - this.logger, - this.policyMetadataStorage, - ); - } - return await this.enforcer.enforce(userIdentity, permission, action, roles); }; diff --git a/plugins/rbac-backend/src/service/policies-rest-api.test.ts b/plugins/rbac-backend/src/service/policies-rest-api.test.ts index 90705e1e5c4..04e4476a35e 100644 --- a/plugins/rbac-backend/src/service/policies-rest-api.test.ts +++ b/plugins/rbac-backend/src/service/policies-rest-api.test.ts @@ -24,7 +24,6 @@ import { Source, } from '@janus-idp/backstage-plugin-rbac-common'; -import { PolicyMetadataStorage } from '../database/policy-metadata-storage'; import { RoleMetadataDao, RoleMetadataStorage, @@ -139,13 +138,6 @@ const roleMetadataStorageMock: RoleMetadataStorage = { removeRoleMetadata: jest.fn().mockImplementation(), }; -const policyMetadataStorageMock: PolicyMetadataStorage = { - findPolicyMetadataBySource: jest.fn().mockImplementation(), - findPolicyMetadata: jest.fn().mockImplementation(), - createPolicyMetadata: jest.fn().mockImplementation(), - removePolicyMetadata: jest.fn().mockImplementation(), -}; - const conditionalStorage = { filterConditions: jest.fn().mockImplementation(), createCondition: jest.fn().mockImplementation(), @@ -299,7 +291,6 @@ describe('REST policies api', () => { conditionalStorage, mockEnforcer as EnforcerDelegate, roleMetadataStorageMock, - policyMetadataStorageMock, knex, ), }; diff --git a/plugins/rbac-backend/src/service/policies-validation.ts b/plugins/rbac-backend/src/service/policies-validation.ts index 074707e96e1..ba8e2536b8c 100644 --- a/plugins/rbac-backend/src/service/policies-validation.ts +++ b/plugins/rbac-backend/src/service/policies-validation.ts @@ -111,7 +111,7 @@ export function validateEntityReference( return undefined; } -async function validateGroupingPolicy( +export async function validateGroupingPolicy( groupPolicy: string[], preDefinedPoliciesFile: string, roleMetadataStorage: RoleMetadataStorage, @@ -219,6 +219,26 @@ export const checkForDuplicatePolicies = async ( `Duplicate policy: ${policy} found in the file ${policyFile}`, ); } + + const flipPolicyEffect = [ + policy[0], + policy[1], + policy[2], + policy[3] === 'deny' ? 'allow' : 'deny', + ]; + + // Check if the same policy exists but with a different effect + const dupWithDifferentEffect = await fileEnf.getFilteredPolicy( + 0, + ...flipPolicyEffect, + ); + + if (dupWithDifferentEffect.length > 0) { + return new Error( + `Duplicate policy: ${policy[0]}, ${policy[1]}, ${policy[2]} with different effect found in the file ${policyFile}`, + ); + } + return undefined; }; diff --git a/plugins/rbac-backend/src/service/policy-builder.ts b/plugins/rbac-backend/src/service/policy-builder.ts index d43de012e6e..a8f78a027b9 100644 --- a/plugins/rbac-backend/src/service/policy-builder.ts +++ b/plugins/rbac-backend/src/service/policy-builder.ts @@ -104,7 +104,6 @@ export class PolicyBuilder { conditionStorage, enforcerDelegate, roleMetadataStorage, - policyMetadataStorage, knex, ), auth: auth, diff --git a/yarn.lock b/yarn.lock index 86846a609d2..3d5b1637432 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18478,6 +18478,11 @@ csv-parse@^5.3.5: resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.5.tgz#68a271a9092877b830541805e14c8a80e6a22517" integrity sha512-erCk7tyU3yLWAhk6wvKxnyPtftuy/6Ak622gOO7BCJ05+TYffnPCJF905wmOQm+BpkX54OdAl8pveJwUdpnCXQ== +csv-parse@^5.5.5: + version "5.5.5" + resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.5.tgz#68a271a9092877b830541805e14c8a80e6a22517" + integrity sha512-erCk7tyU3yLWAhk6wvKxnyPtftuy/6Ak622gOO7BCJ05+TYffnPCJF905wmOQm+BpkX54OdAl8pveJwUdpnCXQ== + ctrlc-windows@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ctrlc-windows/-/ctrlc-windows-2.1.0.tgz#f2096a96ac1d03181e0ec808c2c8a67fdc20b300" From bd92be83974e0f45111f38cac355d98d18f3143d Mon Sep 17 00:00:00 2001 From: Christoph Jerolimov Date: Tue, 14 May 2024 14:27:53 +0200 Subject: [PATCH 12/17] chore(tests): do not run unit tests in parallel (#1645) --- package.json | 2 +- turbo.json | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 50d42a3fcfe..3ec006e810a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "build:storybook": "yarn --cwd packages/storybook build", "tsc": "turbo run tsc", "clean": "turbo run clean", - "test": "turbo run test", + "test": "turbo run test --concurrency=1", "lint": "turbo run lint", "lint:fix": "turbo run lint -- --fix", "lint-staged": "lint-staged", diff --git a/turbo.json b/turbo.json index f7afef7bd02..f61c5e1351d 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,11 @@ { "$schema": "https://turbo.build/schema.json", + "globalDependencies": [ + ".prettierrc.js", + ".eslintrc.js", + "tsconfig.json", + "yarn.lock" + ], "pipeline": { "start": { "cache": false, @@ -30,6 +36,5 @@ "outputs": ["../../dist-types/**"], "dependsOn": ["^tsc"] } - }, - "globalDependencies": ["prettierrc.js"] + } } From bf9335404232f8ec66253f56387d3432d8839406 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Tue, 14 May 2024 15:39:02 +0300 Subject: [PATCH 13/17] fix(rbac): fix sonar cloud issues for rbac-backend plugin (#1619) Signed-off-by: Oleksandr Andriienko --- .../src/role-manager/ancestor-search-memo.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts b/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts index 24f193ee257..e436a70264d 100644 --- a/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts +++ b/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts @@ -147,7 +147,6 @@ export class AncestorSearchMemo { return; } - const groupsRefs = new Set(); const groupName = `group:${group.metadata.namespace?.toLocaleLowerCase( 'en-US', )}/${group.metadata.name.toLocaleLowerCase('en-US')}`; @@ -161,13 +160,12 @@ export class AncestorSearchMemo { if (parentGroup) { const parentName = `group:${group.metadata.namespace?.toLocaleLowerCase( 'en-US', - )}/${parent.toLocaleLowerCase('en-US')}`; + )}/${parentGroup.metadata.name.toLocaleLowerCase('en-US')}`; memo.setEdge(parentName, groupName); - groupsRefs.add(parentName); - } - if (groupsRefs.size > 0 && memo.isAcyclic()) { - this.traverseGroups(memo, parentGroup!, allGroups, depth); + if (memo.isAcyclic()) { + this.traverseGroups(memo, parentGroup, allGroups, depth); + } } } @@ -194,7 +192,7 @@ export class AncestorSearchMemo { ); if (parentGroup && memo.isAcyclic()) { - this.traverseRelations(memo, parentGroup!, allRelations, depth); + this.traverseRelations(memo, parentGroup, allRelations, depth); } } From 27abfcad459cf295831be0a8073766634b16b4de Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 14 May 2024 13:16:09 +0000 Subject: [PATCH 14/17] chore(release): 1.2.0 [skip ci] ## @janus-idp/backstage-plugin-feedback [1.2.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-feedback@1.1.7...@janus-idp/backstage-plugin-feedback@1.2.0) (2024-05-14) ### Features * **feedback:** use backstage auth service in backend plugin ([#1646](https://github.com/janus-idp/backstage-plugins/issues/1646)) ([7d9ee11](https://github.com/janus-idp/backstage-plugins/commit/7d9ee11d1ed0dbc1ff026de3b1e50f2888c3f542)), closes [#1626](https://github.com/janus-idp/backstage-plugins/issues/1626) --- plugins/feedback/CHANGELOG.md | 7 +++++++ plugins/feedback/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/feedback/CHANGELOG.md b/plugins/feedback/CHANGELOG.md index 53e8f6a7d25..fabeba16df1 100644 --- a/plugins/feedback/CHANGELOG.md +++ b/plugins/feedback/CHANGELOG.md @@ -1,3 +1,10 @@ +## @janus-idp/backstage-plugin-feedback [1.2.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-feedback@1.1.7...@janus-idp/backstage-plugin-feedback@1.2.0) (2024-05-14) + + +### Features + +* **feedback:** use backstage auth service in backend plugin ([#1646](https://github.com/janus-idp/backstage-plugins/issues/1646)) ([7d9ee11](https://github.com/janus-idp/backstage-plugins/commit/7d9ee11d1ed0dbc1ff026de3b1e50f2888c3f542)), closes [#1626](https://github.com/janus-idp/backstage-plugins/issues/1626) + ## @janus-idp/backstage-plugin-feedback [1.1.7](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-feedback@1.1.6...@janus-idp/backstage-plugin-feedback@1.1.7) (2024-05-09) diff --git a/plugins/feedback/package.json b/plugins/feedback/package.json index 4ef257bf50e..3b66d12e4d4 100644 --- a/plugins/feedback/package.json +++ b/plugins/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@janus-idp/backstage-plugin-feedback", - "version": "1.1.7", + "version": "1.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From a26b3d5c57750eace8d74e7d44b699c9a7b4bcb0 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 14 May 2024 13:16:42 +0000 Subject: [PATCH 15/17] chore(release): 1.3.0 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.3.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-feedback-backend@1.2.6...@janus-idp/backstage-plugin-feedback-backend@1.3.0) (2024-05-14) ### ⚠ BREAKING CHANGES * **rbac:** remove token manager for auth service (#1632) ### Features * **feedback:** use backstage auth service in backend plugin ([#1646](https://github.com/janus-idp/backstage-plugins/issues/1646)) ([7d9ee11](https://github.com/janus-idp/backstage-plugins/commit/7d9ee11d1ed0dbc1ff026de3b1e50f2888c3f542)), closes [#1626](https://github.com/janus-idp/backstage-plugins/issues/1626) * **rbac:** implement a file watcher for csv reloads ([#1587](https://github.com/janus-idp/backstage-plugins/issues/1587)) ([62fcafc](https://github.com/janus-idp/backstage-plugins/commit/62fcafcdb3ab3cb308b16b8fab0a14916b921b82)) ### Bug Fixes * **argocd:** make refreshInterval configuration as optional ([#1647](https://github.com/janus-idp/backstage-plugins/issues/1647)) ([2c24d35](https://github.com/janus-idp/backstage-plugins/commit/2c24d35f050801801c597967e890b6d2e647fb06)) * **kiali:** removing unnecessary afterAll hook ([#1642](https://github.com/janus-idp/backstage-plugins/issues/1642)) ([a314607](https://github.com/janus-idp/backstage-plugins/commit/a3146073bebb17b6f990891a277323a19e3731d6)) * **orchestrator:** typos mentioning OpenShift ([#1639](https://github.com/janus-idp/backstage-plugins/issues/1639)) ([7ff4c75](https://github.com/janus-idp/backstage-plugins/commit/7ff4c754f73681e1a596d56721972af8872f3211)) * **rbac:** fix sonar cloud issues for rbac-backend plugin ([#1619](https://github.com/janus-idp/backstage-plugins/issues/1619)) ([bf93354](https://github.com/janus-idp/backstage-plugins/commit/bf9335404232f8ec66253f56387d3432d8839406)) * **rbac:** remove token manager for auth service ([#1632](https://github.com/janus-idp/backstage-plugins/issues/1632)) ([2f19655](https://github.com/janus-idp/backstage-plugins/commit/2f196556cffc61c83239721b1cd51d6a2c64eee7)) --- plugins/feedback-backend/CHANGELOG.md | 7 +++++++ plugins/feedback-backend/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/feedback-backend/CHANGELOG.md b/plugins/feedback-backend/CHANGELOG.md index fd791780ac8..ec4a46ef2d4 100644 --- a/plugins/feedback-backend/CHANGELOG.md +++ b/plugins/feedback-backend/CHANGELOG.md @@ -1,3 +1,10 @@ +## @janus-idp/backstage-plugin-feedback-backend [1.3.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-feedback-backend@1.2.6...@janus-idp/backstage-plugin-feedback-backend@1.3.0) (2024-05-14) + + +### Features + +* **feedback:** use backstage auth service in backend plugin ([#1646](https://github.com/janus-idp/backstage-plugins/issues/1646)) ([7d9ee11](https://github.com/janus-idp/backstage-plugins/commit/7d9ee11d1ed0dbc1ff026de3b1e50f2888c3f542)), closes [#1626](https://github.com/janus-idp/backstage-plugins/issues/1626) + ## @janus-idp/backstage-plugin-feedback-backend [1.2.6](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-feedback-backend@1.2.5...@janus-idp/backstage-plugin-feedback-backend@1.2.6) (2024-05-09) diff --git a/plugins/feedback-backend/package.json b/plugins/feedback-backend/package.json index e4d63bc9bb7..fc8524f61c4 100644 --- a/plugins/feedback-backend/package.json +++ b/plugins/feedback-backend/package.json @@ -1,6 +1,6 @@ { "name": "@janus-idp/backstage-plugin-feedback-backend", - "version": "1.2.6", + "version": "1.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 5e27a8c0c838780eb8dc73865bfe508cec3de778 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 14 May 2024 13:17:13 +0000 Subject: [PATCH 16/17] chore(release): 3.1.0 [skip ci] ## [3.1.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-rbac-backend@3.0.0...@janus-idp/backstage-plugin-rbac-backend@3.1.0) (2024-05-14) ### Features * **feedback:** use backstage auth service in backend plugin ([#1646](https://github.com/janus-idp/backstage-plugins/issues/1646)) ([7d9ee11](https://github.com/janus-idp/backstage-plugins/commit/7d9ee11d1ed0dbc1ff026de3b1e50f2888c3f542)), closes [#1626](https://github.com/janus-idp/backstage-plugins/issues/1626) * **rbac:** implement a file watcher for csv reloads ([#1587](https://github.com/janus-idp/backstage-plugins/issues/1587)) ([62fcafc](https://github.com/janus-idp/backstage-plugins/commit/62fcafcdb3ab3cb308b16b8fab0a14916b921b82)) ### Bug Fixes * **argocd:** make refreshInterval configuration as optional ([#1647](https://github.com/janus-idp/backstage-plugins/issues/1647)) ([2c24d35](https://github.com/janus-idp/backstage-plugins/commit/2c24d35f050801801c597967e890b6d2e647fb06)) * **kiali:** removing unnecessary afterAll hook ([#1642](https://github.com/janus-idp/backstage-plugins/issues/1642)) ([a314607](https://github.com/janus-idp/backstage-plugins/commit/a3146073bebb17b6f990891a277323a19e3731d6)) * **orchestrator:** typos mentioning OpenShift ([#1639](https://github.com/janus-idp/backstage-plugins/issues/1639)) ([7ff4c75](https://github.com/janus-idp/backstage-plugins/commit/7ff4c754f73681e1a596d56721972af8872f3211)) * **rbac:** fix sonar cloud issues for rbac-backend plugin ([#1619](https://github.com/janus-idp/backstage-plugins/issues/1619)) ([bf93354](https://github.com/janus-idp/backstage-plugins/commit/bf9335404232f8ec66253f56387d3432d8839406)) --- plugins/rbac-backend/CHANGELOG.md | 12 ++++++++++++ plugins/rbac-backend/package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/plugins/rbac-backend/CHANGELOG.md b/plugins/rbac-backend/CHANGELOG.md index 355f6419c96..8148630a5cb 100644 --- a/plugins/rbac-backend/CHANGELOG.md +++ b/plugins/rbac-backend/CHANGELOG.md @@ -1,3 +1,15 @@ +## @janus-idp/backstage-plugin-rbac-backend [3.1.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-rbac-backend@3.0.0...@janus-idp/backstage-plugin-rbac-backend@3.1.0) (2024-05-14) + + +### Features + +* **rbac:** implement a file watcher for csv reloads ([#1587](https://github.com/janus-idp/backstage-plugins/issues/1587)) ([62fcafc](https://github.com/janus-idp/backstage-plugins/commit/62fcafcdb3ab3cb308b16b8fab0a14916b921b82)) + + +### Bug Fixes + +* **rbac:** fix sonar cloud issues for rbac-backend plugin ([#1619](https://github.com/janus-idp/backstage-plugins/issues/1619)) ([bf93354](https://github.com/janus-idp/backstage-plugins/commit/bf9335404232f8ec66253f56387d3432d8839406)) + ## @janus-idp/backstage-plugin-rbac-backend [3.0.0](https://github.com/janus-idp/backstage-plugins/compare/@janus-idp/backstage-plugin-rbac-backend@2.8.2...@janus-idp/backstage-plugin-rbac-backend@3.0.0) (2024-05-10) diff --git a/plugins/rbac-backend/package.json b/plugins/rbac-backend/package.json index 53807a37ee0..54a859887ef 100644 --- a/plugins/rbac-backend/package.json +++ b/plugins/rbac-backend/package.json @@ -1,6 +1,6 @@ { "name": "@janus-idp/backstage-plugin-rbac-backend", - "version": "3.0.0", + "version": "3.1.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 8263bf099736cbb0d0f2316082d338ba81fa6927 Mon Sep 17 00:00:00 2001 From: Christoph Jerolimov Date: Tue, 14 May 2024 15:42:08 +0200 Subject: [PATCH 17/17] feat(deps): use RHDH themes in the backstage app and dev pages (#1480) --- packages/app/package.json | 7 ++--- packages/app/src/App.tsx | 2 ++ plugins/acr/dev/index.tsx | 3 +++ plugins/acr/package.json | 3 ++- plugins/analytics-module-matomo/dev/index.tsx | 3 +++ plugins/analytics-module-matomo/package.json | 1 + .../analytics-provider-segment/dev/index.tsx | 3 +++ .../analytics-provider-segment/package.json | 1 + plugins/argocd/dev/index.tsx | 2 ++ plugins/argocd/package.json | 1 + plugins/bulk-import/dev/index.tsx | 3 +++ plugins/bulk-import/package.json | 5 ++-- plugins/dynamic-plugins-info/dev/index.tsx | 3 +++ plugins/dynamic-plugins-info/package.json | 1 + plugins/feedback/dev/index.tsx | 3 +++ plugins/feedback/package.json | 1 + plugins/jfrog-artifactory/dev/index.tsx | 3 +++ plugins/jfrog-artifactory/package.json | 1 + plugins/kiali/dev/index.tsx | 2 ++ plugins/kiali/package.json | 1 + plugins/matomo/dev/index.tsx | 3 +++ plugins/matomo/package.json | 1 + .../nexus-repository-manager/dev/index.tsx | 3 +++ plugins/nexus-repository-manager/package.json | 7 ++--- plugins/notifications/dev/index.tsx | 3 +++ plugins/notifications/package.json | 1 + plugins/ocm/dev/index.tsx | 2 ++ plugins/ocm/package.json | 1 + .../openshift-image-registry/dev/index.tsx | 3 +++ plugins/openshift-image-registry/package.json | 1 + plugins/orchestrator/dev/index.tsx | 3 +++ plugins/orchestrator/package.json | 3 ++- plugins/quay/dev/index.tsx | 3 +++ plugins/quay/package.json | 1 + plugins/rbac/dev/index.tsx | 3 +++ plugins/rbac/package.json | 3 ++- plugins/tekton/dev/index.tsx | 3 +++ plugins/tekton/package.json | 3 ++- plugins/topology/dev/index.tsx | 3 +++ plugins/topology/package.json | 1 + plugins/web-terminal/dev/index.tsx | 3 +++ plugins/web-terminal/package.json | 1 + yarn.lock | 27 +++++++------------ 43 files changed, 102 insertions(+), 29 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 15457ef23a6..a36277a7a78 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -23,6 +23,8 @@ "lint": "backstage-cli package lint" }, "dependencies": { + "@backstage-community/plugin-github-actions": "^0.6.16", + "@backstage-community/plugin-tech-radar": "^0.7.4", "@backstage/app-defaults": "^1.5.4", "@backstage/catalog-model": "^1.4.5", "@backstage/core-app-api": "^1.12.4", @@ -48,13 +50,12 @@ "@mui/icons-material": "^5.15.16", "@mui/material": "^5.15.16", "@mui/styles": "^5.15.16", + "@redhat-developer/red-hat-developer-hub-theme": "^0.0.40", "react": "^18.0.0", "react-dom": "^18.0.0", "react-router": "^6.23.0", "react-router-dom": "^6.23.0", - "tss-react": "^4.9.10", - "@backstage-community/plugin-github-actions": "^0.6.16", - "@backstage-community/plugin-tech-radar": "^0.7.4" + "tss-react": "^4.9.10" }, "devDependencies": { "@backstage/cli": "0.26.4", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 127f9ad64ec..c0284fa4b68 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -33,6 +33,7 @@ import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { UserSettingsPage } from '@backstage/plugin-user-settings'; import { TechRadarPage } from '@backstage-community/plugin-tech-radar'; +import { getThemes } from '@redhat-developer/red-hat-developer-hub-theme'; import { apis } from './apis'; import { entityPage } from './components/catalog/EntityPage'; @@ -61,6 +62,7 @@ const app = createApp({ components: { SignInPage: props => , }, + themes: getThemes(), }); const routes = ( diff --git a/plugins/acr/dev/index.tsx b/plugins/acr/dev/index.tsx index 80b63d48a7e..2525a0b1b39 100644 --- a/plugins/acr/dev/index.tsx +++ b/plugins/acr/dev/index.tsx @@ -4,6 +4,8 @@ import { createDevApp } from '@backstage/dev-utils'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TestApiProvider } from '@backstage/test-utils'; +import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme'; + import { mockAcrTagsData } from '../src/__fixtures__/acrTagsObject'; import { mockEntity } from '../src/__fixtures__/mockEntity'; import { @@ -29,6 +31,7 @@ class MockAzureContainerRegistryApiClient createDevApp() .registerPlugin(acrPlugin) + .addThemes(createDevAppThemes()) .addPage({ element: ( , title: 'Bulk import', diff --git a/plugins/bulk-import/package.json b/plugins/bulk-import/package.json index 921af246bd0..113d245f63d 100644 --- a/plugins/bulk-import/package.json +++ b/plugins/bulk-import/package.json @@ -35,8 +35,8 @@ "@material-ui/lab": "^4.0.0-alpha.61", "@mui/icons-material": "5.14.11", "@mui/material": "^5.12.2", - "lodash": "^4.17.21", "formik": "^2.4.5", + "lodash": "^4.17.21", "react-use": "^17.2.4" }, "peerDependencies": { @@ -49,10 +49,11 @@ "@backstage/dev-utils": "1.0.31", "@backstage/test-utils": "1.5.4", "@janus-idp/cli": "1.7.10", + "@playwright/test": "1.41.2", + "@redhat-developer/red-hat-developer-hub-theme": "*", "@testing-library/jest-dom": "6.0.0", "@testing-library/react": "14.0.0", "@testing-library/user-event": "14.0.0", - "@playwright/test": "1.41.2", "msw": "1.0.0" }, "scalprum": { diff --git a/plugins/dynamic-plugins-info/dev/index.tsx b/plugins/dynamic-plugins-info/dev/index.tsx index 05f66811ffc..22bbc2f18da 100644 --- a/plugins/dynamic-plugins-info/dev/index.tsx +++ b/plugins/dynamic-plugins-info/dev/index.tsx @@ -4,6 +4,8 @@ import { Content, Header, HeaderTabs, Page } from '@backstage/core-components'; import { createDevApp } from '@backstage/dev-utils'; import { TestApiProvider } from '@backstage/test-utils'; +import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme'; + import { listLoadedPluginsResult } from '../src/__fixtures__/listLoadedPluginsResult'; import { dynamicPluginsInfoApiRef } from '../src/api/types'; import { DynamicPluginsInfoContent } from '../src/components/DynamicPluginsInfoContent/DynamicPluginsInfoContent'; @@ -17,6 +19,7 @@ const mockedApi = { createDevApp() .registerPlugin(dynamicPluginsInfoPlugin) + .addThemes(createDevAppThemes()) .addPage({ element: ( diff --git a/plugins/dynamic-plugins-info/package.json b/plugins/dynamic-plugins-info/package.json index 3d33fd6cd6f..b0de1650979 100644 --- a/plugins/dynamic-plugins-info/package.json +++ b/plugins/dynamic-plugins-info/package.json @@ -40,6 +40,7 @@ "@backstage/core-app-api": "1.12.4", "@backstage/dev-utils": "1.0.31", "@backstage/test-utils": "1.5.4", + "@redhat-developer/red-hat-developer-hub-theme": "*", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.1", diff --git a/plugins/feedback/dev/index.tsx b/plugins/feedback/dev/index.tsx index 29a15e8abaa..d705571b702 100644 --- a/plugins/feedback/dev/index.tsx +++ b/plugins/feedback/dev/index.tsx @@ -3,6 +3,8 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme'; + import { mockEntity } from '../src/mocks'; import { EntityFeedbackPage, @@ -13,6 +15,7 @@ import { createDevApp() .registerPlugin(feedbackPlugin) + .addThemes(createDevAppThemes()) .addPage({ element: ( <> diff --git a/plugins/feedback/package.json b/plugins/feedback/package.json index 3b66d12e4d4..04ab8f28289 100644 --- a/plugins/feedback/package.json +++ b/plugins/feedback/package.json @@ -47,6 +47,7 @@ "@backstage/dev-utils": "1.0.31", "@backstage/test-utils": "1.5.4", "@janus-idp/cli": "1.8.6", + "@redhat-developer/red-hat-developer-hub-theme": "*", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.0.0", diff --git a/plugins/jfrog-artifactory/dev/index.tsx b/plugins/jfrog-artifactory/dev/index.tsx index 9119c9218c7..461c3d290c6 100644 --- a/plugins/jfrog-artifactory/dev/index.tsx +++ b/plugins/jfrog-artifactory/dev/index.tsx @@ -4,6 +4,8 @@ import { Entity } from '@backstage/catalog-model'; import { createDevApp } from '@backstage/dev-utils'; import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme'; + import { JfrogArtifactoryPage, jfrogArtifactoryPlugin } from '../src/plugin'; const mockEntity: Entity = { @@ -25,6 +27,7 @@ const mockEntity: Entity = { createDevApp() .registerPlugin(jfrogArtifactoryPlugin) + .addThemes(createDevAppThemes()) .addPage({ element: ( diff --git a/plugins/jfrog-artifactory/package.json b/plugins/jfrog-artifactory/package.json index 59ea90f1d9a..300e737a8e6 100644 --- a/plugins/jfrog-artifactory/package.json +++ b/plugins/jfrog-artifactory/package.json @@ -46,6 +46,7 @@ "@backstage/dev-utils": "1.0.31", "@backstage/test-utils": "1.5.4", "@janus-idp/cli": "1.8.6", + "@redhat-developer/red-hat-developer-hub-theme": "*", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.1", diff --git a/plugins/kiali/dev/index.tsx b/plugins/kiali/dev/index.tsx index 50fc93a9a2c..70a8ae770b6 100644 --- a/plugins/kiali/dev/index.tsx +++ b/plugins/kiali/dev/index.tsx @@ -13,6 +13,7 @@ import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TestApiProvider } from '@backstage/test-utils'; import { Grid } from '@material-ui/core'; +import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme'; import { EntityKialiResourcesCard, kialiPlugin } from '../src'; import { getEntityRoutes, getRoutes } from '../src/components/Router'; @@ -619,6 +620,7 @@ const MockKialiError = () => { createDevApp() .registerPlugin(kialiPlugin) + .addThemes(createDevAppThemes()) .addPage({ element: , title: 'KialiPage', diff --git a/plugins/kiali/package.json b/plugins/kiali/package.json index b463cb0d463..01c3307c2e5 100644 --- a/plugins/kiali/package.json +++ b/plugins/kiali/package.json @@ -77,6 +77,7 @@ "@backstage/test-utils": "1.5.4", "@janus-idp/cli": "1.8.6", "@playwright/test": "1.41.2", + "@redhat-developer/red-hat-developer-hub-theme": "*", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.1", diff --git a/plugins/matomo/dev/index.tsx b/plugins/matomo/dev/index.tsx index bc863574cb4..981d81fddf6 100644 --- a/plugins/matomo/dev/index.tsx +++ b/plugins/matomo/dev/index.tsx @@ -2,10 +2,13 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; +import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme'; + import { MatomoPage, matomoPlugin } from '../src/plugin'; createDevApp() .registerPlugin(matomoPlugin) + .addThemes(createDevAppThemes()) .addPage({ element: , title: 'Root Page', diff --git a/plugins/matomo/package.json b/plugins/matomo/package.json index e1655b5a348..027f6ca5c3e 100644 --- a/plugins/matomo/package.json +++ b/plugins/matomo/package.json @@ -48,6 +48,7 @@ "@backstage/dev-utils": "1.0.31", "@backstage/test-utils": "1.5.4", "@janus-idp/cli": "1.8.6", + "@redhat-developer/red-hat-developer-hub-theme": "*", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.1", diff --git a/plugins/nexus-repository-manager/dev/index.tsx b/plugins/nexus-repository-manager/dev/index.tsx index edde8443ce7..01906eaa8da 100644 --- a/plugins/nexus-repository-manager/dev/index.tsx +++ b/plugins/nexus-repository-manager/dev/index.tsx @@ -4,6 +4,8 @@ import { createDevApp } from '@backstage/dev-utils'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TestApiProvider } from '@backstage/test-utils'; +import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme'; + import { entityMock, NexusRepositoryManagerApiClientMock, @@ -16,6 +18,7 @@ import { createDevApp() .registerPlugin(nexusRepositoryManagerPlugin) + .addThemes(createDevAppThemes()) .addPage({ element: ( , title: 'Root Page', diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index efd8d5a8164..c1521ed4fde 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -49,6 +49,7 @@ "@backstage/test-utils": "1.5.4", "@janus-idp/cli": "1.8.6", "@openapitools/openapi-generator-cli": "2.7.0", + "@redhat-developer/red-hat-developer-hub-theme": "*", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.1", diff --git a/plugins/ocm/dev/index.tsx b/plugins/ocm/dev/index.tsx index 1d849e0f7f2..8b327902183 100644 --- a/plugins/ocm/dev/index.tsx +++ b/plugins/ocm/dev/index.tsx @@ -12,6 +12,7 @@ import { import { SearchApi, searchApiRef } from '@backstage/plugin-search-react'; import { Grid } from '@material-ui/core'; +import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme'; import { ClusterAvailableResourceCard, @@ -83,6 +84,7 @@ createDevApp() }), ) .registerPlugin(ocmPlugin) + .addThemes(createDevAppThemes()) .addPage({ element: , title: 'Root Page', diff --git a/plugins/ocm/package.json b/plugins/ocm/package.json index ce08aa9e840..cbcdb80d168 100644 --- a/plugins/ocm/package.json +++ b/plugins/ocm/package.json @@ -54,6 +54,7 @@ "@backstage/plugin-catalog": "1.19.0", "@backstage/test-utils": "1.5.4", "@janus-idp/cli": "1.8.6", + "@redhat-developer/red-hat-developer-hub-theme": "*", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.1", diff --git a/plugins/openshift-image-registry/dev/index.tsx b/plugins/openshift-image-registry/dev/index.tsx index ce00d3ddb78..3d482b7eccc 100644 --- a/plugins/openshift-image-registry/dev/index.tsx +++ b/plugins/openshift-image-registry/dev/index.tsx @@ -3,6 +3,8 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; import { TestApiProvider } from '@backstage/test-utils'; +import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme'; + import { openshiftImageRegistryApiRef, OpenshiftImageRegistryApiV1, @@ -81,6 +83,7 @@ class MockOpenshiftImageRegistryApi implements OpenshiftImageRegistryApiV1 { createDevApp() .registerPlugin(openshiftImageRegistryPlugin) + .addThemes(createDevAppThemes()) .addPage({ element: ( , title: 'Root Page', diff --git a/plugins/orchestrator/package.json b/plugins/orchestrator/package.json index 288e532fe7f..192e30461a4 100644 --- a/plugins/orchestrator/package.json +++ b/plugins/orchestrator/package.json @@ -73,9 +73,10 @@ "@backstage/dev-utils": "1.0.31", "@backstage/test-utils": "1.5.4", "@janus-idp/cli": "1.8.6", + "@redhat-developer/red-hat-developer-hub-theme": "*", + "@storybook/preview-api": "7.5.3", "@storybook/react": "7.5.3", "@testing-library/react": "14.2.1", - "@storybook/preview-api": "7.5.3", "react": "18.3.1", "react-dom": "18.3.1" }, diff --git a/plugins/quay/dev/index.tsx b/plugins/quay/dev/index.tsx index 60a3c3d038e..4f1668cecbc 100644 --- a/plugins/quay/dev/index.tsx +++ b/plugins/quay/dev/index.tsx @@ -4,6 +4,8 @@ import { Entity } from '@backstage/catalog-model'; import { createDevApp } from '@backstage/dev-utils'; import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme'; + import { QuayPage, quayPlugin } from '../src/plugin'; const mockEntity: Entity = { @@ -25,6 +27,7 @@ const mockEntity: Entity = { createDevApp() .registerPlugin(quayPlugin) + .addThemes(createDevAppThemes()) .addPage({ element: ( diff --git a/plugins/quay/package.json b/plugins/quay/package.json index 269a0a6ade3..fc3923586c9 100644 --- a/plugins/quay/package.json +++ b/plugins/quay/package.json @@ -50,6 +50,7 @@ "@backstage/test-utils": "1.5.4", "@janus-idp/cli": "1.8.6", "@playwright/test": "1.41.2", + "@redhat-developer/red-hat-developer-hub-theme": "*", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "14.2.1", "@testing-library/react-hooks": "8.0.1", diff --git a/plugins/rbac/dev/index.tsx b/plugins/rbac/dev/index.tsx index 71ef6b8003b..5318d901f55 100644 --- a/plugins/rbac/dev/index.tsx +++ b/plugins/rbac/dev/index.tsx @@ -9,6 +9,8 @@ import { TestApiProvider, } from '@backstage/test-utils'; +import { createDevAppThemes } from '@redhat-developer/red-hat-developer-hub-theme'; + import { PermissionPolicy, Role, @@ -130,6 +132,7 @@ const mockConfigApi = new MockConfigApi({ createDevApp() .registerPlugin(rbacPlugin) + .addThemes(createDevAppThemes()) .addPage({ element: ( , title: 'Root Page', diff --git a/plugins/web-terminal/package.json b/plugins/web-terminal/package.json index 2f2e3e1b2d1..6098021d1f0 100644 --- a/plugins/web-terminal/package.json +++ b/plugins/web-terminal/package.json @@ -49,6 +49,7 @@ "@backstage/dev-utils": "1.0.31", "@backstage/test-utils": "1.5.4", "@janus-idp/cli": "1.8.6", + "@redhat-developer/red-hat-developer-hub-theme": "*", "@testing-library/jest-dom": "6.4.2", "@testing-library/react": "14.2.1", "@testing-library/user-event": "14.5.1", diff --git a/yarn.lock b/yarn.lock index 3d5b1637432..d8e9262b865 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9485,6 +9485,11 @@ dependencies: "@react-hookz/deep-equal" "^1.0.4" +"@redhat-developer/red-hat-developer-hub-theme@*", "@redhat-developer/red-hat-developer-hub-theme@^0.0.40": + version "0.0.40" + resolved "https://registry.yarnpkg.com/@redhat-developer/red-hat-developer-hub-theme/-/red-hat-developer-hub-theme-0.0.40.tgz#870379e2107d40824de2c05e48b4dfe9156ccdc9" + integrity sha512-or2YShmjhcPCuGDYmio8abABLAkFXqcqFPhUA6QQJ9BFYHL97AFJ+h+1JNi29lZCcw4PHEUDACHJJZkN86Ld3g== + "@remix-run/router@1.16.0": version "1.16.0" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.16.0.tgz#0e10181e5fec1434eb071a9bc4bdaac843f16dcc" @@ -18473,12 +18478,7 @@ csstype@^3.0.11, csstype@^3.0.2, csstype@^3.1.2, csstype@^3.1.3: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== -csv-parse@^5.3.5: - version "5.5.5" - resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.5.tgz#68a271a9092877b830541805e14c8a80e6a22517" - integrity sha512-erCk7tyU3yLWAhk6wvKxnyPtftuy/6Ak622gOO7BCJ05+TYffnPCJF905wmOQm+BpkX54OdAl8pveJwUdpnCXQ== - -csv-parse@^5.5.5: +csv-parse@^5.3.5, csv-parse@^5.5.5: version "5.5.5" resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.5.tgz#68a271a9092877b830541805e14c8a80e6a22517" integrity sha512-erCk7tyU3yLWAhk6wvKxnyPtftuy/6Ak622gOO7BCJ05+TYffnPCJF905wmOQm+BpkX54OdAl8pveJwUdpnCXQ== @@ -32089,10 +32089,10 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: - version "7.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" - integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== +semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@~7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" @@ -32101,13 +32101,6 @@ semver@^7.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== -semver@~7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"