diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6c7947fb03..07115092ab 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -36,6 +36,7 @@ yarn.lock @backstage/community-plugins /workspaces/octopus-deploy @backstage/community-plugins-maintainers @jmezach /workspaces/pingidentity @backstage/community-plugins-maintainers @jessicajhee /workspaces/playlist @backstage/community-plugins-maintainers @kuangp +/workspaces/rbac @backstage/community-plugins-maintainers @AndrienkoAleksandr @PatAKnight /workspaces/redhat-argocd @backstage/community-plugins-maintainers @karthikjeeyar @rohitkrai03 @Eswaraiahsapram /workspaces/redhat-resource-optimization @backstage/community-plugins-maintainers /workspaces/report-portal @backstage/community-plugins-maintainers @yashoswalyo diff --git a/workspaces/rbac/.changeset/README.md b/workspaces/rbac/.changeset/README.md new file mode 100644 index 0000000000..e5b6d8d6a6 --- /dev/null +++ b/workspaces/rbac/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/workspaces/rbac/.changeset/config.json b/workspaces/rbac/.changeset/config.json new file mode 100644 index 0000000000..4d034bb99f --- /dev/null +++ b/workspaces/rbac/.changeset/config.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch" +} diff --git a/workspaces/rbac/.changeset/migrate-1730470757532.md b/workspaces/rbac/.changeset/migrate-1730470757532.md new file mode 100644 index 0000000000..8d1699b32c --- /dev/null +++ b/workspaces/rbac/.changeset/migrate-1730470757532.md @@ -0,0 +1,8 @@ +--- +'@backstage-community/plugin-rbac': patch +'@backstage-community/plugin-rbac-backend': patch +'@backstage-community/plugin-rbac-common': patch +'@backstage-community/plugin-rbac-node': patch +--- + +Migrated from [janus-idp/backstage-plugins](https://github.com/janus-idp/backstage-plugins). diff --git a/workspaces/rbac/.dockerignore b/workspaces/rbac/.dockerignore new file mode 100644 index 0000000000..05edb62650 --- /dev/null +++ b/workspaces/rbac/.dockerignore @@ -0,0 +1,8 @@ +.git +.yarn/cache +.yarn/install-state.gz +node_modules +packages/*/src +packages/*/node_modules +plugins +*.local.yaml diff --git a/workspaces/rbac/.eslintignore b/workspaces/rbac/.eslintignore new file mode 100644 index 0000000000..e5b19947ff --- /dev/null +++ b/workspaces/rbac/.eslintignore @@ -0,0 +1 @@ +playwright.config.ts diff --git a/workspaces/rbac/.eslintrc.js b/workspaces/rbac/.eslintrc.js new file mode 100644 index 0000000000..59b86f8412 --- /dev/null +++ b/workspaces/rbac/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('../../.eslintrc.cjs'); diff --git a/workspaces/rbac/.gitignore b/workspaces/rbac/.gitignore new file mode 100644 index 0000000000..fbf813909c --- /dev/null +++ b/workspaces/rbac/.gitignore @@ -0,0 +1,54 @@ +# macOS +.DS_Store + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Coverage directory generated when running tests with coverage +coverage + +# Dependencies +node_modules/ + +# Yarn 3 files +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Node version directives +.nvmrc + +# dotenv environment variables file +.env +.env.test + +# Build output +dist +dist-types + +# Temporary change files created by Vim +*.swp + +# MkDocs build output +site + +# Local configuration files +*.local.yaml + +# Sensitive credentials +*-credentials.yaml + +# vscode database functionality support files +*.session.sql + +# E2E test reports +e2e-test-report/ diff --git a/workspaces/rbac/.prettierignore b/workspaces/rbac/.prettierignore new file mode 100644 index 0000000000..1cfaa89479 --- /dev/null +++ b/workspaces/rbac/.prettierignore @@ -0,0 +1,5 @@ +dist +dist-types +coverage +.vscode +.eslintrc.js diff --git a/workspaces/rbac/README.md b/workspaces/rbac/README.md new file mode 100644 index 0000000000..94044a3fa6 --- /dev/null +++ b/workspaces/rbac/README.md @@ -0,0 +1,16 @@ +# [Backstage](https://backstage.io) + +This is your newly scaffolded Backstage App, Good Luck! + +To start the app, run: + +```sh +yarn install +yarn dev +``` + +To generate knip reports for this app, run: + +```sh +yarn backstage-repo-tools knip-reports +``` diff --git a/workspaces/rbac/backstage.json b/workspaces/rbac/backstage.json new file mode 100644 index 0000000000..1efaf2de35 --- /dev/null +++ b/workspaces/rbac/backstage.json @@ -0,0 +1 @@ +{ "version": "1.32.0" } diff --git a/workspaces/rbac/catalog-info.yaml b/workspaces/rbac/catalog-info.yaml new file mode 100644 index 0000000000..65bf9bb81d --- /dev/null +++ b/workspaces/rbac/catalog-info.yaml @@ -0,0 +1,13 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: rbac + description: An example of a Backstage application. + # Example for optional annotations + # annotations: + # github.com/project-slug: backstage/backstage + # backstage.io/techdocs-ref: dir:. +spec: + type: website + owner: john@example.com + lifecycle: experimental diff --git a/workspaces/rbac/package.json b/workspaces/rbac/package.json new file mode 100644 index 0000000000..b328fa9376 --- /dev/null +++ b/workspaces/rbac/package.json @@ -0,0 +1,63 @@ +{ + "name": "@internal/rbac", + "version": "1.0.0", + "private": true, + "engines": { + "node": "18 || 20" + }, + "scripts": { + "tsc": "tsc", + "tsc:full": "tsc --skipLibCheck true --incremental false", + "build:all": "backstage-cli repo build --all", + "build:api-reports": "yarn build:api-reports:only", + "build:api-reports:only": "backstage-repo-tools api-reports --allow-all-warnings -o ae-wrong-input-file-type --validate-release-tags", + "clean": "backstage-cli repo clean", + "test": "backstage-cli repo test", + "test:all": "backstage-cli repo test --coverage", + "fix": "backstage-cli repo fix", + "lint": "backstage-cli repo lint --since origin/main", + "lint:all": "backstage-cli repo lint", + "prettier:check": "prettier --check .", + "new": "backstage-cli new --scope @backstage-community", + "postinstall": "cd ../../ && yarn install" + }, + "workspaces": { + "packages": [ + "packages/*", + "plugins/*" + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/community-plugins", + "directory": "workspaces/rbac" + }, + "devDependencies": { + "@backstage/cli": "^0.28.0", + "@backstage/e2e-test-utils": "^0.1.1", + "@backstage/repo-tools": "^0.10.0", + "@changesets/cli": "^2.27.1", + "@spotify/prettier-config": "^12.0.0", + "node-gyp": "^9.0.0", + "prettier": "^2.3.2", + "typescript": "~5.3.0" + }, + "dependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.3.1", + "knip": "^5.27.4" + }, + "resolutions": { + "@types/react": "^18", + "@types/react-dom": "^18" + }, + "prettier": "@spotify/prettier-config", + "lint-staged": { + "*.{js,jsx,ts,tsx,mjs,cjs}": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,md}": [ + "prettier --write" + ] + } +} diff --git a/workspaces/rbac/plugins/README.md b/workspaces/rbac/plugins/README.md new file mode 100644 index 0000000000..d7865fdba3 --- /dev/null +++ b/workspaces/rbac/plugins/README.md @@ -0,0 +1,9 @@ +# The Plugins Folder + +This is where your own plugins and their associated modules live, each in a +separate folder of its own. + +If you want to create a new plugin here, go to your project root directory, run +the command `yarn new`, and follow the on-screen instructions. + +You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)! diff --git a/workspaces/rbac/plugins/rbac-backend/.eslintignore b/workspaces/rbac/plugins/rbac-backend/.eslintignore new file mode 100644 index 0000000000..6a77e2728b --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/.eslintignore @@ -0,0 +1,4 @@ +dist-dynamic +dist-scalprum +!.eslintrc.js +!.prettierrc.js \ No newline at end of file diff --git a/workspaces/rbac/plugins/rbac-backend/.eslintrc.js b/workspaces/rbac/plugins/rbac-backend/.eslintrc.js new file mode 100644 index 0000000000..8bd689af97 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/.eslintrc.js @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/workspaces/rbac/plugins/rbac-backend/.lintstagedrc.json b/workspaces/rbac/plugins/rbac-backend/.lintstagedrc.json new file mode 100644 index 0000000000..14b2263def --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/.lintstagedrc.json @@ -0,0 +1,4 @@ +{ + "*": "prettier --ignore-unknown --write", + "*.{js,jsx,ts,tsx,mjs,cjs}": "backstage-cli package lint --fix" +} diff --git a/workspaces/rbac/plugins/rbac-backend/.prettierignore b/workspaces/rbac/plugins/rbac-backend/.prettierignore new file mode 100644 index 0000000000..fc8357d99e --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/.prettierignore @@ -0,0 +1,12 @@ +dist +dist-types +coverage +.vscode +CHANGELOG.md +generated +templates +*.hbs +renovate.json +dist-dynamic +dist-scalprum +playwright-report diff --git a/workspaces/rbac/plugins/rbac-backend/.prettierrc.js b/workspaces/rbac/plugins/rbac-backend/.prettierrc.js new file mode 100644 index 0000000000..5b35247f3f --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/.prettierrc.js @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */ +module.exports = { + ...require('@spotify/prettier-config'), + plugins: ['@ianvs/prettier-plugin-sort-imports'], + importOrder: [ + '^react(.*)$', + '', + '^@backstage/(.*)$', + '', + '', + '', + '^@backstage-community/(.*)$', + '', + '', + '', + '^[.]', + ], +}; diff --git a/workspaces/rbac/plugins/rbac-backend/.versionhistory.md b/workspaces/rbac/plugins/rbac-backend/.versionhistory.md new file mode 100644 index 0000000000..6e0a77f09a --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/.versionhistory.md @@ -0,0 +1 @@ +- Bumped to 4.11.0 in main branch for next release 1.3.0 diff --git a/workspaces/rbac/plugins/rbac-backend/CHANGELOG.md b/workspaces/rbac/plugins/rbac-backend/CHANGELOG.md new file mode 100644 index 0000000000..424bf09c36 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/CHANGELOG.md @@ -0,0 +1,656 @@ +### Dependencies + +## 5.2.1 + +### Patch Changes + +- 0646434: Fix broken plugin startup: don't attempt to store permission policies that are already stored. + +## 5.2.0 + +### Minor Changes + +- 8244f28: chore(deps): update to backstage 1.32 + +### Patch Changes + +- Updated dependencies [8244f28] + - @janus-idp/backstage-plugin-audit-log-node@1.7.0 + - @backstage-community/plugin-rbac-common@1.12.0 + - @backstage-community/plugin-rbac-node@1.8.0 + +## 5.1.2 + +### Patch Changes + +- 7342e9b: chore: remove @janus-idp/cli dep and relink local packages + + This update removes `@janus-idp/cli` from all plugins, as it’s no longer necessary. Additionally, packages are now correctly linked with a specified version. + +## 5.1.1 + +### Patch Changes + +- e6ef910: Refactors the rbac backend plugin to prevent the creation of permission policies and roles whenever the plugin and permission framework is disabled + +## 5.1.0 + +### Minor Changes + +- d9551ae: feat(deps): update to backstage 1.31 + +### Patch Changes + +- d9551ae: Refactors the rbac backend plugin to move the admin role and admin permission creation to a separate file +- d9551ae: Change local package references to a `*` +- d9551ae: upgrade to yarn v3 +- Updated dependencies [d9551ae] +- Updated dependencies [d9551ae] +- Updated dependencies [d9551ae] + - @backstage-community/plugin-rbac-common@1.11.0 + - @janus-idp/backstage-plugin-audit-log-node@1.6.0 + - @backstage-community/plugin-rbac-node@1.7.0 + +* **@janus-idp/backstage-plugin-audit-log-node:** upgraded to 1.5.1 + +### Dependencies + +- **@janus-idp/backstage-plugin-audit-log-node:** upgraded to 1.5.0 +- **@backstage-community/plugin-rbac-common:** upgraded to 1.10.0 +- **@backstage-community/plugin-rbac-node:** upgraded to 1.6.0 + +### Dependencies + +- **@janus-idp/backstage-plugin-audit-log-node:** upgraded to 1.4.1 + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.9.0 +- **@backstage-community/plugin-rbac-node:** upgraded to 1.5.0 + +## @backstage-community/plugin-rbac-backend [4.7.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.7.2...@backstage-community/plugin-rbac-backend@4.7.3) (2024-08-06) + +### Bug Fixes + +- **rbac:** implement conditional aliases ([#1847](https://github.com/janus-idp/backstage-plugins/issues/1847)) ([dbc9a0b](https://github.com/janus-idp/backstage-plugins/commit/dbc9a0bc92f19a4382e406f83b4889905dc6e33d)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.8.2 + +## @backstage-community/plugin-rbac-backend [4.7.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.7.1...@backstage-community/plugin-rbac-backend@4.7.2) (2024-08-05) + +### Bug Fixes + +- **rbac:** add additional validation for permission policies ([#1908](https://github.com/janus-idp/backstage-plugins/issues/1908)) ([592498f](https://github.com/janus-idp/backstage-plugins/commit/592498f34a3b605162d3c242184aa6877b0360e8)), closes [#1939](https://github.com/janus-idp/backstage-plugins/issues/1939) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.8.1 + +## @backstage-community/plugin-rbac-backend [4.7.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.7.0...@backstage-community/plugin-rbac-backend@4.7.1) (2024-08-02) + +### Bug Fixes + +- **rbac:** log when plugin has no permissions ([#1917](https://github.com/janus-idp/backstage-plugins/issues/1917)) ([cc8752b](https://github.com/janus-idp/backstage-plugins/commit/cc8752b159364fdab62e7bbdaa51ca811288197b)) + +## @backstage-community/plugin-rbac-backend [4.7.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.6.1...@backstage-community/plugin-rbac-backend@4.7.0) (2024-07-30) + +### Features + +- **argocd:** add permission support for argocd ([#1855](https://github.com/janus-idp/backstage-plugins/issues/1855)) ([3b78237](https://github.com/janus-idp/backstage-plugins/commit/3b782377683605ea4d584c43bea14be2f435003d)) + +## @backstage-community/plugin-rbac-backend [4.6.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.6.0...@backstage-community/plugin-rbac-backend@4.6.1) (2024-07-29) + +### Bug Fixes + +- **rbac:** fix uncommited knex transaction in the addGroupingPolicies ([#1968](https://github.com/janus-idp/backstage-plugins/issues/1968)) ([24d5eef](https://github.com/janus-idp/backstage-plugins/commit/24d5eeffbce685bbe05f8895fe3a69ee26a4eb8a)) + +## @backstage-community/plugin-rbac-backend [4.6.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.5.0...@backstage-community/plugin-rbac-backend@4.6.0) (2024-07-26) + +### Features + +- **tekton:** add permissions support for tekton plugin ([#1854](https://github.com/janus-idp/backstage-plugins/issues/1854)) ([f744896](https://github.com/janus-idp/backstage-plugins/commit/f7448963c252574e0309a091563c19e1ed9a58fd)) + +## @backstage-community/plugin-rbac-backend [4.5.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.4.3...@backstage-community/plugin-rbac-backend@4.5.0) (2024-07-26) + +### Features + +- **deps:** update to backstage 1.29 ([#1900](https://github.com/janus-idp/backstage-plugins/issues/1900)) ([f53677f](https://github.com/janus-idp/backstage-plugins/commit/f53677fb02d6df43a9de98c43a9f101a6db76802)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.8.0 +- **@backstage-community/plugin-rbac-node:** upgraded to 1.4.0 + +## @backstage-community/plugin-rbac-backend [4.4.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.4.2...@backstage-community/plugin-rbac-backend@4.4.3) (2024-07-25) + +### Documentation + +- **rbac:** add curl request examples ([#1913](https://github.com/janus-idp/backstage-plugins/issues/1913)) ([e496eb7](https://github.com/janus-idp/backstage-plugins/commit/e496eb73349987d43caba86a29e4c98c86179250)) + +## @backstage-community/plugin-rbac-backend [4.4.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.4.1...@backstage-community/plugin-rbac-backend@4.4.2) (2024-07-24) + +### Bug Fixes + +- **deps:** rollback unreleased plugins ([#1951](https://github.com/janus-idp/backstage-plugins/issues/1951)) ([8b77969](https://github.com/janus-idp/backstage-plugins/commit/8b779694f02f8125587296305276b84cdfeeaebe)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.7.2 +- **@backstage-community/plugin-rbac-node:** upgraded to 1.3.1 + +## @backstage-community/plugin-rbac-backend [4.4.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.4.0...@backstage-community/plugin-rbac-backend@4.4.1) (2024-07-24) + +### Bug Fixes + +- **rbac:** don't start transaction if there no group policies ([#1923](https://github.com/janus-idp/backstage-plugins/issues/1923)) ([dffa964](https://github.com/janus-idp/backstage-plugins/commit/dffa9643b500a19dc70c66cedf9016508cdb5947)) + +## @backstage-community/plugin-rbac-backend [4.4.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.3.4...@backstage-community/plugin-rbac-backend@4.4.0) (2024-07-24) + +### Features + +- **deps:** update to backstage 1.28 ([#1891](https://github.com/janus-idp/backstage-plugins/issues/1891)) ([1ba1108](https://github.com/janus-idp/backstage-plugins/commit/1ba11088e0de60e90d138944267b83600dc446e5)) + +### Bug Fixes + +- **deps:** fix rbac dependencies ([#1918](https://github.com/janus-idp/backstage-plugins/issues/1918)) ([fcc4e1d](https://github.com/janus-idp/backstage-plugins/commit/fcc4e1dde55bc0fb2dd284d256330c7f9f928036)) +- **deps:** move backend-test-utils to devDependencies ([#1944](https://github.com/janus-idp/backstage-plugins/issues/1944)) ([9052a3f](https://github.com/janus-idp/backstage-plugins/commit/9052a3f41cae1cd57fb8f52033ea2c6f752f64fe)) + +### Documentation + +- added OpenAPI spec for rbac-backend ([#1830](https://github.com/janus-idp/backstage-plugins/issues/1830)) ([4eb2035](https://github.com/janus-idp/backstage-plugins/commit/4eb20351bf9713355cb79905a2e49aeec9ad6ec9)) +- **rbac:** fix condition rules api url ([#1914](https://github.com/janus-idp/backstage-plugins/issues/1914)) ([e6fa0ae](https://github.com/janus-idp/backstage-plugins/commit/e6fa0ae7265ea56b50fffbf1466540a61d714ed8)) + +## @backstage-community/plugin-rbac-backend [4.3.4](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.3.3...@backstage-community/plugin-rbac-backend@4.3.4) (2024-07-17) + +### Bug Fixes + +- **rbac:** simplify db logic ([#1842](https://github.com/janus-idp/backstage-plugins/issues/1842)) ([cbe263b](https://github.com/janus-idp/backstage-plugins/commit/cbe263b2901c0d57105667caf2d3ab7c0583468a)) + +## @backstage-community/plugin-rbac-backend [4.3.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.3.2...@backstage-community/plugin-rbac-backend@4.3.3) (2024-07-16) + +### Bug Fixes + +- **rbac:** catch errors whenever a plugin token is not generated ([#1866](https://github.com/janus-idp/backstage-plugins/issues/1866)) ([c9abf44](https://github.com/janus-idp/backstage-plugins/commit/c9abf441591347753fe94fe2590b8059804baeb7)) + +## @backstage-community/plugin-rbac-backend [4.3.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.3.1...@backstage-community/plugin-rbac-backend@4.3.2) (2024-07-05) + +### Bug Fixes + +- **rbac:** casbinDBAdapterFactory supporting postgres schema configuration ([#1841](https://github.com/janus-idp/backstage-plugins/issues/1841)) ([c0e63f9](https://github.com/janus-idp/backstage-plugins/commit/c0e63f9541edc121c77d6569d6fe6958ce937c0b)) +- **rbac:** correct plugin ID matching to permission policy ([#1795](https://github.com/janus-idp/backstage-plugins/issues/1795)) ([6dc4b1c](https://github.com/janus-idp/backstage-plugins/commit/6dc4b1c23d22252f394eecd8b795ac15507ecc50)) +- **rbac:** update rbac common to fix compilation ([#1858](https://github.com/janus-idp/backstage-plugins/issues/1858)) ([48f142b](https://github.com/janus-idp/backstage-plugins/commit/48f142b447f0d1677ba3f16b2a3c8972b22d0588)) + +## @backstage-community/plugin-rbac-backend [4.3.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.3.0...@backstage-community/plugin-rbac-backend@4.3.1) (2024-06-19) + +## @backstage-community/plugin-rbac-backend [4.3.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.2.0...@backstage-community/plugin-rbac-backend@4.3.0) (2024-06-13) + +### Features + +- **deps:** update to backstage 1.27 ([#1683](https://github.com/janus-idp/backstage-plugins/issues/1683)) ([a14869c](https://github.com/janus-idp/backstage-plugins/commit/a14869c3f4177049cb8d6552b36c3ffd17e7997d)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.6.0 +- **@backstage-community/plugin-rbac-node:** upgraded to 1.2.0 +- **@janus-idp/backstage-plugin-audit-log-node:** upgraded to 1.2.0 + +## @backstage-community/plugin-rbac-backend [4.2.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.1.0...@backstage-community/plugin-rbac-backend@4.2.0) (2024-06-05) + +### Features + +- **rbac:** add type checks with generics for audit log ([#1789](https://github.com/janus-idp/backstage-plugins/issues/1789)) ([ac69838](https://github.com/janus-idp/backstage-plugins/commit/ac698382f64fe91e0f9f9232dd3eecd9cc9247be)) + +### Dependencies + +- **@janus-idp/backstage-plugin-audit-log-node:** upgraded to 1.1.0 + +## @backstage-community/plugin-rbac-backend [4.1.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.0.2...@backstage-community/plugin-rbac-backend@4.1.0) (2024-06-04) + +### Features + +- **rbac:** add audit log for RBAC backend ([#1726](https://github.com/janus-idp/backstage-plugins/issues/1726)) ([e50464b](https://github.com/janus-idp/backstage-plugins/commit/e50464bcb38e9897ddfe208fdeef699e4bfeda3a)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.5.0 +- **@backstage-community/plugin-rbac-node:** upgraded to 1.1.2 +- **@janus-idp/backstage-plugin-audit-log-node:** upgraded to 1.0.3 + +## @backstage-community/plugin-rbac-backend [4.0.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.0.1...@backstage-community/plugin-rbac-backend@4.0.2) (2024-06-04) + +### Bug Fixes + +- **rbac:** fix handling condition action conflicts ([#1781](https://github.com/janus-idp/backstage-plugins/issues/1781)) ([966b2b2](https://github.com/janus-idp/backstage-plugins/commit/966b2b200e0ade0ce600901a7853a4a94751df22)) + +## @backstage-community/plugin-rbac-backend [4.0.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@4.0.0...@backstage-community/plugin-rbac-backend@4.0.1) (2024-06-03) + +### Bug Fixes + +- **rbac:** add support for scaling ([#1757](https://github.com/janus-idp/backstage-plugins/issues/1757)) ([caddc83](https://github.com/janus-idp/backstage-plugins/commit/caddc832e0df5199a455539d3538635448691c2d)) + +## @backstage-community/plugin-rbac-backend [4.0.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@3.3.0...@backstage-community/plugin-rbac-backend@4.0.0) (2024-05-31) + +### ⚠ BREAKING CHANGES + +- **rbac:** This will lead to more strict validation on the source of permission policies and roles based on the where the first role is defined. + +Improves the validation of the different sources of permission policies and roles. Aims to make policy definition more consistent. + +Now checks if a permission policy or role with new member matches the originating role's source and prevents any action if the sources do not match. Exception includes the event of adding +new permission policies to the RBAC Admin role defined by the configuration file. Sources include 'REST, 'CSV', 'Configuration', and 'legacy'. + +Before updating, ensure that you have attempted to migrate all permission policies and roles to a single source. This can be done by checking source information through the REST API and +by querying the database. Make updates through one of the available avenues: REST API, CSV file, and the database. + +To view the originating source for a particular role, query the role-metadata table or use the GET roles endpoint. + +- feat(rbac): remove the ability to add permission policies to configuration role + +- feat(rbac): remove no longer needed check for source in EnforcerDelegate + +- feat(rbac): update yarn lock + +- feat(rbac): address review comments + +### Features + +- **rbac:** improve validation from source ([#1643](https://github.com/janus-idp/backstage-plugins/issues/1643)) ([5f983cb](https://github.com/janus-idp/backstage-plugins/commit/5f983cbc0184e0a8e74f7e89cdff71d5ed5cd2fa)) + +## @backstage-community/plugin-rbac-backend [3.3.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@3.2.0...@backstage-community/plugin-rbac-backend@3.3.0) (2024-05-29) + +### Features + +- **rbac:** improve conditional policy validation ([#1673](https://github.com/janus-idp/backstage-plugins/issues/1673)) ([15dac91](https://github.com/janus-idp/backstage-plugins/commit/15dac91b673c63a4e7ac41f95296651df2ef8053)) + +## @backstage-community/plugin-rbac-backend [3.2.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@3.1.1...@backstage-community/plugin-rbac-backend@3.2.0) (2024-05-21) + +### Features + +- **topology:** add permissions to topology plugin ([#1665](https://github.com/janus-idp/backstage-plugins/issues/1665)) ([9d8f244](https://github.com/janus-idp/backstage-plugins/commit/9d8f244ae136cdf1980a5abf416180bce3f235ea)) + +## @backstage-community/plugin-rbac-backend [3.1.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@3.1.0...@backstage-community/plugin-rbac-backend@3.1.1) (2024-05-16) + +## @backstage-community/plugin-rbac-backend [3.1.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@3.0.0...@backstage-community/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)) + +## @backstage-community/plugin-rbac-backend [3.0.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.8.2...@backstage-community/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)) + +## @backstage-community/plugin-rbac-backend [2.8.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.8.1...@backstage-community/plugin-rbac-backend@2.8.2) (2024-05-09) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.4.2 +- **@backstage-community/plugin-rbac-node:** upgraded to 1.1.1 + +## @backstage-community/plugin-rbac-backend [2.8.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.8.0...@backstage-community/plugin-rbac-backend@2.8.1) (2024-05-07) + +### Bug Fixes + +- **rbac:** implement ability to disable rbac-backend plugin ([#1501](https://github.com/janus-idp/backstage-plugins/issues/1501)) ([6367965](https://github.com/janus-idp/backstage-plugins/commit/6367965c550286dc8423b0942341ecee178dc6c1)) + +## @backstage-community/plugin-rbac-backend [2.8.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.7.1...@backstage-community/plugin-rbac-backend@2.8.0) (2024-05-07) + +### Features + +- **rbac:** add support for the new backend services ([#1607](https://github.com/janus-idp/backstage-plugins/issues/1607)) ([2892709](https://github.com/janus-idp/backstage-plugins/commit/2892709860987c6f4b36d821afa2e612b220d030)) + +## @backstage-community/plugin-rbac-backend [2.7.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.7.0...@backstage-community/plugin-rbac-backend@2.7.1) (2024-05-06) + +### Bug Fixes + +- **ocm:** update ocm frontend plugin readme ([#1611](https://github.com/janus-idp/backstage-plugins/issues/1611)) ([9960cc0](https://github.com/janus-idp/backstage-plugins/commit/9960cc0c2d611cdd1ee10a82ed02b7be9becefcf)) + +## @backstage-community/plugin-rbac-backend [2.7.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.6.4...@backstage-community/plugin-rbac-backend@2.7.0) (2024-04-25) + +### Features + +- **rbac:** add the optional maxDepth feature ([#1486](https://github.com/janus-idp/backstage-plugins/issues/1486)) ([ea87f34](https://github.com/janus-idp/backstage-plugins/commit/ea87f3412eb374123ea623332de0648d4c7bda5c)) +- **rbac:** lazy load temporary enforcer ([#1513](https://github.com/janus-idp/backstage-plugins/issues/1513)) ([b5f1552](https://github.com/janus-idp/backstage-plugins/commit/b5f1552f069068af43a4ca2756a5a38187f6d453)) + +## @backstage-community/plugin-rbac-backend [2.6.4](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.6.3...@backstage-community/plugin-rbac-backend@2.6.4) (2024-04-17) + +### Bug Fixes + +- **rbac:** reduce the number of permissions returned, add isResourced flag ([#1474](https://github.com/janus-idp/backstage-plugins/issues/1474)) ([e5dda95](https://github.com/janus-idp/backstage-plugins/commit/e5dda95bfc87d1d5d404726cbbe05c8bfdb73845)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.4.1 + +## @backstage-community/plugin-rbac-backend [2.6.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.6.2...@backstage-community/plugin-rbac-backend@2.6.3) (2024-04-15) + +### Dependencies + +- **@backstage-community/plugin-rbac-node:** upgraded to 1.1.0 + +## @backstage-community/plugin-rbac-backend [2.6.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.6.1...@backstage-community/plugin-rbac-backend@2.6.2) (2024-04-09) + +### Dependencies + +- **@backstage-community/plugin-rbac-node:** upgraded to 1.0.6 + +## @backstage-community/plugin-rbac-backend [2.6.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.6.0...@backstage-community/plugin-rbac-backend@2.6.1) (2024-04-08) + +### Dependencies + +- **@backstage-community/plugin-rbac-node:** upgraded to 1.0.5 + +## @backstage-community/plugin-rbac-backend [2.6.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.5.1...@backstage-community/plugin-rbac-backend@2.6.0) (2024-04-05) + +### Features + +- **rbac:** save role modification information to the metadata ([#1280](https://github.com/janus-idp/backstage-plugins/issues/1280)) ([0454509](https://github.com/janus-idp/backstage-plugins/commit/0454509e41db2ae332d1b2bf8f72d34241483efd)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.4.0 +- **@backstage-community/plugin-rbac-node:** upgraded to 1.0.5 + +## @backstage-community/plugin-rbac-backend [2.5.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.5.0...@backstage-community/plugin-rbac-backend@2.5.1) (2024-04-04) + +### Bug Fixes + +- **rbac:** rework condition policies to bound them to RBAC roles ([#1330](https://github.com/janus-idp/backstage-plugins/issues/1330)) ([55c00b2](https://github.com/janus-idp/backstage-plugins/commit/55c00b21b27b449cb0e5100c7b64a6ae742536ac)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.3.2 + +## @backstage-community/plugin-rbac-backend [2.5.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.4.1...@backstage-community/plugin-rbac-backend@2.5.0) (2024-03-29) + +### Features + +- **rbac:** load filtered policies before enforcing ([#1387](https://github.com/janus-idp/backstage-plugins/issues/1387)) ([66980ba](https://github.com/janus-idp/backstage-plugins/commit/66980baebd4d8b5b398646bcab1750c0edec715e)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.3.1 +- **@backstage-community/plugin-rbac-node:** upgraded to 1.0.4 + +## @backstage-community/plugin-rbac-backend [2.4.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.4.0...@backstage-community/plugin-rbac-backend@2.4.1) (2024-03-19) + +### Bug Fixes + +- **rbac:** pass token to readUrl for well-known permission endpoint ([#1342](https://github.com/janus-idp/backstage-plugins/issues/1342)) ([36b7c77](https://github.com/janus-idp/backstage-plugins/commit/36b7c7739753bd1cc55d10aa68d41ed7e15162e6)) + +## @backstage-community/plugin-rbac-backend [2.4.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.3.5...@backstage-community/plugin-rbac-backend@2.4.0) (2024-03-14) + +### Features + +- **rbac:** query the catalog database when building graph ([#1298](https://github.com/janus-idp/backstage-plugins/issues/1298)) ([c2c9e22](https://github.com/janus-idp/backstage-plugins/commit/c2c9e22e90a594e2a44d1683a05d3111c4baa97b)) + +### Bug Fixes + +- **rbac:** remove admin metadata, when all admins removed from config ([#1314](https://github.com/janus-idp/backstage-plugins/issues/1314)) ([cc6555e](https://github.com/janus-idp/backstage-plugins/commit/cc6555ea22a191c9f9f554b1909b67e517deee71)) + +## @backstage-community/plugin-rbac-backend [2.3.5](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.3.4...@backstage-community/plugin-rbac-backend@2.3.5) (2024-03-07) + +### Bug Fixes + +- **rbac:** check source before throwing duplicate warning ([#1278](https://github.com/janus-idp/backstage-plugins/issues/1278)) ([a100eef](https://github.com/janus-idp/backstage-plugins/commit/a100eef67983ba73d929864f0b64991de69718d0)) + +## @backstage-community/plugin-rbac-backend [2.3.4](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.3.3...@backstage-community/plugin-rbac-backend@2.3.4) (2024-03-04) + +### Dependencies + +- **@backstage-community/plugin-rbac-node:** upgraded to 1.0.3 + +## @backstage-community/plugin-rbac-backend [2.3.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.3.2...@backstage-community/plugin-rbac-backend@2.3.3) (2024-02-29) + +### Documentation + +- **rbac:** update to the rbac documentation ([#1268](https://github.com/janus-idp/backstage-plugins/issues/1268)) ([5c7253b](https://github.com/janus-idp/backstage-plugins/commit/5c7253b7d0646433c55f185092648f0816aee88e)) + +## @backstage-community/plugin-rbac-backend [2.3.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.3.1...@backstage-community/plugin-rbac-backend@2.3.2) (2024-02-28) + +### Bug Fixes + +- **rbac:** improve error handling in retrieving permission metadata. ([#1285](https://github.com/janus-idp/backstage-plugins/issues/1285)) ([77f5f0e](https://github.com/janus-idp/backstage-plugins/commit/77f5f0efaadf1873b68876f11ca633646ce882b9)) + +## @backstage-community/plugin-rbac-backend [2.3.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.3.0...@backstage-community/plugin-rbac-backend@2.3.1) (2024-02-27) + +### Dependencies + +- **@backstage-community/plugin-rbac-node:** upgraded to 1.0.2 + +## @backstage-community/plugin-rbac-backend [2.3.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.2.4...@backstage-community/plugin-rbac-backend@2.3.0) (2024-02-21) + +### Features + +- **rbac:** backend part - store role description to the database ([#1178](https://github.com/janus-idp/backstage-plugins/issues/1178)) ([ec8b1c2](https://github.com/janus-idp/backstage-plugins/commit/ec8b1c27cce5c36997f84a068dc4cc5cc542f428)) + +### Bug Fixes + +- **rbac:** reduce the catalog calls when build graph ([#1203](https://github.com/janus-idp/backstage-plugins/issues/1203)) ([e63aac2](https://github.com/janus-idp/backstage-plugins/commit/e63aac2a8e7513974a5aabb3ce25c838d6b34dde)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.3.0 +- **@backstage-community/plugin-rbac-node:** upgraded to 1.0.1 + +## @backstage-community/plugin-rbac-backend [2.2.4](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.2.3...@backstage-community/plugin-rbac-backend@2.2.4) (2024-02-20) + +### Bug Fixes + +- **rbac:** drop database disabled mode ([#1214](https://github.com/janus-idp/backstage-plugins/issues/1214)) ([b18d80d](https://github.com/janus-idp/backstage-plugins/commit/b18d80dd14e6b7f4f9c90d72ec418609ff1f6a67)) + +## @backstage-community/plugin-rbac-backend [2.2.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.2.2...@backstage-community/plugin-rbac-backend@2.2.3) (2024-02-14) + +### Bug Fixes + +- **rbac:** allow for super users to have allow all access ([#1208](https://github.com/janus-idp/backstage-plugins/issues/1208)) ([c02a4b0](https://github.com/janus-idp/backstage-plugins/commit/c02a4b029a800b1bcf1f2e2722185faae1e5837e)) + +## @backstage-community/plugin-rbac-backend [2.2.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.2.1...@backstage-community/plugin-rbac-backend@2.2.2) (2024-02-13) + +### Bug Fixes + +- **rbac:** display resource typed permissions by name too ([#1197](https://github.com/janus-idp/backstage-plugins/issues/1197)) ([bc4e8e7](https://github.com/janus-idp/backstage-plugins/commit/bc4e8e783b1acd8088a45ffed4d902fd9515c2e8)) + +## @backstage-community/plugin-rbac-backend [2.2.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.2.0...@backstage-community/plugin-rbac-backend@2.2.1) (2024-02-12) + +### Bug Fixes + +- **rbac:** csv updates no longer require server restarts ([#1171](https://github.com/janus-idp/backstage-plugins/issues/1171)) ([ed6fe65](https://github.com/janus-idp/backstage-plugins/commit/ed6fe65d99a2c2facf832a84d29dabc8d339e328)) + +## @backstage-community/plugin-rbac-backend [2.2.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.1.3...@backstage-community/plugin-rbac-backend@2.2.0) (2024-02-08) + +### Features + +- add support for the new backend system to the `rbac-backend` plugin ([#1179](https://github.com/janus-idp/backstage-plugins/issues/1179)) ([d625cb2](https://github.com/janus-idp/backstage-plugins/commit/d625cb2470513862027e048c70944275043ce70a)) + +### Dependencies + +- **@backstage-community/plugin-rbac-node:** upgraded to 1.0.0 + +## @backstage-community/plugin-rbac-backend [2.1.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.1.2...@backstage-community/plugin-rbac-backend@2.1.3) (2024-02-02) + +### Bug Fixes + +- **rbac:** set up higher jest timeout for rbac db tests ([#1163](https://github.com/janus-idp/backstage-plugins/issues/1163)) ([b8541f3](https://github.com/janus-idp/backstage-plugins/commit/b8541f3ac149446238dc07432116fafc23a48a82)) +- **rbac:** split policies and roles by source ([#1042](https://github.com/janus-idp/backstage-plugins/issues/1042)) ([03a678d](https://github.com/janus-idp/backstage-plugins/commit/03a678d96deeb1d42448e94ac95d735e61393a40)), closes [#1103](https://github.com/janus-idp/backstage-plugins/issues/1103) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.2.1 + +## @backstage-community/plugin-rbac-backend [2.1.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.1.1...@backstage-community/plugin-rbac-backend@2.1.2) (2024-01-30) + +### Bug Fixes + +- **rbac:** enable create button for default role:default/rbac_admin ([#1137](https://github.com/janus-idp/backstage-plugins/issues/1137)) ([9926463](https://github.com/janus-idp/backstage-plugins/commit/9926463c8c46871b823796adf77bbd52eb8e6758)) + +## @backstage-community/plugin-rbac-backend [2.1.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.1.0...@backstage-community/plugin-rbac-backend@2.1.1) (2024-01-23) + +### Bug Fixes + +- **rbac:** fix work resource permission specified by name ([#940](https://github.com/janus-idp/backstage-plugins/issues/940)) ([3601eb8](https://github.com/janus-idp/backstage-plugins/commit/3601eb8d0c19e0aad27031ab61f1afa0edc78945)) + +## @backstage-community/plugin-rbac-backend [2.1.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@2.0.0...@backstage-community/plugin-rbac-backend@2.1.0) (2024-01-17) + +### Features + +- **Notifications:** new notifications FE plugin, API and backend ([#933](https://github.com/janus-idp/backstage-plugins/issues/933)) ([4d4cb78](https://github.com/janus-idp/backstage-plugins/commit/4d4cb781ca9fc331a2c621583e9203f9e4585ee7)) +- **rbac:** add doc about RBAC backend conditions API ([#1027](https://github.com/janus-idp/backstage-plugins/issues/1027)) ([fc9ad53](https://github.com/janus-idp/backstage-plugins/commit/fc9ad5348d768423cbce0df7e2a4239c9a24a11e)) + +### Bug Fixes + +- **rbac:** fix role validation ([#1020](https://github.com/janus-idp/backstage-plugins/issues/1020)) ([49c7975](https://github.com/janus-idp/backstage-plugins/commit/49c7975f74a1791e205fe3a322f1efe6504212ed)) + +## @backstage-community/plugin-rbac-backend [2.0.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.7.1...@backstage-community/plugin-rbac-backend@2.0.0) (2023-12-14) + +### ⚠ BREAKING CHANGES + +- **rbac:** add support for multiple policies CRUD (#984) + +### Features + +- **rbac:** add support for multiple policies CRUD ([#984](https://github.com/janus-idp/backstage-plugins/issues/984)) ([518c767](https://github.com/janus-idp/backstage-plugins/commit/518c7674aa037669fe9c2fc6f8dc9be5f0c8fa84)) + +## @backstage-community/plugin-rbac-backend [1.7.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.7.0...@backstage-community/plugin-rbac-backend@1.7.1) (2023-12-08) + +### Documentation + +- **rbac:** add documentation for api and known permissions ([#1000](https://github.com/janus-idp/backstage-plugins/issues/1000)) ([8f8133f](https://github.com/janus-idp/backstage-plugins/commit/8f8133f12d2a74dc6503f7545942f11c40b52092)) + +## @backstage-community/plugin-rbac-backend [1.7.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.6.6...@backstage-community/plugin-rbac-backend@1.7.0) (2023-12-07) + +### Features + +- **rbac:** list roles with no permission policies ([#998](https://github.com/janus-idp/backstage-plugins/issues/998)) ([217b7b0](https://github.com/janus-idp/backstage-plugins/commit/217b7b0db3414788c8e77247f378a51cf0eeda0d)) + +## @backstage-community/plugin-rbac-backend [1.6.6](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.6.5...@backstage-community/plugin-rbac-backend@1.6.6) (2023-12-05) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.2.0 + +## @backstage-community/plugin-rbac-backend [1.6.5](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.6.4...@backstage-community/plugin-rbac-backend@1.6.5) (2023-12-04) + +### Documentation + +- **rbac:** additional docs for backend configuration ([#982](https://github.com/janus-idp/backstage-plugins/issues/982)) ([17b95a0](https://github.com/janus-idp/backstage-plugins/commit/17b95a0c51e97ee5a9160dc7bec7559c075eca88)) + +## @backstage-community/plugin-rbac-backend [1.6.4](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.6.3...@backstage-community/plugin-rbac-backend@1.6.4) (2023-11-20) + +### Bug Fixes + +- **aap+3scale+ocm:** don't log sensitive data from errors ([#945](https://github.com/janus-idp/backstage-plugins/issues/945)) ([7a5e7b8](https://github.com/janus-idp/backstage-plugins/commit/7a5e7b8a57c9841003d9b16e1a65fb62e101fbf1)) + +## @backstage-community/plugin-rbac-backend [1.6.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.6.2...@backstage-community/plugin-rbac-backend@1.6.3) (2023-11-13) + +### Bug Fixes + +- **rbac:** use the same Knex version with Backstage ([#929](https://github.com/janus-idp/backstage-plugins/issues/929)) ([6923ce0](https://github.com/janus-idp/backstage-plugins/commit/6923ce07d787ea6edd911ab348704ba6b9f95ada)) + +## @backstage-community/plugin-rbac-backend [1.6.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.6.1...@backstage-community/plugin-rbac-backend@1.6.2) (2023-11-10) + +### Bug Fixes + +- **rbac:** handle postgres ssl connection for rbac backend plugin ([#923](https://github.com/janus-idp/backstage-plugins/issues/923)) ([deb2026](https://github.com/janus-idp/backstage-plugins/commit/deb202642f456cda446a99f55a475eeaddc59e7c)) + +## @backstage-community/plugin-rbac-backend [1.6.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.6.0...@backstage-community/plugin-rbac-backend@1.6.1) (2023-11-01) + +### Bug Fixes + +- **rbac:** add migration folder to rbac-backend package ([#897](https://github.com/janus-idp/backstage-plugins/issues/897)) ([694a9d6](https://github.com/janus-idp/backstage-plugins/commit/694a9d65bd986eb8e7fde3d66e012963033741af)) + +## @backstage-community/plugin-rbac-backend [1.6.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.5.1...@backstage-community/plugin-rbac-backend@1.6.0) (2023-10-31) + +### Features + +- **rbac:** implement REST method to list all plugin permission policies ([#808](https://github.com/janus-idp/backstage-plugins/issues/808)) ([0a17e67](https://github.com/janus-idp/backstage-plugins/commit/0a17e67cbb72416176e978fc3ed8868855375a8b)) + +## @backstage-community/plugin-rbac-backend [1.5.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.5.0...@backstage-community/plugin-rbac-backend@1.5.1) (2023-10-30) + +### Bug Fixes + +- **rbac:** fix service to service requests for RBAC CRUD ([#886](https://github.com/janus-idp/backstage-plugins/issues/886)) ([0b72d73](https://github.com/janus-idp/backstage-plugins/commit/0b72d7373dddc3f4d8c5076ca3800745bf619d85)) + +## @backstage-community/plugin-rbac-backend [1.5.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.4.0...@backstage-community/plugin-rbac-backend@1.5.0) (2023-10-30) + +### Features + +- **rbac:** implement conditional policies feature. ([#833](https://github.com/janus-idp/backstage-plugins/issues/833)) ([3c0675b](https://github.com/janus-idp/backstage-plugins/commit/3c0675ba6ebf91274848981fa1e6eab9e4a1e659)) + +## @backstage-community/plugin-rbac-backend [1.4.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.3.0...@backstage-community/plugin-rbac-backend@1.4.0) (2023-10-30) + +### Features + +- **rbac:** add role support for policies-csv-file ([#894](https://github.com/janus-idp/backstage-plugins/issues/894)) ([7ad4902](https://github.com/janus-idp/backstage-plugins/commit/7ad4902be12a9900149a73427a6c52cbb65659f3)) + +## @backstage-community/plugin-rbac-backend [1.3.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.2.1...@backstage-community/plugin-rbac-backend@1.3.0) (2023-10-27) + +### Features + +- **rbac:** implement the concept of roles in rbac ([#867](https://github.com/janus-idp/backstage-plugins/issues/867)) ([4d878a2](https://github.com/janus-idp/backstage-plugins/commit/4d878a29babd86bd7896d69e6b2b63392b6e6cc8)) + +### Bug Fixes + +- **rbac:** add models folder and config.d.ts to package ([#891](https://github.com/janus-idp/backstage-plugins/issues/891)) ([406c147](https://github.com/janus-idp/backstage-plugins/commit/406c14703110018c702834482d32fdd4f8a36cef)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.1.0 + +## @backstage-community/plugin-rbac-backend [1.2.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.2.0...@backstage-community/plugin-rbac-backend@1.2.1) (2023-10-24) + +### Bug Fixes + +- **rbac:** use token manager for catalog requests ([#866](https://github.com/janus-idp/backstage-plugins/issues/866)) ([8ad3480](https://github.com/janus-idp/backstage-plugins/commit/8ad348029cec4eabf605c7065e76a5305be3cac8)) + +## @backstage-community/plugin-rbac-backend [1.2.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.1.1...@backstage-community/plugin-rbac-backend@1.2.0) (2023-10-23) + +### Features + +- **cli:** add frontend dynamic plugins base build config ([#747](https://github.com/janus-idp/backstage-plugins/issues/747)) ([91e06da](https://github.com/janus-idp/backstage-plugins/commit/91e06da8ab108c17fd2a6531f25e01c7a7350276)), closes [#831](https://github.com/janus-idp/backstage-plugins/issues/831) + +## @backstage-community/plugin-rbac-backend [1.1.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.1.0...@backstage-community/plugin-rbac-backend@1.1.1) (2023-10-19) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.0.1 + +## @backstage-community/plugin-rbac-backend [1.1.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.0.2...@backstage-community/plugin-rbac-backend@1.1.0) (2023-10-06) + +### Features + +- **rbac:** implement RBAC group support ([#803](https://github.com/janus-idp/backstage-plugins/issues/803)) ([4c72f5c](https://github.com/janus-idp/backstage-plugins/commit/4c72f5c23324ea2f7538b406d60730ea224ae758)) + +## @backstage-community/plugin-rbac-backend [1.0.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.0.1...@backstage-community/plugin-rbac-backend@1.0.2) (2023-10-04) + +### Bug Fixes + +- **rbac:** add models folder to package ([#823](https://github.com/janus-idp/backstage-plugins/issues/823)) ([e2bc66e](https://github.com/janus-idp/backstage-plugins/commit/e2bc66edac61a16ec92f75fb48c8ad459f24a23a)) + +## @backstage-community/plugin-rbac-backend [1.0.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-backend@1.0.0...@backstage-community/plugin-rbac-backend@1.0.1) (2023-10-03) + +### Documentation + +- **rbac:** initial documentation for RBAC ([#814](https://github.com/janus-idp/backstage-plugins/issues/814)) ([d5cd566](https://github.com/janus-idp/backstage-plugins/commit/d5cd5666c43be5ca2790b1c548f56350ef50c96c)) + +## @backstage-community/plugin-rbac-backend 1.0.0 (2023-09-29) + +### Bug Fixes + +- **rbac:** remove private package ([#809](https://github.com/janus-idp/backstage-plugins/issues/809)) ([cf59d6d](https://github.com/janus-idp/backstage-plugins/commit/cf59d6d1c5a65363a7ccdd7490d3148d665e7d46)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.0.0 diff --git a/workspaces/rbac/plugins/rbac-backend/README.md b/workspaces/rbac/plugins/rbac-backend/README.md new file mode 100644 index 0000000000..a744c4b0e9 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/README.md @@ -0,0 +1,220 @@ +# RBAC backend plugin for Backstage + +This plugin seamlessly integrates with the [Backstage permission framework](https://backstage.io/docs/permissions/overview/) to empower you with robust role-based access control capabilities within your Backstage environment. + +The Backstage permission framework is a core component of the Backstage project, designed to provide meticulous control over resource and action access. Our RBAC plugin harnesses the power of this framework, allowing you to tailor access permissions without the need for coding. Instead, you can effortlessly manage your access policies through User interface embedded within Backstage or via the configuration files. + +With the RBAC plugin, you'll have the means to efficiently administer permissions within your Backstage instance by assigning them to users and groups. + +## Prerequisites + +Before you dive into utilizing the RBAC plugin for Backstage, there are a few essential prerequisites to ensure a seamless experience. Please review the following requirements to make sure your environment is properly set up + +### Setup Permission Framework + +**NOTE**: This section is only relevant if you are still on the old backend system. + +To effectively utilize the RBAC plugin, you must have the Backstage permission framework in place. If you're using the Red Hat Developer Hub, some of these steps may have already been completed for you. However, for other Backstage application instances, please verify that the following prerequisites are satisfied: + +You need to [set up the permission framework in Backstage](https://backstage.io/docs/permissions/getting-started/).Since this plugin provides a dynamic policy that replaces the traditional one, there's no need to create a policy manually. Please note that one of the requirements for permission framework is enabling the [service-to-service authentication](https://backstage.io/docs/auth/service-to-service-auth/#setup). Ensure that you complete these authentication setup steps as well. + +### Identity resolver + +The permission framework, and consequently, this RBAC plugin, rely on the concept of group membership. To ensure smooth operation, please follow the [Sign-in identities and resolvers](https://backstage.io/docs/auth/identity-resolver/) documentation. It's crucial that when populating groups, you include any groups that you plan to assign permissions to. + +## Installation + +To integrate the RBAC plugin into your Backstage instance, follow these steps. + +### Installing the plugin + +Add the RBAC plugin packages as dependencies by running the following command. + +```SHELL +yarn workspace backend add @backstage-community/plugin-rbac-backend +``` + +**NOTE**: If you are using Red Hat Developer Hub backend plugin is pre-installed and you do not need this step. + +### Configuring the Backend + +#### New Backend System + +The RBAC plugin supports the integration with the new backend system. + +Add the RBAC plugin to the `packages/backend/src/index.ts` file and remove the Permission backend plugin and Allow All Permission policy module. + +```diff +// permission plugin +- backend.add(import('@backstage/plugin-permission-backend/alpha')); +- backend.add( +- import('@backstage/plugin-permission-backend-module-allow-all-policy'), +- ); ++ backend.add(import('@backstage-community/plugin-rbac-backend')); +``` + +### Configure policy admins + +The RBAC plugin empowers you to manage permission policies for users and groups with a designated group of individuals known as policy administrators. These administrators are granted access to the RBAC plugin's REST API and user interface as well as the ability to read from the catalog. + +You can specify the policy administrators in your application configuration as follows: + +```YAML +permission: + enabled: true + rbac: + admin: + users: + - name: user:default/alice + - name: group:default/admins +``` + +The RBAC plugin also enables you to grant users the title of 'super user,' which provides them with unrestricted access throughout the Backstage instance. + +You can specify the super users in your application configuration as follows: + +```YAML +permission: + enabled: true + rbac: + admin: + superUsers: + - name: user:default/alice + - name: user:default/mike +``` + +For more information on the available API endpoints accessible to the policy administrators, refer to the [API documentation](./docs/apis.md). + +### Configuring policies via file + +The RBAC plugin also allows you to import policies from an external file. These policies are defined in the [Casbin rules format](https://casbin.org/docs/category/the-basics), known for its simplicity and clarity. For a quick start, please refer to the format details in the provided link. + +Here's an example of an external permission policies configuration file named `rbac-policy.csv`: + +```CSV +p, role:default/team_a, catalog-entity, read, deny +p, role:default/team_b, catalog.entity.create, create, deny + +g, user:default/bob, role:default/team_a + +g, group:default/team_b, role:default/team_b +``` + +--- + +**NOTE**: When you add a role in the permission policies configuration file, ensure that the role is associated with at least one permission policy with the `allow` effect. + +--- + +You can specify the path to this configuration file in your application configuration: + +```YAML +permission: + enabled: true + rbac: + policies-csv-file: /some/path/rbac-policy.csv +``` + +Also, there is an additional configuration value that allows for the reloading of the CSV file without the need to restart. + +```YAML +permission: + enabled: true + rbac: + policies-csv-file: /some/path/rbac-policy.csv + policyFileReload: true +``` + +For more information on the available permissions within Showcase and RHDH, refer to the [permissions documentation](./docs/permissions.md). + +We also have a fairly strict validation for permission policies and roles based on the originating role's source information, refer to the [api documentation](./docs/apis.md). + +### Configuring conditional policies via file + +The RBAC plugin allows you to import conditional policies from an external file. User can defined conditional policies for roles created with the help of the policies-csv-file. Conditional policies should be defined as object sequences in the YAML format. + +You can specify the path to this configuration file in your application configuration: + +```YAML +permission: + enabled: true + rbac: + conditionalPoliciesFile: /some/path/conditional-policies.yaml + policies-csv-file: /some/path/rbac-policy.csv +``` + +Also, there is an additional configuration value that allows for the reloading of the file without the need to restart. + +```YAML +permission: + enabled: true + rbac: + conditionalPoliciesFile: /some/path/conditional-policies.yaml + policies-csv-file: /some/path/rbac-policy.csv + policyFileReload: true +``` + +This feature supports nested conditional policies. + +Example of the conditional policies file: + +```yaml +--- +result: CONDITIONAL +roleEntityRef: 'role:default/test' +pluginId: catalog +resourceType: catalog-entity +permissionMapping: + - read + - update +conditions: + rule: IS_ENTITY_OWNER + resourceType: catalog-entity + params: + claims: + - 'group:default/team-a' + - 'group:default/team-b' +--- +result: CONDITIONAL +roleEntityRef: 'role:default/test' +pluginId: catalog +resourceType: catalog-entity +permissionMapping: + - delete +conditions: + rule: IS_ENTITY_OWNER + resourceType: catalog-entity + params: + claims: + - 'group:default/team-a' +``` + +Information about condition policies format you can find in the doc: [Conditional policies documentation](./docs/conditions.md). There is only one difference: yaml format compare to json. But yaml and json are back convertiable. + +### Configuring Database Storage for policies + +The RBAC plugin offers the option to store policies in a database. It supports two database storage options: + +- sqlite3: Suitable for development environments. +- postgres: Recommended for production environments. + +Ensure that you have already configured the database backend for your Backstage instance, as the RBAC plugin utilizes the same database configuration. + +### Optional maximum depth + +The RBAC plugin also includes an option max depth feature for organizations with potentially complex group hierarchy, this configuration value will ensure that the RBAC plugin will stop at a certain depth when building user graphs. + +```YAML +permission: + enabled: true + rbac: + maxDepth: 1 +``` + +The maxDepth must be greater than 0 to ensure that the graphs are built correctly. Also the graph will be built with a hierarchy of 1 + maxDepth. + +More information about group hierarchy can be found in the doc: [Group hierarchy](./docs/group-hierarchy.md). + +### Optional RBAC provider module support + +We also include the ability to create and load in RBAC backend plugin modules that can be used to make connections to third part access management tools. For more information, consult the [RBAC Providers documentation](./docs/providers.md). diff --git a/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/invalid-conditions/bad-conditions-yaml.yaml b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/invalid-conditions/bad-conditions-yaml.yaml new file mode 100644 index 0000000000..862169502d --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/invalid-conditions/bad-conditions-yaml.yaml @@ -0,0 +1 @@ +some bad yaml.... diff --git a/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/invalid-conditions/invalid-yaml.yaml b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/invalid-conditions/invalid-yaml.yaml new file mode 100644 index 0000000000..1575881403 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/invalid-conditions/invalid-yaml.yaml @@ -0,0 +1 @@ +result: diff --git a/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/invalid-csv/duplicate-policy.csv b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/invalid-csv/duplicate-policy.csv new file mode 100644 index 0000000000..181fb0868b --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/invalid-csv/duplicate-policy.csv @@ -0,0 +1,12 @@ +g, user:default/guest, role:default/catalog-deleter +g, user:default/guest, role:default/catalog-deleter + +g, user:default/guest, role:default/catalog-updater + +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/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/invalid-csv/error-policy.csv b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/invalid-csv/error-policy.csv new file mode 100644 index 0000000000..963ad4cceb --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/invalid-csv/error-policy.csv @@ -0,0 +1,10 @@ +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 + +p, role:default/rest, catalog-entity, update, allow +g, user:default/guest, role:default/rest + +p, role:default/config, catalog-entity, update, allow +g, user:default/guest, role:default/config diff --git a/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-conditions/conditions.yaml b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-conditions/conditions.yaml new file mode 100644 index 0000000000..0af4cab20b --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-conditions/conditions.yaml @@ -0,0 +1,28 @@ +--- +result: CONDITIONAL +roleEntityRef: 'role:default/test' +pluginId: catalog +resourceType: catalog-entity +permissionMapping: + - update +conditions: + rule: IS_ENTITY_OWNER + resourceType: catalog-entity + params: + claims: + - 'group:default/team-a' +--- +result: CONDITIONAL +roleEntityRef: 'role:default/test' +pluginId: catalog +resourceType: catalog-entity +permissionMapping: + - read + - delete +conditions: + rule: IS_ENTITY_OWNER + resourceType: catalog-entity + params: + claims: + - 'group:default/team-a' + - 'group:default/team-b' diff --git a/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-conditions/empty-conditions.yaml b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-conditions/empty-conditions.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-csv/basic-and-resource-policies.csv b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-csv/basic-and-resource-policies.csv new file mode 100644 index 0000000000..7615d8aa63 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-csv/basic-and-resource-policies.csv @@ -0,0 +1,21 @@ +# ========== basic type permission policies ========== # +# case 1 +p, user:default/known_user, test.resource.deny, use, deny +# case 2 is about user without listed permissions +# case 3 +p, user:default/duplicated, test.resource, use, allow +p, user:default/duplicated, test.resource, use, deny +# case 4 +p, user:default/known_user, test.resource, use, allow +# case 5 +unknown user + +# ========== resource type permission policies ========== # +# case 1 +p, user:default/known_user, test-resource-deny, update, deny +# case 2 is about user without listed permissions +# case 3 +p, user:default/duplicated, test-resource, update, allow +p, user:default/duplicated, test-resource, update, deny +# case 4 +p, user:default/known_user, test-resource, update, allow diff --git a/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-csv/policy-checks.csv b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-csv/policy-checks.csv new file mode 100644 index 0000000000..96aa84f201 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-csv/policy-checks.csv @@ -0,0 +1,67 @@ +# basic type permission policies +### Let's deny 'use' action for 'test.resource' for group:default/data_admin +p, group:default/data_admin, test.resource, use, deny + +# case1: +# g, user:default/alice, group:default/data_admin +p, user:default/alice, test.resource, use, allow + +# case2: +# g, user:default/akira, group:default/data_admin + +# case3: +# g, user:default/antey, group:default/data_admin +p, user:default/antey, test.resource, use, deny + +### Let's allow 'use' action for 'test.resource' for group:default/data_read_admin +p, group:default/data_read_admin, test.resource, use, allow + +# case4: +# g, user:default/julia, group:default/data_read_admin +p, user:default/julia, test.resource, use, allow + +# case5: +# g, user:default/mike, group:default/data_read_admin + +# case6: +# g, user:default/tom, group:default/data_read_admin +p, user:default/tom, test.resource, use, deny + + +# resource type permission policies +### Let's deny 'read' action for 'test.resource' permission for group:default/data_admin +p, group:default/data_admin, test-resource, read, deny + +# case1: +# g, user:default/alice, group:default/data_admin +p, user:default/alice, test-resource, read, allow + +# case2: +# g, user:default/akira, group:default/data_admin + +# case3: +# g, user:default/antey, group:default/data_admin +p, user:default/antey, test-resource, read, deny + +### Let's allow 'read' action for 'test-resource' permission for group:default/data_read_admin +p, group:default/data_read_admin, test-resource, read, allow + +# case4: +# g, user:default/julia, group:default/data_read_admin +p, user:default/julia, test-resource, read, allow + +# case5: +# g, user:default/mike, group:default/data_read_admin + +# case6: +# g, user:default/tom, group:default/data_read_admin +p, user:default/tom, test-resource, read, deny + + +# group inheritance: +# g, group:default/data-read-admin, group:default/data_parent_admin +# and we know case5: +# g, user:default/mike, data-read-admin + +p, group:default/data_parent_admin, test.resource.2, use, allow +p, group:default/data_parent_admin, test-resource, create, allow diff --git a/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-csv/rbac-policy.csv b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-csv/rbac-policy.csv new file mode 100644 index 0000000000..afc0ad96da --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-csv/rbac-policy.csv @@ -0,0 +1,14 @@ +g, user:default/guest, role:default/catalog-writer +g, user:default/guest, role:default/legacy +g, user:default/guest, role:default/catalog-reader +g, user:default/guest, role:default/catalog-deleter + +p, role:default/catalog-writer, catalog-entity, update, allow +p, role:default/legacy, catalog-entity, update, allow +p, role:default/catalog-writer, catalog-entity, read, allow +p, role:default/catalog-writer, catalog.entity.create, use, allow +p, role:default/catalog-deleter, catalog-entity, delete, deny + +p, role:default/known_role, test.resource.deny, use, allow + +g, user:default/known_user, role:default/known_role diff --git a/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-csv/simple-policy.csv b/workspaces/rbac/plugins/rbac-backend/__fixtures__/data/valid-csv/simple-policy.csv new file mode 100644 index 0000000000..15c68d545a --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/__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/workspaces/rbac/plugins/rbac-backend/__fixtures__/mock-utils.ts b/workspaces/rbac/plugins/rbac-backend/__fixtures__/mock-utils.ts new file mode 100644 index 0000000000..7ce4add7c2 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/__fixtures__/mock-utils.ts @@ -0,0 +1,140 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; + +import type { Enforcer } from 'casbin'; +import * as Knex from 'knex'; +import { MockClient } from 'knex-mock-client'; +import type TypeORMAdapter from 'typeorm-adapter'; + +import type { RBACProvider } from '@backstage-community/plugin-rbac-node'; + +import { resolve } from 'path'; + +import { CasbinDBAdapterFactory } from '../src/database/casbin-adapter-factory'; +import { ConditionalStorage } from '../src/database/conditional-storage'; +import { RoleMetadataStorage } from '../src/database/role-metadata'; +import { + EnforcerDelegate, + RoleEventEmitter, + RoleEvents, +} from '../src/service/enforcer-delegate'; +import { PluginPermissionMetadataCollector } from '../src/service/plugin-endpoints'; + +// TODO: Move to 'catalogServiceMock' from '@backstage/plugin-catalog-node/testUtils' +// once '@backstage/plugin-catalog-node' is upgraded +export const catalogApiMock = { + 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(), +}; + +export const conditionalStorageMock: ConditionalStorage = { + filterConditions: jest.fn().mockImplementation(() => []), + createCondition: jest.fn().mockImplementation(), + checkConflictedConditions: jest.fn().mockImplementation(), + getCondition: jest.fn().mockImplementation(), + deleteCondition: jest.fn().mockImplementation(), + updateCondition: jest.fn().mockImplementation(), +}; + +export const roleMetadataStorageMock: RoleMetadataStorage = { + filterRoleMetadata: jest.fn().mockImplementation(() => []), + findRoleMetadata: jest.fn().mockImplementation(), + createRoleMetadata: jest.fn().mockImplementation(), + updateRoleMetadata: jest.fn().mockImplementation(), + removeRoleMetadata: jest.fn().mockImplementation(), +}; + +export const auditLoggerMock = { + getActorId: jest.fn().mockImplementation(), + createAuditLogDetails: jest.fn().mockImplementation(), + auditLog: jest.fn().mockImplementation(), +}; + +export const pluginMetadataCollectorMock: Partial = + { + getPluginConditionRules: jest.fn().mockImplementation(), + getPluginPolicies: jest.fn().mockImplementation(), + getMetadataByPluginId: jest.fn().mockImplementation(), + }; + +export const roleEventEmitterMock: RoleEventEmitter = { + on: jest.fn().mockImplementation(), +}; + +export const enforcerMock: Partial = { + loadPolicy: jest.fn().mockImplementation(async () => {}), + enableAutoSave: jest.fn().mockImplementation(() => {}), + setRoleManager: jest.fn().mockImplementation(() => {}), + enableAutoBuildRoleLinks: jest.fn().mockImplementation(() => {}), + buildRoleLinks: jest.fn().mockImplementation(() => {}), +}; + +export const enforcerDelegateMock: Partial = { + hasPolicy: jest.fn().mockImplementation(), + hasGroupingPolicy: jest.fn().mockImplementation(), + getPolicy: jest.fn().mockImplementation(), + getGroupingPolicy: jest.fn().mockImplementation(), + getFilteredPolicy: jest.fn().mockImplementation(), + getFilteredGroupingPolicy: jest.fn().mockImplementation(), + addPolicy: jest.fn().mockImplementation(), + addPolicies: jest.fn().mockImplementation(), + addGroupingPolicies: jest.fn().mockImplementation(), + removePolicy: jest.fn().mockImplementation(), + removePolicies: jest.fn().mockImplementation(), + removeGroupingPolicy: jest.fn().mockImplementation(), + removeGroupingPolicies: jest.fn().mockImplementation(), + updatePolicies: jest.fn().mockImplementation(), + updateGroupingPolicies: jest.fn().mockImplementation(), +}; + +export const dataBaseAdapterFactoryMock: Partial = { + createAdapter: jest.fn((): Promise => { + return Promise.resolve({} as TypeORMAdapter); + }), +}; + +export const providerMock: RBACProvider = { + getProviderName: jest.fn().mockImplementation(), + connect: jest.fn().mockImplementation(), + refresh: jest.fn().mockImplementation(), +}; + +export const mockClientKnex = Knex.knex({ client: MockClient }); + +export const mockHttpAuth = mockServices.httpAuth(); +export const mockAuthService = mockServices.auth(); +export const credentials = mockCredentials.user(); +export const mockLoggerService = mockServices.logger.mock(); +export const mockUserInfoService = mockServices.userInfo(); +export const mockDiscovery = mockServices.discovery.mock(); + +export const csvPermFile = resolve( + __dirname, + './../__fixtures__/data/valid-csv/rbac-policy.csv', +); diff --git a/workspaces/rbac/plugins/rbac-backend/__fixtures__/test-utils.ts b/workspaces/rbac/plugins/rbac-backend/__fixtures__/test-utils.ts new file mode 100644 index 0000000000..6435770055 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/__fixtures__/test-utils.ts @@ -0,0 +1,157 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { LoggerService } from '@backstage/backend-plugin-api'; +import { mockServices } from '@backstage/backend-test-utils'; +import { Config } from '@backstage/config'; + +import { + Adapter, + Enforcer, + Model, + newEnforcer, + newModelFromString, +} from 'casbin'; +import * as Knex from 'knex'; +import { MockClient } from 'knex-mock-client'; + +import { CasbinDBAdapterFactory } from '../src/database/casbin-adapter-factory'; +import { RoleMetadataStorage } from '../src/database/role-metadata'; +import { RBACPermissionPolicy } from '../src/policies/permission-policy'; +import { BackstageRoleManager } from '../src/role-manager/role-manager'; +import { EnforcerDelegate } from '../src/service/enforcer-delegate'; +import { MODEL } from '../src/service/permission-model'; +import { PluginPermissionMetadataCollector } from '../src/service/plugin-endpoints'; +import { + auditLoggerMock, + catalogApiMock, + conditionalStorageMock, + csvPermFile, + mockAuthService, + mockClientKnex, + pluginMetadataCollectorMock, + roleMetadataStorageMock, +} from './mock-utils'; + +export function newConfig( + permFile?: string, + users?: Array<{ name: string }>, + superUsers?: Array<{ name: string }>, +): Config { + const testUsers = [ + { + name: 'user:default/guest', + }, + { + name: 'group:default/guests', + }, + ]; + + return mockServices.rootConfig({ + data: { + permission: { + rbac: { + 'policies-csv-file': permFile || csvPermFile, + policyFileReload: true, + admin: { + users: users || testUsers, + superUsers: superUsers, + }, + }, + }, + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + }, + }); +} + +export async function newAdapter(config: Config): Promise { + return await new CasbinDBAdapterFactory( + config, + mockClientKnex, + ).createAdapter(); +} + +export async function createEnforcer( + theModel: Model, + adapter: Adapter, + logger: LoggerService, + config: Config, +): Promise { + const catalogDBClient = Knex.knex({ client: MockClient }); + const rbacDBClient = Knex.knex({ client: MockClient }); + const enf = await newEnforcer(theModel, adapter); + + const rm = new BackstageRoleManager( + catalogApiMock, + logger, + catalogDBClient, + rbacDBClient, + config, + mockAuthService, + ); + enf.setRoleManager(rm); + enf.enableAutoBuildRoleLinks(false); + await enf.buildRoleLinks(); + + return enf; +} + +export async function newEnforcerDelegate( + adapter: Adapter, + config: Config, + storedPolicies?: string[][], + storedGroupingPolicies?: string[][], +): Promise { + const theModel = newModelFromString(MODEL); + const logger = mockServices.logger.mock(); + + const enf = await createEnforcer(theModel, adapter, logger, config); + + if (storedPolicies) { + await enf.addPolicies(storedPolicies); + } + + if (storedGroupingPolicies) { + await enf.addGroupingPolicies(storedGroupingPolicies); + } + + return new EnforcerDelegate(enf, roleMetadataStorageMock, mockClientKnex); +} + +export async function newPermissionPolicy( + config: Config, + enfDelegate: EnforcerDelegate, + roleMock?: RoleMetadataStorage, +): Promise { + const logger = mockServices.logger.mock(); + const permissionPolicy = await RBACPermissionPolicy.build( + logger, + auditLoggerMock, + config, + conditionalStorageMock, + enfDelegate, + roleMock || roleMetadataStorageMock, + mockClientKnex, + pluginMetadataCollectorMock as PluginPermissionMetadataCollector, + mockAuthService, + ); + auditLoggerMock.auditLog.mockReset(); + return permissionPolicy; +} diff --git a/workspaces/rbac/plugins/rbac-backend/catalog-info.yaml b/workspaces/rbac/plugins/rbac-backend/catalog-info.yaml new file mode 100644 index 0000000000..1b58a835cd --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/catalog-info.yaml @@ -0,0 +1,28 @@ +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-community-rbac-backend + title: '@backstage-community/backstage-plugin-rbac-backend' + description: RBAC backend plugin for Backstage + annotations: + backstage.io/source-location: url:https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac-backend + backstage.io/view-url: https://github.com/backstage/community-plugins/blob/main/workspaces/rbac/plugins/rbac-backend/catalog-info.yaml + backstage.io/edit-url: https://github.com/backstage/community-plugins/edit/main/workspaces/rbac/plugins/rbac-backend/catalog-info.yaml + github.com/project-slug: backstage-community/backstage-plugins + github.com/team-slug: backstage/maintainers-plugins + sonarqube.org/project-key: backstage-community_plugins + tags: + - security + - rbac + links: + - url: https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac-backend + title: GitHub Source + icon: source + type: source +spec: + type: backstage-backend-plugin + lifecycle: production + owner: backstage-team + system: backstage + subcomponentOf: backstage-community-rbac diff --git a/workspaces/rbac/plugins/rbac-backend/config.d.ts b/workspaces/rbac/plugins/rbac-backend/config.d.ts new file mode 100644 index 0000000000..ba58c13038 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/config.d.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface Config { + permission: { + rbac: { + 'policies-csv-file'?: string; + /** + * The path to the yaml file containing the conditional policies + * @visibility frontend + */ + conditionalPoliciesFile?: string; + /** + * Allow for reloading of the CSV and conditional policies files. + * @visibility frontend + */ + policyFileReload?: boolean; + /** + * Optional configuration for admins + * @visibility frontend + */ + admin?: { + /** + * The list of users and / or groups with admin access + * @visibility frontend + */ + users?: Array<{ + /** + * @visibility frontend + */ + name: string; + }>; + /** + * The list of super users that will have allow all access, should be a list of only users + * @visibility frontend + */ + superUsers?: Array<{ + /** + * @visibility frontend + */ + name: string; + }>; + }; + /** + * An optional list of plugin IDs. + * The RBAC plugin will handle access control for plugins included in this list. + */ + pluginsWithPermission?: string[]; + /** + * An optional value that limits the depth when building the hierarchy group graph + * @visibility frontend + */ + maxDepth?: number; + }; + }; +} diff --git a/workspaces/rbac/plugins/rbac-backend/docs/apis.md b/workspaces/rbac/plugins/rbac-backend/docs/apis.md new file mode 100644 index 0000000000..e5c9c0ce3f --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/docs/apis.md @@ -0,0 +1,892 @@ +# APIs + +## Requirements + +To access the APIs for the RBAC Backend plugin, a user will need to have admin access. Refer to the [README](../README.md#configure-policy-admins) on how to set up admin access. + +Each endpoint also requires an Authorization header with the Bearer token that was generated by Backstage. If not using the RBAC Frontend plugin, then to access this token, traverse to your deployed instance and inspect the web page. Here are two places and example network calls that will have the Bearer token + +- From the Homepage, the network call `query?term=` + +- From the Catalog, any network call with `entity-facets` + +## Source + +Each permission policy and role that is created through the RBAC Backend plugin will have a source associated with it. When adding new permission policies and roles, we evaluate the ability to modify them based on the location of the first role that was defined. This means that permissions policies that are associated with a role that was created in the CSV file will also need to be defined in the CSV file. + +This strictness helps to keep the integrity and consistency of the data. A permission policy defined in by the REST API with a role in the CSV file can lead to issues in the event that the role was removed from the CSV file. The permission policy would be hanging without a role and would not show up in the RBAC Frontend. + +We have four options for source locations: CSV file, Configuration file, REST API, and legacy. The CSV file and REST API are fairly straightforward and involve modifying the role and permissions policies from that particular source only. Configuration file involves the `role:default/rbac_admin` role where the role can only be modified from the `app-config.yaml`. You are unable to add permission policies to the `role:default/rbac_admin` role. This is because we consider this as a default role for starting out. It is encouraged to either use the `superUsers` configuration feature or to craft your own admin role with the permission policies that are required of your admins. + +Finally, the legacy source is a source that may appear if your permission policies and roles were defined prior to RBAC Backend plugin `2.1.3`. It is recommended to update these legacy sourced roles and permission polices to a source of REST API or CSV file. This can be done by redefining these permission policies and roles using one of the available options. Remember that future permission policies and members of the role will be based on the first originating role with the new source. Be sure to add the role first through one of the described methods, then proceed to add additional members and permission policies to the roles. + +## Role + +### GET role + +GET + +Lists all roles. + +Returns: + +```json +[ + { + "memberReferences": ["user:default/adam"], + "name": "role:default/rbac_admin", + "metadata": { + "source": "configuration", + "description": null + } + }, + { + "memberReferences": [ + "group:default/backstage-community-authors", + "user:default/matt" + ], + "name": "role:default/test", + "metadata": { + "source": "csv-file", + "description": null + } + } +] +``` + +--- + +GET +ex. + +List the single role and the members associated with that role. + +Request Parameters: + +| Parameter name | Description | Type | +| -------------- | --------------------- | ------ | +| kind | role | String | +| namespace | Namespace of the role | String | +| name | name of the role | String | + +Returns: + +```json +[ + { + "memberReferences": [ + "group:default/backstage-community-authors", + "user:default/matt" + ], + "name": "role:default/test", + "metadata": { + "source": "csv-file", + "description": null + } + } +] +``` + +--- + +### POST role + +POST + +Creates a new role. + +Request Parameters: + +| Parameter name | Description | Type | +| -------------------- | ---------------------------------------------------------------- | ------ | +| memberReferences | users / groups to be added to the role `:/` | Array | +| name | name of the role | String | +| metadata.description | description of the role | String | + +body: + +```json +{ + "memberReferences": ["group:default/test"], + "name": "role:default/test_admin", + "metadata": { + "description": "This is a test admin role" + } +} +``` + +Returns a status code of 201 upon success. + +--- + +### PUT role + +PUT +ex. + +Updates a specified role. + +Request Parameters: + +| Parameter name | Description | Type | +| -------------- | --------------------- | ------ | +| kind | role | String | +| namespace | Namespace of the role | String | +| name | name of the role | String | + +Request Parameters for oldRole and newRole: + +| Parameter name | Description | Type | +| -------------------- | ---------------------------------------------------------------- | ------ | +| memberReferences | users / groups to be added to the role `:/` | Array | +| name | name of the role | String | +| metadata.description | description of the role | String | + +body: + +```json +{ + "oldRole": { + "memberReferences": ["group:default/test"], + "name": "role:default/test_admin", + "metadata": { + "description": "This is a test admin role" + } + }, + "newRole": { + "memberReferences": ["group:default/test", "user:default/test2"], + "name": "role:default/test_admin", + "metadata": { + "description": "This is a test admin role with a group and user" + } + } +} +``` + +Returns a status code of 200 upon success. + +--- + +### DELETE role + +DELETE +ex. + +Deletes a single user / group from a role. + +Request Parameters: + +| Parameter name | Description | Type | +| ---------------- | ---------------------------------------------------------------- | ------ | +| kind | role | String | +| namespace | Namespace of the role | String | +| name | name of the role | String | +| memberReferences | users / groups to be added to the role `:/` | String | +| name | name of the role | String | + +before: + +```json +{ + "memberReferences": ["group:default/test, user:default/test2"], + "name": "role:default/test_admin", + "metadata": { + "description": "This is a test admin role with a group and user" + } +} +``` + +after: + +```json +{ + "memberReferences": ["group:default/test"], + "name": "role:default/test_admin", + "metadata": { + "description": "This is a test admin role with a group and user" + } +} +``` + +Returns a status code of 204 upon success. + +--- + +DELETE +ex. + +Deletes a single role and all users associated with that role. + +Request Parameters: + +| Parameter name | Description | Type | +| -------------- | --------------------- | ------ | +| kind | role | String | +| namespace | Namespace of the role | String | +| name | name of the role | String | + +Returns a status code of 204 upon success. + +--- + +## Permission + +### GET permission + +GET + +Lists all permission polices. + +Returns: + +```json +[ + { + "entityReference": "role:default/test", + "permission": "catalog-entity", + "policy": "read", + "effect": "allow", + "metadata": { + "source": "csv-file" + } + }, + { + "entityReference": "role:default/test", + "permission": "catalog.entity.create", + "policy": "create", + "effect": "allow", + "metadata": { + "source": "csv-file" + } + }, + ... +] +``` + +--- + +GET +ex. + +List permission policies related to the specified entity reference `:/`. + +Request parameters: + +| Parameter name | Description | Type | +| -------------- | ----------------------- | ------ | +| kind | Kind of the entity | String | +| namespace | Namespace of the entity | String | +| name | Username of the entity | String | + +Returns: + +```json +[ + { + "entityReference": "role:default/test", + "permission": "catalog-entity", + "policy": "read", + "effect": "allow", + "metadata": { + "source": "csv-file" + } + }, + { + "entityReference": "role:default/test", + "permission": "catalog.entity.create", + "policy": "create", + "effect": "allow", + "metadata": { + "source": "csv-file" + } + } +] +``` + +--- + +### POST permission + +POST + +Creates one or more permission policies for a specified entity. + +Request parameters: + +| Parameter name | Description | Type | +| --------------- | ----------------------------------------------------------------------------- | ------ | +| entityReference | Entity `:/` | String | +| permission | Permission from a specific plugin, Resource type or name | String | +| policy | Policy action for the permission, `create`, `read`, `update`, `delete`, `use` | String | +| effect | `allow` or `deny` | String | + +body: + +```json +[ + { + "entityReference": "role:default/test", + "permission": "catalog-entity", + "policy": "delete", + "effect": "allow" + } +] +``` + +Returns a status code of 201 upon success. + +--- + +### PUT permission + +PUT +ex. + +Updates one or more permission policies for a specified entity. + +Request parameters: + +| Parameter name | Description | Type | +| -------------- | ----------------------- | ------ | +| kind | Kind of the entity | String | +| namespace | Namespace of the entity | String | +| name | Username of the entity | String | + +Request parameters for oldPolicy and newPolicy objects: + +| Parameter name | Description | Type | +| -------------- | ----------------------------------------------------------------------------- | ------ | +| permission | Permission from a specific plugin, Resource type or name | String | +| policy | Policy action for the permission, `create`, `read`, `update`, `delete`, `use` | String | +| effect | `allow` or `deny` | String | + +body: + +```json +{ + "oldPolicy": [ + { + "permission": "catalog-entity", + "policy": "read", + "effect": "allow" + }, + { + "permission": "catalog.entity.create", + "policy": "create", + "effect": "allow" + } + ], + "newPolicy": [ + { + "permission": "catalog-entity", + "policy": "read", + "effect": "deny" + }, + { + "permission": "policy-entity", + "policy": "create", + "effect": "allow" + } + ] +} +``` + +Returns a status code of 200 upon success. + +--- + +### Delete permission + +DELETE +ex. + +Deletes a permission policy of a specified entity. + +Returns a status code of 204 upon success. + +--- + +DELETE +ex. + +Deletes a group of permission policies of a specified entity. + +Request Parameters: + +| Parameter name | Description | Type | +| -------------- | ----------------------- | ------ | +| kind | Kind of the entity | String | +| namespace | Namespace of the entity | String | +| name | Username of the entity | String | + +body: + +```json +[ + { + "entityReference": "role:default/test", + "permission": "catalog-entity", + "policy": "delete", + "effect": "allow" + }, + { + "entityReference": "role:default/test", + "permission": "catalog-entity", + "policy": "update", + "effect": "allow" + } +] +``` + +Returns a status code of 204 upon success. + +--- + +## Plugin + +### GET plugin permission policies + +GET + +Lists all plugin permission policies from plugins installed in your Backstage instance. + +Returns: + +```json +[ + { + "pluginId": "catalog", + "policies": [ + { + "name": "catalog.entity.read", + "policy": "read", + "resourceType": "catalog-entity" + }, + { + "name": "catalog.entity.create", + "policy": "create" + }, + { + "name": "catalog.entity.delete", + "policy": "delete", + "resourceType": "catalog-entity" + }, + { + "name": "catalog.entity.refresh", + "policy": "update", + "resourceType": "catalog-entity" + }, + { + "name": "catalog.location.read", + "policy": "read" + }, + { + "name": "catalog.location.create", + "policy": "create" + }, + { + "name": "catalog.location.delete", + "policy": "delete" + } + ] + }, + ... +] +``` + +--- + +## Conditions + +Conditional permission policies are fairly complex. For more information on how to structure your conditional policies, consult our documentation on [conditions](./conditions.md). + +### GET conditional rules + +GET + +Provides conditional rule parameter schemas. + +```json +[ + { + "pluginId": "catalog", + "rules": [ + { + "name": "HAS_ANNOTATION", + "description": "Allow entities with the specified annotation", + "resourceType": "catalog-entity", + "paramsSchema": { + "type": "object", + "properties": { + "annotation": { + "type": "string", + "description": "Name of the annotation to match on" + }, + "value": { + "type": "string", + "description": "Value of the annotation to match on" + } + }, + "required": [ + "annotation" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + { + "name": "HAS_LABEL", + "description": "Allow entities with the specified label", + "resourceType": "catalog-entity", + "paramsSchema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Name of the label to match on" + } + }, + "required": [ + "label" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + { + "name": "HAS_METADATA", + "description": "Allow entities with the specified metadata subfield", + "resourceType": "catalog-entity", + "paramsSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Property within the entities metadata to match on" + }, + "value": { + "type": "string", + "description": "Value of the given property to match on" + } + }, + "required": [ + "key" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + { + "name": "HAS_SPEC", + "description": "Allow entities with the specified spec subfield", + "resourceType": "catalog-entity", + "paramsSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Property within the entities spec to match on" + }, + "value": { + "type": "string", + "description": "Value of the given property to match on" + } + }, + "required": [ + "key" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + { + "name": "IS_ENTITY_KIND", + "description": "Allow entities matching a specified kind", + "resourceType": "catalog-entity", + "paramsSchema": { + "type": "object", + "properties": { + "kinds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of kinds to match at least one of" + } + }, + "required": [ + "kinds" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + { + "name": "IS_ENTITY_OWNER", + "description": "Allow entities owned by a specified claim", + "resourceType": "catalog-entity", + "paramsSchema": { + "type": "object", + "properties": { + "claims": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of claims to match at least one on within ownedBy" + } + }, + "required": [ + "claims" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + ] + } + ... +] +``` + +--- + +### POST condition + +POST + +Creates a new condition. + +Request Parameters: condition object in json format described above. + +body: + +```json +{ + "result": "CONDITIONAL", + "roleEntityRef": "role:default/test", + "pluginId": "catalog", + "resourceType": "catalog-entity", + "permissionMapping": ["read"], + "conditions": { + "rule": "IS_ENTITY_OWNER", + "resourceType": "catalog-entity", + "params": { + "claims": ["group:default/team-a"] + } + } +} +``` + +Returns a status code of 201 and json with id upon success: + +```json +{ + "id": 1 +} +``` + +--- + +### PUT condition + +PUT + +Update conditions by id. + +Request Parameters: condition object in json format described above. + +body: + +```json +{ + "result": "CONDITIONAL", + "roleEntityRef": "role:default/test", + "pluginId": "catalog", + "resourceType": "catalog-entity", + "permissionMapping": ["read"], + "conditions": { + "anyOf": [ + { + "rule": "IS_ENTITY_OWNER", + "resourceType": "catalog-entity", + "params": { + "claims": ["group:default/team-a"] + } + }, + { + "rule": "IS_ENTITY_KIND", + "resourceType": "catalog-entity", + "params": { + "kinds": ["Group"] + } + } + ] + } +} +``` + +Returns a status code of 200 upon success. + +--- + +### Get condition by id + +GET + +Returns condition by id: + +```json +{ + "id": 1, + "result": "CONDITIONAL", + "roleEntityRef": "role:default/test", + "pluginId": "catalog", + "resourceType": "catalog-entity", + "permissionMapping": ["read"], + "conditions": { + "anyOf": [ + { + "rule": "IS_ENTITY_OWNER", + "resourceType": "catalog-entity", + "params": { + "claims": ["group:default/team-a"] + } + }, + { + "rule": "IS_ENTITY_KIND", + "resourceType": "catalog-entity", + "params": { + "kinds": ["Group"] + } + } + ] + } +} +``` + +Returns a status code of 200 upon success. + +--- + +### GET conditions + +GET + +Returns lists all conditions: + +```json +[ + { + "id": 1, + "result": "CONDITIONAL", + "roleEntityRef": "role:default/test", + "pluginId": "catalog", + "resourceType": "catalog-entity", + "permissionMapping": ["read"], + "conditions": { + "anyOf": [ + { + "rule": "IS_ENTITY_OWNER", + "resourceType": "catalog-entity", + "params": { + "claims": ["group:default/team-a"] + } + }, + { + "rule": "IS_ENTITY_KIND", + "resourceType": "catalog-entity", + "params": { + "kinds": ["Group"] + } + } + ] + } + } +] +``` + +Returns a status code of 200 upon success. + +--- + +### DELETE condition by id + +DELETE + +Deletes condition by id. + +Returns a status code of 204 upon success. + +## HTTP status codes + +| Code | Descriptions | +| ---- | ----------------------------------------------- | +| 200 | Request was successful | +| 201 | New resource was successfully created | +| 204 | No additional content to send in response | +| 400 | Input Error | +| 401 | Lacks valid authentication | +| 403 | Refusal to authorize | +| 404 | Could not find resource | +| 409 | Conflict with current state and target resource | + +--- + +## Curl Request Examples + +Create role `role:default/test` for `group:default/example`: + +```bash +curl -X POST "http://localhost:7007/api/permission/roles" \ + -d '{ + "memberReferences": [ + "group:default/example" + ], + "name": "role:default/test", + "metadata": { + "description": "This is a test role" + } + }' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + -v +``` + +Create permission policy for `role:default/test`: + +```bash +curl -X POST "http://localhost:7007/api/permission/policies" \ + -d '[{ + "entityReference": "role:default/test", + "permission": "catalog-entity", + "policy": "read", + "effect": "allow" + }]' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + -v +``` + +Create conditional permission policy for `role:default/test`: + +```bash +curl -X POST "http://localhost:7007/api/permission/roles/conditions" \ + -d '{ + "result": "CONDITIONAL", + "roleEntityRef": "role:default/test", + "pluginId": "catalog", + "resourceType": "catalog-entity", + "permissionMapping": ["read"], + "conditions": { + "rule": "IS_ENTITY_OWNER", + "resourceType": "catalog-entity", + "params": { + "claims": ["group:default/backstage-community-authors"] + } + } + }' \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + -v +``` diff --git a/workspaces/rbac/plugins/rbac-backend/docs/audit-log.md b/workspaces/rbac/plugins/rbac-backend/docs/audit-log.md new file mode 100644 index 0000000000..896aaad6b7 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/docs/audit-log.md @@ -0,0 +1,49 @@ +# Audit logging + +The RBAC backend plugin supports audit logging with the help of the @janus-idp/backstage-plugin-audit-log-node library. Audit logging helps to track the latest changes and events from the RBAC plugin: + +- RBAC role changes; +- RBAC permissions changes; +- RBAC conditions changes; +- Changes causing modification of application configuration; +- Changes causing modification of the permission policy file; +- GET requests for RBAC permission information; +- User authorization results to RBAC resources. + +The RBAC backend plugin logging doesn't provide information about the actual state of the permissions. The actual state of RBAC permissions can be found in the RBAC UI. Audit logging provides information about the event name, event message, RBAC permission changes, the actor who made these changes, time, log level, stage, status, some part of the request, response, and so on. You can use this information like a history of the RBAC permission hierarchy. + +Notice: RBAC permissions and conditions are bound to RBAC roles. However, the RBAC backend plugin logs information about permissions and conditions with the help of separated log messages. That's because for now, the RBAC plugin has a separated API for RBAC roles, RBAC permissions, and RBAC conditions. + +## Audit log actor + +The audit log actor can be a real REST API user or the RBAC plugin itself. When the actor is a REST API user, then the RBAC plugin logs the user's IP, browser agent, and hostname. The RBAC plugin can also be the actor of the events. In this case, the actor has a name: "rbac-backend". In this case, the plugin typically applies changes from the configuration or permission policy file. Application configuration and permission policy files usually mount to the application deployment with the help of config maps. Unfortunately, the RBAC plugin cannot track who originally made modifications to these resources. But you can enable Kubernetes API audit log: https://kubernetes.io/docs/tasks/debug/debug-cluster/audit. Then you can match RBAC plugin audit log events to the events from Kubernetes logs by time. + +## Audit log format + +The RBAC plugin prints information to the backend log in JSON format. The format of these messages is defined in the @janus-idp/backstage-plugin-audit-log-node library. Each audit log line contains the key "isAuditLog". + +You can change the log level with the help of the environment variable: LOG_LEVEL. + +Example logged RBAC events: + +a) RBAC role created with corresponding basic permissions and conditional permission: + +```json +backend:start: {"actor":{"actorId":"user:default/andrienkoaleksandr","hostname":"localhost","ip":"::1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"},"eventName":"CreateRole","isAuditLog":true,"level":"info","message":"Created role:default/test","meta":{"author":"user:default/andrienkoaleksandr","createdAt":"Tue, 04 Jun 2024 13:51:45 GMT","description":"some test role","lastModified":"Tue, 04 Jun 2024 13:51:45 GMT","members":["user:default/logarifm","group:default/team-a"],"modifiedBy":"user:default/andrienkoaleksandr","roleEntityRef":"role:default/test","source":"rest"},"plugin":"permission","request":{"body":{"memberReferences":["user:default/logarifm","group:default/team-a"],"metadata":{"description":"some test role"},"name":"role:default/test"},"method":"POST","params":{},"query":{},"url":"/api/permission/roles"},"response":{"status":201},"service":"backstage","stage":"sendResponse","status":"succeeded","timestamp":"2024-06-04 16:51:45"} + +backend:start: {"actor":{"actorId":"user:default/andrienkoaleksandr","hostname":"localhost","ip":"::1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"},"eventName":"CreatePolicy","isAuditLog":true,"level":"info","message":"Created permission policies","meta":{"policies":[["role:default/test","scaffolder-template","read","allow"]],"source":"rest"},"plugin":"permission","request":{"body":[{"effect":"allow","entityReference":"role:default/test","permission":"scaffolder-template","policy":"read"}],"method":"POST","params":{},"query":{},"url":"/api/permission/policies"},"response":{"status":201},"service":"backstage","stage":"sendResponse","status":"succeeded","timestamp":"2024-06-04 16:51:45"} + +backend:start: {"actor":{"actorId":"user:default/andrienkoaleksandr","hostname":"localhost","ip":"::1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"},"eventName":"CreateCondition","isAuditLog":true,"level":"info","message":"Created conditional permission policy","meta":{"condition":{"conditions":{"params":{"claims":["group:default/team-a"]},"resourceType":"catalog-entity","rule":"IS_ENTITY_OWNER"},"permissionMapping":[{"action":"read","name":"catalog.entity.read"},{"action":"delete","name":"catalog.entity.delete"},{"action":"update","name":"catalog.entity.refresh"}],"pluginId":"catalog","resourceType":"catalog-entity","result":"CONDITIONAL","roleEntityRef":"role:default/test"}},"plugin":"permission","request":{"body":{"conditions":{"params":{"claims":["group:default/team-a"]},"resourceType":"catalog-entity","rule":"IS_ENTITY_OWNER"},"permissionMapping":["read","delete","update"],"pluginId":"catalog","resourceType":"catalog-entity","result":"CONDITIONAL","roleEntityRef":"role:default/test"},"method":"POST","params":{},"query":{},"url":"/api/permission/roles/conditions"},"response":{"body":{"id":9},"status":201},"service":"backstage","stage":"sendResponse","status":"succeeded","timestamp":"2024-06-04 16:51:45"} +``` + +b) Check access user to application resource: + +```json +backend:start: {"actor":{"actorId":"user:default/andrienkoaleksandr"},"eventName":"PermissionEvaluationStarted","isAuditLog":true,"level":"info","message":"Policy check for user:default/andrienkoaleksandr","meta":{"action":"create","permissionName":"policy.entity.create","resourceType":"policy-entity","userEntityRef":"user:default/andrienkoaleksandr"},"plugin":"permission","service":"backstage","stage":"evaluatePermissionAccess","status":"succeeded","timestamp":"2024-06-04 16:51:45"} + +backend:start: {"actor":{"actorId":"user:default/andrienkoaleksandr"},"eventName":"PermissionEvaluationCompleted","isAuditLog":true,"level":"info","message":"user:default/andrienkoaleksandr is ALLOW for permission 'policy.entity.create', resource type 'policy-entity' and action 'create'","meta":{"action":"create","decision":{"result":"ALLOW"},"permissionName":"policy.entity.create","resourceType":"policy-entity","userEntityRef":"user:default/andrienkoaleksandr"},"plugin":"permission","service":"backstage","stage":"evaluatePermissionAccess","status":"succeeded","timestamp":"2024-06-04 16:51:45"} +``` + +Most audit log lines contain a metadata object. The RBAC plugin includes information about RBAC roles, permissions, conditions, and authorization results in this metadata. Metadata types can be found in the RBAC plugin file audit-log/audit-logger.ts. + +Notice: You need to properly configure the logger to see nested JSON objects in the audit log lines. diff --git a/workspaces/rbac/plugins/rbac-backend/docs/conditions.md b/workspaces/rbac/plugins/rbac-backend/docs/conditions.md new file mode 100644 index 0000000000..96cb4fcbf4 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/docs/conditions.md @@ -0,0 +1,389 @@ +# Conditional Permission Policies + +The Backstage permission framework provides conditions, and the RBAC backend plugin supports this feature. Conditions work like content filters for Backstage resources (provided by plugins). The RBAC backend API stores conditions assigned to the role in the database. When a user requests access to the frontend resources, the RBAC backend API searches for corresponding conditions and delegates the condition for this resource to the corresponding plugin by its plugin ID. If a user was assigned to multiple roles, and each of these roles contains its own condition, the RBAC backend merges conditions using the anyOf criteria. + +The corresponding plugin analyzes conditional parameters and makes a decision about which part of the content the user should see. Consequently, the user can view not all resource content but only some allowed parts. The RBAC backend plugin supports conditions bounded to the RBAC role. + +A Backstage condition can be a simple condition with a rule and parameters. But also a Backstage condition could consists of a parameter or an array of parameters joined by criteria. The list of supported conditional criteria includes: + +- allOf +- anyOf +- not + +The plugin defines the supported condition parameters. API users can retrieve the conditional object schema from the RBAC API endpoint to determine how to build a condition JSON object and utilize it through the RBAC backend plugin API. + +The structure of the condition JSON object is as follows: + +| Json field | Description | Type | +| ----------------- | --------------------------------------------------------------------- | ------------ | +| result | Always has the value "CONDITIONAL" | String | +| roleEntityRef | String entity reference to the RBAC role ('role:default/dev') | String | +| pluginId | Corresponding plugin ID (e.g., "catalog") | String | +| permissionMapping | Array permission actions (['read', 'update', 'delete']) | String array | +| resourceType | Resource type provided by the plugin (e.g., "catalog-entity") | String | +| conditions | Condition JSON with parameters or array parameters joined by criteria | JSON | + +To get the available conditional rules that can be used to create conditional permission policies, use the GET API request `api/permission/plugins/condition-rules` as seen below. + +GET + +Provides condition parameters schemas. + +```json +[ + { + "pluginId": "catalog", + "rules": [ + { + "name": "HAS_ANNOTATION", + "description": "Allow entities with the specified annotation", + "resourceType": "catalog-entity", + "paramsSchema": { + "type": "object", + "properties": { + "annotation": { + "type": "string", + "description": "Name of the annotation to match on" + }, + "value": { + "type": "string", + "description": "Value of the annotation to match on" + } + }, + "required": [ + "annotation" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + { + "name": "HAS_LABEL", + "description": "Allow entities with the specified label", + "resourceType": "catalog-entity", + "paramsSchema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Name of the label to match on" + } + }, + "required": [ + "label" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + { + "name": "HAS_METADATA", + "description": "Allow entities with the specified metadata subfield", + "resourceType": "catalog-entity", + "paramsSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Property within the entities metadata to match on" + }, + "value": { + "type": "string", + "description": "Value of the given property to match on" + } + }, + "required": [ + "key" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + { + "name": "HAS_SPEC", + "description": "Allow entities with the specified spec subfield", + "resourceType": "catalog-entity", + "paramsSchema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Property within the entities spec to match on" + }, + "value": { + "type": "string", + "description": "Value of the given property to match on" + } + }, + "required": [ + "key" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + { + "name": "IS_ENTITY_KIND", + "description": "Allow entities matching a specified kind", + "resourceType": "catalog-entity", + "paramsSchema": { + "type": "object", + "properties": { + "kinds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of kinds to match at least one of" + } + }, + "required": [ + "kinds" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + }, + { + "name": "IS_ENTITY_OWNER", + "description": "Allow entities owned by a specified claim", + "resourceType": "catalog-entity", + "paramsSchema": { + "type": "object", + "properties": { + "claims": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of claims to match at least one on within ownedBy" + } + }, + "required": [ + "claims" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + } + } + ] + } + ... +] +``` + +From this condition schema, the RBAC backend API user can determine how to build a condition JSON object. + +For example, consider a condition without criteria: displaying catalogs only if the user is a member of the owner group. The Catalog plugin schema "IS_ENTITY_OWNER" can be utilized to achieve this goal. To construct the condition JSON object based on this schema, the following information should be used: + +- rule: the parameter name is "IS_ENTITY_OWNER" in this case +- resourceType: "catalog-entity" +- criteria: in this example, criteria are not used since we need to use only one conditional parameter +- params: from the schema, it is evident that it should be an object named "claims" with a string array. This string array constitutes a list of user or group string entity references. + +Based on the above schema condition is: + +```json +{ + "rule": "IS_ENTITY_OWNER", + "resourceType": "catalog-entity", + "params": { + "claims": ["group:default/team-a"] + } +} +``` + +To utilize this condition to the RBAC REST api you need to wrap it with more info + +```json +{ + "result": "CONDITIONAL", + "roleEntityRef": "role:default/test", + "pluginId": "catalog", + "resourceType": "catalog-entity", + "permissionMapping": ["read"], + "conditions": { + "rule": "IS_ENTITY_OWNER", + "resourceType": "catalog-entity", + "params": { + "claims": ["group:default/team-a"] + } + } +} +``` + +**Example condition with criteria**: display catalogs only if user is a member of owner group "OR" display list of all catalog user groups. + +We can reuse previous condition parameter to display catalogs only for owner. Also we can use one more condition "IS_ENTITY_KIND" to display catalog groups for any user: + +- rule - the parameter name is "IS_ENTITY_KIND" in this case. +- resource type: "catalog-entity". +- criteria - "anyOf". +- params - from the schema, it is evident that it should be an object named "kinds" with string array. This string array is a list of catalog kinds. It should be array with single element "Group" in our case. + +Based on the above schema: + +```json +{ + "anyOf": [ + { + "rule": "IS_ENTITY_OWNER", + "resourceType": "catalog-entity", + "params": { + "claims": ["group:default/team-a"] + } + }, + { + "rule": "IS_ENTITY_KIND", + "resourceType": "catalog-entity", + "params": { + "kinds": ["Group"] + } + } + ] +} +``` + +To utilize this condition to the RBAC REST api you need to wrap it with more info: + +```json +{ + "result": "CONDITIONAL", + "roleEntityRef": "role:default/test", + "pluginId": "catalog", + "resourceType": "catalog-entity", + "permissionMapping": ["read"], + "conditions": { + "anyOf": [ + { + "rule": "IS_ENTITY_OWNER", + "resourceType": "catalog-entity", + "params": { + "claims": ["group:default/team-a"] + } + }, + { + "rule": "IS_ENTITY_KIND", + "resourceType": "catalog-entity", + "params": { + "kinds": ["Group"] + } + } + ] + } +} +``` + +## Conditional Policy Aliases + +The RBAC-backend plugin allows for the use of aliases in the conditional policy rule parameters. These aliases are dynamically replaced with corresponding values during the policy evaluation process. Each alias is prefixed with a `$` sign to denote its special function. + +### Supported Aliases + +1. **`$currentUser`**: + + - **Description**: This alias is replaced with the user entity reference for the user currently requesting access to the resource. + - **Example**: If the user "Tom" from the "default" namespace is requesting access, `$currentUser` will be replaced with `user:default/tom`. + +2. **`$ownerRefs`**: + - **Description**: This alias is replaced with ownership references, typically in the form of an array. The array usually contains the user entity reference and the user's parent group entity reference. + - **Example**: For a user "Tom" who belongs to "team-a", `$ownerRefs` will be replaced with `['user:default/tom', 'group:default/team-a']`. + +### Example of a Conditional Policy Object with Alias + +This condition should allow members of the `role:default/developer` to delete only their own catalogs and no others: + +```json +{ + "result": "CONDITIONAL", + "roleEntityRef": "role:default/developer", + "pluginId": "catalog", + "resourceType": "catalog-entity", + "permissionMapping": ["delete"], + "conditions": { + "rule": "IS_ENTITY_OWNER", + "resourceType": "catalog-entity", + "params": { + "claims": ["$currentUser"] + } + } +} +``` + +## Examples of Conditional Policies + +Below are a few examples that can be used on some of the Janus IDP plugins. These can help in determining how based to define conditional policies + +### Keycloak plugin + +```json +{ + "result": "CONDITIONAL", + "roleEntityRef": "role:default/developer", + "pluginId": "catalog", + "resourceType": "catalog-entity", + "permissionMapping": ["update", "delete"], + "conditions": { + "not": { + "rule": "HAS_ANNOTATION", + "resourceType": "catalog-entity", + "params": { "annotation": "keycloak.org/realm", "value": "" } + } + } +} +``` + +This example will prevent users in the role `role:default/developer` from updating or deleting users that ingested into the catalog from the Keycloak plugin. + +Notice the use of the annotation `keycloak.org/realm` requires the value of `` + +### Quay Actions + +```json +{ + "result": "CONDITIONAL", + "roleEntityRef": "role:default/developer", + "pluginId": "scaffolder", + "resourceType": "scaffolder-action", + "permissionMapping": ["use"], + "conditions": { + "not": { + "rule": "HAS_ACTION_ID", + "resourceType": "scaffolder-action", + "params": { "actionId": "quay:create-repository" } + } + } +} +``` + +This example will prevent users from using the Quay scaffolder action if they are a part of the role `role:default/developer`. + +Notice, we use the `permissionMapping` field with `use`. This is because the `scaffolder-action` resource type permission does not have a permission policy. More information can be found in our documentation on [permissions](./permissions.md). + +**NOTE**: We do not support the ability to run conditions in parallel during creation. An example can be found below, notice that `anyOf` and `not` are on the same level. Consider making separate condition requests, or nest your conditions based on the available criteria. + +```json +{ + "anyOf": [ + { + "rule": "IS_ENTITY_OWNER", + "resourceType": "catalog-entity", + "params": { + "claims": ["group:default/team-a"] + } + }, + { + "rule": "IS_ENTITY_KIND", + "resourceType": "catalog-entity", + "params": { + "kinds": ["Group"] + } + } + ], + "not": { + "rule": "IS_ENTITY_KIND", + "resourceType": "catalog-entity", + "params": { "kinds": ["Api"] } + } +} +``` diff --git a/workspaces/rbac/plugins/rbac-backend/docs/group-hierarchy.md b/workspaces/rbac/plugins/rbac-backend/docs/group-hierarchy.md new file mode 100644 index 0000000000..13f4a3bb28 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/docs/group-hierarchy.md @@ -0,0 +1,236 @@ +# Group Hierarchy + +RBAC access control is configured by defining roles and their associated permission policies, which +are then assigned to users or groups. Leveraging group hierarchy can greatly simplify RBAC management, +making it more scalable and flexible. + +## Group-Based Role Assignment + +Role can be assigned to a specific group. If a user is a member of that group, or a member of any of +its child groups, the role (and its associated permissions) will automatically be applied to that user. + +Examples: + +- Sam will inherit `role:default/test` from `team-group` via `subteam-group`. + + ![Group hierarchy diagram with sam as a member of subteam-group that is child of team-group](./images/group-hierarchy-1.svg) + + ```yaml + # catalog-entity.yaml + apiVersion: backstage.io/v1alpha1 + kind: Group + metadata: + name: team-group + spec: + type: team + children: [subteam-group] + --- + apiVersion: backstage.io/v1alpha1 + kind: Group + metadata: + name: subteam-group + spec: + type: team + children: [] + parent: team-group + --- + apiVersion: backstage.io/v1alpha1 + kind: User + metadata: + name: sam + spec: + memberOf: + - subteam-group + ``` + + ```CSV + g, group:default/team-group, role:default/test + p, role:default/test, catalog-entity, read, allow + ``` + +- Sam will have `role:default/test` via `team-group`. + + ![Group hierarchy diagram with sam as a member of team-group](./images/group-hierarchy-2.svg) + + ```yaml + # catalog-entity.yaml + apiVersion: backstage.io/v1alpha1 + kind: User + metadata: + name: sam + --- + apiVersion: backstage.io/v1alpha1 + kind: Group + metadata: + name: team-group + spec: + type: team + children: [] + members: + - sam + ``` + + ```CSV + g, group:default/team-group, role:default/test + p, role:default/test, catalog-entity, read, allow + ``` + +- Sam will inherit `role:default/role-a` from `group-a` and `role:default/role-c` from `group-c`. + + ![Group hierarchy diagram with sam as a member of group-b and group-c, group-a is parent of group-b](./images/group-hierarchy-3.svg) + + ```yaml + # catalog-entity.yaml + apiVersion: backstage.io/v1alpha1 + kind: Group + metadata: + name: group-a + spec: + type: team + children: [group-b] + --- + apiVersion: backstage.io/v1alpha1 + kind: Group + metadata: + name: group-b + spec: + type: team + children: [] + --- + apiVersion: backstage.io/v1alpha1 + kind: Group + metadata: + name: group-c + spec: + type: team + children: [] + --- + apiVersion: backstage.io/v1alpha1 + kind: User + metadata: + name: sam + spec: + memberOf: + - group-b + - group-c + ``` + + ```CSV + g, group:default/group-a, role:default/role-a + g, group:default/group-c, role:default/role-c + p, role:default/role-a, catalog-entity, read, allow + p, role:default/role-c, catalog-entity, delete, allow + ``` + +## Managing Group Hierarchy Depth + +While group hierarchy provides powerful inheritance features, it can have performance implications. +Organizations with potentially complex group hierarchy can specify `maxDepth` configuration value, +that will ensure that the RBAC plugin will stop at a certain depth when building user graphs. + +```YAML +permission: + enabled: true + rbac: + maxDepth: 1 +``` + +The `maxDepth` must be greater than or equal to 0 to ensure that the graphs are built correctly. Also the graph +will be built with a hierarchy of 1 + maxDepth. + +A value of 0 for maxDepth disables the group inheritance feature. + +## Non-Existent Groups in the Hierarchy + +For group hierarchy to function, groups don't need to be present in the catalog as long as the group +has an existing parent group or is a member of existing group or an existing user is a member of +that group. +(Note that this does not work with in-memory database.) + +Examples: + +- Sam will inherit `role:default/test`, although `team-group` isn't explicitly defined. + + ![Group hierarchy diagram with sam as a member of team-group](./images/group-hierarchy-2.svg) + + ```yaml + # catalog-entity.yaml + apiVersion: backstage.io/v1alpha1 + kind: User + metadata: + name: sam + spec: + memberOf: + - team-group + ``` + + ```CSV + g, group:default/team-group, role:default/test + p, role:default/test, catalog-entity, read, allow + ``` + +- Sam will inherit `role:default/test` via `subteam-group` that is a child of `team-group`, although `subteam-group` isn't explicitly defined. + + ![Group hierarchy diagram with sam as a member of subteam-group that is child of team-group](./images/group-hierarchy-1.svg) + + ```yaml + # catalog-entity.yaml + apiVersion: backstage.io/v1alpha1 + kind: User + metadata: + name: sam + spec: + memberOf: + - subteam-group + --- + apiVersion: backstage.io/v1alpha1 + kind: Group + metadata: + name: team-group + spec: + type: team + children: [subteam-group] + ``` + + ```CSV + g, group:default/team-group, role:default/test + p, role:default/test, catalog-entity, read, allow + ``` + +- Sam will inherit `role:default/test` via `group-d` <- `group-c` <- `group-b` <- `group-a`, + although `group-d` and `group-b` aren't explicitly defined. + + ![Group hierarchy diagram with sam as a member of group-a with parent group-b with parent group-c with parent group-d](./images/group-hierarchy-4.svg) + + ```yaml + # catalog-entity.yaml + apiVersion: backstage.io/v1alpha1 + kind: User + metadata: + name: sam + spec: + memberOf: + - group-d + --- + apiVersion: backstage.io/v1alpha1 + kind: Group + metadata: + name: group-c + spec: + type: team + children: [group-d] + parent: group-b + --- + apiVersion: backstage.io/v1alpha1 + kind: Group + metadata: + name: group-a + spec: + type: team + children: [group-b] + ``` + + ```CSV + g, group:default/group-a, role:default/test + p, role:default/test, catalog-entity, read, allow + ``` diff --git a/workspaces/rbac/plugins/rbac-backend/docs/images/group-hierarchy-1.svg b/workspaces/rbac/plugins/rbac-backend/docs/images/group-hierarchy-1.svg new file mode 100644 index 0000000000..573bb26c68 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/docs/images/group-hierarchy-1.svg @@ -0,0 +1 @@ +

role:default/test

group:default/team-group

group:default/subteam-group

user:default/sam

\ No newline at end of file diff --git a/workspaces/rbac/plugins/rbac-backend/docs/images/group-hierarchy-2.svg b/workspaces/rbac/plugins/rbac-backend/docs/images/group-hierarchy-2.svg new file mode 100644 index 0000000000..86156c7fcd --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/docs/images/group-hierarchy-2.svg @@ -0,0 +1 @@ +

role:default/test

group:default/team-group

user:default/sam

\ No newline at end of file diff --git a/workspaces/rbac/plugins/rbac-backend/docs/images/group-hierarchy-3.svg b/workspaces/rbac/plugins/rbac-backend/docs/images/group-hierarchy-3.svg new file mode 100644 index 0000000000..62f819f0d0 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/docs/images/group-hierarchy-3.svg @@ -0,0 +1 @@ +

role:default/role-a

role:default/role-c

group:default/group-a

group:default/group-b

user:default/sam

group:default/group-c

\ No newline at end of file diff --git a/workspaces/rbac/plugins/rbac-backend/docs/images/group-hierarchy-4.svg b/workspaces/rbac/plugins/rbac-backend/docs/images/group-hierarchy-4.svg new file mode 100644 index 0000000000..3fd7d85ee8 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/docs/images/group-hierarchy-4.svg @@ -0,0 +1 @@ +

role:default/test

group:default/group-a

group:default/group-b

group:default/group-c

group:default/group-d

user:default/sam

diff --git a/workspaces/rbac/plugins/rbac-backend/docs/permissions.md b/workspaces/rbac/plugins/rbac-backend/docs/permissions.md new file mode 100644 index 0000000000..6b5b51d367 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/docs/permissions.md @@ -0,0 +1,149 @@ +# Example permissions within Showcase / RHDH + +Note: The requirements section primarily pertains to the frontend and may not be strictly necessary for the backend. + +When defining a permission for the RBAC Backend plugin to consume, follow these guidelines: + +- Permission policies defined using the name of the permission will have higher priority over permission policies that are defined using the resource type. + + - Example: + + ```CSV + p, role:default/myrole, catalog-entity, read, allow + p, role:default/myrole, catalog.entity.read, read, deny + g, user:default/myuser, role:default/myrole + ``` + + Where 'myuser' will have a deny for reading catalog entities, because the permission name takes priority over the permission resource type. + +- If the permission does not have a policy associated with it, use the keyword `use` in its place. + - Example: `p, role:default/test, kubernetes.proxy, use, allow` + +## Resource Type vs Basic Named Permissions + +There are two types of permissions within Backstage that can be defined using the RBAC Backend plugin. These are resource permissions and basic named permissions. The difference between the two is whether or not a permission has a resource type. Resource type permissions can be defined either using their associated resource type or their name. Basic named permissions must use their name. + +Basic name permissions are simple permissions that handle most use cases for plugins. These permissions on require a name and an attribute during creation. While the name and attribute for the basic named permission are required, the actions under the attributes are optional. These actions are what we consider policies within the RBAC Backend plugin. + +- Example of the `catalog.location.read` permission and how it would be defined using the RBAC Backend plugin: + + ```ts + export const catalogLocationReadPermission = createPermission({ + name: 'catalog.location.read', + attributes: { + action: 'read', + }, + }); + ``` + + ```CSV + p, role:default/myrole, catalog.location.read, read, allow + g, user:default/myuser, role:default/myrole + ``` + +Resource type permissions on the other hand are basic named permissions with a resource type. These permissions are typically associated with conditional permission rules based on that particular resource type. We can define these permissions using either their name or resource type. + +- Example of the `catalog.entity.read` permission and two ways that we can define its permissions using the RBAC Backend plugin: + + ```ts + export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; + + export const catalogEntityReadPermission = createPermission({ + name: 'catalog.entity.read', + attributes: { + action: 'read', + }, + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, + }); + ``` + + ```CSV + p, role:default/myrole, catalog.entity.read, read, allow + g, user:default/myuser, role:default/myrole + + p, role:default/another-role, catalog-entity, read, allow + g, user:default/another-user, role:default/another-role + ``` + +## Catalog + +| Name | Resource Type | Policy | Description | Requirements | +| ----------------------- | -------------- | ------ | ------------------------------------------------------- | ----------------------- | +| catalog.entity.read | catalog-entity | read | Allows the user to read from the catalog | X | +| catalog.entity.create | | create | Allows the user to create catalog entities | catalog.location.create | +| catalog.entity.refresh | catalog-entity | update | Allows the user to refresh one or more catalog entities | catalog.entity.read | +| catalog.entity.delete | catalog-entity | delete | Allows the user to delete one or more catalog entities | catalog.entity.read | +| catalog.location.read | | read | Allows the user to read one or more catalog locations | catalog.entity.read | +| catalog.location.create | | create | Allows the user to create one or more catalog locations | catalog.entity.create | +| catalog.location.delete | | delete | Allows the user to delete one or more catalog locations | catalog.entity.delete | + +## Jenkins + +| Name | Resource Type | Policy | Description | Requirements | +| --------------- | -------------- | ------ | ---------------------------------------------------------- | ------------------- | +| jenkins.execute | catalog-entity | update | Allows the user to execute an action in the Jenkins plugin | catalog.entity.read | + +## Kubernetes + +| Name | Resource Type | Policy | Description | Requirements | +| ---------------- | ------------- | ------ | ----------------------------------------------------------------------------------------------------------- | ------------------- | +| kubernetes.proxy | | | Allows the user to access the proxy endpoint (ability to read pod logs and events within Showcase and RHDH) | catalog.entity.read | + +## RBAC + +| Name | Resource Type | Policy | Description | Requirements | +| -------------------- | ------------- | ------ | ----------------------------------------------------- | ------------ | +| policy.entity.read | policy-entity | read | Allows the user to read permission policies / roles | X | +| policy.entity.create | policy-entity | create | Allows the user to create permission policies / roles | X | +| policy.entity.update | policy-entity | update | Allow the user to update permission policies / roles | X | +| policy.entity.delete | policy-entity | delete | Allow the user to delete permission policies / roles | X | + +## Scaffolder + +| Name | Resource Type | Policy | Description | Requirements | +| ---------------------------------- | ------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| scaffolder.action.execute | scaffolder-action | | Allows the execution of an action from a template | scaffolder.template.parameter.read, scaffolder.template.step.read | +| scaffolder.template.parameter.read | scaffolder-template | read | Allows the user to read parameters of a template | scaffolder.template.step.read | +| scaffolder.template.step.read | scaffolder-template | read | Allows the user to read steps of a template | scaffolder.template.paramater.read | +| scaffolder.task.create | | create | This permission is used to authorize actions that involve the creation of tasks in the scaffolder | scaffolder.template.parameter.read, scaffolder.template.step.read | +| scaffolder.task.read | | read | This permission is used to authorize actions that involve reading one or more tasks in the scaffolder and reading logs of tasks | scaffolder.template.parameter.read, scaffolder.template.step.read | +| scaffolder.task.cancel | | use | This permission is used to authorize actions that involve the cancellation of tasks in the scaffolder | scaffolder.template.parameter.read, scaffolder.template.step.read | + +## OCM + +| Name | Resource Type | Policy | Description | Requirements | +| ---------------- | ------------- | ------ | ----------------------------------------------------------------- | ------------ | +| ocm.entity.read | | read | Allows the user to read from the ocm plugin | X | +| ocm.cluster.read | | read | Allows the user to read the cluster information in the ocm plugin | X | + +## Tekton + +| Name | Resource Type | Policy | Description | Requirements | +| ---------------- | ------------- | ------ | ------------------------------------------------------------------------------------------------------------------ | ------------------- | +| tekton.view.read | | read | Allows the user to view the tekton plugin | catalog.entity.read | +| kubernetes.proxy | | | Allows the user to access the proxy endpoint (ability to read tekton pod logs and events within Showcase and RHDH) | catalog.entity.read | + +## Topology + +| Name | Resource Type | Policy | Description | Requirements | +| ------------------ | ------------- | ------ | ----------------------------------------------------------------------------------------------------------- | ------------------- | +| topology.view.read | | read | Allows the user to view the topology plugin | X | +| kubernetes.proxy | | | Allows the user to access the proxy endpoint (ability to read pod logs and events within Showcase and RHDH) | catalog.entity.read | + +## Argocd + +| Name | Resource Type | Policy | Description | Requirements | +| ---------------- | ------------- | ------ | ----------------------------------------- | ------------------- | +| argocd.view.read | | read | Allows the user to view the argocd plugin | catalog.entity.read | + +## Quay + +| Name | Resource Type | Policy | Description | Requirements | +| -------------- | ------------- | ------ | --------------------------------------- | ------------------- | +| quay.view.read | | read | Allows the user to view the quay plugin | catalog.entity.read | + +## Bulk Import + +| Name | Resource Type | Policy | Description | Requirements | +| ----------- | ------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ | +| bulk.import | bulk-import | | Allows the user to access the bulk import endpoints (listing all repositories and organizations accessible by all GitHub integrations, as well as managing the import requests, ...) | X | diff --git a/workspaces/rbac/plugins/rbac-backend/docs/providers.md b/workspaces/rbac/plugins/rbac-backend/docs/providers.md new file mode 100644 index 0000000000..09e8de1ea1 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/docs/providers.md @@ -0,0 +1,327 @@ +# RBAC Providers + +The RBAC plugins also has the ability to apply roles and permissions from third party access management tools through the use of the RBAC extension points. These extension points allow you to create a backend plugin module that connects your third part access management tool to the RBAC backend plugin. In this documentation, we will discuss how to create a simple RBAC backend module that will be used to apply roles and permissions. + +## Getting started + +Our first step is to create an RBAC backend module using the following command: + +```bash +yarn new +``` + +This will start an interactive setup to create a new plugin. The following are what will need to be selected to create the new plugin module: + +```bash +? What do you want to create? backend-module - A new backend module +? Enter the ID of the plugin [required] permission +? Enter the ID of the module [required] test +? Enter an owner to add to CODEOWNERS [optional] +``` + +This will then create a simple backend plugin module that is ready to updated based on your needs. + +## Creating the Test Provider + +Add the dependencies `@backstage-community/plugin-rbac-node` and `@backstage/config` to your newly created backend module using `yarn --cwd plugins/rbac-backend-module-test add @backstage-community/plugin-rbac-node @backstage/config`. + +Add the test provider to the newly created plugin module `/plugins/rbac-backend-module-test/TestProvider.ts` and populate it with the following: + +```ts +import { LoggerService } from '@backstage/backend-plugin-api'; + +import { + RBACProvider, + RBACProviderConnection, +} from '@backstage-community/plugin-rbac-node'; + +export class TestProvider implements RBACProvider { + private readonly logger: LoggerService; + private connection?: RBACProviderConnection; + + private constructor(logger: LoggerService) { + this.logger = logger.child({ + target: this.getProviderName(), + }); + } + + // The name of the provider, used to distinguish between multiple providers + getProviderName(): string { + return `testProvider`; + } + + // Used to connect the RBACProvider to the RBAC backend plugin + async connect(connection: RBACProviderConnection): Promise {} + + // Used to manually refresh the RBACProvider using an endpoint available in the RBAC backend plugin + async refresh(): Promise {} +} +``` + +Now, we will include a `run` method that will add a new role and permission to the RBAC backend plugin through the use of the extension points. + +```ts +export class TestProvider implements RBACProvider { + // Addition code above + private async run(): Promise { + if (!this.connection) { + throw new Error('Not initialized'); + } + + const roles: string[][] = [ + ['user:default/tony', 'role:default/test-provider-role'], + ]; + const permissions: string[][] = [ + ['role:default/test-provider-role', 'catalog-entity', 'read', 'allow'], + ]; + + await this.connection.applyRoles(roles); + await this.connection.applyPermissions(permissions); + } +} +``` + +Next, we will provider a scheduler option so that we can ensure our provider will be periodically synced. But first we want to include an option to read this schedule from the `app-config`. + +```ts +import { + LoggerService, + readSchedulerServiceTaskScheduleDefinitionFromConfig, + SchedulerServiceTaskScheduleDefinition, +} from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; + +// Additional imports above + +export class TestProvider implements RBACProvider { + private readonly logger: LoggerService; + private connection?: RBACProviderConnection; + + private constructor( + logger: LoggerService, + schedulerServiceTaskRunner: SchedulerServiceTaskRunner, + ) { + this.logger = logger.child({ + target: this.getProviderName(), + }); + } + + static fromConfig( + config: Config, + options: { + logger: LoggerService; + schedule?: SchedulerServiceTaskRunner; + scheduler?: SchedulerService; + }, + ): TestProvider { + const providerSchedule = readProviderConfig(config); + let schedulerServiceTaskRunner; + + if (options.scheduler && providerSchedule) { + schedulerServiceTaskRunner = + options.scheduler.createScheduledTaskRunner(providerSchedule); + } else if (options.schedule) { + schedulerServiceTaskRunner = options.schedule; + } else { + throw new Error('Neither schedule nor scheduler is provided.'); + } + + return new TestProvider(options.logger, schedulerServiceTaskRunner); + } + // Additional code below +} + +function readProviderConfig( + config: Config, +): SchedulerServiceTaskScheduleDefinition | undefined { + const rbacConfig = config.getOptionalConfig('permission.rbac.providers.test'); + if (!rbacConfig) { + return undefined; + } + + const schedule = rbacConfig.has('schedule') + ? readSchedulerServiceTaskScheduleDefinitionFromConfig( + rbacConfig.getConfig('schedule'), + ) + : undefined; + + return schedule; +} +``` + +We can then began to create our schedule function that will ensure we sync based on the schedule that is provider. + +```ts +// Additional imports above +export class TestProvider implements RBACProvider { + private readonly logger: LoggerService; + private connection?: RBACProviderConnection; + private readonly scheduleFn: () => Promise; + + private constructor( + logger: LoggerService, + schedulerServiceTaskRunner: SchedulerServiceTaskRunner, + ) { + this.logger = logger.child({ + target: this.getProviderName(), + }); + + this.scheduleFn = this.createScheduleFN(schedulerServiceTaskRunner); + } + + // Additional code + + // Creates our schedule function that will periodically call our run method + private createScheduleFN( + schedulerServiceTaskRunner: SchedulerServiceTaskRunner, + ): () => Promise { + return async () => { + const taskId = `${this.getProviderName()}:run`; + return schedulerServiceTaskRunner.run({ + id: taskId, + fn: async () => { + try { + await this.run(); + } catch (error: any) { + this.logger.error(`Error occurred, here is the error ${error}`); + } + }, + }); + }; + } +} +``` + +After setting up the scheduler, we can supply the option to manually refresh the module. + +```ts +// Additional imports above + +export class TestProvider implements RBACProvider { + // Addition code + + // Used to manually refresh the RBACProvider using an endpoint available in the RBAC backend plugin + async refresh(): Promise { + try { + await this.run(); + } catch (error: any) { + this.logger.error(`Error occurred, here is the error ${error}`); + } + } +} +``` + +Finally, we just need to supply the logic for the connection. + +```ts +// Additional imports above + +export class TestProvider implements RBACProvider { + // Addition code + + // Used to connect the RBACProvider to the RBAC backend plugin + async connect(connection: RBACProviderConnection): Promise { + this.connection = connection; + this.scheduleFn(); + } +} +``` + +## Updating the Module + +Our final step is to update the dependencies that will be supplied and add the provider in `module.ts`. + +```ts +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; + +import { rbacProviderExtensionPoint } from '@backstage-community/plugin-rbac-node'; + +import { TestProvider } from './TestProvider'; + +/** + * The test backend module for the rbac plugin. + * + * @alpha + */ +export const rbacModuleTest = createBackendModule({ + pluginId: 'permission', + moduleId: 'test', + register(reg) { + reg.registerInit({ + deps: { + logger: coreServices.logger, + rbac: rbacProviderExtensionPoint, + scheduler: coreServices.scheduler, + config: coreServices.rootConfig, + }, + async init({ logger, rbac, scheduler, config }) { + rbac.addRBACProvider( + TestProvider.fromConfig(config, { + logger, + scheduler: scheduler, + schedule: scheduler.createScheduledTaskRunner({ + frequency: { minutes: 30 }, + timeout: { minutes: 3 }, + }), + }), + ); + }, + }); + }, +}); +``` + +## Testing your newly created backend module + +Install the provider and add it to `packages/backend/src/index.ts`. + +```bash +yarn --cwd packages/app add @backstage-community/plugin-rbac-backend-module-test +``` + +```ts +backend.add( + import('@backstage-community/plugin-rbac-backend-module-test/alpha'), +); +``` + +Configure the test provider in the `app-config`. + +```yaml +permission: + rbac: + providers: + test: + schedule: + frequency: { minutes: 1 } + timeout: { minutes: 1 } + initialDelay: { seconds: 1 } +``` + +This will set the provider schedule to apply the roles and permissions every minute. + +Finally, to test the manual refresh capability update the config to adjust the frequency of the schedule. + +```yaml +permission: + rbac: + providers: + test: + schedule: + frequency: { minutes: 10 } + timeout: { minutes: 1 } + initialDelay: { seconds: 1 } +``` + +10 Minutes should give you enough time to manually trigger refresh. + +Call the refresh endpoint. + +```bash +curl -X POST "http://localhost:7007/api/permission/refresh/testProvider" -H "Authorization: Bearer $token" -v +``` + +Should return a 200. diff --git a/workspaces/rbac/plugins/rbac-backend/knexfile.js b/workspaces/rbac/plugins/rbac-backend/knexfile.js new file mode 100644 index 0000000000..c0245f5723 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/knexfile.js @@ -0,0 +1,28 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// To create new migration file use: "yarn knex migrate:make migrations", +// open generated new migration file and edit it to complete code. +// To run new migration use: "yarn knex migrate:make some_file_name" + +module.exports = { + client: 'better-sqlite3', + connection: ':memory:', + useNullAsDefault: true, + migrations: { + directory: './migrations', + }, +}; diff --git a/workspaces/rbac/plugins/rbac-backend/migrations/20231015161232_migrations.js b/workspaces/rbac/plugins/rbac-backend/migrations/20231015161232_migrations.js new file mode 100644 index 0000000000..e084a54824 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/migrations/20231015161232_migrations.js @@ -0,0 +1,41 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.up = async function up(knex) { + await knex.schema.createTable('policy-conditions', table => { + table.increments('id').primary(); + table.string('result'); + table.string('pluginId'); + table.string('resourceType'); + // Conditions is potentially long json. + // In the future maybe we can use `json` or `jsonb` type instead of `text`: + // table.json('conditions') or table.jsonb('conditions'). + // But let's start with text type. + // Data type "text" can be unlimited by size for Postgres. + // Also postgres has a lot of build in features for this data type. + table.text('conditionsJson'); + }); +}; + +/** + * down - reverts(undo) migration. + * + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + await knex.schema.dropTable('policy-conditions'); +}; diff --git a/workspaces/rbac/plugins/rbac-backend/migrations/20231212224526_migrations.js b/workspaces/rbac/plugins/rbac-backend/migrations/20231212224526_migrations.js new file mode 100644 index 0000000000..18daaa19d3 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/migrations/20231212224526_migrations.js @@ -0,0 +1,84 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.up = async function up(knex) { + const casbinDoesExist = await knex.schema.hasTable('casbin_rule'); + const policyMetadataDoesExist = await knex.schema.hasTable('policy-metadata'); + let policies = []; + let groupPolicies = []; + + if (casbinDoesExist) { + policies = await knex + .select('*') + .from('casbin_rule') + .where('ptype', 'p') + .then(listPolicies => { + const allPolicies = []; + for (const policy of listPolicies) { + const { v0, v1, v2, v3 } = policy; + allPolicies.push(`[${v0}, ${v1}, ${v2}, ${v3}]`); + } + return allPolicies; + }); + groupPolicies = await knex + .select('*') + .from('casbin_rule') + .where('ptype', 'g') + .then(listGroupPolicies => { + const allGroupPolicies = []; + for (const groupPolicy of listGroupPolicies) { + const { v0, v1 } = groupPolicy; + allGroupPolicies.push(`[${v0}, ${v1}]`); + } + return allGroupPolicies; + }); + } + + if (!policyMetadataDoesExist) { + await knex.schema + .createTable('policy-metadata', table => { + table.increments('id').primary(); + table.string('policy').primary(); + table.string('source'); + }) + .then(async () => { + const metadata = []; + for (const policy of policies) { + metadata.push({ source: 'legacy', policy: policy }); + } + if (metadata.length > 0) { + await knex.table('policy-metadata').insert(metadata); + } + }) + .then(async () => { + const metadata = []; + for (const groupPolicy of groupPolicies) { + metadata.push({ source: 'legacy', policy: groupPolicy }); + } + if (metadata.length > 0) { + await knex.table('policy-metadata').insert(metadata); + } + }); + } +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + await knex.schema.dropTable('policy-metadata'); +}; diff --git a/workspaces/rbac/plugins/rbac-backend/migrations/20231221113214_migrations.js b/workspaces/rbac/plugins/rbac-backend/migrations/20231221113214_migrations.js new file mode 100644 index 0000000000..6fbbc621da --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/migrations/20231221113214_migrations.js @@ -0,0 +1,60 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.up = async function up(knex) { + const casbinDoesExist = await knex.schema.hasTable('casbin_rule'); + const roleMetadataDoesExist = await knex.schema.hasTable('role-metadata'); + const groupPolicies = new Set(); + + if (casbinDoesExist) { + await knex + .select('*') + .from('casbin_rule') + .where('ptype', 'g') + .then(listGroupPolicies => { + for (const groupPolicy of listGroupPolicies) { + const { v1 } = groupPolicy; + groupPolicies.add(v1); + } + }); + } + + if (!roleMetadataDoesExist) { + await knex.schema + .createTable('role-metadata', table => { + table.increments('id').primary(); + table.string('roleEntityRef').primary(); + table.string('source'); + }) + .then(async () => { + const metadata = []; + for (const groupPolicy of groupPolicies) { + metadata.push({ source: 'legacy', roleEntityRef: groupPolicy }); + } + if (metadata.length > 0) { + await knex.table('role-metadata').insert(metadata); + } + }); + } +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + await knex.schema.dropTable('role-metadata'); +}; diff --git a/workspaces/rbac/plugins/rbac-backend/migrations/20240201144429_migrations.js b/workspaces/rbac/plugins/rbac-backend/migrations/20240201144429_migrations.js new file mode 100644 index 0000000000..143a4a6364 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/migrations/20240201144429_migrations.js @@ -0,0 +1,37 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.up = async function up(knex) { + const isRoleMetaDataExist = await knex.schema.hasTable('role-metadata'); + if (isRoleMetaDataExist) { + await knex.schema.alterTable('role-metadata', table => { + table.string('description'); + }); + } +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + const isRoleMetaDataExist = await knex.schema.hasTable('role-metadata'); + if (isRoleMetaDataExist) { + await knex.schema.alterTable('role-metadata', table => { + table.dropColumn('description'); + }); + } +}; diff --git a/workspaces/rbac/plugins/rbac-backend/migrations/20240215154456_migrations.js b/workspaces/rbac/plugins/rbac-backend/migrations/20240215154456_migrations.js new file mode 100644 index 0000000000..9df4f5e3d2 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/migrations/20240215154456_migrations.js @@ -0,0 +1,143 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.up = async function up(knex) { + const casbinDoesExist = await knex.schema.hasTable('casbin_rule'); + const policyMetadataExist = await knex.schema.hasTable('policy-metadata'); + const roleMetadataExist = await knex.schema.hasTable('role-metadata'); + + if (casbinDoesExist && policyMetadataExist) { + const policyMetadataColumns = await knex('policy-metadata').select( + 'id', + 'policy', + ); + + const policiesToCheck = policyMetadataColumns.map(metadataColumn => { + const policy = metadataColumn.policy + .replace(/\[/g, '') + .replace(/\]/g, '') + .split(',') + .map(str => str.trim()); + return { policy, id: metadataColumn.id }; + }); + + const existingPolicies = await knex('casbin_rule') + .whereIn( + 'v0', + policiesToCheck.map(policyToCheck => policyToCheck.policy[0]), + ) + .whereIn( + 'v1', + policiesToCheck.map(policyToCheck => policyToCheck.policy[1]), + ) + .andWhere(query => { + query + .where(innerQuery => { + innerQuery.whereNotNull('v2').whereIn( + 'v2', + policiesToCheck + .filter(policy => policy.policy.length === 4) + .map(policy => policy.policy[2]), + ); + }) + .orWhereNull('v2'); + }) + .andWhere(query => { + query + .where(innerQuery => { + innerQuery.whereNotNull('v3').whereIn( + 'v3', + policiesToCheck + .filter(policy => policy.policy.length === 4) + .map(policy => policy.policy[3]), + ); + }) + .orWhereNull('v3'); + }) + .select('v0', 'v1', 'v2', 'v3'); + + const existingPoliciesSet = new Set( + existingPolicies.map(policy => + policy.v2 + ? `${policy.v0},${policy.v1},${policy.v2},${policy.v3}` + : `${policy.v0},${policy.v1}`, + ), + ); + + const policiesToDelete = policiesToCheck.filter( + policyToCheck => !existingPoliciesSet.has(policyToCheck.policy.join(',')), + ); + + if (policiesToDelete.length > 0) { + await knex('policy-metadata') + .whereIn( + 'id', + policiesToDelete.map(policyToDel => policyToDel.id), + ) + .del(); + console.log( + `Deleted inconsistent policy metadata ${JSON.stringify( + policiesToDelete, + )} from 'policy-metadata' table.`, + ); + } + } + + if (casbinDoesExist && roleMetadataExist) { + const roleMetadataColumns = await knex('role-metadata').select( + 'id', + 'roleEntityRef', + ); + const roleMetadata = roleMetadataColumns.map(rm => { + return { roleEntityRef: rm.roleEntityRef, id: rm.id }; + }); + const existingPoliciesForRoles = await knex('casbin_rule') + .orWhereIn( + 'v1', + roleMetadata.map(rm => rm.roleEntityRef), + ) + .select('v1'); + + const existingRoles = new Set( + existingPoliciesForRoles.map(policy => policy.v1), + ); + const rolesMetadataToDelete = roleMetadata.filter( + rm => !existingRoles.has(rm.roleEntityRef), + ); + + if (rolesMetadataToDelete.length > 0) { + await knex('role-metadata') + .whereIn( + 'id', + rolesMetadataToDelete.map(rm => rm.id), + ) + .del(); + console.log( + `Deleted inconsistent role metadata ${JSON.stringify( + rolesMetadataToDelete, + )} from 'role-metadata' table.`, + ); + } + } +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function down(_knex) { + // do nothing +}; diff --git a/workspaces/rbac/plugins/rbac-backend/migrations/20240308134410_migrations.js b/workspaces/rbac/plugins/rbac-backend/migrations/20240308134410_migrations.js new file mode 100644 index 0000000000..f5b5ca08ab --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/migrations/20240308134410_migrations.js @@ -0,0 +1,31 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.up = async function up(knex) { + const policyConditionsExist = await knex.schema.hasTable('policy-conditions'); + + if (policyConditionsExist) { + // We drop policy condition table, because we decided to rework this feature + // and bound policy condition to the role + await knex.schema.dropTable('policy-conditions'); + } +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(_knex) {}; diff --git a/workspaces/rbac/plugins/rbac-backend/migrations/20240308134941_migrations.js b/workspaces/rbac/plugins/rbac-backend/migrations/20240308134941_migrations.js new file mode 100644 index 0000000000..0516e48691 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/migrations/20240308134941_migrations.js @@ -0,0 +1,43 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.up = async function up(knex) { + await knex.schema.createTable('role-condition-policies', table => { + table.increments('id').primary(); + table.string('roleEntityRef'); + table.string('result'); + table.string('pluginId'); + table.string('resourceType'); + table.string('permissions'); + // Conditions is potentially long json. + // In the future maybe we can use `json` or `jsonb` type instead of `text`: + // table.json('conditions') or table.jsonb('conditions'). + // But let's start with text type. + // Data type "text" can be unlimited by size for Postgres. + // Also postgres has a lot of build in features for this data type. + table.text('conditionsJson'); + }); +}; + +/** + * down - reverts(undo) migration. + * + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + await knex.schema.dropTable('policy-conditions'); +}; diff --git a/workspaces/rbac/plugins/rbac-backend/migrations/20240404111242_migrations.js b/workspaces/rbac/plugins/rbac-backend/migrations/20240404111242_migrations.js new file mode 100644 index 0000000000..5fda96eccd --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/migrations/20240404111242_migrations.js @@ -0,0 +1,53 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.up = async function up(knex) { + const isRoleMetaDataExist = await knex.schema.hasTable('role-metadata'); + if (isRoleMetaDataExist) { + await knex.schema.alterTable('role-metadata', table => { + table.string('author'); + table.string('modifiedBy'); + table.dateTime('createdAt'); + table.dateTime('lastModified'); + }); + + await knex('role-metadata') + .update({ + description: + 'The default permission policy for the admin role allows for the creation, deletion, updating, and reading of roles and permission policies.', + author: 'application configuration', + modifiedBy: 'application configuration', + lastModified: new Date().toUTCString(), + }) + .where('roleEntityRef', 'role:default/rbac_admin'); + } +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + const isRoleMetaDataExist = await knex.schema.hasTable('role-metadata'); + if (isRoleMetaDataExist) { + await knex.schema.alterTable('role-metadata', table => { + table.dropColumn('author'); + table.dropColumn('modifiedBy'); + table.dropColumn('createdAt'); + table.dropColumn('lastModified'); + }); + } +}; diff --git a/workspaces/rbac/plugins/rbac-backend/migrations/20240611092136_migrations.js b/workspaces/rbac/plugins/rbac-backend/migrations/20240611092136_migrations.js new file mode 100644 index 0000000000..65ad48666c --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/migrations/20240611092136_migrations.js @@ -0,0 +1,29 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.up = async function up(knex) { + const policyMetadataExist = await knex.schema.hasTable('policy-metadata'); + + if (policyMetadataExist) { + await knex.schema.dropTable('policy-metadata'); + } +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function down(_knex) {}; diff --git a/workspaces/rbac/plugins/rbac-backend/openapi.yaml b/workspaces/rbac/plugins/rbac-backend/openapi.yaml new file mode 100644 index 0000000000..acb232c81d --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/openapi.yaml @@ -0,0 +1,682 @@ +openapi: 3.0.0 +info: + title: RBAC Backend API + description: >- + Harnesses the power of the Backstage permission framework to empower you + with robust role-based access control capabilities within your Backstage + environment. + version: latest +servers: + - url: 'http://localhost:7007' +components: + schemas: + RoleResponse: + type: array + items: + type: object + properties: + memberReferences: + type: array + description: Users / groups to be added to the role :/. + items: + type: string + name: + type: string + description: The name of the role. + metadata: + type: object + description: Metadata about the role. + properties: + author: + type: string + description: The author of the role. + createdAt: + type: string + description: The date and time the role was created. + lastModified: + type: string + description: The date and time the role was last modified. + modifiedBy: + type: string + description: The user who last modified the role. + source: + type: string + description: The source from which the role was defined. + description: + type: string + description: A description of the role.``` + Role: + type: object + properties: + memberReferences: + type: array + description: Users / groups to be added to the role :/. + items: + type: string + name: + type: string + description: The name of the role. + metadata: + type: object + description: Metadata about the role. + properties: + description: + type: string + description: A description of the role. + Condition: + type: object + oneOf: + - properties: + anyOf: + type: array + items: + $ref: '#/components/schemas/Condition' + required: [anyOf] + - properties: + allOf: + type: array + items: + $ref: '#/components/schemas/Condition' + required: [allOf] + - properties: + not: + $ref: '#/components/schemas/Condition' + required: [not] + - properties: + rule: + type: string + resourceType: + type: string + params: + type: object + required: [rule, resourceType, params] + PropertyObject: + type: object + properties: + type: + type: string + description: + type: string + required: [type, description] + PropertyArray: + type: object + properties: + type: + type: string + description: + type: string + items: + type: object + properties: + type: + type: string + required: [type, description, items] + + PermissionPolicy: + type: object + properties: + entityReference: + type: string + description: Entity :/. + permission: + type: string + description: Permission from a specific plugin, Resource type or name + policy: + type: string + description: 'Policy action for the permission: create, read, update, delete, use' + effect: + type: string + description: allow or deny + + PermissionResponse: + type: object + properties: + entityReference: + type: string + description: Entity :/. + permission: + type: string + description: Permission from a specific plugin, Resource type or name + policy: + type: string + description: 'Policy action for the permission: create, read, update, delete, use' + effect: + type: string + description: allow or deny + metadata: + type: object + description: Metadata about the role. + properties: + source: + type: string + description: The source from which the permission policy was defined. + parameters: + nameParam: + name: name + in: path + description: Name of the role. + required: true + schema: + type: string + namespaceParam: + name: namespace + in: path + description: Namespace of the role. + required: true + schema: + type: string + kindParam: + name: kind + in: path + description: role + required: true + schema: + type: string + memberReferencesParam: + name: memberReferences + in: query + description: users / groups to be deleted from the role :/ + required: false + schema: + type: array + description: Users / groups to be added to the role :/. + items: + type: string +paths: + /api/permission/roles: + get: + description: Lists all roles + responses: + '200': + description: Request was successful. + content: + application/json: + schema: + type: object + '$ref': '#/components/schemas/RoleResponse' + '403': + description: Refusal to authorize + post: + description: Creates a new role. + requestBody: + required: true + content: + application/json: + schema: + '$ref': '#/components/schemas/Role' + responses: + '201': + description: New resource was successfully created. + '400': + description: Invalid role definition. + '403': + description: Refusal to authorize + '409': + description: Conflict with current state and target resource. + /api/permission/roles/{kind}/{namespace}/{name}: + get: + description: List the single role and the members associated with that role. + parameters: + - $ref: '#/components/parameters/nameParam' + - $ref: '#/components/parameters/namespaceParam' + - $ref: '#/components/parameters/kindParam' + responses: + '200': + description: Request was successful. + content: + application/json: + schema: + '$ref': '#/components/schemas/RoleResponse' + '403': + description: Refusal to authorize + '404': + description: Could not find resource + put: + description: Updates a specified role. + parameters: + - $ref: '#/components/parameters/nameParam' + - $ref: '#/components/parameters/namespaceParam' + - $ref: '#/components/parameters/kindParam' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + oldRole: + '$ref': '#/components/schemas/Role' + newRole: + '$ref': '#/components/schemas/Role' + responses: + '200': + description: Request was successful. + '400': + description: Input Error + '403': + description: Refusal to authorize + '404': + description: Could not find resource + '409': + description: Conflict with current state and target resource. + delete: + description: >- + Deletes a single role and all users associated with that role if no + memberReferences is specified. Otherwise deletes the single user/group + specified in the memberReferences parameter. + parameters: + - $ref: '#/components/parameters/nameParam' + - $ref: '#/components/parameters/namespaceParam' + - $ref: '#/components/parameters/kindParam' + - $ref: '#/components/parameters/memberReferencesParam' + responses: + '204': + description: ok + '403': + description: Refusal to authorize + '404': + description: Could not find resource. + /api/permission/policies: + get: + description: Lists all permission polices. + parameters: + - name: entityReference + in: query + description: Entity :/. + required: false + schema: + type: string + - name: permission + in: query + description: Permission from a specific plugin, Resource type or name + required: false + schema: + type: string + - name: policy + in: query + description: 'Policy action for the permission: create, read, update, delete, use' + required: false + schema: + type: string + - name: effect + in: query + description: allow or deny + required: false + schema: + type: string + responses: + '200': + description: Request was successful. + content: + application/json: + schema: + type: array + items: + '$ref': '#/components/schemas/PermissionResponse' + '403': + description: Refusal to authorize + post: + description: Creates one or more permission policies for a specified entity. + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + '$ref': '#/components/schemas/PermissionPolicy' + responses: + '201': + description: New resource was successfully created. + '400': + description: Input Error + '403': + description: Refusal to authorize + /api/permission/policies/{kind}/{namespace}/{name}: + get: + description: List permission policies related to the specified entity reference + parameters: + - $ref: '#/components/parameters/nameParam' + - $ref: '#/components/parameters/namespaceParam' + - $ref: '#/components/parameters/kindParam' + responses: + '200': + description: Request was successful. + content: + application/json: + schema: + type: array + items: + '$ref': '#/components/schemas/PermissionResponse' + '403': + description: Refusal to authorize + '404': + description: Could not find resource + put: + description: Updates one or more permission policies for a specified entity. + parameters: + - $ref: '#/components/parameters/nameParam' + - $ref: '#/components/parameters/namespaceParam' + - $ref: '#/components/parameters/kindParam' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + oldPolicy: + type: array + items: + type: object + properties: + permission: + type: string + description: >- + Permission from a specific plugin, Resource type or + name + policy: + type: string + description: >- + Policy action for the permission: create, read, + update, delete, use + effect: + type: string + description: allow or deny + newPolicy: + type: array + items: + type: object + properties: + permission: + type: string + description: >- + Permission from a specific plugin, Resource type or + name + policy: + type: string + description: >- + Policy action for the permission: create, read, + update, delete, use + effect: + type: string + description: allow or deny + responses: + '200': + description: Request was successful. + '400': + description: Input Error + '403': + description: Refusal to authorize + delete: + description: >- + Deletes a permission policy or a group of permission policies of a + specified entity. + parameters: + - $ref: '#/components/parameters/nameParam' + - $ref: '#/components/parameters/namespaceParam' + - $ref: '#/components/parameters/kindParam' + requestBody: + required: false + content: + application/json: + schema: + type: array + items: + '$ref': '#/components/schemas/PermissionPolicy' + responses: + '204': + description: ok + '400': + description: Input Error + '403': + description: Refusal to authorize + /api/permission/plugins/policies: + get: + description: >- + Lists all plugin permission policies from plugins installed in your + Backstage instance. + responses: + '200': + description: Request was successful + content: + application/json: + schema: + type: array + items: + type: object + properties: + pluginId: + type: string + policies: + type: array + items: + type: object + properties: + name: + type: string + description: Permission from a specific plugin. + resourceType: + type: string + description: Resource type. + policy: + type: string + description: >- + Policy action for the permission: create, read, + update, delete, use. + required: [name, policy] + '403': + description: Refusal to authorize + /api/permission/plugins/condition-rules: + get: + description: Provides conditional rule parameter schemas. + responses: + '200': + description: Request was successful + content: + application/json: + schema: + type: array + items: + type: object + properties: + pluginId: + type: string + rules: + type: array + items: + type: object + properties: + name: + type: string + description: + type: string + resourceType: + type: string + paramsSchema: + type: object + properties: + $schema: + type: string + additionalProperties: + type: boolean + required: + type: string + type: + type: string + oneOf: + - properties: + properties: + type: object + additionalProperties: + $ref: '#/components/schemas/PropertyArray' + - properties: + properties: + type: object + additionalProperties: + $ref: '#/components/schemas/PropertyObject' + '403': + description: Refusal to authorize + /api/permission/roles/conditions: + get: + description: Lists all conditions + responses: + '200': + description: Request was successful + content: + application/json: + schema: + type: array + items: + type: object + required: + [ + result, + roleEntityRef, + pluginId, + resourceType, + permissionMapping, + conditions, + ] + properties: + id: + type: integer + result: + type: string + roleEntityRef: + type: string + pluginId: + type: string + resourceType: + type: string + permissionMapping: + type: array + items: + type: string + conditions: + $ref: '#/components/schemas/Condition' + '403': + description: Refusal to authorize + post: + description: Creates a new condition. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + result: + type: string + roleEntityRef: + type: string + pluginId: + type: string + resourceType: + type: string + permissionMapping: + type: array + items: + type: string + conditions: + $ref: '#/components/schemas/Condition' + responses: + '201': + description: New resource was successfully created. + content: + application/json: + schema: + type: object + properties: + id: + type: integer + '403': + description: Refusal to authorize + /api/permission/roles/conditions/{id}: + get: + description: Returns condition by id. + responses: + '200': + description: Request was successful + content: + application/json: + schema: + type: object + properties: + id: + type: integer + result: + type: string + roleEntityRef: + type: string + pluginId: + type: string + resourceType: + type: string + permissionMapping: + type: array + items: + type: string + conditions: + $ref: '#/components/schemas/Condition' + '400': + description: Input Error + '403': + description: Refusal to authorize + '404': + description: Could not find resource + put: + description: Update conditions by id. + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + result: + type: string + roleEntityRef: + type: string + pluginId: + type: string + resourceType: + type: string + permissionMapping: + type: array + items: + type: string + conditions: + $ref: '#/components/schemas/Condition' + responses: + '200': + description: Request was successful + '400': + description: Id is not a valid number. + '403': + description: Refusal to authorize + '404': + description: Id is not a valid number. + delete: + description: Deletes condition by id. + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: ok + '400': + description: Id is not a valid number. + '403': + description: Refusal to authorize + '404': + description: Could not find resource. diff --git a/workspaces/rbac/plugins/rbac-backend/package.json b/workspaces/rbac/plugins/rbac-backend/package.json new file mode 100644 index 0000000000..302af95c32 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/package.json @@ -0,0 +1,98 @@ +{ + "name": "@backstage-community/plugin-rbac-backend", + "version": "5.2.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin", + "supported-versions": "1.32.5", + "pluginId": "rbac", + "pluginPackages": [ + "@backstage-community/plugin-rbac", + "@backstage-community/plugin-rbac-backend", + "@backstage-community/plugin-rbac-common", + "@backstage-community/plugin-rbac-node" + ] + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "tsc": "tsc", + "prettier:check": "prettier --ignore-unknown --check .", + "prettier:fix": "prettier --ignore-unknown --write .", + "lint:check": "backstage-cli package lint", + "lint:fix": "backstage-cli package lint --fix", + "test": "backstage-cli package test --passWithNoTests --coverage", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage-community/plugin-rbac-common": "^1.12.0", + "@backstage-community/plugin-rbac-node": "^1.8.0", + "@backstage/backend-defaults": "^0.5.2", + "@backstage/backend-plugin-api": "^1.0.1", + "@backstage/catalog-client": "^1.7.1", + "@backstage/catalog-model": "^1.7.0", + "@backstage/errors": "^1.2.4", + "@backstage/plugin-auth-node": "^0.5.3", + "@backstage/plugin-permission-backend": "^0.5.50", + "@backstage/plugin-permission-common": "^0.8.1", + "@backstage/plugin-permission-node": "^0.8.4", + "@dagrejs/graphlib": "^2.1.13", + "@janus-idp/backstage-plugin-audit-log-node": "^1.7.0", + "casbin": "^5.27.1", + "chokidar": "^3.6.0", + "csv-parse": "^5.5.5", + "express": "^4.18.2", + "js-yaml": "^4.1.0", + "knex": "^3.0.0", + "lodash": "^4.17.21", + "typeorm-adapter": "^1.6.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "1.0.2", + "@backstage/cli": "0.28.2", + "@backstage/config": "1.2.0", + "@backstage/core-plugin-api": "1.10.0", + "@backstage/types": "1.1.1", + "@spotify/prettier-config": "^15.0.0", + "@types/express": "4.17.21", + "@types/lodash": "^4.14.151", + "@types/node": "18.19.34", + "@types/supertest": "2.0.16", + "knex-mock-client": "2.0.1", + "msw": "1.3.3", + "prettier": "3.3.3", + "qs": "6.11.2", + "supertest": "6.3.4" + }, + "files": [ + "dist", + "config.d.ts", + "migrations" + ], + "repository": { + "type": "git", + "url": "https://github.com/backstage/community-plugins", + "directory": "workspaces/rbac/plugins/rbac-backend" + }, + "keywords": [ + "support:production", + "lifecycle:active", + "backstage", + "plugin" + ], + "homepage": "https://red.ht/rhdh", + "bugs": "https://github.com/backstage/community-plugins/issues", + "maintainers": [ + "@PatAKnight" + ], + "author": "Red Hat" +} diff --git a/workspaces/rbac/plugins/rbac-backend/report.api.md b/workspaces/rbac/plugins/rbac-backend/report.api.md new file mode 100644 index 0000000000..2d55aabe2c --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/report.api.md @@ -0,0 +1,93 @@ +## API Report File for "@backstage-community/plugin-rbac-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { AuthService } from '@backstage/backend-plugin-api'; +import { BackendFeature } from '@backstage/backend-plugin-api'; +import type { Config } from '@backstage/config'; +import type { DiscoveryService } from '@backstage/backend-plugin-api'; +import express from 'express'; +import type { HttpAuthService } from '@backstage/backend-plugin-api'; +import type { LifecycleService } from '@backstage/backend-plugin-api'; +import type { LoggerService } from '@backstage/backend-plugin-api'; +import type { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { PermissionPolicy } from '@backstage/plugin-permission-node'; +import { PluginIdProvider } from '@backstage-community/plugin-rbac-node'; +import type { RBACProvider } from '@backstage-community/plugin-rbac-node'; +import type { Router } from 'express'; +import type { UserInfoService } from '@backstage/backend-plugin-api'; + +// Warning: (ae-missing-release-tag) "createRouter" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// Warning: (ae-missing-release-tag) "EnvOptions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type EnvOptions = { + config: Config; + logger: LoggerService; + discovery: DiscoveryService; + permissions: PermissionEvaluator; + auth: AuthService; + httpAuth: HttpAuthService; + userInfo: UserInfoService; + lifecycle: LifecycleService; +}; + +export { PluginIdProvider } + +// Warning: (ae-missing-release-tag) "PolicyBuilder" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class PolicyBuilder { + // (undocumented) + static build(env: EnvOptions, pluginIdProvider?: PluginIdProvider, rbacProviders?: Array): Promise; +} + +// Warning: (ae-missing-release-tag) "rbacPlugin" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +const rbacPlugin: BackendFeature; +export default rbacPlugin; + +// Warning: (ae-missing-release-tag) "RBACRouterOptions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type RBACRouterOptions = { + config: Config; + logger: LoggerService; + discovery: DiscoveryService; + policy: PermissionPolicy; + auth: AuthService; + httpAuth: HttpAuthService; + userInfo: UserInfoService; +}; + +// Warning: (ae-missing-release-tag) "RouterOptions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + config: Config; + // (undocumented) + logger: LoggerService; +} + +// Warnings were encountered during analysis: +// +// src/service/policy-builder.d.ts:7:1 - (ae-undocumented) Missing documentation for "EnvOptions". +// src/service/policy-builder.d.ts:17:1 - (ae-undocumented) Missing documentation for "RBACRouterOptions". +// src/service/policy-builder.d.ts:26:1 - (ae-undocumented) Missing documentation for "PolicyBuilder". +// src/service/policy-builder.d.ts:27:5 - (ae-undocumented) Missing documentation for "build". +// src/service/router.d.ts:4:1 - (ae-undocumented) Missing documentation for "RouterOptions". +// src/service/router.d.ts:5:5 - (ae-undocumented) Missing documentation for "logger". +// src/service/router.d.ts:6:5 - (ae-undocumented) Missing documentation for "config". +// src/service/router.d.ts:8:1 - (ae-undocumented) Missing documentation for "createRouter". + +// (No @packageDocumentation comment for this package) + +``` diff --git a/workspaces/rbac/plugins/rbac-backend/src/admin-permissions/admin-creation.test.ts b/workspaces/rbac/plugins/rbac-backend/src/admin-permissions/admin-creation.test.ts new file mode 100644 index 0000000000..6787cce00f --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/admin-permissions/admin-creation.test.ts @@ -0,0 +1,214 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { mockServices } from '@backstage/backend-test-utils'; +import { Config } from '@backstage/config'; + +import * as Knex from 'knex'; + +import type { RoleMetadata } from '@backstage-community/plugin-rbac-common'; + +import { + auditLoggerMock, + catalogApiMock, + csvPermFile, + mockClientKnex, + roleMetadataStorageMock, +} from '../../__fixtures__/mock-utils'; +import { + newAdapter, + newConfig, + newEnforcerDelegate, + newPermissionPolicy, +} from '../../__fixtures__/test-utils'; +import { RoleMetadataDao } from '../database/role-metadata'; +import { EnforcerDelegate } from '../service/enforcer-delegate'; +import { + ADMIN_ROLE_NAME, + setAdminPermissions, + useAdminsFromConfig, +} from './admin-creation'; + +const modifiedBy = 'user:default/some-admin'; +const adminRole = 'role:default/rbac_admin'; +const groupPolicy = [['user:default/test_admin', 'role:default/rbac_admin']]; +const permissions = [ + ['role:default/rbac_admin', 'policy-entity', 'read', 'allow'], + ['role:default/rbac_admin', 'policy-entity', 'create', 'allow'], + ['role:default/rbac_admin', 'policy-entity', 'delete', 'allow'], + ['role:default/rbac_admin', 'policy-entity', 'update', 'allow'], + ['role:default/rbac_admin', 'catalog-entity', 'read', 'allow'], +]; +const oldGroupPolicy = ['user:default/old_admin', 'role:default/rbac_admin']; + +describe('Admin Creation', () => { + describe('Admin role and permission creation to a user', () => { + let enfDelegate: EnforcerDelegate; + let config: Config; + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async ( + _roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + return { + roleEntityRef: 'role:default/catalog-writer', + source: 'legacy', + modifiedBy, + }; + }, + ); + + const admins = new Array<{ name: string }>(); + admins.push({ name: 'user:default/test_admin' }); + const superUser = new Array<{ name: string }>(); + superUser.push({ name: 'user:default/super_user' }); + + catalogApiMock.getEntities.mockReturnValue({ items: [] }); + + beforeEach(async () => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async ( + _roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + return { + roleEntityRef: 'role:default/catalog-writer', + source: 'legacy', + modifiedBy, + }; + }, + ); + + config = newConfig(csvPermFile, admins, superUser); + const adapter = await newAdapter(config); + + enfDelegate = await newEnforcerDelegate(adapter, config); + + await enfDelegate.addGroupingPolicy(oldGroupPolicy, { + source: 'configuration', + roleEntityRef: ADMIN_ROLE_NAME, + modifiedBy: `user:default/tom`, + }); + + const adminUsers = config.getOptionalConfigArray( + 'permission.rbac.admin.users', + ); + await useAdminsFromConfig( + adminUsers || [], + enfDelegate, + auditLoggerMock, + roleMetadataStorageMock, + mockClientKnex, + ); + await setAdminPermissions(enfDelegate, auditLoggerMock); + }); + + it('should assign an admin to the admin role and permissions', async () => { + const enfRole = await enfDelegate.getFilteredGroupingPolicy(1, adminRole); + const enfPermission = await enfDelegate.getFilteredPolicy(0, adminRole); + expect(enfRole).toEqual(groupPolicy); + expect(enfPermission).toEqual(permissions); + }); + + it(`should not assign an admin to the permissions if permissions are already assigned`, async () => { + await expect(async () => { + await setAdminPermissions(enfDelegate, auditLoggerMock); + }).not.toThrow(); + }); + + it(`should assign an admin to the new permission`, async () => { + const newDefaultPermission = [ + adminRole, + 'something-new', + 'create', + 'allow', + ]; + await enfDelegate.addPolicy(newDefaultPermission); + await setAdminPermissions(enfDelegate, auditLoggerMock); + const enfPermission = await enfDelegate.getFilteredPolicy( + 0, + ...newDefaultPermission, + ); + expect(enfPermission.length).toEqual(1); + }); + + it('should fail to build the admin permissions, problem with creating role metadata', async () => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation(async (): Promise => { + return undefined; + }); + + roleMetadataStorageMock.createRoleMetadata = jest + .fn() + .mockImplementation(async (): Promise => { + throw new Error(`Failed to create`); + }); + + config = mockServices.rootConfig({ + data: { + permission: { + rbac: { + 'policies-csv-file': csvPermFile, + policyFileReload: true, + }, + }, + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + }, + }); + + await expect( + newPermissionPolicy(config, enfDelegate, roleMetadataStorageMock), + ).rejects.toThrow('Failed to create'); + }); + + it('should build and update a legacy admin permission', async () => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementationOnce( + async ( + _roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + return { source: 'legacy' }; + }, + ); + + const enfRole = await enfDelegate.getFilteredGroupingPolicy(1, adminRole); + const enfPermission = await enfDelegate.getFilteredPolicy(0, adminRole); + + expect(enfRole).toEqual(groupPolicy); + expect(enfPermission).toEqual(permissions); + expect(roleMetadataStorageMock.updateRoleMetadata).toHaveBeenCalled(); + }); + + it('should remove users that are no longer in the config file', async () => { + const enfRole = await enfDelegate.getFilteredGroupingPolicy(1, adminRole); + const enfPermission = await enfDelegate.getFilteredPolicy(0, adminRole); + expect(enfRole).toEqual(groupPolicy); + expect(enfRole).not.toContain(oldGroupPolicy); + expect(enfPermission).toEqual(permissions); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/admin-permissions/admin-creation.ts b/workspaces/rbac/plugins/rbac-backend/src/admin-permissions/admin-creation.ts new file mode 100644 index 0000000000..a626ae7e39 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/admin-permissions/admin-creation.ts @@ -0,0 +1,171 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Config } from '@backstage/config'; + +import { AuditLogger } from '@janus-idp/backstage-plugin-audit-log-node'; +import { Knex } from 'knex'; + +import { + HANDLE_RBAC_DATA_STAGE, + PermissionAuditInfo, + PermissionEvents, + RBAC_BACKEND, + RoleAuditInfo, + RoleEvents, +} from '../audit-log/audit-logger'; +import { + RoleMetadataDao, + RoleMetadataStorage, +} from '../database/role-metadata'; +import { removeTheDifference } from '../helper'; +import { EnforcerDelegate } from '../service/enforcer-delegate'; +import { validateEntityReference } from '../validation/policies-validation'; + +export const ADMIN_ROLE_NAME = 'role:default/rbac_admin'; +export const ADMIN_ROLE_AUTHOR = 'application configuration'; +const DEF_ADMIN_ROLE_DESCRIPTION = + 'The default permission policy for the admin role allows for the creation, deletion, updating, and reading of roles and permission policies.'; + +const getAdminRoleMetadata = (): RoleMetadataDao => { + const currentDate: Date = new Date(); + return { + source: 'configuration', + roleEntityRef: ADMIN_ROLE_NAME, + description: DEF_ADMIN_ROLE_DESCRIPTION, + author: ADMIN_ROLE_AUTHOR, + modifiedBy: ADMIN_ROLE_AUTHOR, + lastModified: currentDate.toUTCString(), + createdAt: currentDate.toUTCString(), + }; +}; + +export const useAdminsFromConfig = async ( + admins: Config[], + enf: EnforcerDelegate, + auditLogger: AuditLogger, + roleMetadataStorage: RoleMetadataStorage, + knex: Knex, +) => { + const addedGroupPolicies = new Map(); + const newGroupPolicies = new Map(); + + for (const admin of admins) { + const entityRef = admin.getString('name'); + validateEntityReference(entityRef); + + addedGroupPolicies.set(entityRef, ADMIN_ROLE_NAME); + + if (!(await enf.hasGroupingPolicy(...[entityRef, ADMIN_ROLE_NAME]))) { + newGroupPolicies.set(entityRef, ADMIN_ROLE_NAME); + } + } + + const adminRoleMeta = await roleMetadataStorage.findRoleMetadata( + ADMIN_ROLE_NAME, + ); + + const trx = await knex.transaction(); + let addedRoleMembers; + try { + if (!adminRoleMeta) { + // even if there are no user, we still create default role metadata for admins + await roleMetadataStorage.createRoleMetadata(getAdminRoleMetadata(), trx); + } else if (adminRoleMeta.source === 'legacy') { + await roleMetadataStorage.updateRoleMetadata( + getAdminRoleMetadata(), + ADMIN_ROLE_NAME, + trx, + ); + } + + addedRoleMembers = Array.from(newGroupPolicies.entries()); + await enf.addGroupingPolicies( + addedRoleMembers, + getAdminRoleMetadata(), + trx, + ); + + await trx.commit(); + } catch (error) { + await trx.rollback(error); + throw error; + } + + await auditLogger.auditLog({ + actorId: RBAC_BACKEND, + message: `Created or updated role`, + eventName: RoleEvents.CREATE_OR_UPDATE_ROLE, + metadata: { + ...getAdminRoleMetadata(), + members: addedRoleMembers.map(gp => gp[0]), + }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); + + const configGroupPolicies = await enf.getFilteredGroupingPolicy( + 1, + ADMIN_ROLE_NAME, + ); + + await removeTheDifference( + configGroupPolicies.map(gp => gp[0]), + Array.from(addedGroupPolicies.keys()), + 'configuration', + ADMIN_ROLE_NAME, + enf, + auditLogger, + ADMIN_ROLE_AUTHOR, + ); +}; + +const addAdminPermissions = async ( + policies: string[][], + enf: EnforcerDelegate, + auditLogger: AuditLogger, +) => { + const policiesToAdd: string[][] = []; + for (const policy of policies) { + if (!(await enf.hasPolicy(...policy))) { + policiesToAdd.push(policy); + } + } + await enf.addPolicies(policiesToAdd); + + await auditLogger.auditLog({ + actorId: RBAC_BACKEND, + message: `Created RBAC admin permissions`, + eventName: PermissionEvents.CREATE_POLICY, + metadata: { policies: policies, source: 'configuration' }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); +}; + +export const setAdminPermissions = async ( + enf: EnforcerDelegate, + auditLogger: AuditLogger, +) => { + const adminPermissions = [ + [ADMIN_ROLE_NAME, 'policy-entity', 'read', 'allow'], + [ADMIN_ROLE_NAME, 'policy-entity', 'create', 'allow'], + [ADMIN_ROLE_NAME, 'policy-entity', 'delete', 'allow'], + [ADMIN_ROLE_NAME, 'policy-entity', 'update', 'allow'], + // Needed for the RBAC frontend plugin. + [ADMIN_ROLE_NAME, 'catalog-entity', 'read', 'allow'], + ]; + await addAdminPermissions(adminPermissions, enf, auditLogger); +}; diff --git a/workspaces/rbac/plugins/rbac-backend/src/audit-log/audit-logger.ts b/workspaces/rbac/plugins/rbac-backend/src/audit-log/audit-logger.ts new file mode 100644 index 0000000000..8672d53ffb --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/audit-log/audit-logger.ts @@ -0,0 +1,170 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + AuthorizeResult, + PolicyDecision, + ResourcePermission, +} from '@backstage/plugin-permission-common'; +import type { PolicyQuery } from '@backstage/plugin-permission-node'; + +import type { AuditLogOptions } from '@janus-idp/backstage-plugin-audit-log-node'; + +import { + PermissionAction, + RoleConditionalPolicyDecision, + Source, + toPermissionAction, +} from '@backstage-community/plugin-rbac-common'; + +export const RoleEvents = { + CREATE_ROLE: 'CreateRole', + UPDATE_ROLE: 'UpdateRole', + DELETE_ROLE: 'DeleteRole', + CREATE_OR_UPDATE_ROLE: 'CreateOrUpdateRole', + GET_ROLE: 'GetRole', + + CREATE_ROLE_ERROR: 'CreateRoleError', + UPDATE_ROLE_ERROR: 'UpdateRoleError', + DELETE_ROLE_ERROR: 'DeleteRoleError', + GET_ROLE_ERROR: 'GetRoleError', +} as const; + +export const PermissionEvents = { + CREATE_POLICY: 'CreatePolicy', + UPDATE_POLICY: 'UpdatePolicy', + DELETE_POLICY: 'DeletePolicy', + GET_POLICY: 'GetPolicy', + + CREATE_POLICY_ERROR: 'CreatePolicyError', + UPDATE_POLICY_ERROR: 'UpdatePolicyError', + DELETE_POLICY_ERROR: 'DeletePolicyError', + GET_POLICY_ERROR: 'GetPolicyError', +} as const; + +export type RoleAuditInfo = { + roleEntityRef: string; + description?: string; + source: Source; + + members: string[]; +}; + +export type PermissionAuditInfo = { + policies: string[][]; + source: Source; +}; + +export const EvaluationEvents = { + PERMISSION_EVALUATION_STARTED: 'PermissionEvaluationStarted', + PERMISSION_EVALUATION_COMPLETED: 'PermissionEvaluationCompleted', + CONDITION_EVALUATION_COMPLETED: 'ConditionEvaluationCompleted', + PERMISSION_EVALUATION_FAILED: 'PermissionEvaluationFailed', +} as const; + +export const ListPluginPoliciesEvents = { + GET_PLUGINS_POLICIES: 'GetPluginsPolicies', + GET_PLUGINS_POLICIES_ERROR: 'GetPluginsPoliciesError', +}; + +export const ListConditionEvents = { + GET_CONDITION_RULES: 'GetConditionRules', + GET_CONDITION_RULES_ERROR: 'GetConditionRulesError', +}; + +export type EvaluationAuditInfo = { + userEntityRef: string; + permissionName: string; + action: PermissionAction; + resourceType?: string; + decision?: PolicyDecision; +}; + +export const ConditionEvents = { + CREATE_CONDITION: 'CreateCondition', + UPDATE_CONDITION: 'UpdateCondition', + DELETE_CONDITION: 'DeleteCondition', + GET_CONDITION: 'GetCondition', + + CREATE_CONDITION_ERROR: 'CreateConditionError', + UPDATE_CONDITION_ERROR: 'UpdateConditionError', + DELETE_CONDITION_ERROR: 'DeleteConditionError', + GET_CONDITION_ERROR: 'GetConditionError', + PARSE_CONDITION_ERROR: 'ParseConditionError', + CHANGE_CONDITIONAL_POLICIES_FILE_ERROR: 'ChangeConditionalPoliciesError', + CONDITIONAL_POLICIES_FILE_NOT_FOUND: 'ConditionalPoliciesFileNotFound', +}; + +export type ConditionAuditInfo = { + condition: RoleConditionalPolicyDecision; +}; + +export const RBAC_BACKEND = 'rbac-backend'; + +// Audit log stage for processing Role-Based Access Control (RBAC) data +export const HANDLE_RBAC_DATA_STAGE = 'handleRBACData'; + +// Audit log stage for determining access rights based on user permissions and resource information +export const EVALUATE_PERMISSION_ACCESS_STAGE = 'evaluatePermissionAccess'; + +// Audit log stage for sending the response to the client about handled permission policies, roles, and condition policies +export const SEND_RESPONSE_STAGE = 'sendResponse'; +export const RESPONSE_ERROR = 'responseError'; + +export function createPermissionEvaluationOptions( + message: string, + userEntityRef: string, + request: PolicyQuery, + policyDecision?: PolicyDecision, +): AuditLogOptions { + const auditInfo: EvaluationAuditInfo = { + userEntityRef, + permissionName: request.permission.name, + action: toPermissionAction(request.permission.attributes), + }; + + const resourceType = (request.permission as ResourcePermission).resourceType; + if (resourceType) { + auditInfo.resourceType = resourceType; + } + + let eventName; + if (!policyDecision) { + eventName = EvaluationEvents.PERMISSION_EVALUATION_STARTED; + } else { + auditInfo.decision = policyDecision; + + switch (policyDecision.result) { + case AuthorizeResult.DENY: + case AuthorizeResult.ALLOW: + eventName = EvaluationEvents.PERMISSION_EVALUATION_COMPLETED; + break; + case AuthorizeResult.CONDITIONAL: + eventName = EvaluationEvents.CONDITION_EVALUATION_COMPLETED; + break; + default: + throw new Error('Unknown policy decision result'); + } + } + + return { + actorId: userEntityRef, + message, + eventName, + metadata: auditInfo, + stage: EVALUATE_PERMISSION_ACCESS_STAGE, + status: 'succeeded', + }; +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/audit-log/rest-errors-interceptor.ts b/workspaces/rbac/plugins/rbac-backend/src/audit-log/rest-errors-interceptor.ts new file mode 100644 index 0000000000..84f9f847f1 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/audit-log/rest-errors-interceptor.ts @@ -0,0 +1,135 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { AuditLogger } from '@janus-idp/backstage-plugin-audit-log-node'; +import type { + ErrorRequestHandler, + NextFunction, + Request, + Response, +} from 'express'; + +import { + ConditionEvents, + ListConditionEvents, + ListPluginPoliciesEvents, + PermissionEvents, + RESPONSE_ERROR, + RoleEvents, +} from './audit-logger'; + +// Mapping paths and methods to corresponding events and messages +const eventMap: { + [key: string]: { [key: string]: { event: string; message: string } }; +} = { + '/policies': { + POST: { + event: PermissionEvents.CREATE_POLICY_ERROR, + message: 'Failed to create permission policies', + }, + PUT: { + event: PermissionEvents.UPDATE_POLICY_ERROR, + message: 'Failed to update permission policies', + }, + DELETE: { + event: PermissionEvents.DELETE_POLICY_ERROR, + message: 'Failed to delete permission policies', + }, + GET: { + event: PermissionEvents.GET_POLICY_ERROR, + message: 'Failed to get permission policies', + }, + }, + '/roles': { + POST: { + event: RoleEvents.CREATE_ROLE_ERROR, + message: 'Failed to create role', + }, + PUT: { + event: RoleEvents.UPDATE_ROLE_ERROR, + message: 'Failed to update role', + }, + DELETE: { + event: RoleEvents.DELETE_ROLE_ERROR, + message: 'Failed to delete role', + }, + GET: { event: RoleEvents.GET_ROLE_ERROR, message: 'Failed to get role' }, + }, + '/roles/conditions': { + POST: { + event: ConditionEvents.CREATE_CONDITION_ERROR, + message: 'Failed to create condition', + }, + PUT: { + event: ConditionEvents.UPDATE_CONDITION_ERROR, + message: 'Failed to update condition', + }, + DELETE: { + event: ConditionEvents.DELETE_CONDITION_ERROR, + message: 'Failed to delete condition', + }, + GET: { + event: ConditionEvents.GET_CONDITION_ERROR, + message: 'Failed to get condition', + }, + }, + '/plugins/policies': { + GET: { + event: ListPluginPoliciesEvents.GET_PLUGINS_POLICIES_ERROR, + message: 'Failed to get list permission policies', + }, + }, + '/plugins/condition-rules': { + GET: { + event: ListConditionEvents.GET_CONDITION_RULES_ERROR, + message: 'Failed to get list condition rules and schemas', + }, + }, +}; + +// Audit log REST api errors interceptor. +export function auditError(auditLogger: AuditLogger): ErrorRequestHandler { + return async ( + err: Error, + req: Request, + _resp: Response, + next: NextFunction, + ) => { + const matchedPath = Object.keys(eventMap).find(path => + req.path.startsWith(path), + ); + if (matchedPath) { + const methodEvents = eventMap[matchedPath][req.method]; + if (methodEvents) { + const { event, message } = methodEvents; + try { + await auditLogger.auditLog({ + message, + eventName: event, + stage: RESPONSE_ERROR, + status: 'failed', + request: req, + errors: [err], + }); + next(err); + } catch (auditLogError) { + next(auditLogError); + } + return; + } + } + next(err); + }; +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/conditional-aliases/alias-resolver.test.ts b/workspaces/rbac/plugins/rbac-backend/src/conditional-aliases/alias-resolver.test.ts new file mode 100644 index 0000000000..b47e9f5b24 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/conditional-aliases/alias-resolver.test.ts @@ -0,0 +1,559 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { + PermissionCondition, + PermissionCriteria, + PermissionRuleParams, +} from '@backstage/plugin-permission-common'; + +import { replaceAliases } from './alias-resolver'; + +describe('replaceAliases', () => { + describe('should replace "currentUser" aliases', () => { + it('should replace aliases in the string value', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + rule: 'TEST', + resourceType: 'test-entity', + params: { + test: '$currentUser', + }, + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + rule: 'TEST', + resourceType: 'test-entity', + params: { + test: 'user:default/tim', + }, + }); + }); + }); + + it('should replace aliases in the string array', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$currentUser'], + }, + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim'], + }, + }); + }); + + it('should replace aliases with criteria not', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + not: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$currentUser'], + }, + }, + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + not: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim'], + }, + }, + }); + }); + + it('should replace aliases with criteria anyOf', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$currentUser'], + }, + }, + ], + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim'], + }, + }, + ], + }); + }); + + it('should replace aliases with criteria anyOf and few values', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$currentUser'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }); + }); + + it('should replace aliases with criteria allOf', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$currentUser'], + }, + }, + ], + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim'], + }, + }, + ], + }); + }); + + it('should replace aliases with criteria allOf and few values', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$currentUser'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }); + }); + + it('should replace aliases with nested criteria', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + allOf: [ + { + not: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$currentUser'], + }, + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + allOf: [ + { + not: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim'], + }, + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }); + }); + + describe('should replace "ownerRefs" aliases', () => { + it('should replace aliases without criteria', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$ownerRefs'], + }, + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim', 'group:default/team-a'], + }, + }); + }); + + it('should replace aliases with criteria not', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + not: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$ownerRefs'], + }, + }, + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + not: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim', 'group:default/team-a'], + }, + }, + }); + }); + + it('should replace aliases with criteria anyOf', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$ownerRefs'], + }, + }, + ], + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim', 'group:default/team-a'], + }, + }, + ], + }); + }); + + it('should replace aliases with criteria anyOf and few values', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$ownerRefs'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }); + }); + + it('should replace aliases with criteria allOf', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$ownerRefs'], + }, + }, + ], + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim', 'group:default/team-a'], + }, + }, + ], + }); + }); + + it('should replace aliases with criteria allOf and few values', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$ownerRefs'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }); + }); + + it('should replace aliases with nested criteria', () => { + const conditionParam: PermissionCriteria< + PermissionCondition + > = { + allOf: [ + { + not: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$ownerRefs'], + }, + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }; + + replaceAliases(conditionParam, { + userEntityRef: 'user:default/tim', + ownershipEntityRefs: ['user:default/tim', 'group:default/team-a'], + }); + + expect(conditionParam).toEqual({ + allOf: [ + { + not: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/tim', 'group:default/team-a'], + }, + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/conditional-aliases/alias-resolver.ts b/workspaces/rbac/plugins/rbac-backend/src/conditional-aliases/alias-resolver.ts new file mode 100644 index 0000000000..6246686e5f --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/conditional-aliases/alias-resolver.ts @@ -0,0 +1,127 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { BackstageUserInfo } from '@backstage/backend-plugin-api'; +import type { + PermissionCondition, + PermissionCriteria, + PermissionRuleParam, + PermissionRuleParams, +} from '@backstage/plugin-permission-common'; +import type { JsonPrimitive } from '@backstage/types'; + +import { + CONDITION_ALIAS_SIGN, + ConditionalAliases, +} from '@backstage-community/plugin-rbac-common'; + +interface Predicate { + (item: T): boolean; +} + +function isOwnerRefsAlias(value: PermissionRuleParam): boolean { + const alias = `${CONDITION_ALIAS_SIGN}${ConditionalAliases.OWNER_REFS}`; + return value === alias; +} + +function isCurrentUserAlias(value: PermissionRuleParam): boolean { + const alias = `${CONDITION_ALIAS_SIGN}${ConditionalAliases.CURRENT_USER}`; + return value === alias; +} + +function replaceAliasWithValue< + K extends string, + V extends JsonPrimitive | JsonPrimitive[], +>( + params: Record | undefined, + key: K, + predicate: Predicate, + newValue: V, +): Record | undefined { + if (!params) { + return params; + } + + if (Array.isArray(params[key])) { + const oldValues = params[key] as JsonPrimitive[]; + const nonAliasValues: JsonPrimitive[] = []; + for (const oldValue of oldValues) { + const isAliasMatched = predicate(oldValue); + if (isAliasMatched) { + const newValues = Array.isArray(newValue) ? newValue : [newValue]; + nonAliasValues.push(...newValues); + } else { + nonAliasValues.push(oldValue); + } + } + return { ...params, [key]: nonAliasValues }; + } + + const oldValue = params[key] as JsonPrimitive; + const isAliasMatched = predicate(oldValue); + if (isAliasMatched && !Array.isArray(newValue)) { + return { ...params, [key]: newValue }; + } + + return params; +} + +export function replaceAliases( + conditions: PermissionCriteria< + PermissionCondition + >, + userInfo: BackstageUserInfo, +) { + if ('not' in conditions) { + replaceAliases(conditions.not, userInfo); + return; + } + if ('allOf' in conditions) { + for (const condition of conditions.allOf) { + replaceAliases(condition, userInfo); + } + return; + } + if ('anyOf' in conditions) { + for (const condition of conditions.anyOf) { + replaceAliases(condition, userInfo); + return; + } + } + + const params = ( + conditions as PermissionCondition + ).params; + if (params) { + for (const key of Object.keys(params)) { + let modifiedParams = replaceAliasWithValue( + params, + key, + isCurrentUserAlias, + userInfo.userEntityRef, + ); + + modifiedParams = replaceAliasWithValue( + modifiedParams, + key, + isOwnerRefsAlias, + userInfo.ownershipEntityRefs, + ); + + (conditions as PermissionCondition).params = + modifiedParams; + } + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/database/casbin-adapter-factory.test.ts b/workspaces/rbac/plugins/rbac-backend/src/database/casbin-adapter-factory.test.ts new file mode 100644 index 0000000000..82e3e082c7 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/database/casbin-adapter-factory.test.ts @@ -0,0 +1,302 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { mockServices } from '@backstage/backend-test-utils'; + +import knex, { Knex } from 'knex'; +import TypeORMAdapter from 'typeorm-adapter'; + +import { CasbinDBAdapterFactory } from './casbin-adapter-factory'; + +jest.mock('typeorm-adapter', () => { + return { + newAdapter: jest.fn((): Promise => { + return Promise.resolve({} as TypeORMAdapter); + }), + }; +}); + +describe('CasbinAdapterFactory', () => { + let newAdapterMock: jest.Mock>; + let db: Knex; + + beforeEach(() => { + newAdapterMock = TypeORMAdapter.newAdapter as jest.Mock< + Promise + >; + jest.clearAllMocks(); + }); + it('test building an adapter using a better-sqlite3 configuration.', async () => { + db = knex.knex({ + client: 'better-sqlite3', + connection: ':memory', + }); + const config = mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + }, + }); + const adapterFactory = new CasbinDBAdapterFactory(config, db); + const adapter = adapterFactory.createAdapter(); + expect(adapter).not.toBeNull(); + expect(newAdapterMock).toHaveBeenCalled(); + }); + + describe('build adapter with postgres configuration', () => { + beforeEach(() => { + db = knex.knex({ + client: 'pg', + connection: { + database: 'test-database', + }, + }); + process.env.TEST = 'test'; + }); + + it('test building an adapter using a PostgreSQL configuration.', async () => { + const config = mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + port: '5432', + schema: 'public', + user: 'postgresUser', + password: process.env.TEST, + }, + }, + }, + }, + }); + const factory = new CasbinDBAdapterFactory(config, db); + const adapter = await factory.createAdapter(); + expect(adapter).not.toBeNull(); + expect(newAdapterMock).toHaveBeenCalledWith({ + type: 'postgres', + host: 'localhost', + port: 5432, + schema: 'public', + username: 'postgresUser', + password: process.env.TEST, + database: 'test-database', + ssl: undefined, + }); + }); + + it('test building an adapter using a PostgreSQL configuration with enabled ssl.', async () => { + const config = mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + port: '5432', + schema: 'public', + user: 'postgresUser', + password: process.env.TEST, + ssl: true, + }, + }, + }, + }, + }); + const factory = new CasbinDBAdapterFactory(config, db); + const adapter = await factory.createAdapter(); + expect(adapter).not.toBeNull(); + expect(newAdapterMock).toHaveBeenCalledWith({ + type: 'postgres', + host: 'localhost', + port: 5432, + schema: 'public', + username: 'postgresUser', + password: process.env.TEST, + database: 'test-database', + ssl: true, + }); + }); + + it('test building an adapter using a PostgreSQL configuration with intentionally disabled ssl.', async () => { + const config = mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + port: '5432', + schema: 'public', + user: 'postgresUser', + password: process.env.TEST, + ssl: false, + }, + }, + }, + }, + }); + const factory = new CasbinDBAdapterFactory(config, db); + const adapter = await factory.createAdapter(); + expect(adapter).not.toBeNull(); + expect(newAdapterMock).toHaveBeenCalledWith({ + type: 'postgres', + host: 'localhost', + port: 5432, + schema: 'public', + username: 'postgresUser', + password: process.env.TEST, + database: 'test-database', + ssl: false, + }); + }); + + it('test building an adapter using a PostgreSQL configuration with intentionally ssl and ca cert.', async () => { + const config = mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + port: '5432', + schema: 'public', + user: 'postgresUser', + password: process.env.TEST, + ssl: { + ca: 'abc', + }, + }, + }, + }, + }, + }); + const factory = new CasbinDBAdapterFactory(config, db); + const adapter = await factory.createAdapter(); + expect(adapter).not.toBeNull(); + expect(newAdapterMock).toHaveBeenCalledWith({ + type: 'postgres', + host: 'localhost', + port: 5432, + schema: 'public', + username: 'postgresUser', + password: process.env.TEST, + database: 'test-database', + ssl: { + ca: 'abc', + }, + }); + }); + + it('test building an adapter using a PostgreSQL configuration with intentionally ssl and TLS options.', async () => { + const config = mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + port: '5432', + user: 'postgresUser', + password: process.env.TEST, + ssl: { + ca: 'abc', + rejectUnauthorized: false, + }, + }, + }, + }, + }, + }); + const factory = new CasbinDBAdapterFactory(config, db); + const adapter = await factory.createAdapter(); + expect(adapter).not.toBeNull(); + expect(newAdapterMock).toHaveBeenCalledWith({ + type: 'postgres', + host: 'localhost', + port: 5432, + schema: 'public', + username: 'postgresUser', + password: process.env.TEST, + database: 'test-database', + ssl: { + ca: 'abc', + rejectUnauthorized: false, + }, + }); + }); + + it('test building an adapter using a PostgreSQL configuration with intentionally ssl without CA.', async () => { + const config = mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + port: '5432', + user: 'postgresUser', + password: process.env.TEST, + ssl: { + rejectUnauthorized: false, + }, + }, + }, + }, + }, + }); + const factory = new CasbinDBAdapterFactory(config, db); + const adapter = await factory.createAdapter(); + expect(adapter).not.toBeNull(); + expect(newAdapterMock).toHaveBeenCalledWith({ + type: 'postgres', + host: 'localhost', + port: 5432, + schema: 'public', + username: 'postgresUser', + password: process.env.TEST, + database: 'test-database', + ssl: { + rejectUnauthorized: false, + }, + }); + }); + }); + + it('ensure that building an adapter with an unknown configuration fails.', async () => { + const client = 'unknown-db'; + const expectedError = new Error(`Unsupported database client ${client}`); + const config = mockServices.rootConfig({ + data: { + backend: { + database: { + client, + }, + }, + }, + }); + const adapterFactory = new CasbinDBAdapterFactory(config, db); + + await expect(adapterFactory.createAdapter()).rejects.toStrictEqual( + expectedError, + ); + expect(newAdapterMock).not.toHaveBeenCalled(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/database/casbin-adapter-factory.ts b/workspaces/rbac/plugins/rbac-backend/src/database/casbin-adapter-factory.ts new file mode 100644 index 0000000000..65cf876d2f --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/database/casbin-adapter-factory.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Config } from '@backstage/config'; +import type { ConfigApi } from '@backstage/core-plugin-api'; + +import { Knex } from 'knex'; +import TypeORMAdapter from 'typeorm-adapter'; + +import { resolve } from 'path'; +import type { ConnectionOptions, TlsOptions } from 'tls'; + +import '@backstage/backend-defaults/database'; + +const DEFAULT_SQLITE3_STORAGE_FILE_NAME = 'rbac.sqlite'; + +export class CasbinDBAdapterFactory { + public constructor( + private readonly config: ConfigApi, + private readonly databaseClient: Knex, + ) {} + + public async createAdapter(): Promise { + const databaseConfig = this.config.getOptionalConfig('backend.database'); + const client = databaseConfig?.getOptionalString('client'); + + let adapter; + if (client === 'pg') { + const dbName = await this.databaseClient.client.config.connection + .database; + const schema = + (await this.databaseClient.client.searchPath?.[0]) ?? 'public'; + + const ssl = this.handlePostgresSSL(databaseConfig!); + + adapter = await TypeORMAdapter.newAdapter({ + type: 'postgres', + host: databaseConfig?.getString('connection.host'), + port: databaseConfig?.getNumber('connection.port'), + username: databaseConfig?.getString('connection.user'), + password: databaseConfig?.getString('connection.password'), + ssl, + database: dbName, + schema: schema, + }); + } + + if (client === 'better-sqlite3') { + let storage; + if (typeof databaseConfig?.get('connection')?.valueOf() === 'string') { + storage = databaseConfig?.getString('connection'); + } else if (databaseConfig?.has('connection.directory')) { + const storageDir = databaseConfig?.getString('connection.directory'); + storage = resolve(storageDir, DEFAULT_SQLITE3_STORAGE_FILE_NAME); + } + + adapter = await TypeORMAdapter.newAdapter({ + type: 'better-sqlite3', + // Storage type or path to the storage. + database: storage || ':memory:', + }); + } + + if (!adapter) { + throw new Error(`Unsupported database client ${client}`); + } + + return adapter; + } + + private handlePostgresSSL( + dbConfig: Config, + ): boolean | TlsOptions | undefined { + const connection = dbConfig.getOptional( + 'connection', + ); + if (!connection) { + return undefined; + } + + if (typeof connection === 'string' || connection instanceof String) { + throw new Error( + `rbac backend plugin doesn't support postgres connection in a string format yet`, + ); + } + + const ssl: boolean | ConnectionOptions | undefined = connection.ssl; + + if (ssl === undefined) { + return undefined; + } + + if (typeof ssl === 'boolean') { + return ssl; + } + + if (typeof ssl === 'object') { + const { ca, rejectUnauthorized } = ssl as ConnectionOptions; + const tlsOpts = { ca, rejectUnauthorized }; + + // SSL object was defined with some options that we don't support yet. + if (Object.values(tlsOpts).every(el => el === undefined)) { + return true; + } + + return tlsOpts; + } + + return undefined; + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/database/conditional-storage.test.ts b/workspaces/rbac/plugins/rbac-backend/src/database/conditional-storage.test.ts new file mode 100644 index 0000000000..de3725a82e --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/database/conditional-storage.test.ts @@ -0,0 +1,669 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + mockServices, + TestDatabaseId, + TestDatabases, +} from '@backstage/backend-test-utils'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; + +import * as Knex from 'knex'; +import { createTracker, MockClient } from 'knex-mock-client'; + +import type { + PermissionInfo, + RoleConditionalPolicyDecision, +} from '@backstage-community/plugin-rbac-common'; + +import { + CONDITIONAL_TABLE, + ConditionalPolicyDecisionDAO, + DataBaseConditionalStorage, +} from './conditional-storage'; +import { migrate } from './migration'; + +jest.setTimeout(60000); + +describe('DataBaseConditionalStorage', () => { + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'SQLITE_3'], + }); + + const conditionDao1: ConditionalPolicyDecisionDAO = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + permissions: '[{"action":"read","name":"catalog.entity.read"}]', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + conditionsJson: + `{` + + `"rule":"IS_ENTITY_OWNER",` + + `"resourceType":"catalog-entity",` + + `"params":{"claims":["group:default/test-group"]}` + + `}`, + }; + const conditionDao2: ConditionalPolicyDecisionDAO = { + pluginId: 'test', + resourceType: 'test-entity', + permissions: '[{"action": "delete", "name": "catalog.entity.delete"}]', + roleEntityRef: 'role:default/test-2', + result: AuthorizeResult.CONDITIONAL, + conditionsJson: + `{` + + `"rule": "IS_ENTITY_OWNER",` + + `"resourceType": "test-entity",` + + `"params": {"claims": ["group:default/test-group"]}` + + `}`, + }; + const condition1: RoleConditionalPolicyDecision = { + id: 1, + pluginId: 'catalog', + resourceType: 'catalog-entity', + permissionMapping: [{ action: 'read', name: 'catalog.entity.read' }], + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['group:default/test-group'], + }, + }, + }; + const condition2: RoleConditionalPolicyDecision = { + id: 2, + pluginId: 'test', + resourceType: 'test-entity', + permissionMapping: [{ action: 'delete', name: 'catalog.entity.delete' }], + roleEntityRef: 'role:default/test-2', + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'test-entity', + params: { + claims: ['group:default/test-group'], + }, + }, + }; + + async function createDatabase(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + const mockDatabaseService = mockServices.database.mock({ + getClient: async () => knex, + migrations: { skip: false }, + }); + + await migrate(mockDatabaseService); + return { + knex, + db: new DataBaseConditionalStorage(knex), + }; + } + + describe('filterConditions', () => { + it.each(databases.eachSupportedId())( + 'should return all conditions', + async databaseId => { + const { knex, db } = await createDatabase(databaseId); + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + await knex(CONDITIONAL_TABLE).insert( + conditionDao2, + ); + + const conditions = await db.filterConditions(); + expect(conditions.length).toEqual(2); + + expect(conditions[0]).toEqual(condition1); + expect(conditions[1]).toEqual(condition2); + }, + ); + + it.each(databases.eachSupportedId())( + 'should return condition by roleEntityRef', + async databaseId => { + const { knex, db } = await createDatabase(databaseId); + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + await knex(CONDITIONAL_TABLE).insert( + conditionDao2, + ); + + const conditions = await db.filterConditions(`role:default/test`); + expect(conditions.length).toEqual(1); + + expect(conditions[0]).toEqual(condition1); + }, + ); + + it.each(databases.eachSupportedId())( + 'should return condition by pluginId', + async databaseId => { + const { knex, db } = await createDatabase(databaseId); + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + await knex(CONDITIONAL_TABLE).insert( + conditionDao2, + ); + + const conditions = await db.filterConditions(undefined, 'catalog'); + expect(conditions.length).toEqual(1); + + expect(conditions[0]).toEqual(condition1); + }, + ); + + it.each(databases.eachSupportedId())( + 'should return condition by pluginId', + async databaseId => { + const { knex, db } = await createDatabase(databaseId); + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + await knex(CONDITIONAL_TABLE).insert( + conditionDao2, + ); + + const conditions = await db.filterConditions( + undefined, + undefined, + 'catalog-entity', + ); + expect(conditions.length).toEqual(1); + + expect(conditions[0]).toEqual(condition1); + }, + ); + + it.each(databases.eachSupportedId())( + 'should return condition by action', + async databaseId => { + const { knex, db } = await createDatabase(databaseId); + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + await knex(CONDITIONAL_TABLE).insert( + conditionDao2, + ); + + const conditions = await db.filterConditions( + undefined, + undefined, + undefined, + ['read'], + ); + expect(conditions.length).toEqual(1); + + expect(conditions[0]).toEqual(condition1); + }, + ); + + it.each(databases.eachSupportedId())( + 'should return condition by permission name', + async databaseId => { + const { knex, db } = await createDatabase(databaseId); + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + await knex(CONDITIONAL_TABLE).insert( + conditionDao2, + ); + + const conditions = await db.filterConditions( + undefined, + undefined, + undefined, + undefined, + ['catalog.entity.read'], + ); + expect(conditions.length).toEqual(1); + + expect(conditions[0]).toEqual(condition1); + }, + ); + + it.each(databases.eachSupportedId())( + 'should return condition by all arguments', + async databaseId => { + const { knex, db } = await createDatabase(databaseId); + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + await knex(CONDITIONAL_TABLE).insert( + conditionDao2, + ); + + const conditions = await db.filterConditions( + 'role:default/test', + 'catalog', + 'catalog-entity', + ['read'], + ['catalog.entity.read'], + ); + expect(conditions.length).toEqual(1); + + expect(conditions[0]).toEqual(condition1); + }, + ); + }); + + describe('createCondition', () => { + it.each(databases.eachSupportedId())( + 'should successfully create new conditional policy', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + + const id = await db.createCondition(condition1); + + const condition = await knex( + CONDITIONAL_TABLE, + ).where('id', id); + expect(condition.length).toEqual(1); + expect(condition[0]).toEqual({ + id: 1, + ...conditionDao1, + }); + }, + ); + + it.each(databases.eachSupportedId())( + 'should throw conflict error', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + + await expect(async () => { + await db.createCondition(condition1); + }).rejects.toThrow( + `Found condition with conflicted permission action '["read"]'. Role could have multiple conditions for the same resource type 'catalog-entity', but with different permission action sets.`, + ); + }, + ); + + it('should throw failed to create metadata error, because inserted result is undefined', async () => { + const knex = Knex.knex({ client: MockClient }); + const tracker = createTracker(knex); + tracker.on.select(CONDITIONAL_TABLE).response(undefined); + tracker.on.insert(CONDITIONAL_TABLE).response(undefined); + + const db = new DataBaseConditionalStorage(knex); + + await expect(async () => { + await db.createCondition(condition1); + }).rejects.toThrow(`Failed to create the condition.`); + }); + }); + + describe('checkConflictedConditions', () => { + it.each(databases.eachSupportedId())( + 'should check conflicted condition', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + + await expect(async () => { + await db.checkConflictedConditions( + 'role:default/test', + 'catalog-entity', + 'catalog', + ['read'], + ); + }).rejects.toThrow( + `Found condition with conflicted permission action '["read"]'. Role could have multiple conditions for the same resource type 'catalog-entity', but with different permission action sets.`, + ); + }, + ); + + it.each(databases.eachSupportedId())( + 'should fail check, when there is condition with one conflicted action "read"', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + const conditionDaoWithFewActions = { + ...conditionDao1, + permissions: + '[{"action":"read","name":"catalog.entity.read"}, {"action":"delete","name":"catalog.entity.delete"}]', + }; + await knex(CONDITIONAL_TABLE).insert( + conditionDaoWithFewActions, + ); + + await expect(async () => { + await db.checkConflictedConditions( + 'role:default/test', + 'catalog-entity', + 'catalog', + ['read'], + ); + }).rejects.toThrow( + `Found condition with conflicted permission action '["read"]'. Role could have multiple conditions for the same resource type 'catalog-entity', but with different permission action sets.`, + ); + }, + ); + + it.each(databases.eachSupportedId())( + 'should fail check, when there is one condition with two conflicted actions "read" and "update"', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + const conditionDaoWithFewActions = { + ...conditionDao1, + permissions: + '[{"action":"read","name":"catalog.entity.read"}, {"action":"delete","name":"catalog.entity.delete"}, {"action":"update","name":"catalog.entity.update"}]', + }; + await knex(CONDITIONAL_TABLE).insert( + conditionDaoWithFewActions, + ); + + await expect(async () => { + await db.checkConflictedConditions( + 'role:default/test', + 'catalog-entity', + 'catalog', + ['read', 'update'], + ); + }).rejects.toThrow( + `Found condition with conflicted permission action '["read","update"]'. Role could have multiple conditions for the same resource type 'catalog-entity', but with different permission action sets.`, + ); + }, + ); + + it.each(databases.eachSupportedId())( + 'should fail check, when there is condition with three conflicted actions "read", "update", "delete"', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + const conditionDaoWithFewActions = { + ...conditionDao1, + permissions: + '[{"action":"read","name":"catalog.entity.read"}, {"action":"delete","name":"catalog.entity.delete"}, {"action":"update","name":"catalog.entity.update"}]', + }; + await knex(CONDITIONAL_TABLE).insert( + conditionDaoWithFewActions, + ); + + await expect(async () => { + await db.checkConflictedConditions( + 'role:default/test', + 'catalog-entity', + 'catalog', + ['read', 'update', 'delete'], + ); + }).rejects.toThrow( + `Found condition with conflicted permission action '["read","update","delete"]'. Role could have multiple conditions for the same resource type 'catalog-entity', but with different permission action sets.`, + ); + }, + ); + + it.each(databases.eachSupportedId())( + 'should pass check, when there is one non conflicted condition', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + const filterConditionsSpy = jest.spyOn(db, 'filterConditions'); + + const conditionDaoWithFewActions = { + ...conditionDao1, + permissions: + '[{"action":"read","name":"catalog.entity.read"}, {"action":"update","name":"catalog.entity.update"}]', + }; + await knex(CONDITIONAL_TABLE).insert( + conditionDaoWithFewActions, + ); + + await db.checkConflictedConditions( + 'role:default/test', + 'catalog-entity', + 'catalog', + ['delete'], + ); + + expect(filterConditionsSpy).toHaveBeenCalledTimes(1); + const result = await filterConditionsSpy.mock.results[0].value; + expect(result).toEqual([ + { + ...condition1, + permissionMapping: [ + { name: 'catalog.entity.read', action: 'read' }, + { name: 'catalog.entity.update', action: 'update' }, + ], + }, + ]); + }, + ); + + it.each(databases.eachSupportedId())( + 'should pass check, when there are no conditions', + async databasesId => { + const { db } = await createDatabase(databasesId); + const filterConditionsSpy = jest.spyOn(db, 'filterConditions'); + + await db.checkConflictedConditions( + 'role:default/test', + 'catalog-entity', + 'catalog', + ['read'], + ); + + expect(filterConditionsSpy).toHaveBeenCalledTimes(1); + const result = await filterConditionsSpy.mock.results[0].value; + expect(result).toEqual([]); + }, + ); + }); + + describe('getCondition', () => { + it.each(databases.eachSupportedId())( + 'should return condition by id', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + + const condition = await db.getCondition(1); + + expect(condition).toEqual(condition1); + }, + ); + + it.each(databases.eachSupportedId())( + 'should not find condition', + async databasesId => { + const { db } = await createDatabase(databasesId); + + const condition = await db.getCondition(1); + + expect(condition).toBeUndefined(); + }, + ); + }); + + describe('deleteCondition', () => { + it.each(databases.eachSupportedId())( + 'should delete condition by id', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + + await db.deleteCondition(1); + + const conditions = await knex + .table(CONDITIONAL_TABLE) + .select(); + expect(conditions.length).toEqual(0); + }, + ); + + it.each(databases.eachSupportedId())( + 'should not find condition', + async databasesId => { + const { db } = await createDatabase(databasesId); + + await expect(async () => { + await db.deleteCondition(1); + }).rejects.toThrow('Condition with id 1 was not found'); + }, + ); + }); + + describe('updateCondition', () => { + it.each(databases.eachSupportedId())( + 'should update condition with added new action', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + + const updateCondition: RoleConditionalPolicyDecision = { + ...condition1, + permissionMapping: [ + { name: 'catalog.entity.read', action: 'read' }, + { name: 'catalog.entity.delete', action: 'delete' }, + ], + }; + await db.updateCondition(1, updateCondition); + + const condition = await knex + .table(CONDITIONAL_TABLE) + .select() + .where('id', 1); + expect(condition).toEqual([ + { + ...conditionDao1, + permissions: + '[{"name":"catalog.entity.read","action":"read"},{"name":"catalog.entity.delete","action":"delete"}]', + id: 1, + }, + ]); + }, + ); + + it.each(databases.eachSupportedId())( + 'should update condition with removed one action', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + await knex(CONDITIONAL_TABLE).insert({ + ...conditionDao1, + permissions: + '[{"action":"read","name":"catalog.entity.read"}, {"action":"delete","name":"catalog.entity.delete"}]', + }); + + const updateCondition: RoleConditionalPolicyDecision = { + ...condition1, + permissionMapping: [{ name: 'catalog.entity.read', action: 'read' }], + }; + await db.updateCondition(1, updateCondition); + + const condition = await knex + .table(CONDITIONAL_TABLE) + .select() + .where('id', 1); + expect(condition).toEqual([ + { + ...conditionDao1, + permissions: '[{"name":"catalog.entity.read","action":"read"}]', + id: 1, + }, + ]); + }, + ); + + it.each(databases.eachSupportedId())( + 'should fail to update condition, because condition not found', + async databasesId => { + const { db } = await createDatabase(databasesId); + + const updateCondition: RoleConditionalPolicyDecision = { + ...condition1, + permissionMapping: [ + { name: 'catalog.entity.name', action: 'read' }, + { name: 'catalog.entity.delete', action: 'delete' }, + ], + }; + await expect(async () => { + await db.updateCondition(1, updateCondition); + }).rejects.toThrow('Condition with id 1 was not found'); + }, + ); + + it.each(databases.eachSupportedId())( + 'should fail to update condition, because found condition with conflict', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + await knex(CONDITIONAL_TABLE).insert({ + ...conditionDao1, + permissions: + '[{"name": "catalog.entity.delete", "action": "delete"}]', + }); + + const updateCondition: RoleConditionalPolicyDecision = { + ...condition1, + permissionMapping: [ + { name: 'catalog.entity.read', action: 'read' }, + { name: 'catalog.entity.delete', action: 'delete' }, + ], + }; + await expect(async () => { + await db.updateCondition(1, updateCondition); + }).rejects.toThrow( + `Found condition with conflicted permission action '["delete"]'. Role could have multiple conditions for the same resource type 'catalog-entity', but with different permission action sets.`, + ); + }, + ); + + it.each(databases.eachSupportedId())( + 'should fail to update condition, because found condition with two conflicted actions', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + + await knex(CONDITIONAL_TABLE).insert( + conditionDao1, + ); + await knex(CONDITIONAL_TABLE).insert({ + ...conditionDao1, + permissions: + '[{"name": "catalog.entity.delete", "action": "delete"}, {"name": "catalog.entity.read", "action": "read"}]', + }); + + const updateCondition: RoleConditionalPolicyDecision = { + ...condition1, + permissionMapping: [ + { name: 'catalog.entity.read', action: 'read' }, + { name: 'catalog.entity.delete', action: 'delete' }, + ], + }; + await expect(async () => { + await db.updateCondition(1, updateCondition); + }).rejects.toThrow( + `Found condition with conflicted permission action '["read","delete"]'. Role could have multiple ` + + `conditions for the same resource type 'catalog-entity', but with different permission action sets.`, + ); + }, + ); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/database/conditional-storage.ts b/workspaces/rbac/plugins/rbac-backend/src/database/conditional-storage.ts new file mode 100644 index 0000000000..87ee20e329 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/database/conditional-storage.ts @@ -0,0 +1,286 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConflictError, InputError, NotFoundError } from '@backstage/errors'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; + +import { Knex } from 'knex'; + +import type { + PermissionAction, + PermissionInfo, + RoleConditionalPolicyDecision, +} from '@backstage-community/plugin-rbac-common'; + +export const CONDITIONAL_TABLE = 'role-condition-policies'; + +export interface ConditionalPolicyDecisionDAO { + result: AuthorizeResult.CONDITIONAL; + id?: number; + roleEntityRef: string; + permissions: string; + pluginId: string; + resourceType: string; + conditionsJson: string; +} + +export interface ConditionalStorage { + filterConditions( + roleEntityRef?: string | string[], + pluginId?: string, + resourceType?: string, + actions?: PermissionAction[], + permissionNames?: string[], + ): Promise[]>; + createCondition( + conditionalDecision: RoleConditionalPolicyDecision, + ): Promise; + checkConflictedConditions( + roleEntityRef: string, + resourceType: string, + pluginId: string, + queryPermissionNames: string[], + idToExclude?: number, + ): Promise; + getCondition( + id: number, + ): Promise | undefined>; + deleteCondition(id: number): Promise; + updateCondition( + id: number, + conditionalDecision: RoleConditionalPolicyDecision, + ): Promise; +} + +export class DataBaseConditionalStorage implements ConditionalStorage { + public constructor(private readonly knex: Knex) {} + + async filterConditions( + roleEntityRef?: string | string[], + pluginId?: string, + resourceType?: string, + actions?: PermissionAction[], + permissionNames?: string[], + ): Promise[]> { + const daoRaws = await this.knex.table(CONDITIONAL_TABLE).where(builder => { + if (pluginId) { + builder.where('pluginId', pluginId); + } + if (resourceType) { + builder.where('resourceType', resourceType); + } + if (roleEntityRef) { + if (Array.isArray(roleEntityRef)) { + builder.whereIn('roleEntityRef', roleEntityRef); + } else { + builder.where('roleEntityRef', roleEntityRef); + } + } + }); + + let conditions: RoleConditionalPolicyDecision[] = []; + if (daoRaws) { + conditions = daoRaws.map(dao => this.daoToConditionalDecision(dao)); + } + + if (permissionNames && permissionNames.length > 0) { + conditions = conditions.filter(condition => { + return permissionNames.every(permissionName => + condition.permissionMapping + .map(permInfo => permInfo.name) + .includes(permissionName), + ); + }); + } + + if (actions && actions.length > 0) { + conditions = conditions.filter(condition => { + return actions.every(action => + condition.permissionMapping + .map(permInfo => permInfo.action) + .includes(action), + ); + }); + } + + return conditions; + } + + async createCondition( + conditionalDecision: RoleConditionalPolicyDecision, + ): Promise { + await this.checkConflictedConditions( + conditionalDecision.roleEntityRef, + conditionalDecision.resourceType, + conditionalDecision.pluginId, + conditionalDecision.permissionMapping.map(permInfo => permInfo.action), + ); + + const conditionRaw = this.toDAO(conditionalDecision); + const result = await this.knex + .table(CONDITIONAL_TABLE) + .insert(conditionRaw) + .returning('id'); + if (result && result?.length > 0) { + return result[0].id; + } + + throw new Error(`Failed to create the condition.`); + } + + async checkConflictedConditions( + roleEntityRef: string, + resourceType: string, + pluginId: string, + queryConditionActions: PermissionAction[], + idToExclude?: number, + ): Promise { + let conditionsForTheSameResource = await this.filterConditions( + roleEntityRef, + pluginId, + resourceType, + ); + conditionsForTheSameResource = conditionsForTheSameResource.filter( + c => c.id !== idToExclude, + ); + + if (conditionsForTheSameResource) { + const conflictedCondition = conditionsForTheSameResource.find( + condition => { + const conditionActions = condition.permissionMapping.map( + permInfo => permInfo.action, + ); + return queryConditionActions.some(action => + conditionActions.includes(action), + ); + }, + ); + + if (conflictedCondition) { + const conflictedActions = queryConditionActions.filter(action => + conflictedCondition.permissionMapping.some(p => p.action === action), + ); + throw new ConflictError( + `Found condition with conflicted permission action '${JSON.stringify( + conflictedActions, + )}'. Role could have multiple ` + + `conditions for the same resource type '${conflictedCondition.resourceType}', but with different permission action sets.`, + ); + } + } + } + + async getCondition( + id: number, + ): Promise | undefined> { + const daoRaw = await this.knex + .table(CONDITIONAL_TABLE) + .where('id', id) + .first(); + + if (daoRaw) { + return this.daoToConditionalDecision(daoRaw); + } + return undefined; + } + + async deleteCondition(id: number): Promise { + const condition = await this.getCondition(id); + if (!condition) { + throw new NotFoundError(`Condition with id ${id} was not found`); + } + await this.knex?.table(CONDITIONAL_TABLE).delete().whereIn('id', [id]); + } + + async updateCondition( + id: number, + conditionalDecision: RoleConditionalPolicyDecision, + ): Promise { + const condition = await this.getCondition(id); + if (!condition) { + throw new NotFoundError(`Condition with id ${id} was not found`); + } + + await this.checkConflictedConditions( + conditionalDecision.roleEntityRef, + conditionalDecision.resourceType, + conditionalDecision.pluginId, + conditionalDecision.permissionMapping.map(perm => perm.action), + id, + ); + + const conditionRaw = this.toDAO(conditionalDecision); + conditionRaw.id = id; + const result = await this.knex + .table(CONDITIONAL_TABLE) + .where('id', conditionRaw.id) + .update(conditionRaw) + .returning('id'); + + if (!result || result.length === 0) { + throw new Error(`Failed to update the condition with id: ${id}.`); + } + } + + private toDAO( + conditionalDecision: RoleConditionalPolicyDecision, + ): ConditionalPolicyDecisionDAO { + const { + result, + pluginId, + resourceType, + conditions, + roleEntityRef, + permissionMapping, + } = conditionalDecision; + const conditionsJson = JSON.stringify(conditions); + return { + result, + pluginId, + resourceType, + conditionsJson, + roleEntityRef, + permissions: JSON.stringify(permissionMapping), + }; + } + + private daoToConditionalDecision( + dao: ConditionalPolicyDecisionDAO, + ): RoleConditionalPolicyDecision { + if (!dao.id) { + throw new InputError(`Missed id in the dao object: ${dao}`); + } + const { + id, + result, + pluginId, + resourceType, + conditionsJson, + roleEntityRef, + permissions, + } = dao; + + const conditions = JSON.parse(conditionsJson); + return { + id, + result, + pluginId, + resourceType, + conditions, + roleEntityRef, + permissionMapping: JSON.parse(permissions), + }; + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/database/migration.ts b/workspaces/rbac/plugins/rbac-backend/src/database/migration.ts new file mode 100644 index 0000000000..615eec99ad --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/database/migration.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + DatabaseService, + resolvePackagePath, +} from '@backstage/backend-plugin-api'; + +const migrationsDir = resolvePackagePath( + '@backstage-community/plugin-rbac-backend', // Package name + 'migrations', // Migrations directory +); + +export async function migrate(databaseManager: DatabaseService) { + const knex = await databaseManager.getClient(); + + if (!databaseManager.migrations?.skip) { + await knex.migrate.latest({ + directory: migrationsDir, + }); + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/database/role-metadata.test.ts b/workspaces/rbac/plugins/rbac-backend/src/database/role-metadata.test.ts new file mode 100644 index 0000000000..040e5d1271 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/database/role-metadata.test.ts @@ -0,0 +1,602 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + mockServices, + TestDatabaseId, + TestDatabases, +} from '@backstage/backend-test-utils'; + +import * as Knex from 'knex'; +import { createTracker, MockClient } from 'knex-mock-client'; + +import { migrate } from './migration'; +import { + DataBaseRoleMetadataStorage, + ROLE_METADATA_TABLE, + RoleMetadataDao, +} from './role-metadata'; + +jest.setTimeout(60000); + +describe('role-metadata-db-table', () => { + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'SQLITE_3'], + }); + const modifiedBy = 'user:default/some-user'; + + async function createDatabase(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + const mockDatabaseService = mockServices.database.mock({ + getClient: async () => knex, + migrations: { skip: false }, + }); + + await migrate(mockDatabaseService); + return { + knex, + db: new DataBaseRoleMetadataStorage(knex), + }; + } + + describe('findRoleMetadata', () => { + it.each(databases.eachSupportedId())( + 'should return undefined', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + const trx = await knex.transaction(); + try { + const roleMetadata = await db.findRoleMetadata( + 'role:default/some-super-important-role', + trx, + ); + await trx.commit(); + expect(roleMetadata).toBeUndefined(); + } catch (err) { + await trx.rollback(); + throw err; + } + }, + ); + + it.each(databases.eachSupportedId())( + 'should return found metadata', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + await knex(ROLE_METADATA_TABLE).insert({ + roleEntityRef: 'role:default/some-super-important-role', + source: 'rest', + modifiedBy, + }); + + const trx = await knex.transaction(); + try { + const roleMetadata = await db.findRoleMetadata( + 'role:default/some-super-important-role', + trx, + ); + await trx.commit(); + expect(roleMetadata).toEqual({ + author: null, + createdAt: null, + description: null, + id: 1, + lastModified: null, + modifiedBy, + roleEntityRef: 'role:default/some-super-important-role', + source: 'rest', + }); + } catch (err) { + await trx.rollback(); + throw err; + } + }, + ); + }); + + describe('createRoleMetadata', () => { + it.each(databases.eachSupportedId())( + 'should successfully create new role metadata', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + + const trx = await knex.transaction(); + let id; + try { + id = await db.createRoleMetadata( + { + source: 'configuration', + roleEntityRef: 'role:default/some-super-important-role', + modifiedBy, + }, + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(); + throw err; + } + + const metadata = await knex(ROLE_METADATA_TABLE).where( + 'id', + id, + ); + expect(metadata.length).toEqual(1); + expect(metadata[0]).toEqual({ + author: null, + createdAt: null, + roleEntityRef: 'role:default/some-super-important-role', + description: null, + id: 1, + lastModified: null, + modifiedBy, + source: 'configuration', + }); + }, + ); + + it.each(databases.eachSupportedId())( + 'should throw conflict error', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + + await knex(ROLE_METADATA_TABLE).insert({ + roleEntityRef: 'role:default/some-super-important-role', + source: 'configuration', + }); + + const trx = await knex.transaction(); + await expect(async () => { + try { + await db.createRoleMetadata( + { + source: 'configuration', + roleEntityRef: 'role:default/some-super-important-role', + modifiedBy, + }, + + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(); + throw err; + } + }).rejects.toThrow( + `A metadata for role role:default/some-super-important-role has already been stored`, + ); + }, + ); + + it('should throw failed to create metadata error, because inserted result is an empty array.', async () => { + const knex = Knex.knex({ client: MockClient }); + const tracker = createTracker(knex); + tracker.on.select(ROLE_METADATA_TABLE).response(undefined); + tracker.on.insert(ROLE_METADATA_TABLE).response([]); + + const db = new DataBaseRoleMetadataStorage(knex); + const trx = await knex.transaction(); + + await expect( + db.createRoleMetadata( + { + source: 'configuration', + roleEntityRef: 'role:default/some-super-important-role', + modifiedBy, + }, + trx, + ), + ).rejects.toThrow( + `Failed to create the role metadata: '{"source":"configuration","roleEntityRef":"role:default/some-super-important-role","modifiedBy":"user:default/some-user"}'.`, + ); + }); + + it('should throw failed to create metadata error, because inserted result is undefined.', async () => { + const knex = Knex.knex({ client: MockClient }); + const tracker = createTracker(knex); + tracker.on.select(ROLE_METADATA_TABLE).response(undefined); + tracker.on.insert(ROLE_METADATA_TABLE).response(undefined); + + const db = new DataBaseRoleMetadataStorage(knex); + + await expect(async () => { + const trx = await knex.transaction(); + try { + await db.createRoleMetadata( + { + source: 'configuration', + roleEntityRef: 'role:default/some-super-important-role', + modifiedBy, + }, + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(err); + throw err; + } + }).rejects.toThrow( + `Failed to create the role metadata: '{"source":"configuration","roleEntityRef":"role:default/some-super-important-role","modifiedBy":"user:default/some-user"}'.`, + ); + }); + + it('should throw an error on insert metadata operation', async () => { + const knex = Knex.knex({ client: MockClient }); + const tracker = createTracker(knex); + tracker.on.select(ROLE_METADATA_TABLE).response(undefined); + tracker.on + .insert(ROLE_METADATA_TABLE) + .simulateError('connection refused error'); + + const db = new DataBaseRoleMetadataStorage(knex); + + await expect(async () => { + const trx = await knex.transaction(); + try { + await db.createRoleMetadata( + { + source: 'configuration', + roleEntityRef: 'role:default/some-super-important-role', + modifiedBy, + }, + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(err); + throw err; + } + }).rejects.toThrow('connection refused error'); + }); + }); + + describe('updateRoleMetadata', () => { + it.each(databases.eachSupportedId())( + 'should successfully update role metadata from legacy source to new value', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + + await knex(ROLE_METADATA_TABLE).insert({ + roleEntityRef: 'role:default/some-super-important-role', + source: 'legacy', + }); + + const trx = await knex.transaction(); + try { + await db.updateRoleMetadata( + { + roleEntityRef: 'role:default/some-super-important-role', + source: 'rest', + modifiedBy, + }, + 'role:default/some-super-important-role', + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(); + throw err; + } + + const metadata = await knex(ROLE_METADATA_TABLE).where( + 'id', + 1, + ); + expect(metadata.length).toEqual(1); + expect(metadata[0]).toEqual({ + author: null, + createdAt: null, + description: null, + source: 'rest', + roleEntityRef: 'role:default/some-super-important-role', + id: 1, + lastModified: null, + modifiedBy, + }); + }, + ); + + it.each(databases.eachSupportedId())( + 'should fail to update role metadata source to new value, because source is not legacy', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + + await knex(ROLE_METADATA_TABLE).insert({ + roleEntityRef: 'role:default/some-super-important-role', + source: 'rest', + }); + + await expect(async () => { + const trx = await knex.transaction(); + try { + await db.updateRoleMetadata( + { + roleEntityRef: 'role:default/some-super-important-role', + source: 'configuration', + modifiedBy, + }, + 'role:default/some-super-important-role', + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(); + throw err; + } + }).rejects.toThrow(`The RoleMetadata.source field is 'read-only'`); + }, + ); + + it.each(databases.eachSupportedId())( + 'should successfully update role metadata with the new name', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + + await knex(ROLE_METADATA_TABLE).insert({ + roleEntityRef: 'role:default/some-super-important-role', + source: 'configuration', + }); + + const trx = await knex.transaction(); + try { + await db.updateRoleMetadata( + { + roleEntityRef: 'role:default/important-role', + source: 'configuration', + modifiedBy, + }, + 'role:default/some-super-important-role', + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(); + throw err; + } + + const metadata = await knex(ROLE_METADATA_TABLE).where( + 'id', + 1, + ); + expect(metadata.length).toEqual(1); + expect(metadata[0]).toEqual({ + author: null, + createdAt: null, + description: null, + source: 'configuration', + roleEntityRef: 'role:default/important-role', + id: 1, + lastModified: null, + modifiedBy, + }); + }, + ); + + it.each(databases.eachSupportedId())( + 'should fail to update role metadata, because role metadata was not found', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + + await expect(async () => { + const trx = await knex.transaction(); + try { + await db.updateRoleMetadata( + { + roleEntityRef: 'role:default/important-role', + source: 'configuration', + modifiedBy, + }, + 'role:default/some-super-important-role', + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(); + throw err; + } + }).rejects.toThrow( + `A metadata for role 'role:default/some-super-important-role' was not found`, + ); + }, + ); + + it('should throw failed to update metadata error, because update result is an empty array.', async () => { + const knex = Knex.knex({ client: MockClient }); + const tracker = createTracker(knex); + tracker.on.select(ROLE_METADATA_TABLE).response({ + roleEntityRef: 'role:default/some-super-important-role', + source: 'configuration', + id: 1, + }); + tracker.on.update(ROLE_METADATA_TABLE).response([]); + + const db = new DataBaseRoleMetadataStorage(knex); + + await expect(async () => { + const trx = await knex.transaction(); + try { + await db.updateRoleMetadata( + { + roleEntityRef: 'role:default/important-role', + source: 'configuration', + modifiedBy, + }, + 'role:default/some-super-important-role', + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(err); + throw err; + } + }).rejects.toThrow( + `Failed to update the role metadata '{"roleEntityRef":"role:default/some-super-important-role","source":"configuration","id":1}' with new value: '{"roleEntityRef":"role:default/important-role","source":"configuration","modifiedBy":"user:default/some-user"}'.`, + ); + }); + + it('should throw failed to update metadata error, because update result is undefined.', async () => { + const knex = Knex.knex({ client: MockClient }); + const tracker = createTracker(knex); + tracker.on.select(ROLE_METADATA_TABLE).response({ + roleEntityRef: 'role:default/some-super-important-role', + source: 'configuration', + id: 1, + }); + tracker.on.update(ROLE_METADATA_TABLE).response(undefined); + + const db = new DataBaseRoleMetadataStorage(knex); + + await expect(async () => { + const trx = await knex.transaction(); + try { + await db.updateRoleMetadata( + { + roleEntityRef: 'role:default/important-role', + source: 'configuration', + modifiedBy, + }, + 'role:default/some-super-important-role', + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(err); + throw err; + } + }).rejects.toThrow( + `Failed to update the role metadata '{"roleEntityRef":"role:default/some-super-important-role","source":"configuration","id":1}' with new value: '{"roleEntityRef":"role:default/important-role","source":"configuration","modifiedBy":"user:default/some-user"}'.`, + ); + }); + + it('should throw on insert metadata operation', async () => { + const knex = Knex.knex({ client: MockClient }); + const tracker = createTracker(knex); + tracker.on.select(ROLE_METADATA_TABLE).response({ + roleEntityRef: 'role:default/some-super-important-role', + source: 'configuration', + id: 1, + }); + tracker.on + .update(ROLE_METADATA_TABLE) + .simulateError('connection refused error'); + + const db = new DataBaseRoleMetadataStorage(knex); + + await expect(async () => { + const trx = await knex.transaction(); + try { + await db.updateRoleMetadata( + { + roleEntityRef: 'role:default/important-role', + source: 'configuration', + modifiedBy, + }, + 'role:default/some-super-important-role', + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(err); + throw err; + } + }).rejects.toThrow('connection refused error'); + }); + }); + + describe('removeRoleMetadata', () => { + it.each(databases.eachSupportedId())( + 'should successfully delete role metadata', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + + await knex(ROLE_METADATA_TABLE).insert({ + roleEntityRef: 'role:default/some-super-important-role', + source: 'legacy', + }); + + const trx = await knex.transaction(); + try { + await db.removeRoleMetadata( + 'role:default/some-super-important-role', + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(); + throw err; + } + + const metadata = await knex(ROLE_METADATA_TABLE).where( + 'id', + 1, + ); + expect(metadata.length).toEqual(0); + }, + ); + + it.each(databases.eachSupportedId())( + 'should fail to delete role metadata, because nothing to delete', + async databasesId => { + const { knex, db } = await createDatabase(databasesId); + + const trx = await knex.transaction(); + + await expect(async () => { + try { + await db.removeRoleMetadata( + 'role:default/some-super-important-role', + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(); + throw err; + } + }).rejects.toThrow( + `A metadata for role 'role:default/some-super-important-role' was not found`, + ); + }, + ); + + it('should throw an error on delete metadata operation', async () => { + const knex = Knex.knex({ client: MockClient }); + const tracker = createTracker(knex); + tracker.on.select(ROLE_METADATA_TABLE).response({ + roleEntityRef: 'role:default/some-super-important-role', + source: 'configuration', + id: 1, + }); + tracker.on + .delete(ROLE_METADATA_TABLE) + .simulateError('connection refused error'); + + const db = new DataBaseRoleMetadataStorage(knex); + + await expect(async () => { + const trx = await knex.transaction(); + try { + await db.removeRoleMetadata( + 'role:default/some-super-important-role', + trx, + ); + await trx.commit(); + } catch (err) { + await trx.rollback(err); + throw err; + } + }).rejects.toThrow('connection refused error'); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/database/role-metadata.ts b/workspaces/rbac/plugins/rbac-backend/src/database/role-metadata.ts new file mode 100644 index 0000000000..266d4c1411 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/database/role-metadata.ts @@ -0,0 +1,174 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConflictError, InputError, NotFoundError } from '@backstage/errors'; + +import { Knex } from 'knex'; + +import type { + RoleMetadata, + Source, +} from '@backstage-community/plugin-rbac-common'; + +import { deepSortedEqual } from '../helper'; + +export const ROLE_METADATA_TABLE = 'role-metadata'; + +export interface RoleMetadataDao extends RoleMetadata { + id?: number; + roleEntityRef: string; + source: Source; + modifiedBy: string; +} + +export interface RoleMetadataStorage { + filterRoleMetadata(source?: Source): Promise; + findRoleMetadata( + roleEntityRef: string, + trx?: Knex.Transaction, + ): Promise; + createRoleMetadata( + roleMetadata: RoleMetadataDao, + trx: Knex.Transaction, + ): Promise; + updateRoleMetadata( + roleMetadata: RoleMetadataDao, + oldRoleEntityRef: string, + externalTrx?: Knex.Transaction, + ): Promise; + removeRoleMetadata( + roleEntityRef: string, + trx: Knex.Transaction, + ): Promise; +} + +export class DataBaseRoleMetadataStorage implements RoleMetadataStorage { + constructor(private readonly knex: Knex) {} + + async filterRoleMetadata(source?: Source): Promise { + return await this.knex.table(ROLE_METADATA_TABLE).where(builder => { + if (source) { + builder.where('source', source); + } + }); + } + + async findRoleMetadata( + roleEntityRef: string, + trx: Knex.Transaction, + ): Promise { + const db = trx || this.knex; + return await db + .table(ROLE_METADATA_TABLE) + .where('roleEntityRef', roleEntityRef) + // roleEntityRef should be unique. + .first(); + } + + async createRoleMetadata( + metadata: RoleMetadataDao, + trx: Knex.Transaction, + ): Promise { + if (await this.findRoleMetadata(metadata.roleEntityRef, trx)) { + throw new ConflictError( + `A metadata for role ${metadata.roleEntityRef} has already been stored`, + ); + } + + const result = await trx(ROLE_METADATA_TABLE) + .insert(metadata) + .returning<[{ id: number }]>('id'); + if (result && result?.length > 0) { + return result[0].id; + } + + throw new Error( + `Failed to create the role metadata: '${JSON.stringify(metadata)}'.`, + ); + } + + async updateRoleMetadata( + newRoleMetadata: RoleMetadataDao, + oldRoleEntityRef: string, + externalTrx?: Knex.Transaction, + ): Promise { + const trx = externalTrx ?? (await this.knex.transaction()); + const currentMetadataDao = await this.findRoleMetadata( + oldRoleEntityRef, + trx, + ); + + if (!currentMetadataDao) { + throw new NotFoundError( + `A metadata for role '${oldRoleEntityRef}' was not found`, + ); + } + + if ( + currentMetadataDao.source !== 'legacy' && + currentMetadataDao.source !== newRoleMetadata.source + ) { + throw new InputError(`The RoleMetadata.source field is 'read-only'.`); + } + + if (deepSortedEqual(currentMetadataDao, newRoleMetadata)) { + return; + } + + const result = await trx(ROLE_METADATA_TABLE) + .where('id', currentMetadataDao.id) + .update(newRoleMetadata) + .returning('id'); + + if (!externalTrx) { + await trx.commit(); + } + + if (!result || result.length === 0) { + throw new Error( + `Failed to update the role metadata '${JSON.stringify( + currentMetadataDao, + )}' with new value: '${JSON.stringify(newRoleMetadata)}'.`, + ); + } + } + + async removeRoleMetadata( + roleEntityRef: string, + trx: Knex.Transaction, + ): Promise { + const metadataDao = await this.findRoleMetadata(roleEntityRef, trx); + if (!metadataDao) { + throw new NotFoundError( + `A metadata for role '${roleEntityRef}' was not found`, + ); + } + + await trx(ROLE_METADATA_TABLE) + .delete() + .whereIn('id', [metadataDao.id!]); + } +} + +export function daoToMetadata(dao: RoleMetadataDao): RoleMetadata { + return { + source: dao.source, + description: dao.description, + author: dao.author, + modifiedBy: dao.modifiedBy, + createdAt: dao.createdAt, + lastModified: dao.lastModified, + }; +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/file-permissions/csv-file-watcher.test.ts b/workspaces/rbac/plugins/rbac-backend/src/file-permissions/csv-file-watcher.test.ts new file mode 100644 index 0000000000..0f1b8cafab --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/file-permissions/csv-file-watcher.test.ts @@ -0,0 +1,761 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { LoggerService } from '@backstage/backend-plugin-api'; +import { mockServices } from '@backstage/backend-test-utils'; +import type { Config } from '@backstage/config'; + +import { + Adapter, + Enforcer, + Model, + newEnforcer, + newModelFromString, +} from 'casbin'; +import * as Knex from 'knex'; +import { MockClient } from 'knex-mock-client'; + +import type { Source } from '@backstage-community/plugin-rbac-common'; + +import { resolve } from 'path'; + +import { ADMIN_ROLE_AUTHOR } from '../admin-permissions/admin-creation'; +import { CasbinDBAdapterFactory } from '../database/casbin-adapter-factory'; +import { + RoleMetadataDao, + 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 { CSVFileWatcher } from './csv-file-watcher'; + +const legacyPermission = [ + 'role:default/legacy', + 'catalog-entity', + 'update', + 'allow', +]; + +const legacyRole = ['user:default/guest', 'role:default/legacy']; + +const restPermission = [ + 'role:default/rest', + 'catalog-entity', + 'update', + 'allow', +]; + +const restRole = ['user:default/guest', 'role:default/rest']; + +const configPermission = [ + 'role:default/config', + 'catalog-entity', + 'update', + 'allow', +]; + +const configRole = ['user:default/guest', 'role:default/config']; + +// TODO: Move to 'catalogServiceMock' from '@backstage/plugin-catalog-node/testUtils' +// once '@backstage/plugin-catalog-node' is upgraded +const catalogApiMock = { + 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 mockLoggerService = mockServices.logger.mock(); + +const modifiedBy = 'user:default/some-admin'; + +const legacyRoleMetadata: RoleMetadataDao = { + roleEntityRef: legacyPermission[0], + source: 'legacy', + modifiedBy, +}; + +const roleMetadataStorageMock: RoleMetadataStorage = { + filterRoleMetadata: jest.fn().mockImplementation(() => []), + findRoleMetadata: jest + .fn() + .mockImplementation( + async ( + roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + if (roleEntityRef === legacyPermission[0]) { + return legacyRoleMetadata; + } else if (roleEntityRef === restPermission[0]) { + return { + roleEntityRef: restPermission[0], + source: 'rest', + modifiedBy, + }; + } + if (roleEntityRef === configPermission[0]) { + return { + roleEntityRef: configPermission[0], + source: 'configuration', + modifiedBy, + }; + } + return { roleEntityRef: '', source: 'csv-file', modifiedBy }; + }, + ), + createRoleMetadata: jest.fn().mockImplementation(), + updateRoleMetadata: jest.fn().mockImplementation(), + removeRoleMetadata: jest.fn().mockImplementation(), +}; + +const mockClientKnex = Knex.knex({ client: MockClient }); + +const mockAuthService = mockServices.auth(); + +const auditLoggerMock = { + getActorId: jest.fn().mockImplementation(), + createAuditLogDetails: jest.fn().mockImplementation(), + auditLog: jest.fn().mockImplementation(), +}; + +const currentPermissionPolicies = [ + ['role:default/catalog-writer', 'catalog-entity', 'update', 'allow'], + ['role:default/legacy', '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/legacy'], + ['user:default/guest', 'role:default/catalog-reader'], + ['user:default/guest', 'role:default/catalog-deleter'], + ['user:default/known_user', 'role:default/known_role'], +]; + +describe('CSVFileWatcher', () => { + let enforcerDelegate: EnforcerDelegate; + let csvFileName: string; + + beforeEach(async () => { + csvFileName = resolve( + __dirname, + '../../__fixtures__/data/valid-csv/rbac-policy.csv', + ); + + const config = newConfig(); + + const adapter = await new CasbinDBAdapterFactory( + config, + mockClientKnex, + ).createAdapter(); + + const stringModel = newModelFromString(MODEL); + const enf = await createEnforcer(stringModel, adapter, mockLoggerService); + + const knex = Knex.knex({ client: MockClient }); + + enforcerDelegate = new EnforcerDelegate(enf, roleMetadataStorageMock, knex); + + auditLoggerMock.auditLog.mockReset(); + (roleMetadataStorageMock.updateRoleMetadata as jest.Mock).mockClear(); + }); + + afterEach(() => { + (mockLoggerService.warn as jest.Mock).mockReset(); + (roleMetadataStorageMock.removeRoleMetadata as jest.Mock).mockReset(); + }); + + function createCSVFileWatcher(fileName?: string): CSVFileWatcher { + return new CSVFileWatcher( + fileName, + false, + mockLoggerService, + enforcerDelegate, + roleMetadataStorageMock, + auditLoggerMock, + ); + } + + describe('initialize', () => { + it('should be able to add permission policies during initialization', async () => { + const csvFileWatcher = createCSVFileWatcher(csvFileName); + await csvFileWatcher.initialize(); + + const enfPolicies = await enforcerDelegate.getPolicy(); + + expect(enfPolicies).toStrictEqual(currentPermissionPolicies); + }); + + it('should be able to add roles during initialization', async () => { + const csvFileWatcher = createCSVFileWatcher(csvFileName); + await csvFileWatcher.initialize(); + + const enfRoles = await enforcerDelegate.getGroupingPolicy(); + + expect(enfRoles).toStrictEqual(currentRoles); + }); + + it('should be able to update legacy role metadata during initialization', async () => { + const permissionPolicies = [ + ['role:default/legacy', 'catalog-entity', 'update', 'allow'], + ['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'], + ]; + + await enforcerDelegate.addPolicy(legacyPermission); + await enforcerDelegate.addGroupingPolicies( + [['user:default/guest', 'role:default/legacy']], + legacyRoleMetadata!, + ); + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation((source: Source) => { + if (source === 'legacy') { + return [legacyRoleMetadata]; + } + return []; + }); + (roleMetadataStorageMock.updateRoleMetadata as jest.Mock).mockReset(); + + const csvFileWatcher = createCSVFileWatcher(csvFileName); + await csvFileWatcher.initialize(); + + const enfPolicies = await enforcerDelegate.getPolicy(); + + const legacyMetadatas = ( + roleMetadataStorageMock.updateRoleMetadata as jest.Mock + ).mock.calls + .map(call => call[0]) + .filter(metadata => metadata.roleEntityRef === 'role:default/legacy'); + expect(legacyMetadatas.length).toEqual(1); + // legacy source should be updated from legacy to csv-file + expect(legacyMetadatas[0].source).toEqual('csv-file'); + expect(enfPolicies).toStrictEqual(permissionPolicies); + }); + + it('should be able to update legacy roles during initialization', async () => { + const roles = [ + ['user:default/guest', 'role:default/legacy'], + ['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'], + ]; + + await enforcerDelegate.addGroupingPolicy(legacyRole, legacyRoleMetadata); + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation((source: Source) => { + if (source === 'legacy') { + return [legacyRoleMetadata]; + } + return []; + }); + (roleMetadataStorageMock.updateRoleMetadata as jest.Mock).mockReset(); + + const csvFileWatcher = createCSVFileWatcher(csvFileName); + await csvFileWatcher.initialize(); + + const enfPolicies = await enforcerDelegate.getGroupingPolicy(); + + const legacyMetadatas = ( + roleMetadataStorageMock.updateRoleMetadata as jest.Mock + ).mock.calls + .map(call => call[0]) + .filter(metadata => metadata.roleEntityRef === 'role:default/legacy'); + expect(legacyMetadatas.length).toEqual(1); + // legacy source should be updated from legacy to csv-file + expect(legacyMetadatas[0].source).toEqual('csv-file'); + + 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 csvFileWatcher = createCSVFileWatcher(csvFileName); + + 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(); + + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 1, + `Duplicate policy: ${duplicatePolicy} found in the file ${csvFileName}`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 2, + `Duplicate policy: ${duplicatePolicy} found in the file ${csvFileName}`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 3, + `Duplicate policy: ${duplicatePolicyWithDifferentEffect[0]}, ${duplicatePolicyWithDifferentEffect[1]}, ${duplicatePolicyWithDifferentEffect[2]} with different effect found in the file ${csvFileName}`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 4, + `Duplicate policy: ${duplicatePolicyWithDifferentEffect[0]}, ${duplicatePolicyWithDifferentEffect[1]}, ${duplicatePolicyWithDifferentEffect[2]} with different effect found in the file ${csvFileName}`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 5, + `Duplicate role: ${duplicateRole} found in the file ${csvFileName}`, + ); + expect(mockLoggerService.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 csvFileWatcher = createCSVFileWatcher(csvFileName); + + 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(); + + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 1, + `Failed to validate policy from file ${csvFileName}. Cause: Entity reference "${roleErrorPolicy[0]}" was not on the form [:][/]`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 2, + `Failed to validate policy from file ${csvFileName}. Cause: 'effect' has invalid value: '${allowErrorPolicy[3]}'. It should be: 'allow' or 'deny'`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 3, + `Unable to add policy ${restPermission} from file ${csvFileName}. Cause: source does not match originating role ${restPermission[0]}, consider making changes to the 'REST'`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 4, + `Unable to add policy ${configPermission} from file ${csvFileName}. Cause: source does not match originating role ${configPermission[0]}, consider making changes to the 'CONFIGURATION'`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 5, + `Failed to validate group policy ${entityRoleError}. Cause: Entity reference "${entityRoleError[0]}" was not on the form [:][/], error originates from file ${csvFileName}`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 6, + `Failed to validate group policy ${roleError}. Cause: Entity reference "${roleError[1]}" was not on the form [:][/], error originates from file ${csvFileName}`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 7, + `Unable to validate role ${restRole}. Cause: source does not match originating role ${restRole[1]}, consider making changes to the 'REST', error originates from file ${csvFileName}`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 8, + `Unable to validate role ${configRole}. Cause: source does not match originating role ${configRole[1]}, consider making changes to the 'CONFIGURATION', error originates from file ${csvFileName}`, + ); + }); + }); + + describe('onChange', () => { + let csvFileWatcher: CSVFileWatcher; + + beforeEach(async () => { + csvFileName = resolve( + __dirname, + '../../__fixtures__/data/valid-csv/simple-policy.csv', + ); + csvFileWatcher = createCSVFileWatcher(csvFileName); + await csvFileWatcher.initialize(); + }); + + 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 fail to add new permission policies on change if there is a mismatch in source', async () => { + const addContents = [ + ['g', 'user:default/guest', 'role:default/catalog-writer'], + [ + 'p', + 'role:default/catalog-writer', + 'catalog-entity', + 'update', + 'allow', + ], + ['p', 'role:default/config', 'catalog-entity', 'update', 'allow'], + ['p', 'role:default/rest', 'catalog-entity', 'update', 'allow'], + ]; + + const policies = [ + ['role:default/catalog-writer', 'catalog-entity', 'update', 'allow'], + ['role:default/config', 'catalog-entity', 'update', 'allow'], + ['role:default/rest', 'catalog-entity', 'update', 'allow'], + ]; + + await enforcerDelegate.addPolicy(configPermission); + await enforcerDelegate.addPolicy(restPermission); + + csvFileWatcher.parse = jest.fn().mockImplementation(() => { + return addContents; + }); + + await csvFileWatcher.onChange(); + + const enfPolicies = await enforcerDelegate.getPolicy(); + + expect(enfPolicies).toStrictEqual(policies); + + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 1, + `Unable to add policy ${configPermission} from file ${csvFileName}. Cause: source does not match originating role ${configPermission[0]}, consider making changes to the 'CONFIGURATION'`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 2, + `Unable to add policy ${restPermission} from file ${csvFileName}. Cause: source does not match originating role ${restPermission[0]}, consider making changes to the 'REST'`, + ); + }); + + it('should fail to add new roles on change if there is a mismatch in source', async () => { + const addContents = [ + ['g', 'user:default/guest', 'role:default/catalog-writer'], + ['g', 'user:default/guest', 'role:default/rest'], + ['g', 'user:default/guest', 'role:default/config'], + ]; + + const roles = [ + ['user:default/guest', 'role:default/catalog-writer'], + ['user:default/guest', 'role:default/config'], + ['user:default/guest', 'role:default/rest'], + ]; + + await enforcerDelegate.addGroupingPolicy(configRole, { + roleEntityRef: configRole[1], + source: 'configuration', + modifiedBy: ADMIN_ROLE_AUTHOR, + }); + await enforcerDelegate.addGroupingPolicy(restRole, { + roleEntityRef: restRole[1], + source: 'rest', + modifiedBy, + }); + + csvFileWatcher.parse = jest.fn().mockImplementation(() => { + return addContents; + }); + + await csvFileWatcher.onChange(); + + const enfRoles = await enforcerDelegate.getGroupingPolicy(); + + expect(enfRoles).toStrictEqual(roles); + + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 1, + `Unable to validate role ${restRole}. Cause: source does not match originating role ${restRole[1]}, consider making changes to the 'REST', error originates from file ${csvFileName}`, + ); + expect(mockLoggerService.warn).toHaveBeenNthCalledWith( + 2, + `Unable to validate role ${configRole}. Cause: source does not match originating role ${configRole[1]}, consider making changes to the 'CONFIGURATION', error originates from file ${csvFileName}`, + ); + }); + + 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'], + ]); + }); + }); + + describe('cleanUpRolesAndPolicies', () => { + let csvFileWatcher: CSVFileWatcher; + + const roleMetadata: RoleMetadataDao = { + roleEntityRef: 'role:default/dev', + source: 'csv-file', + modifiedBy, + }; + + beforeEach(async () => { + csvFileWatcher = createCSVFileWatcher(); + await csvFileWatcher.initialize(); + }); + + it('should remove all roles and policies', async () => { + const permissionPolicies = [ + ['role:default/dev', 'catalog-entity', 'update', 'allow'], + ['role:default/dev', 'catalog-entity', 'allow', 'allow'], + ]; + + await enforcerDelegate.addPolicies(permissionPolicies); + await enforcerDelegate.addGroupingPolicies( + [['user:default/guest', 'role:default/dev']], + roleMetadata, + ); + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation((source: Source) => { + if (source === 'csv-file') { + return [roleMetadata]; + } + return []; + }); + + await csvFileWatcher.cleanUpRolesAndPolicies(); + const enfPolicies = await enforcerDelegate.getPolicy(); + + expect(enfPolicies).toStrictEqual([]); + + const enfRoles = await enforcerDelegate.getGroupingPolicy(); + expect(enfRoles).toStrictEqual([]); + + expect( + roleMetadataStorageMock.removeRoleMetadata, + ).toHaveBeenNthCalledWith( + 1, + roleMetadata.roleEntityRef, + expect.anything(), + ); + }); + }); +}); + +async function createEnforcer( + theModel: Model, + adapter: Adapter, + logger: LoggerService, +): Promise { + const catalogDBClient = Knex.knex({ client: MockClient }); + const rbacDBClient = Knex.knex({ client: MockClient }); + const enf = await newEnforcer(theModel, adapter); + + const config = newConfig(); + + const rm = new BackstageRoleManager( + catalogApiMock, + logger, + catalogDBClient, + rbacDBClient, + config, + mockAuthService, + ); + enf.setRoleManager(rm); + enf.enableAutoBuildRoleLinks(false); + await enf.buildRoleLinks(); + + return enf; +} + +function newConfig( + users?: Array<{ name: string }>, + superUsers?: Array<{ name: string }>, +): Config { + const testUsers = [ + { + name: 'user:default/guest', + }, + { + name: 'group:default/guests', + }, + ]; + + return mockServices.rootConfig({ + data: { + permission: { + rbac: { + admin: { + users: users || testUsers, + superUsers: superUsers, + }, + }, + }, + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + }, + }); +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/file-permissions/csv-file-watcher.ts b/workspaces/rbac/plugins/rbac-backend/src/file-permissions/csv-file-watcher.ts new file mode 100644 index 0000000000..7d7e402ad2 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/file-permissions/csv-file-watcher.ts @@ -0,0 +1,518 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { LoggerService } from '@backstage/backend-plugin-api'; + +import type { AuditLogger } from '@janus-idp/backstage-plugin-audit-log-node'; +import { Enforcer, FileAdapter, newEnforcer, newModelFromString } from 'casbin'; +import { parse } from 'csv-parse/sync'; +import { difference } from 'lodash'; + +import { + HANDLE_RBAC_DATA_STAGE, + PermissionAuditInfo, + PermissionEvents, + RBAC_BACKEND, + RoleAuditInfo, + RoleEvents, +} from '../audit-log/audit-logger'; +import { + RoleMetadataDao, + RoleMetadataStorage, +} from '../database/role-metadata'; +import { + mergeRoleMetadata, + metadataStringToPolicy, + policyToString, + transformArrayToPolicy, +} from '../helper'; +import { EnforcerDelegate } from '../service/enforcer-delegate'; +import { MODEL } from '../service/permission-model'; +import { + checkForDuplicateGroupPolicies, + checkForDuplicatePolicies, + validateGroupingPolicy, + validatePolicy, + validateSource, +} from '../validation/policies-validation'; +import { AbstractFileWatcher } from './file-watcher'; + +export const CSV_PERMISSION_POLICY_FILE_AUTHOR = 'csv permission policy file'; + +type CSVFilePolicies = { + addedPolicies: string[][]; + addedGroupPolicies: string[][]; + removedPolicies: string[][]; + removedGroupPolicies: string[][]; +}; + +export class CSVFileWatcher extends AbstractFileWatcher { + private currentContent: string[][]; + private csvFilePolicies: CSVFilePolicies; + + constructor( + filePath: string | undefined, + allowReload: boolean, + logger: LoggerService, + private readonly enforcer: EnforcerDelegate, + private readonly roleMetadataStorage: RoleMetadataStorage, + private readonly auditLogger: AuditLogger, + ) { + super(filePath, allowReload, logger); + this.currentContent = []; + this.csvFilePolicies = { + addedPolicies: [], + addedGroupPolicies: [], + removedPolicies: [], + removedGroupPolicies: [], + }; + } + + /** + * 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; + } + + /** + * 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(): Promise { + if (!this.filePath) { + return; + } + let content: string[][] = []; + // If the file is set load the file contents + content = this.parse(); + + const tempEnforcer = await newEnforcer( + newModelFromString(MODEL), + new FileAdapter(this.filePath), + ); + + // 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 roleMetadatas = await this.roleMetadataStorage.filterRoleMetadata( + 'csv-file', + ); + const fileRoles = roleMetadatas.map(meta => meta.roleEntityRef); + + if (fileRoles.length > 0) { + const groupingPoliciesToRemove = + await this.enforcer.getFilteredGroupingPolicy(1, ...fileRoles); + for (const gPolicy of groupingPoliciesToRemove) { + if (!(await tempEnforcer.hasGroupingPolicy(...gPolicy))) { + this.csvFilePolicies.removedGroupPolicies.push(gPolicy); + } + } + const policiesToRemove = await this.enforcer.getFilteredPolicy( + 0, + ...fileRoles, + ); + for (const policy of policiesToRemove) { + if (!(await tempEnforcer.hasPolicy(...policy))) { + this.csvFilePolicies.removedPolicies.push(policy); + } + } + } + + // 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); + } + } + + await this.migrateLegacyMetadata(tempEnforcer); + + // We pass current here because this is during initialization and it has not changed yet + await this.updatePolicies(content, tempEnforcer); + + if (this.allowReload) { + this.watchFile(); + } + } + + // Check for policies that might need to be updated + // This will involve update "legacy" source in the role metadata if it exist in both the + // temp enforcer (csv file) and a role metadata storage. + // We will update role metadata with the new source "csv-file" + private async migrateLegacyMetadata(tempEnforcer: Enforcer) { + let legacyRolesMetadata = await this.roleMetadataStorage.filterRoleMetadata( + 'legacy', + ); + const legacyRoles = legacyRolesMetadata.map(meta => meta.roleEntityRef); + if (legacyRoles.length > 0) { + const legacyGroupPolicies = await tempEnforcer.getFilteredGroupingPolicy( + 1, + ...legacyRoles, + ); + const legacyPolicies = await tempEnforcer.getFilteredPolicy( + 0, + ...legacyRoles, + ); + const legacyRolesFromFile = new Set([ + ...legacyGroupPolicies.map(gp => gp[1]), + ...legacyPolicies.map(p => p[0]), + ]); + legacyRolesMetadata = legacyRolesMetadata.filter(meta => + legacyRolesFromFile.has(meta.roleEntityRef), + ); + for (const legacyRoleMeta of legacyRolesMetadata) { + const nonLegacyRole = mergeRoleMetadata(legacyRoleMeta, { + modifiedBy: CSV_PERMISSION_POLICY_FILE_AUTHOR, + source: 'csv-file', + roleEntityRef: legacyRoleMeta.roleEntityRef, + }); + await this.roleMetadataStorage.updateRoleMetadata( + nonLegacyRole, + legacyRoleMeta.roleEntityRef, + ); + } + } + } + + /** + * 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 newContent = this.parse(); + + const tempEnforcer = await newEnforcer( + newModelFromString(MODEL), + new FileAdapter(this.filePath!), + ); + + 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 + + await this.migrateLegacyMetadata(tempEnforcer); + + 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 + */ + private 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 + */ + private async addPermissionPolicies(tempEnforcer: Enforcer): Promise { + for (const policy of this.csvFilePolicies.addedPolicies) { + const transformedPolicy = transformArrayToPolicy(policy); + const metadata = await this.roleMetadataStorage.findRoleMetadata( + policy[0], + ); + + let err = validatePolicy(transformedPolicy); + if (err) { + this.logger.warn( + `Failed to validate policy from file ${this.filePath}. Cause: ${err.message}`, + ); + continue; + } + + err = await validateSource('csv-file', metadata); + if (err) { + this.logger.warn( + `Unable to add policy ${policy} from file ${this.filePath}. Cause: ${err.message}`, + ); + continue; + } + + err = await checkForDuplicatePolicies( + tempEnforcer, + policy, + this.filePath!, + ); + if (err) { + this.logger.warn(err.message); + continue; + } + try { + await this.enforcer.addPolicy(policy); + + await this.auditLogger.auditLog({ + actorId: RBAC_BACKEND, + message: `Created policy`, + eventName: PermissionEvents.CREATE_POLICY, + metadata: { policies: [policy], source: 'csv-file' }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); + } catch (e) { + this.logger.warn( + `Failed to add or update policy ${policy} after modification ${this.filePath}. Cause: ${e}`, + ); + } + } + + this.csvFilePolicies.addedPolicies = []; + } + + /** + * removePermissionPolicies will remove the permission policies that are no longer present in the CSV file. + */ + private async removePermissionPolicies(): Promise { + try { + await this.enforcer.removePolicies(this.csvFilePolicies.removedPolicies); + + await this.auditLogger.auditLog({ + actorId: RBAC_BACKEND, + message: `Deleted policies`, + eventName: PermissionEvents.DELETE_POLICY, + metadata: { + policies: this.csvFilePolicies.removedPolicies, + source: 'csv-file', + }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); + } catch (e) { + this.logger.warn( + `Failed to remove policies ${JSON.stringify( + this.csvFilePolicies.removedPolicies, + )} after modification ${this.filePath}. 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 + */ + private async addRoles(tempEnforcer: Enforcer): Promise { + for (const groupPolicy of this.csvFilePolicies.addedGroupPolicies) { + let err = await validateGroupingPolicy( + groupPolicy, + this.roleMetadataStorage, + 'csv-file', + ); + if (err) { + this.logger.warn( + `${err.message}, error originates from file ${this.filePath}`, + ); + continue; + } + + err = await checkForDuplicateGroupPolicies( + tempEnforcer, + groupPolicy, + this.filePath!, + ); + if (err) { + this.logger.warn(err.message); + continue; + } + + try { + const roleMetadata: RoleMetadataDao = { + source: 'csv-file', + roleEntityRef: groupPolicy[1], + author: CSV_PERMISSION_POLICY_FILE_AUTHOR, + modifiedBy: CSV_PERMISSION_POLICY_FILE_AUTHOR, + }; + + const currentMetadata = await this.roleMetadataStorage.findRoleMetadata( + roleMetadata.roleEntityRef, + ); + + await this.enforcer.addGroupingPolicy(groupPolicy, roleMetadata); + + const eventName = currentMetadata + ? RoleEvents.UPDATE_ROLE + : RoleEvents.CREATE_ROLE; + const message = currentMetadata ? 'Updated role' : 'Created role'; + await this.auditLogger.auditLog({ + actorId: RBAC_BACKEND, + message, + eventName, + metadata: { ...roleMetadata, members: [groupPolicy[0]] }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); + } catch (e) { + this.logger.warn( + `Failed to add or update group policy ${groupPolicy} after modification ${this.filePath}. 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. + */ + private async removeRoles(): Promise { + for (const groupPolicy of this.csvFilePolicies.removedGroupPolicies) { + const roleEntityRef = groupPolicy[1]; + // this requires knowledge of whether or not it is an update + const isUpdate = await this.enforcer.getFilteredGroupingPolicy( + 1, + roleEntityRef, + ); + + // Need to update the time + try { + const metadata: RoleMetadataDao = { + source: 'csv-file', + roleEntityRef, + author: CSV_PERMISSION_POLICY_FILE_AUTHOR, + modifiedBy: CSV_PERMISSION_POLICY_FILE_AUTHOR, + }; + + await this.enforcer.removeGroupingPolicy( + groupPolicy, + metadata, + isUpdate.length > 1, + ); + + const isRolePresent = await this.roleMetadataStorage.findRoleMetadata( + roleEntityRef, + ); + const eventName = isRolePresent + ? RoleEvents.UPDATE_ROLE + : RoleEvents.DELETE_ROLE; + const message = isRolePresent + ? 'Updated role: deleted members' + : 'Deleted role'; + await this.auditLogger.auditLog({ + actorId: RBAC_BACKEND, + message, + eventName, + metadata: { ...metadata, members: [groupPolicy[0]] }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); + } catch (e) { + this.logger.warn( + `Failed to remove group policy ${groupPolicy} after modification ${this.filePath}. Cause: ${e}`, + ); + } + } + this.csvFilePolicies.removedGroupPolicies = []; + } + + async cleanUpRolesAndPolicies(): Promise { + const roleMetadatas = await this.roleMetadataStorage.filterRoleMetadata( + 'csv-file', + ); + const fileRoles = roleMetadatas.map(meta => meta.roleEntityRef); + + if (fileRoles.length > 0) { + for (const fileRole of fileRoles) { + this.csvFilePolicies.removedGroupPolicies.push( + ...(await this.enforcer.getFilteredGroupingPolicy(1, fileRole)), + ); + this.csvFilePolicies.removedPolicies.push( + ...(await this.enforcer.getFilteredPolicy(0, fileRole)), + ); + } + } + await this.removePermissionPolicies(); + await this.removeRoles(); + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/file-permissions/file-watcher.ts b/workspaces/rbac/plugins/rbac-backend/src/file-permissions/file-watcher.ts new file mode 100644 index 0000000000..06debeb1ed --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/file-permissions/file-watcher.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { LoggerService } from '@backstage/backend-plugin-api'; + +import chokidar from 'chokidar'; + +import fs from 'fs'; + +/** + * Represents a file watcher that can be used to monitor changes in a file. + */ +export abstract class AbstractFileWatcher { + constructor( + protected readonly filePath: string | undefined, + protected readonly allowReload: boolean, + protected readonly logger: LoggerService, + ) {} + + /** + * Initializes the file watcher and starts watching the specified file. + */ + abstract initialize(): Promise; + + /** + * watchFile initializes the file watcher and sets it to begin watching for changes. + */ + watchFile(): void { + if (!this.filePath) { + throw new Error('File path is not specified'); + } + const watcher = chokidar.watch(this.filePath); + watcher.on('change', async path => { + this.logger.info(`file ${path} has changed`); + await this.onChange(); + }); + watcher.on('error', error => { + this.logger.error(`error watching file ${this.filePath}: ${error}`); + }); + } + + /** + * Handles the change event when the watched file is modified. + * @returns A promise that resolves when the change event is handled. + */ + abstract onChange(): Promise; + + /** + * getCurrentContents reads the current contents of the CSV file. + * @returns The current contents of the file. + */ + getCurrentContents(): string { + if (!this.filePath) { + throw new Error('File path is not specified'); + } + return fs.readFileSync(this.filePath, 'utf-8'); + } + + /** + * parse is used to parse the current contents of the file. + * @returns The file parsed into a type . + */ + abstract parse(): T; +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/file-permissions/yaml-conditional-file-watcher.test.ts b/workspaces/rbac/plugins/rbac-backend/src/file-permissions/yaml-conditional-file-watcher.test.ts new file mode 100644 index 0000000000..dc541a1222 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/file-permissions/yaml-conditional-file-watcher.test.ts @@ -0,0 +1,471 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { mockServices } from '@backstage/backend-test-utils'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import type { MetadataResponse } from '@backstage/plugin-permission-node'; + +import { resolve } from 'path'; + +import { ConditionEvents } from '../audit-log/audit-logger'; +import { DataBaseConditionalStorage } from '../database/conditional-storage'; +import { + RoleMetadataDao, + RoleMetadataStorage, +} from '../database/role-metadata'; +import { RoleEventEmitter, RoleEvents } from '../service/enforcer-delegate'; +import { PluginPermissionMetadataCollector } from '../service/plugin-endpoints'; +import { YamlConditinalPoliciesFileWatcher } from './yaml-conditional-file-watcher'; // Adjust the import path as necessary + +const mockLoggerService = mockServices.logger.mock(); + +let loggerWarnSpy: jest.SpyInstance; + +const conditionalStorageMock: Partial = { + filterConditions: jest.fn().mockImplementation(), + createCondition: jest.fn().mockImplementation(), + checkConflictedConditions: jest.fn().mockImplementation(), + getCondition: jest.fn().mockImplementation(), + deleteCondition: jest.fn().mockImplementation(), + updateCondition: jest.fn().mockImplementation(), +}; + +const auditLoggerMock = { + getActorId: jest.fn().mockImplementation(), + createAuditLogDetails: jest.fn().mockImplementation(), + auditLog: jest.fn().mockImplementation(), +}; + +const mockAuthService = mockServices.auth(); + +const testPluginMetadataResp: MetadataResponse = { + permissions: [ + { + type: 'resource', + name: 'catalog.entity.read', + attributes: { + action: 'read', + }, + resourceType: 'catalog-entity', + }, + { + type: 'basic', + name: 'catalog.entity.create', + attributes: { + action: 'create', + }, + }, + { + type: 'resource', + name: 'catalog.entity.delete', + attributes: { + action: 'delete', + }, + resourceType: 'catalog-entity', + }, + { + type: 'resource', + name: 'catalog.entity.refresh', + attributes: { + action: 'update', + }, + resourceType: 'catalog-entity', + }, + ], + rules: [ + { + name: 'IS_ENTITY_OWNER', + description: 'Allow entities owned by a specified claim', + resourceType: 'catalog-entity', + paramsSchema: { + type: 'object', + properties: { + claims: { + type: 'array', + items: { + type: 'string', + }, + description: + 'List of claims to match at least one on within ownedBy', + }, + }, + required: ['claims'], + additionalProperties: false, + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + ], +}; + +const conditionToStore1 = { + result: AuthorizeResult.CONDITIONAL, + roleEntityRef: 'role:default/test', + pluginId: 'catalog', + resourceType: 'catalog-entity', + permissionMapping: [{ name: 'catalog.entity.refresh', action: 'update' }], + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['group:default/team-a'], + }, + }, +}; + +const conditionToStore2 = { + result: AuthorizeResult.CONDITIONAL, + roleEntityRef: 'role:default/test', + pluginId: 'catalog', + resourceType: 'catalog-entity', + permissionMapping: [ + { name: 'catalog.entity.read', action: 'read' }, + { name: 'catalog.entity.delete', action: 'delete' }, + ], + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['group:default/team-a', 'group:default/team-b'], + }, + }, +}; + +const conditionToRemove = { + id: 2, + result: AuthorizeResult.CONDITIONAL, + roleEntityRef: 'role:default/dev', + pluginId: 'catalog', + resourceType: 'catalog-entity', + permissionMapping: [{ name: 'catalog.entity.read', action: 'read' }], + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['group:default/team-dev'], + }, + }, +}; + +const pluginMetadataCollectorMock: Partial = + { + getPluginConditionRules: jest.fn().mockImplementation(), + getPluginPolicies: jest.fn().mockImplementation(), + getMetadataByPluginId: jest + .fn() + .mockImplementation(async () => testPluginMetadataResp), + }; + +const roleMetadataStorageMock: RoleMetadataStorage = { + filterRoleMetadata: jest.fn().mockImplementation(() => []), + findRoleMetadata: jest.fn().mockImplementation(), + createRoleMetadata: jest.fn().mockImplementation(), + updateRoleMetadata: jest.fn().mockImplementation(), + removeRoleMetadata: jest.fn().mockImplementation(), +}; + +const roleEventEmitterMock: RoleEventEmitter = { + on: jest.fn().mockImplementation(), +}; + +describe('YamlConditionalFileWatcher', () => { + let csvFileName: string; + + const csvFileRoles: RoleMetadataDao[] = [ + { + roleEntityRef: 'role:default/test', + source: 'csv-file', + author: 'user:default/tom', + modifiedBy: 'user:default/tom', + createdAt: '2021-09-01T00:00:00Z', + }, + ]; + + beforeEach(() => { + csvFileName = resolve( + __dirname, + '../../__fixtures__/data/valid-conditions/conditions.yaml', + ); + + loggerWarnSpy = jest.spyOn(mockLoggerService, 'warn'); + + auditLoggerMock.auditLog.mockClear(); + conditionalStorageMock.createCondition = jest.fn().mockImplementation(); + conditionalStorageMock.deleteCondition = jest.fn().mockImplementation(); + loggerWarnSpy.mockClear(); + }); + + function createWatcher(filePath?: string): YamlConditinalPoliciesFileWatcher { + return new YamlConditinalPoliciesFileWatcher( + filePath, + false, + mockLoggerService, + conditionalStorageMock as DataBaseConditionalStorage, + auditLoggerMock, + mockAuthService, + pluginMetadataCollectorMock as PluginPermissionMetadataCollector, + roleMetadataStorageMock, + roleEventEmitterMock, + ); + } + + test('handles errors for invalid file paths', async () => { + const invalidFilePath = 'invalid-file-path.yaml'; + const watcher = createWatcher(invalidFilePath); + await watcher.initialize(); + + const auditEvents = auditLoggerMock.auditLog.mock.calls; + expect(auditEvents.length).toBe(1); + expect(auditEvents[0][0].message).toBe( + `File '${invalidFilePath}' was not found`, + ); + }); + + test('handles error on parse invalid yaml file', async () => { + const invalidFilePath = resolve( + __dirname, + '../../__fixtures__/data/invalid-conditions/invalid-yaml.yaml', + ); + const watcher = createWatcher(invalidFilePath); + await watcher.initialize(); + + const auditEvents = auditLoggerMock.auditLog.mock.calls; + expect(auditEvents.length).toBe(1); + expect(auditEvents[0][0].message).toBe( + `Error handling changes from conditional policies file ${invalidFilePath}`, + ); + expect(auditEvents[0][0].errors[0].message).toBe( + `'roleEntityRef' must be specified in the role condition`, + ); + }); + + test('should handle error on create condition', async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation(() => []); + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation(() => csvFileRoles); + conditionalStorageMock.createCondition = jest + .fn() + .mockImplementation(() => { + throw new Error('unknow error message'); + }); + + const watcher = createWatcher(csvFileName); + await watcher.initialize(); + + expect(conditionalStorageMock.createCondition).toHaveBeenCalled(); + + const auditEvents: any[] = auditLoggerMock.auditLog.mock.calls; + expect(auditEvents.length).toBe(1); + expect(auditEvents[0][0].eventName).toBe( + ConditionEvents.CREATE_CONDITION_ERROR, + ); + expect(auditEvents[0][0].message).toBe(`Failed to create condition`); + }); + + test('should add conditional policies from the file on initialization', async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation(() => []); + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation(() => csvFileRoles); + + const watcher = createWatcher(csvFileName); + await watcher.initialize(); + + expect(conditionalStorageMock.createCondition).toHaveBeenCalledWith( + conditionToStore1, + ); + expect(conditionalStorageMock.createCondition).toHaveBeenCalledWith( + conditionToStore2, + ); + const auditEvents: any[] = auditLoggerMock.auditLog.mock.calls; + expect(auditEvents.length).toBe(2); + expect(auditEvents[0][0].eventName).toBe(ConditionEvents.CREATE_CONDITION); + expect(auditEvents[1][0].eventName).toBe(ConditionEvents.CREATE_CONDITION); + }); + + test('should not fail on initialization, when conditional policies contains empty array', async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation(() => []); + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation(() => csvFileRoles); + + csvFileName = resolve( + __dirname, + '../../__fixtures__/data/valid-conditions/empty-conditions.yaml', + ); + const watcher = createWatcher(csvFileName); + await watcher.initialize(); + + expect(conditionalStorageMock.createCondition).not.toHaveBeenCalled(); + const auditEvents: any[] = auditLoggerMock.auditLog.mock.calls; + expect(auditEvents.length).toBe(0); + }); + + test(`should not apply conditions if corresponding role is present, but with non 'csv-file' source`, async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation(() => []); + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation(() => [ + { + ...csvFileRoles[0], + source: 'rest', + }, + ]); + + const watcher = createWatcher(csvFileName); + await watcher.initialize(); + + expect(conditionalStorageMock.createCondition).not.toHaveBeenCalled(); + + const auditEvents: any[] = auditLoggerMock.auditLog.mock.calls; + expect(auditEvents.length).toBe(0); + expect(loggerWarnSpy).toHaveBeenNthCalledWith( + 1, + `skip to add condition for role 'role:default/test'. Role is not from csv-file`, + ); + expect(loggerWarnSpy).toHaveBeenNthCalledWith( + 2, + `skip to add condition for role 'role:default/test'. Role is not from csv-file`, + ); + }); + + test('should not apply conditions if corresponding role is absent', async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation(() => []); + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation(() => []); + + const watcher = createWatcher(csvFileName); + await watcher.initialize(); + + expect(conditionalStorageMock.createCondition).not.toHaveBeenCalled(); + + const auditEvents: any[] = auditLoggerMock.auditLog.mock.calls; + expect(auditEvents.length).toBe(0); + expect(loggerWarnSpy).toHaveBeenNthCalledWith( + 1, + `skip to add condition for role 'role:default/test'. The role either does not exist or was not created from a CSV file.`, + ); + expect(loggerWarnSpy).toHaveBeenNthCalledWith( + 2, + `skip to add condition for role 'role:default/test'. The role either does not exist or was not created from a CSV file.`, + ); + }); + + test('should remove conditions, which is not included to yaml any more', async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation(() => [conditionToRemove]); + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation(() => []); + + const watcher = createWatcher(csvFileName); + await watcher.initialize(); + + expect(conditionalStorageMock.createCondition).not.toHaveBeenCalled(); + + const auditEvents: any[] = auditLoggerMock.auditLog.mock.calls; + expect(auditEvents.length).toBe(1); + expect(auditEvents[0][0].eventName).toBe(ConditionEvents.DELETE_CONDITION); + expect(auditEvents[0][0].message).toBe( + `Deleted conditional permission policy`, + ); + expect(conditionalStorageMock.deleteCondition).toHaveBeenCalledWith(2); + }); + + test('should handle error on delete condition', async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation(() => [conditionToRemove]); + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation(() => []); + conditionalStorageMock.deleteCondition = jest + .fn() + .mockImplementation(() => { + throw new Error('unknow error message'); + }); + + const watcher = createWatcher(csvFileName); + await watcher.initialize(); + + expect(conditionalStorageMock.createCondition).not.toHaveBeenCalled(); + + expect(conditionalStorageMock.deleteCondition).toHaveBeenCalled(); + const auditEvents: any[] = auditLoggerMock.auditLog.mock.calls; + expect(auditEvents.length).toBe(1); + expect(auditEvents[0][0].eventName).toBe( + ConditionEvents.DELETE_CONDITION_ERROR, + ); + expect(auditEvents[0][0].message).toBe(`Failed to delete condition by id`); + }); + + test('should clean up conditions if conditional file was not specified', async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation(() => [conditionToRemove]); + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation(() => csvFileRoles); + + const watcher = createWatcher(); + await watcher.initialize(); + await watcher.cleanUpConditionalPolicies(); + + expect(conditionalStorageMock.createCondition).not.toHaveBeenCalled(); + + const auditEvents: any[] = auditLoggerMock.auditLog.mock.calls; + expect(auditEvents.length).toBe(1); + expect(auditEvents[0][0].eventName).toBe(ConditionEvents.DELETE_CONDITION); + expect(auditEvents[0][0].message).toBe( + `Deleted conditional permission policy`, + ); + expect(conditionalStorageMock.deleteCondition).toHaveBeenNthCalledWith( + 1, + 2, + ); + }); + + test('should not clean up conditions if list contidions is empty', async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation(() => []); + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation(() => csvFileRoles); + + const watcher = createWatcher(); + await watcher.initialize(); + await watcher.cleanUpConditionalPolicies(); + + expect(conditionalStorageMock.createCondition).not.toHaveBeenCalled(); + + const auditEvents: any[] = auditLoggerMock.auditLog.mock.calls; + expect(auditEvents.length).toBe(0); + + expect(conditionalStorageMock.deleteCondition).not.toHaveBeenCalled(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/file-permissions/yaml-conditional-file-watcher.ts b/workspaces/rbac/plugins/rbac-backend/src/file-permissions/yaml-conditional-file-watcher.ts new file mode 100644 index 0000000000..6dfa19f95f --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/file-permissions/yaml-conditional-file-watcher.ts @@ -0,0 +1,283 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { AuthService, LoggerService } from '@backstage/backend-plugin-api'; + +import type { AuditLogger } from '@janus-idp/backstage-plugin-audit-log-node'; +import yaml from 'js-yaml'; +import { omit } from 'lodash'; + +import type { + PermissionAction, + RoleConditionalPolicyDecision, +} from '@backstage-community/plugin-rbac-common'; + +import fs from 'fs'; + +import { + ConditionAuditInfo, + ConditionEvents, + HANDLE_RBAC_DATA_STAGE, +} from '../audit-log/audit-logger'; +import { ConditionalStorage } from '../database/conditional-storage'; +import { RoleMetadataStorage } from '../database/role-metadata'; +import { deepSortEqual, processConditionMapping } from '../helper'; +import { RoleEventEmitter, RoleEvents } from '../service/enforcer-delegate'; +import { PluginPermissionMetadataCollector } from '../service/plugin-endpoints'; +import { validateRoleCondition } from '../validation/condition-validation'; +import { AbstractFileWatcher } from './file-watcher'; + +type ConditionalPoliciesDiff = { + addedConditions: RoleConditionalPolicyDecision[]; + removedConditions: RoleConditionalPolicyDecision[]; +}; + +export class YamlConditinalPoliciesFileWatcher extends AbstractFileWatcher< + RoleConditionalPolicyDecision[] +> { + private conditionsDiff: ConditionalPoliciesDiff; + + constructor( + filePath: string | undefined, + allowReload: boolean, + logger: LoggerService, + private readonly conditionalStorage: ConditionalStorage, + private readonly auditLogger: AuditLogger, + private readonly auth: AuthService, + private readonly pluginMetadataCollector: PluginPermissionMetadataCollector, + private readonly roleMetadataStorage: RoleMetadataStorage, + private readonly roleEventEmitter: RoleEventEmitter, + ) { + super(filePath, allowReload, logger); + + this.conditionsDiff = { + addedConditions: [], + removedConditions: [], + }; + } + + async initialize(): Promise { + if (!this.filePath) { + return; + } + const fileExists = fs.existsSync(this.filePath); + if (!fileExists) { + const err = new Error(`File '${this.filePath}' was not found`); + this.handleError( + err.message, + err, + ConditionEvents.CONDITIONAL_POLICIES_FILE_NOT_FOUND, + ); + return; + } + + this.roleEventEmitter.on('roleAdded', this.onChange.bind(this)); + await this.onChange(); + + if (this.allowReload) { + this.watchFile(); + } + } + + async onChange(): Promise { + try { + const newConds = this.parse().filter(c => c); + + const addedConds: RoleConditionalPolicyDecision[] = []; + const removedConds: RoleConditionalPolicyDecision[] = + []; + + const csvFileRoles = await this.roleMetadataStorage.filterRoleMetadata( + 'csv-file', + ); + const existedFileConds = ( + await this.conditionalStorage.filterConditions( + csvFileRoles.map(role => role.roleEntityRef), + ) + ).map(condition => { + return { + ...condition, + permissionMapping: condition.permissionMapping.map(pm => pm.action), + }; + }); + + // Find added conditions + for (const condition of newConds) { + const roleMetadata = csvFileRoles.find( + role => condition.roleEntityRef === role.roleEntityRef, + ); + if (!roleMetadata) { + this.logger.warn( + `skip to add condition for role '${condition.roleEntityRef}'. The role either does not exist or was not created from a CSV file.`, + ); + continue; + } + if (roleMetadata.source !== 'csv-file') { + this.logger.warn( + `skip to add condition for role '${condition.roleEntityRef}'. Role is not from csv-file`, + ); + continue; + } + + const existingCondition = existedFileConds.find(c => + deepSortEqual(omit(c, ['id']), omit(condition, ['id'])), + ); + + if (!existingCondition) { + addedConds.push(condition); + } + } + + // Find removed conditions + for (const condition of existedFileConds) { + if ( + !newConds.find(c => + deepSortEqual(omit(c, ['id']), omit(condition, ['id'])), + ) + ) { + removedConds.push(condition); + } + } + + this.conditionsDiff = { + addedConditions: addedConds, + removedConditions: removedConds, + }; + + await this.handleFileChanges(); + } catch (error) { + await this.handleError( + `Error handling changes from conditional policies file ${this.filePath}`, + error, + ConditionEvents.CHANGE_CONDITIONAL_POLICIES_FILE_ERROR, + ); + } + } + + /** + * Reads the current contents of the file and parses it. + * @returns parsed data. + */ + parse(): RoleConditionalPolicyDecision[] { + const fileContents = this.getCurrentContents(); + const data = yaml.loadAll( + fileContents, + ) as RoleConditionalPolicyDecision[]; + + for (const condition of data) { + validateRoleCondition(condition); + } + + return data; + } + + private async handleFileChanges(): Promise { + await this.removeConditions(); + await this.addConditions(); + } + + private async addConditions(): Promise { + try { + for (const condition of this.conditionsDiff.addedConditions) { + const conditionToCreate = await processConditionMapping( + condition, + this.pluginMetadataCollector, + this.auth, + ); + + await this.conditionalStorage.createCondition(conditionToCreate); + + await this.auditLogger.auditLog({ + message: `Created conditional permission policy`, + eventName: ConditionEvents.CREATE_CONDITION, + metadata: { condition }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); + } + } catch (error) { + await this.handleError( + 'Failed to create condition', + error, + ConditionEvents.CREATE_CONDITION_ERROR, + ); + } + this.conditionsDiff.addedConditions = []; + } + + private async removeConditions(): Promise { + try { + for (const condition of this.conditionsDiff.removedConditions) { + const conditionToDelete = ( + await this.conditionalStorage.filterConditions( + condition.roleEntityRef, + condition.pluginId, + condition.resourceType, + condition.permissionMapping, + ) + )[0]; + await this.conditionalStorage.deleteCondition(conditionToDelete.id!); + + await this.auditLogger.auditLog({ + message: `Deleted conditional permission policy`, + eventName: ConditionEvents.DELETE_CONDITION, + metadata: { condition }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); + } + } catch (error) { + await this.handleError( + 'Failed to delete condition by id', + error, + ConditionEvents.DELETE_CONDITION_ERROR, + ); + } + + this.conditionsDiff.removedConditions = []; + } + + private async handleError( + message: string, + error: unknown, + event: string, + ): Promise { + await this.auditLogger.auditLog({ + message, + eventName: event, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'failed', + errors: [error], + }); + } + + async cleanUpConditionalPolicies(): Promise { + const csvFileRoles = await this.roleMetadataStorage.filterRoleMetadata( + 'csv-file', + ); + const existedFileConds = ( + await this.conditionalStorage.filterConditions( + csvFileRoles.map(role => role.roleEntityRef), + ) + ).map(condition => { + return { + ...condition, + permissionMapping: condition.permissionMapping.map(pm => pm.action), + }; + }); + this.conditionsDiff.removedConditions = existedFileConds; + await this.removeConditions(); + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/helper.test.ts b/workspaces/rbac/plugins/rbac-backend/src/helper.test.ts new file mode 100644 index 0000000000..ccd0ffaa35 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/helper.test.ts @@ -0,0 +1,529 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ADMIN_ROLE_AUTHOR } from './admin-permissions/admin-creation'; +import { RoleMetadataDao } from './database/role-metadata'; +import { + deepSortedEqual, + isPermissionAction, + mergeRoleMetadata, + metadataStringToPolicy, + policiesToString, + policyToString, + removeTheDifference, + transformArrayToPolicy, + typedPoliciesToString, + typedPolicyToString, +} from './helper'; +// Import the function to test +import { EnforcerDelegate } from './service/enforcer-delegate'; + +const modifiedBy = 'user:default/some-user'; + +const auditLoggerMock = { + getActorId: jest.fn().mockImplementation(), + createAuditLogDetails: jest.fn().mockImplementation(), + auditLog: jest.fn().mockImplementation(), +}; + +describe('helper.ts', () => { + describe('policyToString', () => { + it('should convert permission policy to string', () => { + const policy = [ + 'user:default/some-user', + 'catalog-entity', + 'read', + 'allow', + ]; + const expectedString = + '[user:default/some-user, catalog-entity, read, allow]'; + expect(policyToString(policy)).toEqual(expectedString); + }); + }); + + describe('typedPolicyToString', () => { + it('should convert permission policy to string', () => { + const policy = [ + 'user:default/some-user', + 'catalog-entity', + 'read', + 'allow', + ]; + const type = 'p'; + const expectedString = + 'p, user:default/some-user, catalog-entity, read, allow'; + expect(typedPolicyToString(policy, type)).toEqual(expectedString); + }); + }); + + describe('policiesToString', () => { + it('should convert one permission policy to string', () => { + const policies = [ + ['user:default/some-user', 'catalog-entity', 'read', 'allow'], + ]; + const expectedString = + '[[user:default/some-user, catalog-entity, read, allow]]'; + expect(policiesToString(policies)).toEqual(expectedString); + }); + + it('should convert empty permission policy array to string', () => { + const policies = [[]]; + const expectedString = '[[]]'; + expect(policiesToString(policies)).toEqual(expectedString); + }); + }); + + describe('typedPoliciesToString', () => { + it('should convert one permission policy to string', () => { + const policies = [ + ['user:default/some-user', 'catalog-entity', 'read', 'allow'], + ]; + const type = 'p'; + const expectedString = `\n p, user:default/some-user, catalog-entity, read, allow\n `; + + expect(typedPoliciesToString(policies, type)).toEqual(expectedString); + }); + + it('should convert empty permission policy array to string', () => { + const policies = [[]]; + const expectedString = `\n \n `; + const type = 'p'; + expect(typedPoliciesToString(policies, type)).toEqual(expectedString); + }); + }); + + describe('metadataStringToPolicy', () => { + it('parses a permission policy string', () => { + const policy = '[user:default/some-user, catalog-entity, read, allow]'; + const expectedPolicy = [ + 'user:default/some-user', + 'catalog-entity', + 'read', + 'allow', + ]; + expect(metadataStringToPolicy(policy)).toEqual(expectedPolicy); + }); + + it('parses a grouping policy', () => { + const policy = '[user:default/some-user, role:default/dev]'; + const expectedPolicy = ['user:default/some-user', 'role:default/dev']; + expect(metadataStringToPolicy(policy)).toEqual(expectedPolicy); + }); + }); + + describe('removeTheDifference', () => { + const mockEnforcerDelegate: Partial = { + removeGroupingPolicies: jest.fn().mockImplementation(), + getFilteredGroupingPolicy: jest.fn().mockReturnValue([]), + }; + + beforeEach(() => { + (mockEnforcerDelegate.removeGroupingPolicies as jest.Mock).mockClear(); + auditLoggerMock.auditLog.mockReset(); + }); + + it('removes the difference between originalGroup and addedGroup', async () => { + const originalGroup = [ + 'user:default/some-user', + 'user:default/dev', + 'user:default/admin', + ]; + const addedGroup = ['user:default/some-user', 'user:default/dev']; + const source = 'rest'; + const roleName = 'role:default/admin'; + + await removeTheDifference( + originalGroup, + addedGroup, + source, + roleName, + mockEnforcerDelegate as EnforcerDelegate, + auditLoggerMock, + ADMIN_ROLE_AUTHOR, + ); + + expect(mockEnforcerDelegate.removeGroupingPolicies).toHaveBeenCalledWith( + [['user:default/admin', roleName]], + { + modifiedBy: ADMIN_ROLE_AUTHOR, + roleEntityRef: 'role:default/admin', + source: 'rest', + }, + false, + ); + }); + + it('does nothing when originalGroup and addedGroup are the same', async () => { + const originalGroup = ['user:default/some-user', 'user:default/dev']; + const addedGroup = ['user:default/some-user', 'user:default/dev']; + const source = 'rest'; + const roleName = 'role:default/admin'; + + await removeTheDifference( + originalGroup, + addedGroup, + source, + roleName, + mockEnforcerDelegate as EnforcerDelegate, + auditLoggerMock, + ADMIN_ROLE_AUTHOR, + ); + + expect( + mockEnforcerDelegate.removeGroupingPolicies, + ).not.toHaveBeenCalled(); + }); + + it('does nothing when originalGroup is empty', async () => { + const originalGroup: string[] = []; + const addedGroup = ['user:default/some-user', 'role:default/dev']; + const source = 'rest'; + const roleName = 'admin'; + + await removeTheDifference( + originalGroup, + addedGroup, + source, + roleName, + mockEnforcerDelegate as EnforcerDelegate, + auditLoggerMock, + ADMIN_ROLE_AUTHOR, + ); + + expect( + mockEnforcerDelegate.removeGroupingPolicies, + ).not.toHaveBeenCalled(); + }); + }); + + describe('transformArrayToPolicy', () => { + it('transforms array to RoleBasedPolicy object', () => { + const policyArray = [ + 'role:default/dev', + 'catalog-entity', + 'read', + 'allow', + ]; + const expectedPolicy = { + entityReference: 'role:default/dev', + permission: 'catalog-entity', + policy: 'read', + effect: 'allow', + }; + + const result = transformArrayToPolicy(policyArray); + + expect(result).toEqual(expectedPolicy); + }); + }); + + describe('deepSortedEqual', () => { + it('should return true for identical objects with nested properties in different order', () => { + const obj1: RoleMetadataDao = { + description: 'qa team', + id: 1, + roleEntityRef: 'role:default/qa', + source: 'rest', + modifiedBy, + }; + const obj2: RoleMetadataDao = { + roleEntityRef: 'role:default/qa', + description: 'qa team', + id: 1, + source: 'rest', + modifiedBy, + }; + expect(deepSortedEqual(obj1, obj2)).toBe(true); + }); + + it('should return true for identical objects with different ordering of top-level properties', () => { + const obj1: RoleMetadataDao = { + description: 'qa team', + id: 1, + roleEntityRef: 'role:default/qa', + source: 'rest', + modifiedBy, + }; + const obj2: RoleMetadataDao = { + id: 1, + description: 'qa team', + source: 'rest', + roleEntityRef: 'role:default/qa', + modifiedBy, + }; + expect(deepSortedEqual(obj1, obj2)).toBe(true); + }); + + it('should return true for identical objects with different ordering of top-level properties with exclude read only fields', () => { + const obj1: RoleMetadataDao = { + description: 'qa team', + id: 1, + roleEntityRef: 'role:default/qa', + source: 'rest', + // read only properties + author: 'role:default/some-role', + modifiedBy: 'role:default/some-role', + createdAt: '2024-02-26 12:25:31+00', + lastModified: '2024-02-26 12:25:31+00', + }; + const obj2: RoleMetadataDao = { + id: 1, + description: 'qa team', + source: 'rest', + roleEntityRef: 'role:default/qa', + modifiedBy, + }; + expect( + deepSortedEqual(obj1, obj2, [ + 'author', + 'modifiedBy', + 'createdAt', + 'lastModified', + ]), + ).toBe(true); + }); + + it('should return false for objects with different values', () => { + const obj1: RoleMetadataDao = { + description: 'qa', + id: 1, + roleEntityRef: 'role:default/qa', + source: 'rest', + modifiedBy, + }; + const obj2: RoleMetadataDao = { + description: 'great qa', + id: 1, + roleEntityRef: 'role:default/qa', + source: 'rest', + modifiedBy, + }; + expect(deepSortedEqual(obj1, obj2)).toBe(false); + }); + + it('should return false for objects with different source', () => { + const obj1: RoleMetadataDao = { + description: 'qa teams', + id: 1, + roleEntityRef: 'role:default/qa', + source: 'rest', + modifiedBy, + }; + const obj2: RoleMetadataDao = { + description: 'qa teams', + id: 1, + roleEntityRef: 'role:default/qa', + source: 'configuration', + modifiedBy, + }; + expect(deepSortedEqual(obj1, obj2)).toBe(false); + }); + + it('should return false for objects with different id', () => { + const obj1: RoleMetadataDao = { + description: 'qa teams', + id: 1, + roleEntityRef: 'role:default/qa', + source: 'rest', + modifiedBy, + }; + const obj2: RoleMetadataDao = { + description: 'qa teams', + id: 2, + roleEntityRef: 'role:default/qa', + source: 'rest', + modifiedBy, + }; + expect(deepSortedEqual(obj1, obj2)).toBe(false); + }); + + it('should return false for objects with different role entity reference', () => { + const obj1: RoleMetadataDao = { + description: 'qa teams', + id: 1, + roleEntityRef: 'role:default/qa', + source: 'rest', + modifiedBy, + }; + const obj2: RoleMetadataDao = { + description: 'qa teams', + id: 1, + roleEntityRef: 'role:default/dev', + source: 'rest', + modifiedBy, + }; + expect(deepSortedEqual(obj1, obj2)).toBe(false); + }); + }); + + describe('isPermissionAction', () => { + it('should return true', () => { + let result = isPermissionAction('create'); + expect(result).toBeTruthy(); + + result = isPermissionAction('read'); + expect(result).toBeTruthy(); + + result = isPermissionAction('update'); + expect(result).toBeTruthy(); + + result = isPermissionAction('delete'); + expect(result).toBeTruthy(); + + result = isPermissionAction('use'); + expect(result).toBeTruthy(); + }); + + it('should return false', () => { + const result = isPermissionAction('unknown'); + expect(result).toBeFalsy(); + }); + }); +}); + +describe('mergeRoleMetadata', () => { + it('should merge new metadata into current metadata', () => { + const currentMetadata: RoleMetadataDao = { + lastModified: '2021-01-01T00:00:00Z', + modifiedBy: 'user:default/user1', + description: 'Initial role description', + roleEntityRef: 'user:default/tim', + source: 'legacy', + }; + + const newMetadata: RoleMetadataDao = { + lastModified: '2022-01-01T00:00:00Z', + modifiedBy: 'user:default/user2', + description: 'Updated role description', + roleEntityRef: 'user:default/dev-team', + source: 'rest', + }; + + const expectedMergedMetadata: RoleMetadataDao = { + ...currentMetadata, + ...newMetadata, + }; + + const result = mergeRoleMetadata(currentMetadata, newMetadata); + + expect(result).toEqual(expectedMergedMetadata); + }); + + it('should use current metadata description if new metadata description is undefined', () => { + const currentMetadata: RoleMetadataDao = { + lastModified: '2021-01-01T00:00:00Z', + modifiedBy: 'user:default/user1', + description: 'Initial role description', + roleEntityRef: 'user:default/tim', + source: 'legacy', + }; + + const newMetadata: RoleMetadataDao = { + lastModified: '2022-01-01T00:00:00Z', + modifiedBy: 'user:default/user2', + roleEntityRef: 'user:default/dev-team', + source: 'csv-file', + }; + + const expectedMergedMetadata: RoleMetadataDao = { + ...currentMetadata, + ...newMetadata, + description: currentMetadata.description, + }; + + const result = mergeRoleMetadata(currentMetadata, newMetadata); + + expect(result).toEqual(expectedMergedMetadata); + }); + + it('should use current date if new metadata lastModified is undefined', () => { + const currentMetadata: RoleMetadataDao = { + lastModified: '2021-01-01T00:00:00Z', + modifiedBy: 'user:default/user1', + description: 'Initial role description', + roleEntityRef: 'user:default/tim', + source: 'legacy', + }; + + const newMetadata: RoleMetadataDao = { + modifiedBy: 'user:default/user2', + description: 'Updated role description', + roleEntityRef: 'user:default/dev-team', + source: 'configuration', + }; + + const result = mergeRoleMetadata(currentMetadata, newMetadata); + const resultDate = new Date(result.lastModified!); + expect(resultDate).toBeInstanceOf(Date); + expect(result.modifiedBy).toEqual(newMetadata.modifiedBy); + expect(result.description).toEqual(newMetadata.description); + expect(result.roleEntityRef).toEqual(newMetadata.roleEntityRef); + expect(result.source).toEqual(newMetadata.source); + }); + + it('should not modify original metadata objects', () => { + const currentMetadata: RoleMetadataDao = { + lastModified: '2021-01-01T00:00:00Z', + modifiedBy: 'user:default/user1', + description: 'Initial role description', + roleEntityRef: 'user:default/tim', + source: 'legacy', + }; + + const newMetadata: RoleMetadataDao = { + lastModified: '2022-01-01T00:00:00Z', + modifiedBy: 'user:default/user2', + description: 'Updated role description', + roleEntityRef: 'user:default/dev-team', + source: 'configuration', + }; + + const currentMetadataClone = { ...currentMetadata }; + const newMetadataClone = { ...newMetadata }; + + mergeRoleMetadata(currentMetadata, newMetadata); + + expect(currentMetadata).toEqual(currentMetadataClone); + expect(newMetadata).toEqual(newMetadataClone); + }); + + it('should use current date if new metadata createdAt is undefined', () => { + const currentMetadata: RoleMetadataDao = { + createdAt: '2021-01-01T00:00:00Z', + lastModified: '2021-01-01T00:00:00Z', + modifiedBy: 'user:default/user1', + description: 'Initial role description', + roleEntityRef: 'user:default/tim', + source: 'legacy', + }; + + const newMetadata: RoleMetadataDao = { + lastModified: '2022-01-01T00:00:00Z', + modifiedBy: 'user:default/user2', + description: 'Updated role description', + roleEntityRef: 'user:default/dev-team', + source: 'configuration', + }; + + const result = mergeRoleMetadata(currentMetadata, newMetadata); + const resultDate = new Date(result.createdAt!); + expect(resultDate).toBeInstanceOf(Date); + expect(result.lastModified).toEqual(newMetadata.lastModified); + expect(result.modifiedBy).toEqual(newMetadata.modifiedBy); + expect(result.description).toEqual(newMetadata.description); + expect(result.roleEntityRef).toEqual(newMetadata.roleEntityRef); + expect(result.source).toEqual(newMetadata.source); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/helper.ts b/workspaces/rbac/plugins/rbac-backend/src/helper.ts new file mode 100644 index 0000000000..1af5dc62b3 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/helper.ts @@ -0,0 +1,267 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { AuthService } from '@backstage/backend-plugin-api'; +import type { MetadataResponse } from '@backstage/plugin-permission-node'; + +import { AuditLogger } from '@janus-idp/backstage-plugin-audit-log-node'; +import { + difference, + fromPairs, + isArray, + isEqual, + isPlainObject, + omitBy, + sortBy, + toPairs, + ValueKeyIteratee, +} from 'lodash'; + +import { + PermissionAction, + PermissionInfo, + RoleBasedPolicy, + RoleConditionalPolicyDecision, + Source, +} from '@backstage-community/plugin-rbac-common'; + +import { + HANDLE_RBAC_DATA_STAGE, + RBAC_BACKEND, + RoleAuditInfo, + RoleEvents, +} from './audit-log/audit-logger'; +import { RoleMetadataDao, RoleMetadataStorage } from './database/role-metadata'; +import { EnforcerDelegate } from './service/enforcer-delegate'; +import { PluginPermissionMetadataCollector } from './service/plugin-endpoints'; + +export function policyToString(policy: string[]): string { + return `[${policy.join(', ')}]`; +} + +export function typedPolicyToString(policy: string[], type: string): string { + return `${type}, ${policy.join(', ')}`; +} + +export function policiesToString(policies: string[][]): string { + const policiesString = policies + .map(policy => policyToString(policy)) + .join(','); + return `[${policiesString}]`; +} + +export function typedPoliciesToString( + policies: string[][], + type: string, +): string { + const policiesString = policies + .map(policy => { + return policy.length !== 0 ? typedPolicyToString(policy, type) : ''; + }) + .join('\n'); + return ` + ${policiesString} + `; +} + +export function metadataStringToPolicy(policy: string): string[] { + return policy.replace('[', '').replace(']', '').split(', '); +} + +export async function removeTheDifference( + originalGroup: string[], + addedGroup: string[], + source: Source, + roleEntityRef: string, + enf: EnforcerDelegate, + auditLogger: AuditLogger, + modifiedBy: string, +): Promise { + originalGroup.sort((a, b) => a.localeCompare(b)); + addedGroup.sort((a, b) => a.localeCompare(b)); + const missing = difference(originalGroup, addedGroup); + + const groupPolicies: string[][] = []; + for (const missingRole of missing) { + groupPolicies.push([missingRole, roleEntityRef]); + } + + if (groupPolicies.length === 0) { + return; + } + + const roleMetadata = { source, modifiedBy, roleEntityRef }; + await enf.removeGroupingPolicies(groupPolicies, roleMetadata, false); + + const remainingMembers = await enf.getFilteredGroupingPolicy( + 1, + roleEntityRef, + ); + const message = + remainingMembers.length > 0 + ? 'Updated role: deleted members' + : 'Deleted role'; + const eventName = + remainingMembers.length > 0 + ? RoleEvents.UPDATE_ROLE + : RoleEvents.DELETE_ROLE; + await auditLogger.auditLog({ + actorId: RBAC_BACKEND, + message, + eventName, + metadata: { + ...roleMetadata, + members: groupPolicies.map(gp => gp[0]), + }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); +} + +export function transformArrayToPolicy(policyArray: string[]): RoleBasedPolicy { + const [entityReference, permission, policy, effect] = policyArray; + return { entityReference, permission, policy, effect }; +} + +export function deepSortedEqual( + obj1: Record, + obj2: Record, + excludeFields?: string[], +): boolean { + let copyObj1; + let copyObj2; + if (excludeFields) { + const excludeFieldsPredicate: ValueKeyIteratee = (_value, key) => { + for (const field of excludeFields) { + if (key === field) { + return true; + } + } + return false; + }; + copyObj1 = omitBy(obj1, excludeFieldsPredicate); + copyObj2 = omitBy(obj2, excludeFieldsPredicate); + } + + const sortedObj1 = sortBy(toPairs(copyObj1 || obj1), ([key]) => key); + const sortedObj2 = sortBy(toPairs(copyObj2 || obj2), ([key]) => key); + + return isEqual(sortedObj1, sortedObj2); +} + +export function isPermissionAction(action: string): action is PermissionAction { + return ['create', 'read', 'update', 'delete', 'use'].includes( + action as PermissionAction, + ); +} + +export async function buildRoleSourceMap( + policies: string[][], + roleMetadata: RoleMetadataStorage, +): Promise> { + return await policies.reduce( + async ( + acc: Promise>, + policy: string[], + ): Promise> => { + const roleEntityRef = policy[0]; + const acummulator = await acc; + if (!acummulator.has(roleEntityRef)) { + const metadata = await roleMetadata.findRoleMetadata(roleEntityRef); + acummulator.set(roleEntityRef, metadata?.source); + } + return acummulator; + }, + Promise.resolve(new Map()), + ); +} + +export function mergeRoleMetadata( + currentMetadata: RoleMetadataDao, + newMetadata: RoleMetadataDao, +): RoleMetadataDao { + const mergedMetaData: RoleMetadataDao = { ...currentMetadata }; + mergedMetaData.lastModified = + newMetadata.lastModified ?? new Date().toUTCString(); + mergedMetaData.modifiedBy = newMetadata.modifiedBy; + mergedMetaData.description = + newMetadata.description ?? currentMetadata.description; + mergedMetaData.roleEntityRef = newMetadata.roleEntityRef; + mergedMetaData.source = newMetadata.source; + return mergedMetaData; +} + +export async function processConditionMapping( + roleConditionPolicy: RoleConditionalPolicyDecision, + pluginPermMetaData: PluginPermissionMetadataCollector, + auth: AuthService, +): Promise> { + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: roleConditionPolicy.pluginId, + }); + + const rule: MetadataResponse | undefined = + await pluginPermMetaData.getMetadataByPluginId( + roleConditionPolicy.pluginId, + token, + ); + if (!rule?.permissions) { + throw new Error( + `Unable to get permission list for plugin ${roleConditionPolicy.pluginId}`, + ); + } + + const permInfo: PermissionInfo[] = []; + for (const action of roleConditionPolicy.permissionMapping) { + const perm = rule.permissions.find( + permission => + permission.type === 'resource' && + (action === permission.attributes.action || + (action === 'use' && permission.attributes.action === undefined)), + ); + if (!perm) { + throw new Error( + `Unable to find permission to get permission name for resource type '${ + roleConditionPolicy.resourceType + }' and action ${JSON.stringify(action)}`, + ); + } + permInfo.push({ name: perm.name, action }); + } + + return { + ...roleConditionPolicy, + permissionMapping: permInfo, + }; +} + +export function deepSort(value: any): any { + if (isArray(value)) { + return sortBy(value.map(deepSort)); + } else if (isPlainObject(value)) { + return fromPairs( + sortBy( + toPairs(value).map(([k, v]: [string, any]) => [k, deepSort(v)]), + 0, + ), + ); + } + return value; +} + +export function deepSortEqual(obj1: any, obj2: any): boolean { + return isEqual(deepSort(obj1), deepSort(obj2)); +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/index.ts b/workspaces/rbac/plugins/rbac-backend/src/index.ts new file mode 100644 index 0000000000..de60add2ae --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './service/router'; +export * from './service/policy-builder'; + +// To provide backward compatibility with client code implemented +// before PluginIdProvider was moved to @backstage-community/plugin-rbac-node. +export type { PluginIdProvider } from '@backstage-community/plugin-rbac-node'; + +export { rbacPlugin as default } from './plugin'; diff --git a/workspaces/rbac/plugins/rbac-backend/src/plugin.ts b/workspaces/rbac/plugins/rbac-backend/src/plugin.ts new file mode 100644 index 0000000000..789fe098fe --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/plugin.ts @@ -0,0 +1,113 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +import { PolicyBuilder } from '@backstage-community/plugin-rbac-backend'; +import { + PluginIdProvider, + PluginIdProviderExtensionPoint, + pluginIdProviderExtensionPoint, + RBACProvider, + rbacProviderExtensionPoint, +} from '@backstage-community/plugin-rbac-node'; + +/** + * RBAC plugin + * + */ +export const rbacPlugin = createBackendPlugin({ + pluginId: 'permission', + register(env) { + const pluginIdProviderExtensionPointImpl = new (class PluginIdProviderImpl + implements PluginIdProviderExtensionPoint + { + pluginIdProviders: PluginIdProvider[] = []; + + addPluginIdProvider(pluginIdProvider: PluginIdProvider): void { + this.pluginIdProviders.push(pluginIdProvider); + } + })(); + + env.registerExtensionPoint( + pluginIdProviderExtensionPoint, + pluginIdProviderExtensionPointImpl, + ); + + const rbacProviders = new Array(); + + env.registerExtensionPoint(rbacProviderExtensionPoint, { + addRBACProvider( + ...providers: Array> + ): void { + rbacProviders.push(...providers.flat()); + }, + }); + + env.registerInit({ + deps: { + http: coreServices.httpRouter, + config: coreServices.rootConfig, + logger: coreServices.logger, + discovery: coreServices.discovery, + permissions: coreServices.permissions, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + userInfo: coreServices.userInfo, + lifecycle: coreServices.lifecycle, + }, + async init({ + http, + config, + logger, + discovery, + permissions, + auth, + httpAuth, + userInfo, + lifecycle, + }) { + http.use( + await PolicyBuilder.build( + { + config, + logger, + discovery, + permissions, + auth, + httpAuth, + userInfo, + lifecycle, + }, + { + getPluginIds: () => + Array.from( + new Set( + pluginIdProviderExtensionPointImpl.pluginIdProviders.flatMap( + p => p.getPluginIds(), + ), + ), + ), + }, + rbacProviders, + ), + ); + }, + }); + }, +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/policies/allow-all-policy.test.ts b/workspaces/rbac/plugins/rbac-backend/src/policies/allow-all-policy.test.ts new file mode 100644 index 0000000000..fad05fe2f8 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/policies/allow-all-policy.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + AuthorizeResult, + createPermission, +} from '@backstage/plugin-permission-common'; +import { + PermissionPolicy, + PolicyQuery, + PolicyQueryUser, +} from '@backstage/plugin-permission-node'; + +import { AllowAllPolicy } from './allow-all-policy'; + +describe('Allow All Policy', () => { + describe('Allow all policy should allow all', () => { + let policy: PermissionPolicy; + beforeEach(() => { + policy = new AllowAllPolicy(); + }); + + it('should be able to create an allow all permission policy', () => { + expect(policy).not.toBeNull(); + }); + + it('should allow all when handle is called', async () => { + const result = await policy.handle( + newPolicyQueryWithBasicPermission('catalog.entity.create'), + newPolicyQueryUser('user:default/guest'), + ); + + expect(result).toStrictEqual({ result: AuthorizeResult.ALLOW }); + }); + }); +}); + +function newPolicyQueryWithBasicPermission(name: string): PolicyQuery { + const mockPermission = createPermission({ + name: name, + attributes: {}, + }); + return { permission: mockPermission }; +} + +function newPolicyQueryUser( + user?: string, + ownershipEntityRefs?: string[], +): PolicyQueryUser | undefined { + if (user) { + return { + identity: { + ownershipEntityRefs: ownershipEntityRefs ?? [], + type: 'user', + userEntityRef: user, + }, + credentials: { + $$type: '@backstage/BackstageCredentials', + principal: true, + expiresAt: new Date('2021-01-01T00:00:00Z'), + }, + info: { + userEntityRef: user, + ownershipEntityRefs: ownershipEntityRefs ?? [], + }, + token: 'token', + }; + } + return undefined; +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/policies/allow-all-policy.ts b/workspaces/rbac/plugins/rbac-backend/src/policies/allow-all-policy.ts new file mode 100644 index 0000000000..99c962d2be --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/policies/allow-all-policy.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + AuthorizeResult, + PolicyDecision, +} from '@backstage/plugin-permission-common'; +import { + PermissionPolicy, + PolicyQuery, + PolicyQueryUser, +} from '@backstage/plugin-permission-node'; + +export class AllowAllPolicy implements PermissionPolicy { + async handle( + _request: PolicyQuery, + _user?: PolicyQueryUser, + ): Promise { + return { result: AuthorizeResult.ALLOW }; + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/policies/permission-policy.test.ts b/workspaces/rbac/plugins/rbac-backend/src/policies/permission-policy.test.ts new file mode 100644 index 0000000000..11154548db --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/policies/permission-policy.test.ts @@ -0,0 +1,2284 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { LoggerService } from '@backstage/backend-plugin-api'; +import { mockServices } from '@backstage/backend-test-utils'; +import type { Entity } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { + AuthorizeResult, + createPermission, +} from '@backstage/plugin-permission-common'; +import type { + PolicyQuery, + PolicyQueryUser, +} from '@backstage/plugin-permission-node'; + +import { + Adapter, + Enforcer, + Model, + newEnforcer, + newModelFromString, +} from 'casbin'; +import * as Knex from 'knex'; +import { MockClient } from 'knex-mock-client'; + +import type { RoleMetadata } from '@backstage-community/plugin-rbac-common'; + +import { resolve } from 'path'; + +import { ADMIN_ROLE_NAME } from '../admin-permissions/admin-creation'; +import { CasbinDBAdapterFactory } from '../database/casbin-adapter-factory'; +import { ConditionalStorage } from '../database/conditional-storage'; +import { + RoleMetadataDao, + 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 { PluginPermissionMetadataCollector } from '../service/plugin-endpoints'; +import { RBACPermissionPolicy } from './permission-policy'; + +type PermissionAction = 'create' | 'read' | 'update' | 'delete'; + +// TODO: Move to 'catalogServiceMock' from '@backstage/plugin-catalog-node/testUtils' +// once '@backstage/plugin-catalog-node' is upgraded +const catalogApiMock = { + 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 conditionalStorageMock: ConditionalStorage = { + filterConditions: jest.fn().mockImplementation(() => []), + createCondition: jest.fn().mockImplementation(), + checkConflictedConditions: jest.fn().mockImplementation(), + getCondition: jest.fn().mockImplementation(), + deleteCondition: jest.fn().mockImplementation(), + updateCondition: jest.fn().mockImplementation(), +}; + +const roleMetadataStorageMock: RoleMetadataStorage = { + filterRoleMetadata: jest.fn().mockImplementation(() => []), + 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 csvPermFile = resolve( + __dirname, + '../../__fixtures__/data/valid-csv/rbac-policy.csv', +); + +const mockClientKnex = Knex.knex({ client: MockClient }); + +const mockAuthService = mockServices.auth(); + +const auditLoggerMock = { + getActorId: jest.fn().mockImplementation(), + createAuditLogDetails: jest.fn().mockImplementation(), + auditLog: jest.fn().mockImplementation(), +}; + +const pluginMetadataCollectorMock: Partial = + { + getPluginConditionRules: jest.fn().mockImplementation(), + getPluginPolicies: jest.fn().mockImplementation(), + getMetadataByPluginId: jest.fn().mockImplementation(), + }; + +const modifiedBy = 'user:default/some-admin'; + +describe('RBACPermissionPolicy Tests', () => { + beforeEach(() => { + roleMetadataStorageMock.updateRoleMetadata = jest.fn().mockImplementation(); + }); + + it('should build', async () => { + const config = newConfig(); + const adapter = await newAdapter(config); + const enfDelegate = await newEnforcerDelegate(adapter, config); + + const policy = await newPermissionPolicy(config, enfDelegate); + + expect(policy).not.toBeNull(); + }); + + it('should fail to build when creating admin role', async () => { + roleMetadataStorageMock.updateRoleMetadata = jest + .fn() + .mockImplementation(async (): Promise => { + throw new Error(`Failed to create`); + }); + + const config = newConfig(); + const adapter = await newAdapter(config); + const enfDelegate = await newEnforcerDelegate(adapter, config); + await enfDelegate.addPolicy([ + 'user:default/known_user', + 'test-resource', + 'update', + 'allow', + ]); + + await expect(newPermissionPolicy(config, enfDelegate)).rejects.toThrow( + 'Failed to create', + ); + }); + + describe('Policy checks from csv file', () => { + let enfDelegate: EnforcerDelegate; + let policy: RBACPermissionPolicy; + + beforeEach(async () => { + const config = newConfig(); + const adapter = await newAdapter(config); + enfDelegate = await newEnforcerDelegate(adapter, config); + policy = await newPermissionPolicy(config, enfDelegate); + + catalogApiMock.getEntities.mockReturnValue({ items: [] }); + }); + + // case1 + it('should allow read access to resource permission for user from csv file', async () => { + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.read', + 'catalog-entity', + 'read', + ), + newPolicyQueryUser('user:default/guest'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForResourcedPermission( + 'user:default/guest', + 'catalog.entity.read', + 'read', + 'catalog-entity', + AuthorizeResult.ALLOW, + ); + }); + + // case2 + it('should allow create access to resource permission for user from csv file', async () => { + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('catalog.entity.create'), + newPolicyQueryUser('user:default/guest'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForNonResourcedPermission( + 'user:default/guest', + 'catalog.entity.create', + undefined, + 'use', + AuthorizeResult.ALLOW, + ); + }); + + // case3 + it('should allow deny access to resource permission for user:default/known_user', async () => { + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource.deny'), + newPolicyQueryUser('user:default/known_user'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForNonResourcedPermission( + 'user:default/known_user', + 'test.resource.deny', + undefined, + 'use', + AuthorizeResult.ALLOW, + ); + }); + + // case1 with role + it('should allow update access to resource permission for user from csv file', async () => { + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.read', + 'catalog-entity', + 'update', + ), + newPolicyQueryUser('user:default/guest'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForResourcedPermission( + 'user:default/guest', + 'catalog.entity.read', + 'update', + 'catalog-entity', + AuthorizeResult.ALLOW, + ); + }); + + // case2 with role + it('should allow update access to resource permission for role from csv file', async () => { + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.read', + 'catalog-entity', + 'update', + ), + newPolicyQueryUser('role:default/catalog-writer'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForResourcedPermission( + 'role:default/catalog-writer', + 'catalog.entity.read', + 'update', + 'catalog-entity', + AuthorizeResult.ALLOW, + ); + }); + }); + + describe('Policy checks for clean up old policies for csv file', () => { + let config: Config; + let adapter: Adapter; + let enforcerDelegate: EnforcerDelegate; + let rbacPolicy: RBACPermissionPolicy; + const allEnfRoles = [ + 'role:default/some-role', + 'role:default/rbac_admin', + 'role:default/catalog-writer', + 'role:default/legacy', + 'role:default/catalog-reader', + 'role:default/catalog-deleter', + 'role:default/known_role', + ]; + + const allEnfGroupPolicies = [ + ['user:default/tester', 'role:default/some-role'], + ['user:default/guest', 'role:default/rbac_admin'], + ['group:default/guests', 'role:default/rbac_admin'], + ['user:default/guest', 'role:default/catalog-writer'], + ['user:default/guest', 'role:default/legacy'], + ['user:default/guest', 'role:default/catalog-reader'], + ['user:default/guest', 'role:default/catalog-deleter'], + ['user:default/known_user', 'role:default/known_role'], + ]; + + const allEnfPolicies = [ + // stored policy + ['role:default/some-role', 'test.some.resource', 'use', 'allow'], + // policies from csv file + ['role:default/catalog-writer', 'catalog-entity', 'update', 'allow'], + ['role:default/legacy', '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'], + ]; + + beforeEach(async () => { + (roleMetadataStorageMock.removeRoleMetadata as jest.Mock).mockReset(); + + config = newConfig(); + adapter = await newAdapter(config); + + catalogApiMock.getEntities.mockReturnValue({ items: [] }); + }); + + it('should cleanup old group policies and metadata after re-attach policy file', async () => { + roleMetadataStorageMock.filterRoleMetadata = jest + .fn() + .mockImplementation(() => { + const roleMetadataDao: RoleMetadataDao = { + roleEntityRef: 'role:default/old-role', + source: 'csv-file', + modifiedBy: 'user:default/tom', + }; + return [roleMetadataDao]; + }); + const storedGroupPolicies = [ + // should be removed + ['user:default/user-old-1', 'role:default/old-role'], + ['group:default/team-a-old-1', 'role:default/old-role'], + + // should not be removed: + ['user:default/tester', 'role:default/some-role'], + ]; + const storedPolicies = [ + // should not be removed + ['role:default/some-role', 'test.some.resource', 'use', 'allow'], + ]; + + enforcerDelegate = await newEnforcerDelegate( + adapter, + config, + storedPolicies, + storedGroupPolicies, + ); + + await newPermissionPolicy(config, enforcerDelegate); + + expect(await enforcerDelegate.getGroupingPolicy()).toEqual( + allEnfGroupPolicies, + ); + + expect(await enforcerDelegate.getAllRoles()).toEqual(allEnfRoles); + + const nonAdminPolicies = (await enforcerDelegate.getPolicy()).filter( + (policy: string[]) => policy[0] !== 'role:default/rbac_admin', + ); + + expect(nonAdminPolicies).toEqual(allEnfPolicies); + + // role metadata should be removed + expect(roleMetadataStorageMock.removeRoleMetadata).toHaveBeenCalledWith( + 'role:default/old-role', + expect.anything(), + ); + }); + + it('should cleanup old policies and metadata after re-attach policy file', async () => { + const storedGroupPolicies = [ + // should not be removed: + ['user:default/tester', 'role:default/some-role'], + ]; + const storedPolicies = [ + // should be removed + ['role:default/old-role', 'test.some.resource', 'use', 'allow'], + + // should not be removed + ['role:default/some-role', 'test.some.resource', 'use', 'allow'], + ]; + + enforcerDelegate = await newEnforcerDelegate( + adapter, + config, + storedPolicies, + storedGroupPolicies, + ); + + rbacPolicy = await newPermissionPolicy(config, enforcerDelegate); + + expect(await enforcerDelegate.getAllRoles()).toEqual(allEnfRoles); + + expect(await enforcerDelegate.getGroupingPolicy()).toEqual( + allEnfGroupPolicies, + ); + + const nonAdminPolicies = (await enforcerDelegate.getPolicy()).filter( + (p: string[]) => { + return p[0] !== 'role:default/rbac_admin'; + }, + ); + expect(nonAdminPolicies).toEqual(allEnfPolicies); + + // role metadata should not be removed + expect( + roleMetadataStorageMock.removeRoleMetadata, + ).not.toHaveBeenCalledWith('role:default/old-role', expect.anything()); + + const decision = await rbacPolicy.handle( + newPolicyQueryWithBasicPermission('test.some.resource'), + newPolicyQueryUser('user:default/user-old-1'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'user:default/user-old-1', + 'test.some.resource', + undefined, + 'use', + AuthorizeResult.DENY, + ); + }); + + it('should cleanup old policies and group policies and metadata after re-attach policy file', async () => { + const storedGroupPolicies = [ + // should be removed + ['user:default/user-old-1', 'role:default/old-role'], + ['user:default/user-old-2', 'role:default/old-role'], + ['group:default/team-a-old-1', 'role:default/old-role'], + ['group:default/team-a-old-2', 'role:default/old-role'], + + // should not be removed: + ['user:default/tester', 'role:default/some-role'], + ]; + const storedPolicies = [ + // should be removed + ['role:default/old-role', 'test.some.resource', 'use', 'allow'], + + // should not be removed + ['role:default/some-role', 'test.some.resource', 'use', 'allow'], + ]; + + enforcerDelegate = await newEnforcerDelegate( + adapter, + config, + storedPolicies, + storedGroupPolicies, + ); + + await newPermissionPolicy(config, enforcerDelegate); + + expect(await enforcerDelegate.getAllRoles()).toEqual(allEnfRoles); + + expect(await enforcerDelegate.getGroupingPolicy()).toEqual( + allEnfGroupPolicies, + ); + + const nonAdminPolicies = (await enforcerDelegate.getPolicy()).filter( + (policy: string[]) => { + return policy[0] !== 'role:default/rbac_admin'; + }, + ); + expect(nonAdminPolicies).toEqual(allEnfPolicies); + + // role metadata should be removed + expect(roleMetadataStorageMock.removeRoleMetadata).toHaveBeenCalledWith( + 'role:default/old-role', + expect.anything(), + ); + }); + + it('should cleanup old group policies and metadata after detach policy file', async () => { + const storedGroupPolicies = [ + // should be removed + ['user:default/user-old-1', 'role:default/old-role'], + ['group:default/team-a-old-1', 'role:default/old-role'], + + // should not be removed: + ['user:default/tester', 'role:default/some-role'], + ]; + const storedPolicies = [ + // should not be removed + ['role:default/some-role', 'test.some.resource', 'use', 'allow'], + ]; + + enforcerDelegate = await newEnforcerDelegate( + adapter, + config, + storedPolicies, + storedGroupPolicies, + ); + + await newPermissionPolicy(config, enforcerDelegate); + + expect(await enforcerDelegate.getAllRoles()).toEqual(allEnfRoles); + + expect(await enforcerDelegate.getGroupingPolicy()).toEqual( + allEnfGroupPolicies, + ); + + const nonAdminPolicies = (await enforcerDelegate.getPolicy()).filter( + (policy: string[]) => { + return policy[0] !== 'role:default/rbac_admin'; + }, + ); + expect(nonAdminPolicies).toEqual(allEnfPolicies); + + // role metadata should be removed + expect(roleMetadataStorageMock.removeRoleMetadata).toHaveBeenCalledWith( + 'role:default/old-role', + expect.anything(), + ); + }); + + it('should cleanup old policies after detach policy file', async () => { + const storedGroupPolicies = [ + // should not be removed: + ['user:default/tester', 'role:default/some-role'], + ]; + const storedPolicies = [ + // should be removed + ['role:default/old-role', 'test.some.resource', 'use', 'allow'], + + // should not be removed + ['role:default/some-role', 'test.some.resource', 'use', 'allow'], + ]; + + enforcerDelegate = await newEnforcerDelegate( + adapter, + config, + storedPolicies, + storedGroupPolicies, + ); + + await newPermissionPolicy(config, enforcerDelegate); + + expect(await enforcerDelegate.getAllRoles()).toEqual(allEnfRoles); + + expect(await enforcerDelegate.getGroupingPolicy()).toEqual( + allEnfGroupPolicies, + ); + + const nonAdminPolicies = (await enforcerDelegate.getPolicy()).filter( + (policy: string[]) => { + return policy[0] !== 'role:default/rbac_admin'; + }, + ); + expect(nonAdminPolicies).toEqual(allEnfPolicies); + }); + + it('should cleanup old policies and group policies and metadata after detach policy file', async () => { + const storedGroupPolicies = [ + // should be removed + ['user:default/user-old-1', 'role:default/old-role'], + ['user:default/user-old-2', 'role:default/old-role'], + ['group:default/team-a-old-1', 'role:default/old-role'], + ['group:default/team-a-old-2', 'role:default/old-role'], + + // should not be removed: + ['user:default/tester', 'role:default/some-role'], + ]; + const storedPolicies = [ + // should be removed + ['role:default/old-role', 'test.some.resource', 'use', 'allow'], + + // should not be removed + ['role:default/some-role', 'test.some.resource', 'use', 'allow'], + ]; + + enforcerDelegate = await newEnforcerDelegate( + adapter, + config, + storedPolicies, + storedGroupPolicies, + ); + + await newPermissionPolicy(config, enforcerDelegate); + + expect(await enforcerDelegate.getAllRoles()).toEqual(allEnfRoles); + + expect(await enforcerDelegate.getGroupingPolicy()).toEqual( + allEnfGroupPolicies, + ); + + const nonAdminPolicies = (await enforcerDelegate.getPolicy()).filter( + (policy: string[]) => { + return policy[0] !== 'role:default/rbac_admin'; + }, + ); + expect(nonAdminPolicies).toEqual(allEnfPolicies); + + // role metadata should be removed + expect(roleMetadataStorageMock.removeRoleMetadata).toHaveBeenCalledWith( + 'role:default/old-role', + expect.anything(), + ); + }); + }); + + describe('Policy checks for users', () => { + let policy: RBACPermissionPolicy; + let enfDelegate: EnforcerDelegate; + + const roleMetadataStorageTest: RoleMetadataStorage = { + filterRoleMetadata: jest.fn().mockImplementation(() => []), + findRoleMetadata: jest + .fn() + .mockImplementation( + async ( + roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + if (roleEntityRef.includes('rbac_admin')) { + return { source: 'configuration' }; + } + return { source: 'csv-file' }; + }, + ), + createRoleMetadata: jest.fn().mockImplementation(), + updateRoleMetadata: jest.fn().mockImplementation(), + removeRoleMetadata: jest.fn().mockImplementation(), + }; + + beforeEach(async () => { + const basicAndResourcePermissions = resolve( + __dirname, + '../../__fixtures__/data/valid-csv/basic-and-resource-policies.csv', + ); + const config = newConfig(basicAndResourcePermissions); + const adapter = await newAdapter(config); + enfDelegate = await newEnforcerDelegate(adapter, config); + + policy = await newPermissionPolicy( + config, + enfDelegate, + roleMetadataStorageTest, + ); + + catalogApiMock.getEntities.mockReturnValue({ items: [] }); + }); + // +-------+------+------------------------------------+ + // | allow | deny | result | | + // +-------+------+--------------------------------+---| + // | N | Y | deny | 1 | + // | N | N | deny (user not listed) | 2 | + // | Y | Y | deny (user:default/duplicated) | 3 | + // | Y | N | allow | 4 | + + // Tests for Resource basic type permission + + // case1 + it('should deny access to basic permission for listed user with deny action', async () => { + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource.deny'), + newPolicyQueryUser('user:default/known_user'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'user:default/known_user', + 'test.resource.deny', + undefined, + 'use', + AuthorizeResult.DENY, + ); + }); + // case2 + it('should deny access to basic permission for unlisted user', async () => { + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource'), + newPolicyQueryUser('unuser:default/known_user'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'unuser:default/known_user', + 'test.resource', + undefined, + 'use', + AuthorizeResult.DENY, + ); + }); + // case3 + it('should deny access to basic permission for listed user deny and allow', async () => { + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource'), + newPolicyQueryUser('user:default/duplicated'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'user:default/duplicated', + 'test.resource', + undefined, + 'use', + AuthorizeResult.DENY, + ); + }); + // case4 + it('should allow access to basic permission for user listed on policy', async () => { + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource'), + newPolicyQueryUser('user:default/known_user'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForNonResourcedPermission( + 'user:default/known_user', + 'test.resource', + undefined, + 'use', + AuthorizeResult.ALLOW, + ); + }); + // case5 + it('should deny access to undefined user', async () => { + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource'), + newPolicyQueryUser(), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + undefined, + 'test.resource', + undefined, + 'use', + AuthorizeResult.DENY, + ); + }); + + // Tests for Resource Permission type + + // case1 + it('should deny access to resource permission for user listed on policy', async () => { + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'test.resource.deny', + 'test-resource-deny', + 'update', + ), + newPolicyQueryUser('user:default/known_user'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForResourcedPermission( + 'user:default/known_user', + 'test.resource.deny', + 'update', + 'test-resource-deny', + AuthorizeResult.DENY, + ); + }); + // case 2 + it('should deny access to resource permission for user unlisted on policy', async () => { + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'test.resource.update', + 'test-resource', + 'update', + ), + newPolicyQueryUser('unuser:default/known_user'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForResourcedPermission( + 'unuser:default/known_user', + 'test.resource.update', + 'update', + 'test-resource', + AuthorizeResult.DENY, + ); + }); + // case 3 + it('should deny access to resource permission for user listed deny and allow', async () => { + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'test.resource.update', + 'test-resource', + 'update', + ), + newPolicyQueryUser('user:default/duplicated'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForResourcedPermission( + 'user:default/duplicated', + 'test.resource.update', + 'update', + 'test-resource', + AuthorizeResult.DENY, + ); + }); + // case 4 + it('should allow access to resource permission for user listed on policy', async () => { + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'test.resource.update', + 'test-resource', + 'update', + ), + newPolicyQueryUser('user:default/known_user'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForResourcedPermission( + 'user:default/known_user', + 'test.resource.update', + 'update', + 'test-resource', + AuthorizeResult.ALLOW, + ); + }); + + // Tests for actions on resource permissions + it('should deny access to resource permission for unlisted action for user listed on policy', async () => { + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'test.resource.update', + 'test-resource', + 'delete', + ), + newPolicyQueryUser('user:default/known_user'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + }); + + // Tests for admin added through app config + it('should allow access to permission resources for admin added through app config', async () => { + const adminPerm: { + name: string; + resource: string; + action: PermissionAction; + }[] = [ + { + name: 'policy.entity.read', + resource: 'policy-entity', + action: 'read', + }, + { + name: 'policy.entity.create', + resource: 'policy-entity', + action: 'create', + }, + { + name: 'policy.entity.update', + resource: 'policy-entity', + action: 'update', + }, + { + name: 'policy.entity.delete', + resource: 'policy-entity', + action: 'delete', + }, + { + name: 'catalog.entity.read', + resource: 'catalog-entity', + action: 'read', + }, + ]; + for (const perm of adminPerm) { + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + perm.name, + perm.resource, + perm.action, + ), + newPolicyQueryUser('user:default/guest'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForResourcedPermission( + 'user:default/guest', + perm.name, + perm.action, + perm.resource, + AuthorizeResult.ALLOW, + ); + auditLoggerMock.auditLog.mockReset(); + } + }); + }); + + describe('Policy checks from config file', () => { + let policy: RBACPermissionPolicy; + let enfDelegate: EnforcerDelegate; + const roleMetadataStorageTest: RoleMetadataStorage = { + filterRoleMetadata: jest.fn().mockImplementation(() => []), + findRoleMetadata: jest + .fn() + .mockImplementation( + async ( + _roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + return { + roleEntityRef: 'role:default/catalog-writer', + source: 'legacy', + modifiedBy, + }; + }, + ), + createRoleMetadata: jest.fn().mockImplementation(), + updateRoleMetadata: jest.fn().mockImplementation(), + removeRoleMetadata: jest.fn().mockImplementation(), + }; + + const adminRole = 'role:default/rbac_admin'; + const groupPolicy = [ + ['user:default/test_admin', 'role:default/rbac_admin'], + ]; + const permissions = [ + ['role:default/rbac_admin', 'policy-entity', 'read', 'allow'], + ['role:default/rbac_admin', 'policy-entity', 'create', 'allow'], + ['role:default/rbac_admin', 'policy-entity', 'delete', 'allow'], + ['role:default/rbac_admin', 'policy-entity', 'update', 'allow'], + ['role:default/rbac_admin', 'catalog-entity', 'read', 'allow'], + ]; + const oldGroupPolicy = [ + 'user:default/old_admin', + 'role:default/rbac_admin', + ]; + const admins = new Array<{ name: string }>(); + admins.push({ name: 'user:default/test_admin' }); + const superUser = new Array<{ name: string }>(); + superUser.push({ name: 'user:default/super_user' }); + + catalogApiMock.getEntities.mockReturnValue({ items: [] }); + + beforeEach(async () => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async ( + _roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + return { + roleEntityRef: 'role:default/catalog-writer', + source: 'legacy', + modifiedBy, + }; + }, + ); + + const config = newConfig(csvPermFile, admins, superUser); + const adapter = await newAdapter(config); + + enfDelegate = await newEnforcerDelegate(adapter, config); + + await enfDelegate.addGroupingPolicy(oldGroupPolicy, { + source: 'configuration', + roleEntityRef: ADMIN_ROLE_NAME, + modifiedBy: `user:default/tom`, + }); + + policy = await newPermissionPolicy( + config, + enfDelegate, + roleMetadataStorageTest, + ); + }); + + it('should allow read access to resource permission for user from config file', async () => { + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'policy.entity.read', + 'policy-entity', + 'read', + ), + newPolicyQueryUser('user:default/test_admin'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForResourcedPermission( + 'user:default/test_admin', + 'policy.entity.read', + 'read', + 'policy-entity', + AuthorizeResult.ALLOW, + ); + }); + + it('should allow read access to resource permission for super user from config file', async () => { + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'policy.entity.read', + 'policy-entity', + 'read', + ), + newPolicyQueryUser('user:default/super_user'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForResourcedPermission( + 'user:default/super_user', + 'policy.entity.read', + 'read', + 'policy-entity', + AuthorizeResult.ALLOW, + ); + auditLoggerMock.auditLog.mockReset(); + + const decision2 = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.delete', + 'catalog-entity', + 'delete', + ), + newPolicyQueryUser('user:default/super_user'), + ); + expect(decision2.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForResourcedPermission( + 'user:default/super_user', + 'catalog.entity.delete', + 'delete', + 'catalog-entity', + AuthorizeResult.ALLOW, + ); + }); + + it('should remove users that are no longer in the config file', async () => { + const enfRole = await enfDelegate.getFilteredGroupingPolicy(1, adminRole); + const enfPermission = await enfDelegate.getFilteredPolicy(0, adminRole); + expect(enfRole).toEqual(groupPolicy); + expect(enfRole).not.toContain(oldGroupPolicy); + expect(enfPermission).toEqual(permissions); + }); + }); +}); + +// Notice: There is corner case, when "resourced" permission policy can be defined not by resource type, but by name. +describe('Policy checks for resourced permissions defined by name', () => { + const roleMetadataStorageTest: RoleMetadataStorage = { + filterRoleMetadata: jest.fn().mockImplementation(() => []), + findRoleMetadata: jest + .fn() + .mockImplementation( + async ( + _roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + return { + roleEntityRef: 'role:default/catalog-writer', + source: 'legacy', + modifiedBy, + }; + }, + ), + createRoleMetadata: jest.fn().mockImplementation(), + updateRoleMetadata: jest.fn().mockImplementation(), + removeRoleMetadata: jest.fn().mockImplementation(), + }; + let enfDelegate: EnforcerDelegate; + let policy: RBACPermissionPolicy; + + beforeEach(async () => { + const config = newConfig(); + const adapter = await newAdapter(config); + enfDelegate = await newEnforcerDelegate(adapter, config); + policy = await newPermissionPolicy( + config, + enfDelegate, + roleMetadataStorageTest, + ); + }); + + it('should allow access to resourced permission assigned by name', async () => { + catalogApiMock.getEntities.mockReturnValue({ items: [] }); + + await enfDelegate.addGroupingPolicy( + ['user:default/tor', 'role:default/catalog_reader'], + { + source: 'csv-file', + roleEntityRef: 'role:default/catalog_reader', + modifiedBy, + }, + ); + await enfDelegate.addPolicy([ + 'role:default/catalog_reader', + 'catalog.entity.read', + 'read', + 'allow', + ]); + + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.read', + 'catalog-entity', + 'read', + ), + newPolicyQueryUser('user:default/tor'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + }); + + it('should allow access to resourced permission assigned by name, because it has higher priority then permission for the same resource assigned by resource type', async () => { + catalogApiMock.getEntities.mockReturnValue({ items: [] }); + + await enfDelegate.addGroupingPolicy( + ['user:default/tor', 'role:default/catalog_reader'], + { + source: 'csv-file', + roleEntityRef: 'role:default/catalog_reader', + modifiedBy, + }, + ); + await enfDelegate.addPolicies([ + ['role:default/catalog_reader', 'catalog.entity.read', 'read', 'allow'], + ['role:default/catalog_reader', 'catalog-entity', 'read', 'deny'], + ]); + + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.read', + 'catalog-entity', + 'read', + ), + newPolicyQueryUser('user:default/tor'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + }); + + it('should deny access to resourced permission assigned by name, because it has higher priority then permission for the same resource assigned by resource type', async () => { + catalogApiMock.getEntities.mockReturnValue({ items: [] }); + + await enfDelegate.addGroupingPolicy( + ['user:default/tor', 'role:default/catalog_reader'], + { + source: 'csv-file', + roleEntityRef: 'role:default/catalog_reader', + modifiedBy, + }, + ); + + await enfDelegate.addPolicies([ + ['role:default/catalog_reader', 'catalog.entity.read', 'read', 'deny'], + ['role:default/catalog_reader', 'catalog-entity', 'read', 'allow'], + ]); + + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.read', + 'catalog-entity', + 'read', + ), + newPolicyQueryUser('user:default/tor'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'user:default/tor', + 'catalog.entity.read', + 'catalog-entity', + 'read', + AuthorizeResult.DENY, + ); + }); + + it('should allow access to resourced permission assigned by name, but user inherits policy from his group', async () => { + const groupEntityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'team-a', + namespace: 'default', + }, + spec: { + members: ['tor'], + }, + }; + catalogApiMock.getEntities.mockImplementation(_arg => { + return { items: [groupEntityMock] }; + }); + + await enfDelegate.addGroupingPolicy( + ['group:default/team-a', 'role:default/catalog_user'], + { + source: 'csv-file', + roleEntityRef: 'role:default/catalog_user', + modifiedBy, + }, + ); + + await enfDelegate.addPolicies([ + ['role:default/catalog_user', 'catalog.entity.read', 'read', 'allow'], + ]); + + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.read', + 'catalog-entity', + 'read', + ), + newPolicyQueryUser('user:default/tor'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForNonResourcedPermission( + 'user:default/tor', + 'catalog.entity.read', + 'catalog-entity', + 'read', + AuthorizeResult.ALLOW, + ); + }); + + it('should allow access to resourced permission assigned by name, but user inherits policy from few groups', async () => { + const groupEntityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'team-a', + namespace: 'default', + }, + spec: { + members: ['tor'], + parent: 'team-b', + }, + }; + const groupParentMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'team-b', + namespace: 'default', + }, + }; + catalogApiMock.getEntities.mockImplementation(_arg => { + return { items: [groupParentMock, groupEntityMock] }; + }); + + await enfDelegate.addGroupingPolicy( + ['group:default/team-b', 'role:default/catalog_user'], + { + source: 'csv-file', + roleEntityRef: 'role:default/catalog_user', + modifiedBy, + }, + ); + await enfDelegate.addGroupingPolicy( + ['group:default/team-a', 'group:default/team-b'], + { + source: 'csv-file', + roleEntityRef: 'role:default/catalog_user', + modifiedBy, + }, + ); + + await enfDelegate.addPolicies([ + ['role:default/catalog_user', 'catalog.entity.read', 'read', 'allow'], + ]); + + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.read', + 'catalog-entity', + 'read', + ), + newPolicyQueryUser('user:default/tor'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForNonResourcedPermission( + 'user:default/tor', + 'catalog.entity.read', + 'catalog-entity', + 'read', + AuthorizeResult.ALLOW, + ); + }); +}); + +describe('Policy checks for users and groups', () => { + let policy: RBACPermissionPolicy; + + beforeEach(async () => { + const policyChecksCSV = resolve( + __dirname, + '../../__fixtures__/data/valid-csv/policy-checks.csv', + ); + const config = newConfig(policyChecksCSV); + const adapter = await newAdapter(config); + + const enfDelegate = await newEnforcerDelegate(adapter, config); + + policy = await newPermissionPolicy(config, enfDelegate); + + catalogApiMock.getEntities.mockReset(); + }); + + // User inherits permissions from groups and their parent groups. + // This behavior can be configured with `policy_effect` in the model. + // Also it can be customized using casbin function. + // Test suite table: + // +-------+---------+----------+-------+ + // | Group | User | result | case# | + // +-------+---------+----------+-------+ + // | deny | allow | deny | 1 | + + // | deny | - | deny | 2 | + + // | deny | deny | deny | 3 | + + // +-------+---------+----------+-------+ + // | allow | allow | allow | 4 | + + // | allow | - | allow | 5 | + + // | allow | deny | deny | 6 | + // +-------+---------+----------+-------+ + + // Basic type permissions + + // case1 + it('should deny access to basic permission for user Alice with "allow" "use" action, when her group "deny" this action', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_admin', + namespace: 'default', + }, + spec: { + members: ['alice'], + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource'), + newPolicyQueryUser('user:default/alice'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'user:default/alice', + 'test.resource', + undefined, + 'use', + AuthorizeResult.DENY, + ); + }); + + // case2 + it('should deny access to basic permission for user Akira without("-") "use" action definition, when his group "deny" this action', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_admin', + namespace: 'default', + }, + spec: { + members: ['akira'], + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource'), + newPolicyQueryUser('user:default/akira'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'user:default/akira', + 'test.resource', + undefined, + 'use', + AuthorizeResult.DENY, + ); + }); + + // case3 + it('should deny access to basic permission for user Antey with "deny" "use" action definition, when his group "deny" this action', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_admin', + namespace: 'default', + }, + spec: { + members: ['antey'], + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource'), + newPolicyQueryUser('user:default/antey'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'user:default/antey', + 'test.resource', + undefined, + 'use', + AuthorizeResult.DENY, + ); + }); + + // case4 + it('should allow access to basic permission for user Julia with "allow" "use" action, when her group "allow" this action', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_read_admin', + namespace: 'default', + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource'), + newPolicyQueryUser('user:default/julia'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForNonResourcedPermission( + 'user:default/julia', + 'test.resource', + undefined, + 'use', + AuthorizeResult.ALLOW, + ); + }); + + // case5 + it('should allow access to basic permission for user Mike without("-") "use" action definition, when his group "allow" this action', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_read_admin', + namespace: 'default', + }, + spec: { + members: ['mike'], + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource'), + newPolicyQueryUser('user:default/mike'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForNonResourcedPermission( + 'user:default/mike', + 'test.resource', + undefined, + 'use', + AuthorizeResult.ALLOW, + ); + }); + + // case6 + it('should deny access to basic permission for user Tom with "deny" "use" action definition, when his group "allow" this action', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_read_admin', + namespace: 'default', + }, + spec: { + members: ['tom'], + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource'), + newPolicyQueryUser('user:default/tom'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'user:default/tom', + 'test.resource', + undefined, + 'use', + AuthorizeResult.DENY, + ); + }); + + // inheritance case + it('should allow access to basic permission to test.resource.2 for user Mike with "-" "use" action definition, when parent group of his group "allow" this action', async () => { + const groupMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_read_admin', + namespace: 'default', + }, + spec: { + members: ['mike'], + parent: 'data_parent_admin', + }, + }; + + const groupParentMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_parent_admin', + namespace: 'default', + }, + }; + + catalogApiMock.getEntities.mockImplementation(_arg => { + return { items: [groupMock, groupParentMock] }; + }); + + const decision = await policy.handle( + newPolicyQueryWithBasicPermission('test.resource.2'), + newPolicyQueryUser('user:default/mike'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForNonResourcedPermission( + 'user:default/mike', + 'test.resource.2', + undefined, + 'use', + AuthorizeResult.ALLOW, + ); + }); + + // Resource type permissions + + // case1 + it('should deny access to basic permission for user Alice with "allow" "read" action, when her group "deny" this action', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_admin', + namespace: 'default', + }, + spec: { + members: ['alice'], + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'test.resource.read', + 'test-resource', + 'read', + ), + newPolicyQueryUser('user:default/alice'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'user:default/alice', + 'test.resource.read', + 'test-resource', + 'read', + AuthorizeResult.DENY, + ); + }); + + // case2 + it('should deny access to basic permission for user Akira without("-") "read" action definition, when his group "deny" this action', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_admin', + namespace: 'default', + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'test.resource.read', + 'test-resource', + 'read', + ), + newPolicyQueryUser('user:default/akira'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'user:default/akira', + 'test.resource.read', + 'test-resource', + 'read', + AuthorizeResult.DENY, + ); + }); + + // case3 + it('should deny access to basic permission for user Antey with "deny" "read" action definition, when his group "deny" this action', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_admin', + namespace: 'default', + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'test.resource.read', + 'test-resource', + 'read', + ), + newPolicyQueryUser('user:default/antey'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'user:default/antey', + 'test.resource.read', + 'test-resource', + 'read', + AuthorizeResult.DENY, + ); + }); + + // case4 + it('should allow access to basic permission for user Julia with "allow" "read" action, when her group "allow" this action', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_read_admin', + namespace: 'default', + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'test.resource.read', + 'test-resource', + 'read', + ), + newPolicyQueryUser('user:default/julia'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForNonResourcedPermission( + 'user:default/julia', + 'test.resource.read', + 'test-resource', + 'read', + AuthorizeResult.ALLOW, + ); + }); + + // case5 + it('should allow access to basic permission for user Mike without("-") "read" action definition, when his group "allow" this action', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_read_admin', + namespace: 'default', + }, + spec: { + members: ['mike'], + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'test.resource.read', + 'test-resource', + 'read', + ), + newPolicyQueryUser('user:default/mike'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForNonResourcedPermission( + 'user:default/mike', + 'test.resource.read', + 'test-resource', + 'read', + AuthorizeResult.ALLOW, + ); + }); + + // case6 + it('should deny access to basic permission for user Tom with "deny" "read" action definition, when his group "allow" this action', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_read_admin', + namespace: 'default', + }, + spec: { + members: ['tom'], + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'test.resource.read', + 'test-resource', + 'read', + ), + newPolicyQueryUser('user:default/tom'), + ); + expect(decision.result).toBe(AuthorizeResult.DENY); + verifyAuditLogForNonResourcedPermission( + 'user:default/tom', + 'test.resource.read', + 'test-resource', + 'read', + AuthorizeResult.DENY, + ); + }); + + // inheritance case + it('should allow access to resource permission to test-resource for user Mike with "-" "write" action definition, when parent group of his group "allow" this action', async () => { + const groupMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_read_admin', + namespace: 'default', + }, + spec: { + members: ['mike'], + parent: 'data_parent_admin', + }, + }; + + const groupParentMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'data_parent_admin', + namespace: 'default', + }, + }; + + catalogApiMock.getEntities.mockImplementation(_arg => { + return { items: [groupParentMock, groupMock] }; + }); + + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'test.resource.create', + 'test-resource', + 'create', + ), + newPolicyQueryUser('user:default/mike'), + ); + expect(decision.result).toBe(AuthorizeResult.ALLOW); + verifyAuditLogForNonResourcedPermission( + 'user:default/mike', + 'test.resource.create', + 'test-resource', + 'create', + AuthorizeResult.ALLOW, + ); + }); +}); + +describe('Policy checks for conditional policies', () => { + let policy: RBACPermissionPolicy; + + beforeEach(async () => { + const config = newConfig(undefined, []); + const adapter = await newAdapter(config); + const theModel = newModelFromString(MODEL); + const logger = mockServices.logger.mock(); + const enf = await createEnforcer(theModel, adapter, logger, config); + const policies = [['role:default/test', 'catalog-entity', 'read', 'allow']]; + const groupPolicies = [ + ['group:default/test-group', 'role:default/test'], + ['group:default/qa', 'role:default/qa'], + ]; + await enf.addPolicies(policies); + await enf.addGroupingPolicies(groupPolicies); + + const enfDelegate = new EnforcerDelegate( + enf, + roleMetadataStorageMock, + mockClientKnex, + ); + + policy = await RBACPermissionPolicy.build( + logger, + auditLoggerMock, + config, + conditionalStorageMock, + enfDelegate, + roleMetadataStorageMock, + mockClientKnex, + pluginMetadataCollectorMock as PluginPermissionMetadataCollector, + mockAuthService, + ); + + catalogApiMock.getEntities.mockReset(); + }); + + it('should execute condition policy', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'test-group', + namespace: 'default', + }, + spec: { + members: ['mike'], + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + (conditionalStorageMock.filterConditions as jest.Mock).mockReturnValueOnce([ + { + id: 1, + pluginId: 'catalog', + resourceType: 'catalog-entity', + actions: ['read'], + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['group:default/test-group'], + }, + }, + }, + ]); + + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.read', + 'catalog-entity', + 'read', + ), + newPolicyQueryUser('user:default/mike'), + ); + expect(decision).toStrictEqual({ + pluginId: 'catalog', + resourceType: 'catalog-entity', + result: AuthorizeResult.CONDITIONAL, + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['group:default/test-group'], + }, + }, + ], + }, + }); + }); + + it('should execute condition policy with current user alias', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'test-group', + namespace: 'default', + }, + spec: { + members: ['mike'], + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + (conditionalStorageMock.filterConditions as jest.Mock).mockReturnValueOnce([ + { + id: 1, + pluginId: 'catalog', + resourceType: 'catalog-entity', + actions: ['read'], + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['$currentUser'], + }, + }, + }, + ]); + + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.read', + 'catalog-entity', + 'read', + ), + newPolicyQueryUser('user:default/mike', [ + 'user:default/mike', + 'group:default/team-a', + ]), + ); + expect(decision).toStrictEqual({ + pluginId: 'catalog', + resourceType: 'catalog-entity', + result: AuthorizeResult.CONDITIONAL, + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/mike'], + }, + }, + ], + }, + }); + }); + + it('should merge condition policies for user assigned to few roles', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'test-group', + namespace: 'default', + }, + spec: { + members: ['mike'], + }, + }; + const qaGroupMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'qa', + namespace: 'default', + }, + spec: { + members: ['mike'], + }, + }; + catalogApiMock.getEntities.mockReturnValue({ + items: [entityMock, qaGroupMock], + }); + (conditionalStorageMock.filterConditions as jest.Mock) + .mockReturnValueOnce([ + { + id: 1, + pluginId: 'catalog', + resourceType: 'catalog-entity', + actions: ['read'], + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['group:default/test-group'], + }, + }, + }, + ]) + .mockReturnValueOnce([ + { + id: 2, + pluginId: 'catalog', + resourceType: 'catalog-entity', + actions: ['read'], + roleEntityRef: 'role:default/qa', + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + }, + ]); + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.read', + 'catalog-entity', + 'read', + ), + newPolicyQueryUser('user:default/mike'), + ); + expect(decision).toStrictEqual({ + pluginId: 'catalog', + resourceType: 'catalog-entity', + result: AuthorizeResult.CONDITIONAL, + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['group:default/test-group'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group', 'User'] }, + }, + ], + }, + }); + }); + + it('should deny condition policy caused collision', async () => { + const entityMock: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'test-group', + namespace: 'default', + }, + spec: { + members: ['mike'], + }, + }; + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + (conditionalStorageMock.filterConditions as jest.Mock).mockReturnValueOnce([ + { + id: 1, + pluginId: 'catalog', + resourceType: 'catalog-entity', + actions: ['read'], + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['group:default/test-group'], + }, + }, + }, + { + id: 2, + pluginId: 'catalog-fork', + resourceType: 'catalog-entity', + actions: ['read'], + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['group:default/test-group'], + }, + }, + }, + ]); + + const decision = await policy.handle( + newPolicyQueryWithResourcePermission( + 'catalog.entity.read', + 'catalog-entity', + 'read', + ), + newPolicyQueryUser('user:default/mike'), + ); + expect(decision).toStrictEqual({ + result: AuthorizeResult.DENY, + }); + }); +}); + +function newPolicyQueryWithBasicPermission(name: string): PolicyQuery { + const mockPermission = createPermission({ + name: name, + attributes: {}, + }); + return { permission: mockPermission }; +} + +function newPolicyQueryWithResourcePermission( + name: string, + resource: string, + action: PermissionAction, +): PolicyQuery { + const mockPermission = createPermission({ + name: name, + attributes: {}, + resourceType: resource, + }); + if (action) { + mockPermission.attributes.action = action; + } + return { permission: mockPermission }; +} + +function newPolicyQueryUser( + user?: string, + ownershipEntityRefs?: string[], +): PolicyQueryUser | undefined { + if (user) { + return { + identity: { + ownershipEntityRefs: ownershipEntityRefs ?? [], + type: 'user', + userEntityRef: user, + }, + credentials: { + $$type: '@backstage/BackstageCredentials', + principal: true, + expiresAt: new Date('2021-01-01T00:00:00Z'), + }, + info: { + userEntityRef: user, + ownershipEntityRefs: ownershipEntityRefs ?? [], + }, + token: 'token', + }; + } + return undefined; +} + +function newConfig( + permFile?: string, + users?: Array<{ name: string }>, + superUsers?: Array<{ name: string }>, +): Config { + const testUsers = [ + { + name: 'user:default/guest', + }, + { + name: 'group:default/guests', + }, + ]; + + return mockServices.rootConfig({ + data: { + permission: { + rbac: { + 'policies-csv-file': permFile || csvPermFile, + policyFileReload: false, + admin: { + users: users || testUsers, + superUsers: superUsers, + }, + }, + }, + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + }, + }); +} + +async function newAdapter(config: Config): Promise { + return await new CasbinDBAdapterFactory( + config, + mockClientKnex, + ).createAdapter(); +} + +async function createEnforcer( + theModel: Model, + adapter: Adapter, + logger: LoggerService, + config: Config, +): Promise { + const catalogDBClient = Knex.knex({ client: MockClient }); + const rbacDBClient = Knex.knex({ client: MockClient }); + const enf = await newEnforcer(theModel, adapter); + + const rm = new BackstageRoleManager( + catalogApiMock, + logger, + catalogDBClient, + rbacDBClient, + config, + mockAuthService, + ); + enf.setRoleManager(rm); + enf.enableAutoBuildRoleLinks(false); + await enf.buildRoleLinks(); + + return enf; +} + +async function newEnforcerDelegate( + adapter: Adapter, + config: Config, + storedPolicies?: string[][], + storedGroupingPolicies?: string[][], +): Promise { + const theModel = newModelFromString(MODEL); + const logger = mockServices.logger.mock(); + + const enf = await createEnforcer(theModel, adapter, logger, config); + + if (storedPolicies) { + await enf.addPolicies(storedPolicies); + } + + if (storedGroupingPolicies) { + await enf.addGroupingPolicies(storedGroupingPolicies); + } + + return new EnforcerDelegate(enf, roleMetadataStorageMock, mockClientKnex); +} + +async function newPermissionPolicy( + config: Config, + enfDelegate: EnforcerDelegate, + roleMock?: RoleMetadataStorage, +): Promise { + const logger = mockServices.logger.mock(); + const permissionPolicy = await RBACPermissionPolicy.build( + logger, + auditLoggerMock, + config, + conditionalStorageMock, + enfDelegate, + roleMock || roleMetadataStorageMock, + mockClientKnex, + pluginMetadataCollectorMock as PluginPermissionMetadataCollector, + mockAuthService, + ); + auditLoggerMock.auditLog.mockReset(); + return permissionPolicy; +} + +function verifyAuditLogForNonResourcedPermission( + user: string | undefined, + permissionName: string, + resourceType: string | undefined, + action: string, + result: AuthorizeResult, +) { + const expectedUser = user ?? 'user without entity'; + expect(auditLoggerMock.auditLog).toHaveBeenNthCalledWith(1, { + actorId: expectedUser, + eventName: 'PermissionEvaluationStarted', + message: `Policy check for ${expectedUser}`, + metadata: { + action, + permissionName, + resourceType, + userEntityRef: expectedUser, + }, + stage: 'evaluatePermissionAccess', + status: 'succeeded', + }); + + const message = resourceType + ? `${ + expectedUser ?? 'user without entity' + } is ${result} for permission '${permissionName}', resource type '${resourceType}' and action '${action}'` + : `${ + expectedUser ?? 'user without entity' + } is ${result} for permission '${permissionName}' and action '${action}'`; + expect(auditLoggerMock.auditLog).toHaveBeenNthCalledWith(2, { + actorId: expectedUser, + eventName: 'PermissionEvaluationCompleted', + message, + metadata: { + action, + decision: { result }, + permissionName, + resourceType, + userEntityRef: expectedUser ?? 'user without entity', + }, + stage: 'evaluatePermissionAccess', + status: 'succeeded', + }); +} + +function verifyAuditLogForResourcedPermission( + user: string, + permissionName: string, + action: string, + resourceType: string, + result: AuthorizeResult, +) { + expect(auditLoggerMock.auditLog).toHaveBeenNthCalledWith(1, { + actorId: user, + eventName: 'PermissionEvaluationStarted', + message: `Policy check for ${user}`, + metadata: { + action, + permissionName, + resourceType, + userEntityRef: user, + }, + stage: 'evaluatePermissionAccess', + status: 'succeeded', + }); + expect(auditLoggerMock.auditLog).toHaveBeenNthCalledWith(2, { + actorId: user, + eventName: 'PermissionEvaluationCompleted', + message: `${user} is ${result} for permission '${permissionName}', resource type '${resourceType}' and action '${action}'`, + metadata: { + action, + decision: { + result, + }, + permissionName, + resourceType, + userEntityRef: user, + }, + stage: 'evaluatePermissionAccess', + status: 'succeeded', + }); +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/policies/permission-policy.ts b/workspaces/rbac/plugins/rbac-backend/src/policies/permission-policy.ts new file mode 100644 index 0000000000..95629175b1 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/policies/permission-policy.ts @@ -0,0 +1,385 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { + AuthService, + BackstageUserInfo, + LoggerService, +} from '@backstage/backend-plugin-api'; +import type { ConfigApi } from '@backstage/core-plugin-api'; +import { + AuthorizeResult, + ConditionalPolicyDecision, + isResourcePermission, + Permission, + PermissionCondition, + PermissionCriteria, + PermissionRuleParams, + PolicyDecision, + ResourcePermission, +} from '@backstage/plugin-permission-common'; +import type { + PermissionPolicy, + PolicyQuery, + PolicyQueryUser, +} from '@backstage/plugin-permission-node'; + +import type { AuditLogger } from '@janus-idp/backstage-plugin-audit-log-node'; +import type { Knex } from 'knex'; + +import { + NonEmptyArray, + toPermissionAction, +} from '@backstage-community/plugin-rbac-common'; + +import { + setAdminPermissions, + useAdminsFromConfig, +} from '../admin-permissions/admin-creation'; +import { + createPermissionEvaluationOptions, + EVALUATE_PERMISSION_ACCESS_STAGE, + EvaluationEvents, +} from '../audit-log/audit-logger'; +import { replaceAliases } from '../conditional-aliases/alias-resolver'; +import { ConditionalStorage } from '../database/conditional-storage'; +import { RoleMetadataStorage } from '../database/role-metadata'; +import { CSVFileWatcher } from '../file-permissions/csv-file-watcher'; +import { YamlConditinalPoliciesFileWatcher } from '../file-permissions/yaml-conditional-file-watcher'; +import { EnforcerDelegate } from '../service/enforcer-delegate'; +import { PluginPermissionMetadataCollector } from '../service/plugin-endpoints'; + +const evaluatePermMsg = ( + userEntityRef: string | undefined, + result: AuthorizeResult, + permission: Permission, +) => + `${userEntityRef} is ${result} for permission '${permission.name}'${ + isResourcePermission(permission) + ? `, resource type '${permission.resourceType}'` + : '' + } and action '${toPermissionAction(permission.attributes)}'`; + +export class RBACPermissionPolicy implements PermissionPolicy { + private readonly superUserList?: string[]; + + public static async build( + logger: LoggerService, + auditLogger: AuditLogger, + configApi: ConfigApi, + conditionalStorage: ConditionalStorage, + enforcerDelegate: EnforcerDelegate, + roleMetadataStorage: RoleMetadataStorage, + knex: Knex, + pluginMetadataCollector: PluginPermissionMetadataCollector, + auth: AuthService, + ): Promise { + const superUserList: string[] = []; + const adminUsers = configApi.getOptionalConfigArray( + 'permission.rbac.admin.users', + ); + + const superUsers = configApi.getOptionalConfigArray( + 'permission.rbac.admin.superUsers', + ); + + const policiesFile = configApi.getOptionalString( + 'permission.rbac.policies-csv-file', + ); + + const allowReload = + configApi.getOptionalBoolean('permission.rbac.policyFileReload') || false; + + const conditionalPoliciesFile = configApi.getOptionalString( + 'permission.rbac.conditionalPoliciesFile', + ); + + if (superUsers && superUsers.length > 0) { + for (const user of superUsers) { + const userName = user.getString('name'); + superUserList.push(userName); + } + } + + await useAdminsFromConfig( + adminUsers || [], + enforcerDelegate, + auditLogger, + roleMetadataStorage, + knex, + ); + await setAdminPermissions(enforcerDelegate, auditLogger); + + if ( + (!adminUsers || adminUsers.length === 0) && + (!superUsers || superUsers.length === 0) + ) { + logger.warn( + 'There are no admins or super admins configured for the RBAC-backend plugin.', + ); + } + + const csvFile = new CSVFileWatcher( + policiesFile, + allowReload, + logger, + enforcerDelegate, + roleMetadataStorage, + auditLogger, + ); + await csvFile.initialize(); + + const conditionalFile = new YamlConditinalPoliciesFileWatcher( + conditionalPoliciesFile, + allowReload, + logger, + conditionalStorage, + auditLogger, + auth, + pluginMetadataCollector, + roleMetadataStorage, + enforcerDelegate, + ); + await conditionalFile.initialize(); + + if (!conditionalPoliciesFile) { + // clean up conditional policies corresponding to roles from csv file + logger.info('conditional policies file feature was disabled'); + await conditionalFile.cleanUpConditionalPolicies(); + } + if (!policiesFile) { + // remove roles and policies from csv file + logger.info('csv policies file feature was disabled'); + await csvFile.cleanUpRolesAndPolicies(); + } + + return new RBACPermissionPolicy( + enforcerDelegate, + auditLogger, + conditionalStorage, + superUserList, + ); + } + + private constructor( + private readonly enforcer: EnforcerDelegate, + private readonly auditLogger: AuditLogger, + private readonly conditionStorage: ConditionalStorage, + superUserList?: string[], + ) { + this.superUserList = superUserList; + } + + async handle( + request: PolicyQuery, + user?: PolicyQueryUser, + ): Promise { + const userEntityRef = user?.info.userEntityRef ?? `user without entity`; + + let auditOptions = createPermissionEvaluationOptions( + `Policy check for ${userEntityRef}`, + userEntityRef, + request, + ); + this.auditLogger.auditLog(auditOptions); + + try { + let status = false; + + const action = toPermissionAction(request.permission.attributes); + if (!user) { + const msg = evaluatePermMsg( + userEntityRef, + AuthorizeResult.DENY, + request.permission, + ); + auditOptions = createPermissionEvaluationOptions( + msg, + userEntityRef, + request, + { result: AuthorizeResult.DENY }, + ); + await this.auditLogger.auditLog(auditOptions); + return { result: AuthorizeResult.DENY }; + } + + const permissionName = request.permission.name; + const roles = await this.enforcer.getRolesForUser(userEntityRef); + + if (isResourcePermission(request.permission)) { + const resourceType = request.permission.resourceType; + + // handle conditions if they are present + if (user) { + const conditionResult = await this.handleConditions( + userEntityRef, + request, + roles, + user.info, + ); + if (conditionResult) { + return conditionResult; + } + } + + // handle permission with 'resource' type + const hasNamedPermission = + await this.hasImplicitPermissionSpecifiedByName( + userEntityRef, + permissionName, + action, + ); + // Let's set up higher priority for permission specified by name, than by resource type + const obj = hasNamedPermission ? permissionName : resourceType; + + status = await this.isAuthorized(userEntityRef, obj, action, roles); + } else { + // handle permission with 'basic' type + status = await this.isAuthorized( + userEntityRef, + permissionName, + action, + roles, + ); + } + + const result = status ? AuthorizeResult.ALLOW : AuthorizeResult.DENY; + + const msg = evaluatePermMsg(userEntityRef, result, request.permission); + auditOptions = createPermissionEvaluationOptions( + msg, + userEntityRef, + request, + { result }, + ); + await this.auditLogger.auditLog(auditOptions); + return { result }; + } catch (error) { + await this.auditLogger.auditLog({ + message: 'Permission policy check failed', + eventName: EvaluationEvents.PERMISSION_EVALUATION_FAILED, + stage: EVALUATE_PERMISSION_ACCESS_STAGE, + status: 'failed', + errors: [error], + }); + return { result: AuthorizeResult.DENY }; + } + } + + private async hasImplicitPermissionSpecifiedByName( + userEntityRef: string, + permissionName: string, + action: string, + ): Promise { + const userPerms = await this.enforcer.getImplicitPermissionsForUser( + userEntityRef, + ); + for (const perm of userPerms) { + if (permissionName === perm[1] && action === perm[2]) { + return true; + } + } + return false; + } + + private isAuthorized = async ( + userIdentity: string, + permission: string, + action: string, + roles: string[], + ): Promise => { + if (this.superUserList!.includes(userIdentity)) { + return true; + } + + return await this.enforcer.enforce(userIdentity, permission, action, roles); + }; + + private async handleConditions( + userEntityRef: string, + request: PolicyQuery, + roles: string[], + userInfo: BackstageUserInfo, + ): Promise { + const permissionName = request.permission.name; + const resourceType = (request.permission as ResourcePermission) + .resourceType; + const action = toPermissionAction(request.permission.attributes); + + const conditions: PermissionCriteria< + PermissionCondition + >[] = []; + let pluginId = ''; + for (const role of roles) { + const conditionalDecisions = await this.conditionStorage.filterConditions( + role, + undefined, + resourceType, + [action], + [permissionName], + ); + + if (conditionalDecisions.length === 1) { + pluginId = conditionalDecisions[0].pluginId; + conditions.push(conditionalDecisions[0].conditions); + } + + // this error is unexpected and should not happen, but just in case handle it. + if (conditionalDecisions.length > 1) { + const msg = `Detected ${JSON.stringify( + conditionalDecisions, + )} collisions for conditional policies. Expected to find a stored single condition for permission with name ${permissionName}, resource type ${resourceType}, action ${action} for user ${userEntityRef}`; + const auditOptions = createPermissionEvaluationOptions( + msg, + userEntityRef, + request, + { result: AuthorizeResult.DENY }, + ); + await this.auditLogger.auditLog(auditOptions); + return { + result: AuthorizeResult.DENY, + }; + } + } + + if (conditions.length > 0) { + const result: ConditionalPolicyDecision = { + pluginId, + result: AuthorizeResult.CONDITIONAL, + resourceType, + conditions: { + anyOf: conditions as NonEmptyArray< + PermissionCriteria< + PermissionCondition + > + >, + }, + }; + + replaceAliases(result.conditions, userInfo); + + const msg = `Send condition to plugin with id ${pluginId} to evaluate permission ${permissionName} with resource type ${resourceType} and action ${action} for user ${userEntityRef}`; + const auditOptions = createPermissionEvaluationOptions( + msg, + userEntityRef, + request, + result, + ); + await this.auditLogger.auditLog(auditOptions); + return result; + } + return undefined; + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/providers/connect-providers.test.ts b/workspaces/rbac/plugins/rbac-backend/src/providers/connect-providers.test.ts new file mode 100644 index 0000000000..1fcfffed33 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/providers/connect-providers.test.ts @@ -0,0 +1,519 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { LoggerService } from '@backstage/backend-plugin-api'; +import { mockServices } from '@backstage/backend-test-utils'; + +import { + Adapter, + Enforcer, + Model, + newEnforcer, + newModelFromString, +} from 'casbin'; +import * as Knex from 'knex'; +import { MockClient } from 'knex-mock-client'; + +import type { + RBACProvider, + RBACProviderConnection, +} from '@backstage-community/plugin-rbac-node'; + +import { CasbinDBAdapterFactory } from '../database/casbin-adapter-factory'; +import { + RoleMetadataDao, + 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 { Connection, connectRBACProviders } from './connect-providers'; + +const mockLoggerService = mockServices.logger.mock(); + +const roleMetadataStorageMock: RoleMetadataStorage = { + filterRoleMetadata: jest + .fn() + .mockImplementation( + async ( + _roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + return [ + { + roleEntityRef: 'role:default/old-provider-role', + source: 'test', + modifiedBy: 'test', + }, + { + roleEntityRef: 'role:default/existing-provider-role', + source: 'test', + modifiedBy: 'test', + }, + ]; + }, + ), + findRoleMetadata: jest + .fn() + .mockImplementation( + async ( + roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + if (roleEntityRef === 'role:default/old-provider-role') { + return { + roleEntityRef: 'role:default/old-provider-role', + source: 'test', + modifiedBy: 'test', + }; + } else if (roleEntityRef === 'role:default/existing-provider-role') { + return { + roleEntityRef: 'role:default/existing-provider-role', + source: 'test', + modifiedBy: 'test', + }; + } else if (roleEntityRef === 'role:default/csv-role') { + return { + roleEntityRef: 'role:default/csv-role', + source: 'csv-file', + modifiedBy: 'csv-file', + }; + } + return undefined; + }, + ), + createRoleMetadata: jest.fn().mockImplementation(), + updateRoleMetadata: jest.fn().mockImplementation(), + removeRoleMetadata: jest.fn().mockImplementation(), +}; + +const auditLoggerMock = { + getActorId: jest.fn().mockImplementation(), + createAuditLogDetails: jest.fn().mockImplementation(), + auditLog: jest.fn().mockImplementation(() => Promise.resolve()), +}; + +// TODO: Move to 'catalogServiceMock' from '@backstage/plugin-catalog-node/testUtils' +// once '@backstage/plugin-catalog-node' is upgraded +const catalogApiMock = { + 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 mockAuthService = mockServices.auth(); + +const mockClientKnex = Knex.knex({ client: MockClient }); + +const providerMock: RBACProvider = { + getProviderName: jest.fn().mockImplementation(), + connect: jest.fn().mockImplementation(), + refresh: jest.fn().mockImplementation(), +}; + +const roleToBeRemoved = ['user:default/old', 'role:default/old-provider-role']; +const roleMetaToBeRemoved = { + modifiedBy: 'test', + source: 'test', + roleEntityRef: roleToBeRemoved[1], +}; + +const existingRoles = [ + ['user:default/bruce', 'role:default/existing-provider-role'], + ['user:default/tony', 'role:default/existing-provider-role'], +]; +const existingRoleMetadata = { + modifiedBy: 'test', + source: 'test', + roleEntityRef: existingRoles[0][1], +}; +const existingPolicy = [ + ['role:default/existing-provider-role', 'catalog-entity', 'read', 'allow'], +]; + +const config = mockServices.rootConfig({ + data: { + permission: { + rbac: {}, + }, + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + }, +}); + +describe('Connection', () => { + let provider: Connection; + let enforcerDelegate: EnforcerDelegate; + + beforeEach(async () => { + const id = 'test'; + const adapter = await new CasbinDBAdapterFactory( + config, + mockClientKnex, + ).createAdapter(); + + const stringModel = newModelFromString(MODEL); + const enf = await createEnforcer(stringModel, adapter, mockLoggerService); + + const knex = Knex.knex({ client: MockClient }); + + enforcerDelegate = new EnforcerDelegate(enf, roleMetadataStorageMock, knex); + + await enforcerDelegate.addGroupingPolicy( + roleToBeRemoved, + roleMetaToBeRemoved, + ); + + await enforcerDelegate.addGroupingPolicies( + existingRoles, + existingRoleMetadata, + ); + + await enforcerDelegate.addPolicies(existingPolicy); + + provider = new Connection( + id, + enforcerDelegate, + roleMetadataStorageMock, + mockLoggerService, + auditLoggerMock, + ); + }); + + it('should initialize', () => { + expect(provider).toBeDefined(); + }); + + describe('applyRoles', () => { + let enfAddGroupingPolicySpy: jest.SpyInstance< + Promise, + [ + policy: string[], + roleMetadata: RoleMetadataDao, + externalTrx?: Knex.Knex.Transaction | undefined, + ], + any + >; + let enfRemoveGroupingPolicySpy: jest.SpyInstance< + Promise, + [ + policy: string[], + roleMetadata: RoleMetadataDao, + isUpdate?: boolean | undefined, + externalTrx?: Knex.Knex.Transaction | undefined, + ], + any + >; + + afterEach(() => { + (mockLoggerService.warn as jest.Mock).mockReset(); + }); + + it('should add the new roles', async () => { + enfAddGroupingPolicySpy = jest.spyOn( + enforcerDelegate, + 'addGroupingPolicy', + ); + + const roles = [ + ['user:default/test', 'role:default/test-provider'], + ['user:default/bruce', 'role:default/existing-provider-role'], + ['user:default/tony', 'role:default/existing-provider-role'], + ]; + + const roleToAdd = [['user:default/test', 'role:default/test-provider']]; + const roleMeta = { + createdAt: new Date().toUTCString(), + lastModified: new Date().toUTCString(), + modifiedBy: 'test', + source: 'test', + roleEntityRef: roleToAdd[0][1], + }; + + await provider.applyRoles(roles); + expect(enfAddGroupingPolicySpy).toHaveBeenCalledWith( + ...roleToAdd, + roleMeta, + ); + }); + + it('should remove the old roles', async () => { + enfRemoveGroupingPolicySpy = jest.spyOn( + enforcerDelegate, + 'removeGroupingPolicy', + ); + + await provider.applyRoles([ + ['user:default/bruce', 'role:default/existing-provider-role'], + ['user:default/tony', 'role:default/existing-provider-role'], + ]); + expect(enfRemoveGroupingPolicySpy).toHaveBeenCalledWith( + roleToBeRemoved, + roleMetaToBeRemoved, + ); + }); + + it('should add a role to an already existing role', async () => { + enfAddGroupingPolicySpy = jest.spyOn( + enforcerDelegate, + 'addGroupingPolicy', + ); + + const roles = [ + ['user:default/peter', 'role:default/existing-provider-role'], + ['user:default/bruce', 'role:default/existing-provider-role'], + ['user:default/tony', 'role:default/existing-provider-role'], + ]; + + const roleToAdd = [ + ['user:default/peter', 'role:default/existing-provider-role'], + ]; + const roleMeta = { + modifiedBy: 'test', + source: 'test', + roleEntityRef: roleToAdd[0][1], + }; + + await provider.applyRoles(roles); + expect(enfAddGroupingPolicySpy).toHaveBeenCalledWith( + ...roleToAdd, + roleMeta, + ); + }); + + it('should remove a role member from an already existing role', async () => { + enfRemoveGroupingPolicySpy = jest.spyOn( + enforcerDelegate, + 'removeGroupingPolicy', + ); + + await provider.applyRoles([ + ['user:default/tony', 'role:default/existing-provider-role'], + ]); + expect(enfRemoveGroupingPolicySpy).toHaveBeenNthCalledWith( + 1, + roleToBeRemoved, + roleMetaToBeRemoved, + ); + expect(enfRemoveGroupingPolicySpy).toHaveBeenNthCalledWith( + 2, + existingRoles[0], + existingRoleMetadata, + true, + ); + }); + + it('should log an error if a role is not valid', async () => { + const roles = [ + ['user:default/test', 'role:default/'], + ['user:default/bruce', 'role:default/existing-provider-role'], + ['user:default/tony', 'role:default/existing-provider-role'], + ]; + + const roleToAdd = `user:default/test,role:default/`; + + await provider.applyRoles(roles); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + `Failed to validate group policy ${roleToAdd}. Cause: Entity reference "role:default/" was not on the form [:][/]`, + ); + }); + + it('should still add new role, even if there is an invalid role in array', async () => { + enfAddGroupingPolicySpy = jest.spyOn( + enforcerDelegate, + 'addGroupingPolicy', + ); + + const roles = [ + ['user:default/test', 'role:default/'], + ['user:default/test', 'role:default/test-provider'], + ['user:default/bruce', 'role:default/existing-provider-role'], + ['user:default/tony', 'role:default/existing-provider-role'], + ]; + + const failingRoleToAdd = `user:default/test,role:default/`; + const roleToAdd = [['user:default/test', 'role:default/test-provider']]; + const roleMeta = { + createdAt: new Date().toUTCString(), + lastModified: new Date().toUTCString(), + modifiedBy: 'test', + source: 'test', + roleEntityRef: roleToAdd[0][1], + }; + + await provider.applyRoles(roles); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + `Failed to validate group policy ${failingRoleToAdd}. Cause: Entity reference "role:default/" was not on the form [:][/]`, + ); + expect(enfAddGroupingPolicySpy).toHaveBeenCalledWith( + ...roleToAdd, + roleMeta, + ); + }); + }); + + describe('applyPermissions', () => { + let enfAddPolicySpy: jest.SpyInstance< + Promise, + [ + policy: string[], + externalTrx?: Knex.Knex.Transaction | undefined, + ], + any + >; + let enfRemovePolicySpy: jest.SpyInstance< + Promise, + [ + policy: string[], + externalTrx?: Knex.Knex.Transaction | undefined, + ], + any + >; + + afterEach(() => { + (mockLoggerService.warn as jest.Mock).mockReset(); + }); + + it('should add new permissions', async () => { + enfAddPolicySpy = jest.spyOn(enforcerDelegate, 'addPolicy'); + + const policies = [ + ['role:default/provider-role', 'catalog-entity', 'read', 'allow'], + ]; + + await provider.applyPermissions(policies); + expect(enfAddPolicySpy).toHaveBeenCalledWith(...policies); + }); + + it('should remove old permissions', async () => { + enfRemovePolicySpy = jest.spyOn(enforcerDelegate, 'removePolicy'); + + const policies = [ + ['role:default/provider-role', 'catalog-entity', 'read', 'allow'], + ]; + + await provider.applyPermissions(policies); + expect(enfRemovePolicySpy).toHaveBeenCalledWith(...existingPolicy); + }); + + it('should log an error for an invalid permission', async () => { + enfAddPolicySpy = jest.spyOn(enforcerDelegate, 'addPolicy'); + + const policies = [ + ['role:default/provider-role', 'catalog-entity', 'read', 'temp'], + ]; + + await provider.applyPermissions(policies); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + `Invalid permission policy, Error: 'effect' has invalid value: 'temp'. It should be: 'allow' or 'deny'`, + ); + }); + + it('should log an error for an invalid permission by source', async () => { + enfAddPolicySpy = jest.spyOn(enforcerDelegate, 'addPolicy'); + + const policies = [ + ['role:default/csv-role', 'catalog-entity', 'read', 'allow'], + ]; + + await provider.applyPermissions(policies); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + `Unable to add policy ${policies[0].toString()}. Cause: source does not match originating role ${ + policies[0][0] + }, consider making changes to the 'CSV-FILE'`, + ); + }); + + it('should still add new permission, even if there is an invalid permission in array', () => { + expect('').toEqual(''); + }); + }); +}); + +describe('connectRBACProviders', () => { + let connectSpy: jest.SpyInstance< + Promise, + [connection: RBACProviderConnection], + any + >; + it('should initialize rbac providers', async () => { + connectSpy = jest.spyOn(providerMock, 'connect'); + + const adapter = await new CasbinDBAdapterFactory( + config, + mockClientKnex, + ).createAdapter(); + + const stringModel = newModelFromString(MODEL); + const enf = await createEnforcer(stringModel, adapter, mockLoggerService); + + const knex = Knex.knex({ client: MockClient }); + + const enforcerDelegate = new EnforcerDelegate( + enf, + roleMetadataStorageMock, + knex, + ); + + await connectRBACProviders( + [providerMock], + enforcerDelegate, + roleMetadataStorageMock, + mockLoggerService, + auditLoggerMock, + ); + + expect(connectSpy).toHaveBeenCalled(); + }); +}); + +async function createEnforcer( + theModel: Model, + adapter: Adapter, + logger: LoggerService, +): Promise { + const catalogDBClient = Knex.knex({ client: MockClient }); + const rbacDBClient = Knex.knex({ client: MockClient }); + const enf = await newEnforcer(theModel, adapter); + + const rm = new BackstageRoleManager( + catalogApiMock, + logger, + catalogDBClient, + rbacDBClient, + config, + mockAuthService, + ); + enf.setRoleManager(rm); + enf.enableAutoBuildRoleLinks(false); + await enf.buildRoleLinks(); + + return enf; +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/providers/connect-providers.ts b/workspaces/rbac/plugins/rbac-backend/src/providers/connect-providers.ts new file mode 100644 index 0000000000..757cbfc3a4 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/providers/connect-providers.ts @@ -0,0 +1,310 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { LoggerService } from '@backstage/backend-plugin-api'; + +import type { AuditLogger } from '@janus-idp/backstage-plugin-audit-log-node'; +import { + Enforcer, + newEnforcer, + newModelFromString, + StringAdapter, +} from 'casbin'; + +import type { + RBACProvider, + RBACProviderConnection, +} from '@backstage-community/plugin-rbac-node'; + +import { + HANDLE_RBAC_DATA_STAGE, + PermissionAuditInfo, + PermissionEvents, + RBAC_BACKEND, + RoleAuditInfo, + RoleEvents, +} from '../audit-log/audit-logger'; +import { RoleMetadataStorage } from '../database/role-metadata'; +import { transformArrayToPolicy, typedPoliciesToString } from '../helper'; +import { EnforcerDelegate } from '../service/enforcer-delegate'; +import { MODEL } from '../service/permission-model'; +import { + validateGroupingPolicy, + validatePolicy, + validateSource, +} from '../validation/policies-validation'; + +export class Connection implements RBACProviderConnection { + constructor( + private readonly id: string, + private readonly enforcer: EnforcerDelegate, + private readonly roleMetadataStorage: RoleMetadataStorage, + private readonly logger: LoggerService, + private readonly auditLogger: AuditLogger, + ) {} + + async applyRoles(roles: string[][]): Promise { + const stringPolicy = typedPoliciesToString(roles, 'g'); + + const providerRolesforRemoval: string[][] = []; + + const tempEnforcer = await newEnforcer( + newModelFromString(MODEL), + new StringAdapter(stringPolicy), + ); + + const providerRoles = await this.getProviderRoles(); + + // Get the roles for this provider coming from rbac plugin + for (const providerRole of providerRoles) { + providerRolesforRemoval.push( + ...(await this.enforcer.getFilteredGroupingPolicy(1, providerRole)), + ); + } + + // Remove role + // role exists in rbac but does not exist in provider + await this.removeRoles(providerRolesforRemoval, tempEnforcer); + + // Add the role + // role exists in provider but does not exist in rbac + await this.addRoles(roles); + } + + async applyPermissions(permissions: string[][]): Promise { + const stringPolicy = typedPoliciesToString(permissions, 'p'); + + const providerPermissions: string[][] = []; + + const tempEnforcer = await newEnforcer( + newModelFromString(MODEL), + new StringAdapter(stringPolicy), + ); + + const providerRoles = await this.getProviderRoles(); + + // Get the roles for this provider coming from rbac plugin + for (const providerRole of providerRoles) { + providerPermissions.push( + ...(await this.enforcer.getFilteredPolicy(0, providerRole)), + ); + } + + await this.removePermissions(providerPermissions, tempEnforcer); + + await this.addPermissions(permissions); + } + + private async addRoles(roles: string[][]): Promise { + for (const role of roles) { + if (!(await this.enforcer.hasGroupingPolicy(...role))) { + const err = await validateGroupingPolicy( + role, + this.roleMetadataStorage, + this.id, + ); + + if (err) { + this.logger.warn(err.message); + continue; // Skip adding this role as there was an error + } + + let roleMeta = await this.roleMetadataStorage.findRoleMetadata(role[1]); + + const eventName = roleMeta + ? RoleEvents.UPDATE_ROLE + : RoleEvents.CREATE_ROLE; + const message = roleMeta ? 'Updated role' : 'Created role'; + + // role does not exist in rbac, create the metadata for it + if (!roleMeta) { + roleMeta = { + modifiedBy: this.id, + source: this.id, + roleEntityRef: role[1], + }; + } + + await this.enforcer.addGroupingPolicy(role, roleMeta); + + await this.auditLogger.auditLog({ + actorId: RBAC_BACKEND, + message, + eventName, + metadata: { ...roleMeta, members: [role[0]] }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); + } + } + } + + private async removeRoles( + providerRoles: string[][], + tempEnforcer: Enforcer, + ): Promise { + // Remove role + // role exists in rbac but does not exist in provider + for (const role of providerRoles) { + if (!(await tempEnforcer.hasGroupingPolicy(...role))) { + const roleMeta = await this.roleMetadataStorage.findRoleMetadata( + role[1], + ); + + const currentRole = await this.enforcer.getFilteredGroupingPolicy( + 1, + role[1], + ); + + if (!roleMeta) { + this.logger.warn('role does not exist'); + continue; + } + + const singleRole = roleMeta && currentRole.length === 1; + + let eventName: string; + let message: string; + + // Only one role exists in rbac remove role metadata as well + if (singleRole) { + eventName = RoleEvents.DELETE_ROLE; + message = 'Deleted role'; + await this.enforcer.removeGroupingPolicy(role, roleMeta); + + await this.auditLogger.auditLog({ + actorId: RBAC_BACKEND, + message, + eventName, + metadata: { ...roleMeta, members: [role[0]] }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); + continue; // Move on to the next role + } + + eventName = RoleEvents.UPDATE_ROLE; + message = 'Updated role: deleted members'; + await this.enforcer.removeGroupingPolicy(role, roleMeta, true); + + await this.auditLogger.auditLog({ + actorId: RBAC_BACKEND, + message, + eventName, + metadata: { ...roleMeta, members: [role[0]] }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); + } + } + } + + private async addPermissions(permissions: string[][]): Promise { + for (const permission of permissions) { + if (!(await this.enforcer.hasPolicy(...permission))) { + const transformedPolicy = transformArrayToPolicy(permission); + const metadata = await this.roleMetadataStorage.findRoleMetadata( + permission[0], + ); + + let err = validatePolicy(transformedPolicy); + if (err) { + this.logger.warn(`Invalid permission policy, ${err}`); + continue; // Skip this invalid permission policy + } + + err = await validateSource(this.id, metadata); + if (err) { + this.logger.warn( + `Unable to add policy ${permission}. Cause: ${err.message}`, + ); + continue; + } + + await this.enforcer.addPolicy(permission); + + await this.auditLogger.auditLog({ + actorId: RBAC_BACKEND, + message: `Created policy`, + eventName: PermissionEvents.CREATE_POLICY, + metadata: { policies: [permission], source: this.id }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); + } + } + } + + private async removePermissions( + providerPermissions: string[][], + tempEnforcer: Enforcer, + ): Promise { + const removedPermissions: string[][] = []; + for (const permission of providerPermissions) { + if (!(await tempEnforcer.hasPolicy(...permission))) { + this.enforcer.removePolicy(permission); + removedPermissions.push(permission); + } + + if (removedPermissions.length > 0) { + await this.auditLogger.auditLog({ + actorId: RBAC_BACKEND, + message: `Deleted policies`, + eventName: PermissionEvents.DELETE_POLICY, + metadata: { + policies: removedPermissions, + source: this.id, + }, + stage: HANDLE_RBAC_DATA_STAGE, + status: 'succeeded', + }); + } + } + } + + private async getProviderRoles(): Promise { + const currentRoles = await this.roleMetadataStorage.filterRoleMetadata( + this.id, + ); + return currentRoles.map(meta => meta.roleEntityRef); + } +} + +export async function connectRBACProviders( + providers: RBACProvider[], + enforcer: EnforcerDelegate, + roleMetadataStorage: RoleMetadataStorage, + logger: LoggerService, + auditLogger: AuditLogger, +) { + await Promise.all( + providers.map(async provider => { + try { + const connection = new Connection( + provider.getProviderName(), + enforcer, + roleMetadataStorage, + logger, + auditLogger, + ); + return provider.connect(connection); + } catch (error) { + throw new Error( + `Unable to connect provider ${provider.getProviderName()}, ${error}`, + ); + } + }), + ); +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/role-manager/ancestor-search-memo.test.ts b/workspaces/rbac/plugins/rbac-backend/src/role-manager/ancestor-search-memo.test.ts new file mode 100644 index 0000000000..8e6f0e2e82 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/role-manager/ancestor-search-memo.test.ts @@ -0,0 +1,378 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { mockServices } from '@backstage/backend-test-utils'; +import type { Entity, GroupEntity } from '@backstage/catalog-model'; + +import * as Knex from 'knex'; +import { createTracker, MockClient, Tracker } from 'knex-mock-client'; + +import { AncestorSearchMemo, Relation } from './ancestor-search-memo'; + +const mockAuthService = mockServices.auth(); + +describe('ancestor-search-memo', () => { + const userRelations = [ + { + source_entity_ref: 'user:default/adam', + target_entity_ref: 'group:default/team-a', + }, + ]; + + const allRelations = [ + { + source_entity_ref: 'user:default/adam', + target_entity_ref: 'group:default/team-a', + }, + { + source_entity_ref: 'group:default/team-a', + target_entity_ref: 'group:default/team-b', + }, + { + source_entity_ref: 'group:default/team-b', + target_entity_ref: 'group:default/team-c', + }, + { + source_entity_ref: 'user:default/george', + target_entity_ref: 'group:default/team-d', + }, + { + source_entity_ref: 'group:default/team-d', + target_entity_ref: 'group:default/team-e', + }, + { + source_entity_ref: 'group:default/team-e', + target_entity_ref: 'group:default/team-f', + }, + ]; + + const testGroups = [ + createGroupEntity('team-a', 'team-b', [], ['adam']), + createGroupEntity('team-b', 'team-c', [], []), + createGroupEntity('team-c', '', [], []), + createGroupEntity('team-d', 'team-e', [], ['george']), + createGroupEntity('team-e', 'team-f', [], []), + createGroupEntity('team-f', '', [], []), + ]; + + const testUserGroups = [createGroupEntity('team-a', 'team-b', [], ['adam'])]; + + // TODO: Move to 'catalogServiceMock' from '@backstage/plugin-catalog-node/testUtils' + // once '@backstage/plugin-catalog-node' is upgraded + const catalogApiMock: any = { + getEntities: jest.fn().mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/adam') { + return { items: testUserGroups }; + } + return { items: testGroups }; + }), + }; + + const catalogDBClient = Knex.knex({ client: MockClient }); + + let asm: AncestorSearchMemo; + + beforeEach(() => { + asm = new AncestorSearchMemo( + 'user:default/adam', + catalogApiMock, + catalogDBClient, + mockAuthService, + ); + }); + + describe('getAllGroups and getAllRelations', () => { + let tracker: Tracker; + + beforeAll(() => { + tracker = createTracker(catalogDBClient); + }); + + afterEach(() => { + tracker.reset(); + }); + + it('should return all relations', async () => { + tracker.on + .select( + /select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/, + ) + .response(allRelations); + const allRelationsTest = await asm.getAllRelations(); + expect(allRelationsTest).toEqual(allRelations); + }); + + it('should return all groups', async () => { + const allGroupsTest = await asm.getAllGroups(); + expect(allGroupsTest).toEqual(testGroups); + }); + + it('should fail to return anything when there is an error getting all relations', async () => { + const allRelationsTest = await asm.getAllRelations(); + expect(allRelationsTest).toEqual([]); + }); + }); + + describe('getUserGroups and getUserRelations', () => { + let tracker: Tracker; + + beforeAll(() => { + tracker = createTracker(catalogDBClient); + }); + + afterEach(() => { + tracker.reset(); + }); + + it('should return all user relations', async () => { + tracker.on + .select( + /select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/, + ) + .response(userRelations); + const relations = await asm.getUserRelations(); + + expect(relations).toEqual(userRelations); + }); + + it('should return all user groups', async () => { + const userGroups = await asm.getUserGroups(); + expect(userGroups).toEqual(testUserGroups); + }); + + it('should fail to return anything when there is an error getting user relations', async () => { + const relations = await asm.getUserRelations(); + + expect(relations).toEqual([]); + }); + }); + + describe('traverseRelations', () => { + let tracker: Tracker; + + beforeAll(() => { + tracker = createTracker(catalogDBClient); + }); + + afterEach(() => { + tracker.reset(); + }); + + // user:default/adam -> group:default/team-a -> group:default/team-b -> group:default/team-c + it('should build a graph for a particular user', async () => { + tracker.on + .select( + /select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/, + ) + .response(userRelations); + const userRelationsTest = await asm.getUserRelations(); + + tracker.reset(); + tracker.on + .select( + /select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/, + ) + .response(allRelations); + const allRelationsTest = await asm.getAllRelations(); + + userRelationsTest.forEach(relation => + asm.traverseRelations( + asm, + relation as Relation, + allRelationsTest as Relation[], + 0, + ), + ); + + expect(asm.hasEntityRef('user:default/adam')).toBeTruthy(); + expect(asm.hasEntityRef('group:default/team-a')).toBeTruthy(); + expect(asm.hasEntityRef('group:default/team-b')).toBeTruthy(); + expect(asm.hasEntityRef('group:default/team-c')).toBeTruthy(); + expect(asm.hasEntityRef('group:default/team-d')).toBeFalsy(); + }); + + // maxDepth of one stops here + // | + // user:default/adam -> group:default/team-a -> group:default/team-b -> group:default/team-c + it('should build the graph but stop based on the maxDepth', async () => { + const asmMaxDepth = new AncestorSearchMemo( + 'user:default/adam', + catalogApiMock, + catalogDBClient, + mockAuthService, + 1, + ); + + tracker.on + .select( + /select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/, + ) + .response(userRelations); + const userRelationsTest = await asmMaxDepth.getUserRelations(); + + tracker.reset(); + tracker.on + .select( + /select "source_entity_ref", "target_entity_ref" from "relations" where "type" = ?/, + ) + .response(allRelations); + const allRelationsTest = await asmMaxDepth.getAllRelations(); + + userRelationsTest.forEach(relation => + asmMaxDepth.traverseRelations( + asmMaxDepth, + relation as Relation, + allRelationsTest as Relation[], + 0, + ), + ); + + expect(asmMaxDepth.hasEntityRef('user:default/adam')).toBeTruthy(); + expect(asmMaxDepth.hasEntityRef('group:default/team-a')).toBeTruthy(); + expect(asmMaxDepth.hasEntityRef('group:default/team-b')).toBeTruthy(); + expect(asmMaxDepth.hasEntityRef('group:default/team-c')).toBeFalsy(); + expect(asmMaxDepth.hasEntityRef('group:default/team-d')).toBeFalsy(); + }); + }); + + describe('traverseGroups', () => { + // user:default/adam -> group:default/team-a -> group:default/team-b -> group:default/team-c + it('should build a graph for a particular user', async () => { + const userGroupsTest = await asm.getUserGroups(); + + const allGroupsTest = await asm.getAllGroups(); + + userGroupsTest.forEach(group => + asm.traverseGroups( + asm, + group as GroupEntity, + allGroupsTest as GroupEntity[], + 0, + ), + ); + + expect(asm.hasEntityRef('group:default/team-a')).toBeTruthy(); + expect(asm.hasEntityRef('group:default/team-b')).toBeTruthy(); + expect(asm.hasEntityRef('group:default/team-c')).toBeTruthy(); + expect(asm.hasEntityRef('group:default/team-d')).toBeFalsy(); + }); + + // maxDepth of one stops here + // | + // user:default/adam -> group:default/team-a -> group:default/team-b -> group:default/team-c + it('should build the graph but stop based on the maxDepth', async () => { + const asmMaxDepth = new AncestorSearchMemo( + 'user:default/adam', + catalogApiMock, + catalogDBClient, + mockAuthService, + 1, + ); + + const userGroupsTest = await asmMaxDepth.getUserGroups(); + + const allGroupsTest = await asmMaxDepth.getAllGroups(); + + userGroupsTest.forEach(group => + asmMaxDepth.traverseGroups( + asmMaxDepth, + group as GroupEntity, + allGroupsTest as GroupEntity[], + 0, + ), + ); + + expect(asmMaxDepth.hasEntityRef('group:default/team-a')).toBeTruthy(); + expect(asmMaxDepth.hasEntityRef('group:default/team-b')).toBeTruthy(); + expect(asmMaxDepth.hasEntityRef('group:default/team-c')).toBeFalsy(); + expect(asmMaxDepth.hasEntityRef('group:default/team-d')).toBeFalsy(); + }); + }); + + describe('buildUserGraph', () => { + let tracker: Tracker; + + const asmUserGraph = new AncestorSearchMemo( + 'user:default/adam', + catalogApiMock, + catalogDBClient, + mockAuthService, + ); + + const asmDBSpy = jest + .spyOn(asmUserGraph, 'doesRelationTableExist') + .mockImplementation(() => Promise.resolve(true)); + const userRelationsSpy = jest + .spyOn(asmUserGraph, 'getUserRelations') + .mockImplementation(() => Promise.resolve(userRelations)); + const allRelationsSpy = jest + .spyOn(asmUserGraph, 'getAllRelations') + .mockImplementation(() => Promise.resolve(allRelations)); + + beforeAll(() => { + tracker = createTracker(catalogDBClient); + }); + + afterEach(() => { + tracker.reset(); + }); + + // user:default/adam -> group:default/team-a -> group:default/team-b -> group:default/team-c + it('should build the user graph using relations table', async () => { + await asmUserGraph.buildUserGraph(asmUserGraph); + + expect(asmDBSpy).toHaveBeenCalled(); + expect(userRelationsSpy).toHaveBeenCalled(); + expect(allRelationsSpy).toHaveBeenCalled(); + expect(asmUserGraph.hasEntityRef('user:default/adam')).toBeTruthy(); + expect(asmUserGraph.hasEntityRef('group:default/team-a')).toBeTruthy(); + expect(asmUserGraph.hasEntityRef('group:default/team-b')).toBeTruthy(); + expect(asmUserGraph.hasEntityRef('group:default/team-c')).toBeTruthy(); + expect(asmUserGraph.hasEntityRef('group:default/team-d')).toBeFalsy(); + }); + }); + + function createGroupEntity( + name: string, + parent?: string, + children?: string[], + members?: string[], + ): Entity { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name, + namespace: 'default', + }, + spec: {}, + }; + + if (children) { + entity.spec!.children = children; + } + + if (members) { + entity.spec!.members = members; + } + + if (parent) { + entity.spec!.parent = parent; + } + + return entity; + } +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts b/workspaces/rbac/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts new file mode 100644 index 0000000000..0d7b39c06a --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts @@ -0,0 +1,233 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { AuthService, LoggerService } from '@backstage/backend-plugin-api'; +import type { CatalogApi } from '@backstage/catalog-client'; +import type { Entity } from '@backstage/catalog-model'; + +import { alg, Graph } from '@dagrejs/graphlib'; +import { Knex } from 'knex'; + +export interface Relation { + source_entity_ref: string; + target_entity_ref: string; +} + +export type ASMGroup = Relation | Entity; + +// AncestorSearchMemo - should be used to build group hierarchy graph for User entity reference. +// It supports search group entity reference link in the graph. +// Also AncestorSearchMemo supports detection cycle dependencies between groups in the graph. +// +export class AncestorSearchMemo { + private graph: Graph; + + private catalogApi: CatalogApi; + private catalogDBClient: Knex; + private auth: AuthService; + + private userEntityRef: string; + private maxDepth?: number; + + constructor( + userEntityRef: string, + catalogApi: CatalogApi, + catalogDBClient: Knex, + auth: AuthService, + maxDepth?: number, + ) { + this.graph = new Graph({ directed: true }); + this.userEntityRef = userEntityRef; + this.catalogApi = catalogApi; + this.catalogDBClient = catalogDBClient; + this.auth = auth; + this.maxDepth = maxDepth; + } + + isAcyclic(): boolean { + return alg.isAcyclic(this.graph); + } + + findCycles(): string[][] { + return alg.findCycles(this.graph); + } + + setEdge(parentEntityRef: string, childEntityRef: string) { + this.graph.setEdge(parentEntityRef, childEntityRef); + } + + setNode(entityRef: string): void { + this.graph.setNode(entityRef); + } + + hasEntityRef(groupRef: string): boolean { + return this.graph.hasNode(groupRef); + } + + debugNodesAndEdges(logger: LoggerService, userEntity: string): void { + logger.debug( + `SubGraph edges: ${JSON.stringify(this.graph.edges())} for ${userEntity}`, + ); + logger.debug( + `SubGraph nodes: ${JSON.stringify(this.graph.nodes())} for ${userEntity}`, + ); + } + + getNodes(): string[] { + return this.graph.nodes(); + } + + async doesRelationTableExist(): Promise { + try { + return await this.catalogDBClient.schema.hasTable('relations'); + } catch (error) { + return false; + } + } + + async getAllGroups(): Promise { + const { token } = await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); + + const { items } = await this.catalogApi.getEntities( + { + filter: { kind: 'Group' }, + fields: ['metadata.name', 'metadata.namespace', 'spec.parent'], + }, + { token }, + ); + return items; + } + + async getAllRelations(): Promise { + try { + const rows = await this.catalogDBClient('relations') + .select('source_entity_ref', 'target_entity_ref') + .where('type', 'childOf'); + return rows; + } catch (error) { + return []; + } + } + + async getUserGroups(): Promise { + const { token } = await this.auth.getPluginRequestToken({ + onBehalfOf: await this.auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); + const { items } = await this.catalogApi.getEntities( + { + filter: { kind: 'Group', 'relations.hasMember': this.userEntityRef }, + fields: ['metadata.name', 'metadata.namespace', 'spec.parent'], + }, + { token }, + ); + return items; + } + + async getUserRelations(): Promise { + try { + const rows = await this.catalogDBClient('relations') + .select('source_entity_ref', 'target_entity_ref') + .where({ type: 'memberOf', source_entity_ref: this.userEntityRef }); + return rows; + } catch (error) { + return []; + } + } + + traverseGroups( + memo: AncestorSearchMemo, + group: Entity, + allGroups: Entity[], + current_depth: number, + ) { + const groupName = `group:${group.metadata.namespace?.toLocaleLowerCase( + 'en-US', + )}/${group.metadata.name.toLocaleLowerCase('en-US')}`; + if (!memo.hasEntityRef(groupName)) { + memo.setNode(groupName); + } + + if (this.maxDepth !== undefined && current_depth >= this.maxDepth) { + return; + } + const depth = current_depth + 1; + + const parent = group.spec?.parent as string; + const parentGroup = allGroups.find(g => g.metadata.name === parent); + + if (parentGroup) { + const parentName = `group:${group.metadata.namespace?.toLocaleLowerCase( + 'en-US', + )}/${parentGroup.metadata.name.toLocaleLowerCase('en-US')}`; + memo.setEdge(parentName, groupName); + + if (memo.isAcyclic()) { + this.traverseGroups(memo, parentGroup, allGroups, depth); + } + } + } + + traverseRelations( + memo: AncestorSearchMemo, + relation: Relation, + allRelations: Relation[], + current_depth: number, + ) { + // We add one to the maxDepth here because the user is considered the starting node + if (this.maxDepth !== undefined && current_depth >= this.maxDepth + 1) { + return; + } + const depth = current_depth + 1; + + if (!memo.hasEntityRef(relation.source_entity_ref)) { + memo.setNode(relation.source_entity_ref); + } + + memo.setEdge(relation.target_entity_ref, relation.source_entity_ref); + + const parentGroup = allRelations.find( + g => g.source_entity_ref === relation.target_entity_ref, + ); + + if (parentGroup && memo.isAcyclic()) { + this.traverseRelations(memo, parentGroup, allRelations, depth); + } + } + + async buildUserGraph(memo: AncestorSearchMemo) { + if (await this.doesRelationTableExist()) { + const userRelations = await this.getUserRelations(); + const allRelations = await this.getAllRelations(); + userRelations.forEach(group => + this.traverseRelations( + memo, + group as Relation, + allRelations as Relation[], + 0, + ), + ); + } else { + const userGroups = await this.getUserGroups(); + const allGroups = await this.getAllGroups(); + userGroups.forEach(group => + this.traverseGroups(memo, group as Entity, allGroups as Entity[], 0), + ); + } + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/role-manager/member-list.test.ts b/workspaces/rbac/plugins/rbac-backend/src/role-manager/member-list.test.ts new file mode 100644 index 0000000000..e86cbfc399 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/role-manager/member-list.test.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as Knex from 'knex'; +import { createTracker, MockClient, Tracker } from 'knex-mock-client'; + +import { RoleMemberList } from './member-list'; + +describe('RoleMemberList', () => { + const member = 'user:default/developer'; + + const rbacDBClient = Knex.knex({ client: MockClient }); + let roleList: RoleMemberList; + let newRole: RoleMemberList; + let memberList: RoleMemberList; + + beforeEach(() => { + roleList = new RoleMemberList('role:default/test'); + newRole = new RoleMemberList('role:default/extra'); + memberList = new RoleMemberList('user:default/test'); + }); + + describe('addMembers', () => { + it('should add members to the role', () => { + const members = ['user:default/test', 'user:default/developer']; + roleList.addMembers(members); + + expect(roleList.hasMember('user:default/test')).toBeTruthy(); + expect(roleList.hasMember('user:default/developer')).toBeTruthy(); + }); + }); + + describe('addMember', () => { + it('should add a single member to the role', () => { + roleList.addMember(member); + + expect(roleList.hasMember('user:default/developer')).toBeTruthy(); + }); + + it('should not add a duplicate of an existing member', () => { + roleList.addMember(member); + + expect(roleList.getMembers().length).toEqual(1); + + roleList.addMember(member); + expect(roleList.getMembers().length).not.toEqual(2); + }); + }); + + describe('deleteMember', () => { + it('should delete a member from a role', () => { + roleList.addMember(member); + + expect(roleList.getMembers().length).toEqual(1); + + roleList.deleteMember(member); + + expect(roleList.getMembers().length).not.toEqual(1); + }); + }); + + describe('buildMembers', () => { + let tracker: Tracker; + + beforeEach(() => { + tracker = createTracker(rbacDBClient); + }); + + afterEach(() => { + tracker.reset(); + }); + + it('should build the members associated with a role using the database', async () => { + const data = [{ v0: 'user:default/qa', v1: 'role:default/qa' }]; + + tracker.on.select('casbin_rule').response(data); + + await newRole.buildMembers(newRole, rbacDBClient); + expect(newRole.hasMember('user:default/qa')).toBeTruthy(); + }); + + it('should fail to retrieve users and log an error', async () => { + const error = new Error('test error'); + tracker.on.select('casbin_rule').simulateError(error); + + await expect( + newRole.buildMembers(newRole, rbacDBClient), + ).rejects.toMatchObject({ + message: expect.stringContaining('test error'), + }); + expect(newRole.getMembers().length).toEqual(0); + }); + }); + + describe('addRoles', () => { + it('should add roles to the role member list', () => { + const roles = ['role:default/test', 'role:default/developer']; + memberList.addRoles(roles); + + expect(memberList.getRoles().length).toEqual(2); + }); + }); + + describe('buildRoles', () => { + let tracker: Tracker; + const memberRoles = ['role:default/temp', 'role:default/qa']; + + beforeEach(() => { + tracker = createTracker(rbacDBClient); + }); + + afterEach(() => { + tracker.reset(); + }); + + it('should build the roles associated with a user using the database', async () => { + const data = [{ v0: 'user:default/test', v1: 'role:default/qa' }]; + + tracker.on.select('casbin_rule').response(data); + + await newRole.buildRoles(newRole, memberRoles, rbacDBClient); + const rolesExpect = newRole.getRoles(); + expect(rolesExpect.length).toEqual(1); + expect(rolesExpect[0]).toEqual('role:default/qa'); + }); + + it('should fail to retrieve roles and log an error', async () => { + const error = new Error('test error'); + tracker.on.select('casbin_rule').simulateError(error); + + await expect( + newRole.buildRoles(newRole, memberRoles, rbacDBClient), + ).rejects.toMatchObject({ + message: expect.stringContaining('test error'), + }); + expect(newRole.getRoles().length).toEqual(0); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/role-manager/member-list.ts b/workspaces/rbac/plugins/rbac-backend/src/role-manager/member-list.ts new file mode 100644 index 0000000000..7f316c52e1 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/role-manager/member-list.ts @@ -0,0 +1,141 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Knex } from 'knex'; + +export class RoleMemberList { + public name: string; + + private members: string[]; + private roles: string[]; + + public constructor(name: string) { + this.name = name; + this.members = []; + this.roles = []; + } + + /** + * addMembers will add members to the RoleMemberList + * @param members The members to be added. + */ + public addMembers(members: string[]): void { + this.members = members; + } + + /** + * addMember will add a single member to the RoleMemberList, skips adding the user in the + * event that they already exist in the members array. + * @param member The member to be added. + */ + public addMember(member: string): void { + if (this.members.some(n => n === member)) { + return; + } + this.members.push(member); + } + + /** + * hasMember will check if a particular member exists in the members array. + * @param name The member to be checked for. + */ + public hasMember(name: string): boolean { + return this.members.includes(name); + } + + /** + * deleteMember will remove a user from the members array. + * @param member The member to be removed. + */ + public deleteMember(member: string): void { + this.members = this.members.filter(n => n !== member); + } + + /** + * buildMembers will query the `casbin_rule` database table to ensure that the role + * that we have cached is up to date. + * This is important in multi node scenarios where the cached roles in role manager can become + * out of sync with the database. + * @param roleMemberList The RoleMemberList to be updated. + * @param client The database client. + */ + public async buildMembers( + roleMemberList: RoleMemberList, + client: Knex, + ): Promise { + try { + const members: string[] = await client + .table('casbin_rule') + .where('v1', this.name) + .pluck('v0') + .distinct(); + + roleMemberList.addMembers(members); + } catch (error) { + throw new Error( + `Unable to find members for the role ${this.name}. Cause: ${error}`, + ); + } + } + + /** + * getMembers will return the members of the RoleMemberList + * @returns The members. + */ + getMembers(): string[] { + return this.members; + } + + /** + * addRoles will add roles to the RoleMemberList + * @param roles The roles to be added. + */ + public addRoles(roles: string[]): void { + this.roles = roles; + } + + /** + * buildRoles will query the `casbin_rule` database table to quickly grab all of the + * roles that a particular user is attached to. + * @param roleMemberList The RoleMemberList to be updated. + * @param userAndGroups The user and groups to query with. + * @param client The database client. + */ + public async buildRoles( + roleMemberList: RoleMemberList, + userAndGroups: string[], + client: Knex, + ): Promise { + try { + const roles: string[] = await client + .table('casbin_rule') + .whereIn('v0', userAndGroups) + .pluck('v1') + .distinct(); + + roleMemberList.addRoles(roles); + } catch (error) { + throw new Error(`Unable to find all roles. Cause: ${error}`); + } + } + + /** + * getRoles will return the roles of the RoleMemberList. + * @returns The roles. + */ + getRoles(): string[] { + return this.roles; + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/role-manager/role-manager.test.ts b/workspaces/rbac/plugins/rbac-backend/src/role-manager/role-manager.test.ts new file mode 100644 index 0000000000..7f1afb361d --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/role-manager/role-manager.test.ts @@ -0,0 +1,1774 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { LoggerService } from '@backstage/backend-plugin-api'; +import { mockServices } from '@backstage/backend-test-utils'; +import type { CatalogApi } from '@backstage/catalog-client'; +import type { Entity } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; + +import * as Knex from 'knex'; +import { createTracker, MockClient, Tracker } from 'knex-mock-client'; + +import { BackstageRoleManager } from '../role-manager/role-manager'; + +describe('BackstageRoleManager', () => { + const catalogDBClient = Knex.knex({ client: MockClient }); + const rbacDBClient = Knex.knex({ client: MockClient }); + // TODO: Move to 'catalogServiceMock' from '@backstage/plugin-catalog-node/testUtils' + // once '@backstage/plugin-catalog-node' is upgraded + const catalogApiMock: any = { + getEntities: jest.fn().mockImplementation(), + }; + + const mockLoggerService = mockServices.logger.mock(); + + const mockAuthService = mockServices.auth(); + + let roleManager: BackstageRoleManager; + beforeEach(() => { + catalogApiMock.getEntities = jest + .fn() + .mockImplementation(() => Promise.resolve({ items: [] })); + + const config = newConfig(); + + roleManager = new BackstageRoleManager( + catalogApiMock as CatalogApi, + mockLoggerService as LoggerService, + catalogDBClient, + rbacDBClient, + config, + mockAuthService, + ); + }); + + describe('initialize', () => { + it('should initialize', () => { + expect(roleManager).not.toBeUndefined(); + }); + + it('should throw an error whenever max depth is less than 0', () => { + let expectedError; + let errorRoleManager; + const config = newConfig(-1); + + try { + errorRoleManager = new BackstageRoleManager( + catalogApiMock as CatalogApi, + mockLoggerService as LoggerService, + catalogDBClient, + rbacDBClient, + config, + mockAuthService, + ); + } catch (error) { + expectedError = error; + } + + expect(errorRoleManager).toBeUndefined(); + expect(expectedError).toMatchObject({ + message: + 'Max Depth for RBAC group hierarchy must be greater than or equal to zero', + }); + }); + }); + + describe('unimplemented methods', () => { + it('should throw an error for syncedHasLink', () => { + expect(() => + roleManager.syncedHasLink!('user:default/role1', 'user:default/role2'), + ).toThrow('Method "syncedHasLink" not implemented.'); + }); + + it('should throw an error for getUsers', async () => { + await expect(roleManager.getUsers('name')).rejects.toThrow( + 'Method "getUsers" not implemented.', + ); + }); + }); + + describe('addLink test', () => { + it('should create a link between two entities', async () => { + roleManager.addLink('user:default/test', 'role:default/rbac_admin'); + const result = await roleManager.hasLink( + 'user:default/test', + 'role:default/rbac_admin', + ); + expect(result).toBe(true); + }); + }); + + describe('deleteLink test', () => { + it('should delete a link', async () => { + roleManager.addLink('test', 'role:test', ''); + roleManager.addLink('test', 'role:test2', ''); + + roleManager.deleteLink('test', 'role:test'); + const result = await roleManager.hasLink('test', 'role:test'); + expect(result).toBe(false); + }); + }); + + describe('hasLink tests', () => { + afterEach(() => { + (mockLoggerService.warn as jest.Mock).mockReset(); + (catalogApiMock.getEntities as jest.Mock).mockReset(); + }); + + it('should throw an error for unsupported domain', async () => { + await expect( + roleManager.hasLink( + 'user:default/mike', + 'group:default/somegroup', + 'someDomain', + ), + ).rejects.toThrow('domain argument is not supported.'); + }); + + it('should return true for hasLink when names are the same', async () => { + const result = await roleManager.hasLink( + 'user:default/mike', + 'user:default/mike', + ); + expect(result).toBe(true); + }); + + it('should return false for hasLink when name2 has a user kind', async () => { + const result = await roleManager.hasLink( + 'user:default/mike', + 'user:default/some-user', + ); + expect(result).toBe(false); + }); + + // user:default/mike should not inherits from group:default/somegroup + // + // Hierarchy: + // + // user:default/mike -> user without group + // + it('should return false for hasLink when user without group', async () => { + const result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/somegroup', + ); + expect(catalogApiMock.getEntities).toHaveBeenCalledWith( + { + filter: { + kind: 'Group', + }, + fields: ['metadata.name', 'metadata.namespace', 'spec.parent'], + }, + { + token: 'mock-service-token:{"sub":"plugin:test","target":"catalog"}', + }, + ); + expect(result).toBeFalsy(); + }); + + // user:default/mike should not inherits from role:default/role + // + // Hierarchy: + // + // user:default/mike -> user without role + // + it('should return false for hasLink when user without role', async () => { + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/somerole', + ); + expect(catalogApiMock.getEntities).toHaveBeenCalledWith( + { + filter: { + kind: 'Group', + }, + fields: ['metadata.name', 'metadata.namespace', 'spec.parent'], + }, + { + token: 'mock-service-token:{"sub":"plugin:test","target":"catalog"}', + }, + ); + expect(result).toBeFalsy(); + }); + + // user:default/mike should inherits from role:default/somerole + // + // Hierarchy: + // + // user:default/mike -> role:default/somerole + // + it('should return true for hasLink when user:default/mike inherits from role:default/somerole', async () => { + roleManager.addLink('user:default/mike', 'role:default/somerole'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/somerole', + ); + expect(result).toBeTruthy(); + }); + + // user:default/mike should inherits from group:default/somegroup + // + // Hierarchy: + // + // group:default/somegroup + // | + // user:default/mike + // + it('should return true for hasLink when user:default/mike inherits from group:default/somegroup', async () => { + const entityMock = createGroupEntity( + 'somegroup', + undefined, + [], + ['mike'], + ); + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/somegroup', + ); + expect(result).toBeTruthy(); + }); + + // user:default/mike should inherits from with role:default/somerole from group:default/somegroup + // + // Hierarchy: + // + // group:default/somegroup -> role:default/somerole + // | + // user:default/mike + // + it('should return true for hasLink when user:default/mike inherits role:default/somerole from group:default/somegroup', async () => { + const entityMock = createGroupEntity( + 'somegroup', + undefined, + [], + ['mike'], + ); + roleManager.addLink('group:default/somegroup', 'role:default/somerole'); + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/somerole', + ); + expect(result).toBeTruthy(); + }); + + // user:default/mike should not inherits from group:default/somegroup + // + // Hierarchy: + // + // group:default/not-matched-group + // | + // user:default/mike + // + it('should return false for hasLink when user:default/mike does not inherits group:default/somegroup', async () => { + const entityMock = createGroupEntity('not-matched-group', undefined, [ + 'mike', + ]); + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/somegroup', + ); + expect(result).toBeFalsy(); + }); + + // user:default/mike should not inherits from role:default/somerole + // + // Hierarchy: + // + // group:default/not-matched-group role:default/somerole + // | | + // user:default/mike group:default/somegroup + // + it('should return false for hasLink when user:default/mike does not inherits role:default/somerole', async () => { + const entityMock = createGroupEntity('not-matched-group', undefined, [ + 'mike', + ]); + roleManager.addLink('group:default/somegroup', 'role:default/somerole'); + catalogApiMock.getEntities.mockReturnValue({ items: [entityMock] }); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/somerole', + ); + expect(result).toBeFalsy(); + }); + + // user:default/mike should inherits from group:default/team-a + // + // Hierarchy: + // + // group:default/team-a + // | + // group:default/team-b + // | + // user:default/mike + // + it('should return true for hasLink, when user:default/mike inherits from group:default/team-a', async () => { + const groupMock = createGroupEntity('team-b', 'team-a', [], ['mike']); + const groupParentMock = createGroupEntity('team-a', undefined, [ + 'team-b', + ]); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupMock] }; + } + return { items: [groupMock, groupParentMock] }; + }); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/team-a', + ); + expect(result).toBeTruthy(); + }); + + // user:default/mike should inherits from group:default/team-a + // + // Hierarchy: + // + // group:default/team-a + // | + // group:default/team-b + // | + // user:default/mike + // + it('should disable group inheritance when max-depth=0', async () => { + // max-depth=0 + const config = newConfig(0); + const rm = new BackstageRoleManager( + catalogApiMock as CatalogApi, + mockLoggerService as LoggerService, + catalogDBClient, + rbacDBClient, + config, + mockAuthService, + ); + const groupMock = createGroupEntity('team-b', 'team-a', [], ['mike']); + const groupParentMock = createGroupEntity('team-a', undefined, [ + 'team-b', + ]); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupMock] }; + } + return { items: [groupMock, groupParentMock] }; + }); + + let result = await rm.hasLink( + 'user:default/mike', + 'group:default/team-b', + ); + expect(result).toBeTruthy(); + + result = await rm.hasLink('user:default/mike', 'group:default/team-a'); + expect(result).toBeFalsy(); + }); + + // user:default/mike should inherits role:default/team-a from group:default/team-a + // + // Hierarchy: + // + // group:default/team-a -> role:default/team-a + // | + // group:default/team-b + // | + // user:default/mike + // + it('should return true for hasLink, when user:default/mike inherits role:default/team-a from group:default/team-a', async () => { + const groupMock = createGroupEntity('team-b', 'team-a', [], ['mike']); + const groupParentMock = createGroupEntity('team-a', undefined, [ + 'team-b', + ]); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupMock] }; + } + return { items: [groupMock, groupParentMock] }; + }); + + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-a', + ); + expect(result).toBeTruthy(); + }); + + // user:default/mike should inherits from group:default/team-b. + // + // Hierarchy: + // + // |---------group:default/team-a---------| + // | | | + // user:default/team-c group:default/team-b group:default/team-d + // | | | + // user:default/tom user:default/mike user:default:john + // + it('should return true for hasLink, when user:default/mike inherits from group:default/team-b', async () => { + const groupAMock = createGroupEntity('team-a', undefined, [ + 'team-b', + 'team-c', + 'team-d', + ]); + const groupBMock = createGroupEntity('team-b', 'team-a', [], ['mike']); + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['tom']); + const groupDMock = createGroupEntity('team-d', 'team-a', [], ['john']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupBMock] }; + } + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; + }); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/team-a', + ); + expect(result).toBeTruthy(); + }); + + // user:default/mike should inherits role:default/team-a from group:default/team-a. + // + // Hierarchy: + // + // |---------group:default/team-b------------| -> role:default/team-b + // | | | + // user:default/team-c group:default/team-a group:default/team-d + // | | | + // user:default/tom user:default/mike user:default:john + // + it('should return true for hasLink, when user:default/mike inherits role:default/team-b from group:default/team-b', async () => { + const groupBMock = createGroupEntity('team-b', undefined, [ + 'team-a', + 'team-c', + 'team-d', + ]); + const groupAMock = createGroupEntity('team-a', 'team-b', [], ['mike']); + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['tom']); + const groupDMock = createGroupEntity('team-d', 'team-a', [], ['john']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupAMock] }; + } + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; + }); + + roleManager.addLink('group:default/team-b', 'role:default/team-b'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-b', + ); + expect(result).toBeTruthy(); + }); + + // user:default/mike should not inherits from group:default/team-c + // + // Hierarchy: + // + // group:default/team-a + // | + // group:default/team-b + // | + // user:default/mike + // + it('should return false for hasLink, when user:default/mike does not inherits from group:default/team-c', async () => { + const groupBMock = createGroupEntity('team-b', 'team-a', ['mike']); + const groupAMock = createGroupEntity('team-a', undefined, ['team-b']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupBMock] }; + } + return { items: [groupAMock, groupBMock] }; + }); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/team-c', + ); + expect(result).toBeFalsy(); + }); + + // user:default/mike should not inherits role:default/team-c from group:default/team-c + // + // Hierarchy: + // + // group:default/team-a group:default/team-c -> role:default/team-c + // | + // group:default/team-b + // | + // user:default/mike + // + it('should return false for hasLink, when user:default/mike does not inherits role:default/team-c from group:default/team-c', async () => { + const groupBMock = createGroupEntity('team-b', 'team-a', ['mike']); + const groupAMock = createGroupEntity('team-a', undefined, ['team-b']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupBMock] }; + } + return { items: [groupAMock, groupBMock] }; + }); + + roleManager.addLink('group:default/team-c', 'role:default/team-c'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-c', + ); + expect(result).toBeFalsy(); + }); + + // user:default/mike should inherits from group:default/team-a + // + // Hierarchy: + // + // group:default/team-a group:default/team-b + // | | + // group:default/team-c group:default/team-d + // | | + // user:default/mike + // + it('should return true for hasLink, when user:default/mike inherits group tree with group:default/team-a', async () => { + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', ['mike']); + const groupAMock = createGroupEntity('team-a', undefined, [], ['team-c']); + const groupBMock = createGroupEntity('team-b', undefined, [], ['team-d']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock] }; + } + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; + }); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/team-a', + ); + expect(result).toBeTruthy(); + }); + + // user:default/mike should inherits role:default/team-a from group:default/team-a + // + // Hierarchy: + // + // group:default/team-a -> role:default/team-a group:default/team-b + // | | + // group:default/team-c group:default/team-d + // | | + // |--------user:default/mike -------------------| + // + it('should return true for hasLink, when user:default/mike inherits role:default/team-a group tree with group:default/team-a', async () => { + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); + const groupAMock = createGroupEntity('team-a', undefined, ['team-c']); + const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock] }; + } + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; + }); + + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-a', + ); + expect(result).toBeTruthy(); + }); + + // user:default/mike should not inherits from group:default/team-e + // + // Hierarchy: + // + // group:default/team-a group:default/team-b + // | | + // group:default/team-c group:default/team-d + // | | + // user:default/mike + // + it('should return false for hasLink, when user:default/mike inherits from group:default/team-e', async () => { + const groupCMock = createGroupEntity('team-c', 'team-a', ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', ['mike']); + const groupAMock = createGroupEntity('team-a', undefined, ['team-c']); + const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock] }; + } + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; + }); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/team-e', + ); + expect(result).toBeFalsy(); + }); + + // user:default/mike should not inherits role:default/team-e from group:default/team-e + // + // Hierarchy: + // + // group:default/team-a group:default/team-b group:default/team-e -> role:default/team-e + // | | + // group:default/team-c group:default/team-d + // | | + // user:default/mike + // + it('should return false for hasLink, when user:default/mike inherits role:default/team-e from group:default/team-e', async () => { + const groupCMock = createGroupEntity('team-c', 'team-a', ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', ['mike']); + const groupAMock = createGroupEntity('team-a', undefined, ['team-c']); + const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock] }; + } + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; + }); + + roleManager.addLink('group:default/team-e', 'role:default/team-e'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-e', + ); + expect(result).toBeFalsy(); + }); + + // user:default/mike should inherits from group:default/team-b and group:default/team-a, but we have cycle dependency. + // So return false on call hasLink. + // + // Hierarchy: + // + // group:default/team-a + // ↓ ↑ + // group:default/team-b + // ↓ + // user:default/mike + // + it('should return false for hasLink, when user:default/mike inherits from group:default/team-a and group:default/team-b, but we have cycle dependency', async () => { + const groupBMock = createGroupEntity('team-b', 'team-a', [], ['mike']); + const groupAMock = createGroupEntity('team-a', 'team-b', ['team-b']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupBMock] }; + } + return { items: [groupBMock, groupAMock] }; + }); + + let result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/team-b', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-b"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-b"]]', + ); + + result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/team-a', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-b"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-b"]]', + ); + }); + + // user:default/mike should inherits role:default/team-b and role:default/team-a from group:default/team-b and group:default/team-a, but we have cycle dependency. + // So return false on call hasLink. + // + // Hierarchy: + // + // group:default/team-a -> role:default/team-a + // ↓ ↑ + // group:default/team-b -> role:default/team-b + // ↓ + // user:default/mike + // + it('should return false for hasLink, when user:default/mike inherits role:default/team-b and role:default/team-a from group:default/team-a and group:default/team-b, but we have cycle dependency', async () => { + const groupBMock = createGroupEntity('team-b', 'team-a', ['mike']); + const groupAMock = createGroupEntity('team-a', 'team-b', ['team-b']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupBMock] }; + } + return { items: [groupBMock, groupAMock] }; + }); + + roleManager.addLink('group:default/team-b', 'role:default/team-b'); + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + + let result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-b', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-b"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-b"]]', + ); + + result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-a', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-b"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-b"]]', + ); + }); + + // user:default/mike should inherits from group:default/team-a, group:default/team-b, group:default/team-c, but we have cycle dependency. + // So return false on call hasLink. + // + // Hierarchy: + // + // group:default/team-a + // ↓ ↑ + // group:default/team-b + // ↓ + // group:default/team-c + // ↓ + // user:default/mike + // + it('should return false for hasLink, when user:default/mike inherits from group:default/team-a, group:default/team-b, group:default/team-c, but we have cycle dependency', async () => { + const groupAMock = createGroupEntity('team-a', 'team-b', ['team-b']); + const groupBMock = createGroupEntity('team-b', 'team-a', ['team-c']); + const groupCMock = createGroupEntity('team-c', 'team-b', [], ['mike']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock] }; + } + return { items: [groupAMock, groupBMock, groupCMock] }; + }); + + let result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/team-c', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-b"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-b"]]', + ); + + result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/team-b', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-b"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-b"]]', + ); + + result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/team-a', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-b"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-b"]]', + ); + }); + + // user:default/mike should inherits the roles from group:default/team-a, group:default/team-b, group:default/team-c, but we have cycle dependency. + // So return false on call hasLink. + // + // Hierarchy: + // + // group:default/team-a -> role:default/team-a + // ↓ ↑ + // group:default/team-b -> role:default/team-b + // ↓ + // group:default/team-c -> role:default/team-c + // ↓ + // user:default/mike + // + it('should return false for hasLink, when user:default/mike inherits the roles from group:default/team-a, group:default/team-b, group:default/team-c, but we have cycle dependency', async () => { + const groupAMock = createGroupEntity('team-a', 'team-b', ['team-b']); + const groupBMock = createGroupEntity('team-b', 'team-a', ['team-c']); + const groupCMock = createGroupEntity('team-c', 'team-b', ['mike']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock] }; + } + return { items: [groupAMock, groupBMock, groupCMock] }; + }); + + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + roleManager.addLink('group:default/team-b', 'role:default/team-b'); + roleManager.addLink('group:default/team-c', 'role:default/team-c'); + + let result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-c', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-b"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-b"]]', + ); + + result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-b', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-b"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-b"]]', + ); + + result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-a', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-b"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-b"]]', + ); + }); + + // user:default/mike should inherits from group:default/team-a, but we have cycle dependency: team-a -> team-c. + // So return false on call hasLink. + // + // Hierarchy: + // + // group:default/team-a group:default/team-b + // ↓ ↑ ↓ + // group:default/team-c group:default/team-d + // ↓ ↓ + // user:default/mike + // + it('should return false for hasLink, when user:default/mike inherits group tree with group:default/team-a, but we cycle dependency', async () => { + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); + const groupAMock = createGroupEntity('team-a', 'team-c', ['team-c']); + const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock] }; + } + return { items: [groupCMock, groupDMock, groupAMock, groupBMock] }; + }); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/team-a', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-c"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-c"]]', + ); + }); + + // user:default/mike should inherits role from group:default/team-a, but we have cycle dependency: team-a -> team-c. + // So return false on call hasLink. + // + // Hierarchy: + // + // group:default/team-a -> role:default/team-a group:default/team-b + // ↓ ↑ ↓ + // group:default/team-c -> role:default/team-c group:default/team-d + // ↓ ↓ + // user:default/mike -------------------| + // + it('should return false for hasLink, when user:default/mike inherits role from group tree with group:default/team-a, but we cycle dependency', async () => { + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); + const groupAMock = createGroupEntity('team-a', 'team-c', ['team-c']); + const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock] }; + } + return { items: [groupCMock, groupDMock, groupAMock, groupBMock] }; + }); + + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + roleManager.addLink('group:default/team-c', 'role:default/team-c'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-a', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-c"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-c"]]', + ); + }); + + // user:default/mike should inherits role from group:default/team-f, and we have a complex graph, and cycle dependency + // So return false on call hasLink. + // + // Hierarchy: + // role:default/team-e + // ↓ + // |----------------- group:default/team-e ---------| + // ↓ | + // | ----------------- group:default/team-f ----| | + // ↓ ↓ | + // group:default/team-a -> role:default/team-a group:default/team-b | + // ↓ ↑ ↓ ↓ + // group:default/team-c -> role:default/team-c group:default/team-d group:default/team-g -> role:default/team-g + // ↓ ↓ ↓ + // user:default/mike -------------------|---------------------------------| + // + it('should return false for hasLink, when user:default/mike inherits role from group tree with group:default/team-e, complex tree', async () => { + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); + const groupAMock = createGroupEntity('team-a', 'team-c', ['team-c']); + const groupBMock = createGroupEntity('team-b', 'team-f', ['team-d']); + const groupFMock = createGroupEntity('team-f', 'team-e', [ + 'team-a', + 'team-b', + ]); + const groupEMock = createGroupEntity('team-e', undefined, [ + 'team-f', + 'team-g', + ]); + const groupGMock = createGroupEntity('team-g', 'team-e', [], ['mike']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock, groupGMock] }; + } + return { + items: [ + groupCMock, + groupDMock, + groupAMock, + groupBMock, + groupFMock, + groupEMock, + groupGMock, + ], + }; + }); + + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + roleManager.addLink('group:default/team-c', 'role:default/team-c'); + roleManager.addLink('group:default/team-e', 'role:default/team-e'); + roleManager.addLink('group:default/team-g', 'role:default/team-g'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-e', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-c"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-c"]]', + ); + + const test = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-g', + ); + expect(test).toBeFalsy(); + }); + + // user:default/mike should inherits role from group:default/team-e, and we have a complex graph + // So return true on call hasLink. + // + // Hierarchy: + // role:default/team-e + // ↓ + // |----------------- group:default/team-e ---------| + // ↓ | + // | ----------------- group:default/team-f ----| | + // ↓ ↓ | + // group:default/team-a -> role:default/team-a group:default/team-b | + // ↓ ↓ ↓ + // group:default/team-c -> role:default/team-c group:default/team-d group:default/team-g -> role:default/team-g + // ↓ ↓ ↓ + // user:default/mike -------------------|---------------------------------| + // + it('should return true for hasLink, when user:default/mike inherits role from group tree with group:default/team-e, complex tree', async () => { + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); + const groupAMock = createGroupEntity('team-a', 'team-f', ['team-c']); + const groupBMock = createGroupEntity('team-b', 'team-f', ['team-d']); + const groupFMock = createGroupEntity('team-f', 'team-e', [ + 'team-a', + 'team-b', + ]); + const groupEMock = createGroupEntity('team-e', undefined, [ + 'team-f', + 'team-g', + ]); + const groupGMock = createGroupEntity('team-g', 'team-e', [], ['mike']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock, groupGMock] }; + } + return { + items: [ + groupCMock, + groupDMock, + groupAMock, + groupBMock, + groupFMock, + groupEMock, + groupGMock, + ], + }; + }); + + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + roleManager.addLink('group:default/team-c', 'role:default/team-c'); + roleManager.addLink('group:default/team-e', 'role:default/team-e'); + roleManager.addLink('group:default/team-g', 'role:default/team-g'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-e', + ); + expect(result).toBeTruthy(); + }); + + // user:default/mike should inherits role from group:default/team-e, and we have a complex graph + // So return true on call hasLink. + // + // Hierarchy: + // role:default/team-e + // ↓ + // |----------------- group:default/team-e ---------| + // ↓ | + // | ----------------- group:default/team-f ----| | + // ↓ ↓ | + // group:default/team-a -> role:default/team-a group:default/team-b group:default/team-h -> role:default/team-h + // ↓ ↓ ↓ + // group:default/team-c -> role:default/team-c group:default/team-d group:default/team-g -> role:default/team-g + // ↓ ↓ ↓ + // user:default/mike -------------------|---------------------------------| + // + it('should return false for hasLink, when user:default/mike inherits role from group tree with group:default/team-e, complex tree, maxDepth of 3', async () => { + const config = newConfig(1); + + const roleManagerMaxDepth = new BackstageRoleManager( + catalogApiMock as CatalogApi, + mockLoggerService as LoggerService, + catalogDBClient, + rbacDBClient, + config, + mockAuthService, + ); + + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); + const groupAMock = createGroupEntity('team-a', 'team-f', ['team-c']); + const groupBMock = createGroupEntity('team-b', 'team-f', ['team-d']); + const groupFMock = createGroupEntity('team-f', 'team-e', [ + 'team-a', + 'team-b', + ]); + const groupEMock = createGroupEntity('team-e', undefined, [ + 'team-f', + 'team-g', + ]); + const groupGMock = createGroupEntity('team-g', 'team-h', [], ['mike']); + const groupHMock = createGroupEntity('team-h', 'team-e', ['team-g'], []); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock, groupGMock] }; + } + return { + items: [ + groupCMock, + groupDMock, + groupAMock, + groupBMock, + groupFMock, + groupEMock, + groupGMock, + groupHMock, + ], + }; + }); + + roleManagerMaxDepth.addLink( + 'group:default/team-a', + 'role:default/team-a', + ); + roleManagerMaxDepth.addLink( + 'group:default/team-c', + 'role:default/team-c', + ); + roleManagerMaxDepth.addLink( + 'group:default/team-e', + 'role:default/team-e', + ); + roleManagerMaxDepth.addLink( + 'group:default/team-g', + 'role:default/team-g', + ); + + roleManagerMaxDepth.addLink( + 'group:default/team-h', + 'role:default/team-h', + ); + + const resultE = await roleManagerMaxDepth.hasLink( + 'user:default/mike', + 'role:default/team-e', + ); + const resultG = await roleManagerMaxDepth.hasLink( + 'user:default/mike', + 'role:default/team-g', + ); + const resultH = await roleManagerMaxDepth.hasLink( + 'user:default/mike', + 'role:default/team-h', + ); + + expect(resultE).toBeFalsy(); + expect(resultH).toBeTruthy(); + expect(resultG).toBeTruthy(); + }); + + // user:default/mike should inherits from group:default/team-a, but we have cycle dependency: team-a -> team-c. + // So return false on call hasLink. + // + // user:default/tom should inherits from group:default/team-b. Cycle dependency in the neighbor subgraph, should + // not affect evaluation user:default/tom inheritance. + // + // Hierarchy: + // + // group:default/root + // ↓ ↓ + // group:default/team-a group:default/team-b + // ↓ ↑ ↓ + // group:default/team-c group:default/team-d + // ↓ ↓ + // user:default/mike user:default/tom + // + // This test passes now ? + it('should return false for hasLink for user:default/mike and group:default/team-a(cycle dependency), but should be true for user:default/tom and group:default/team-b', async () => { + const groupRootMock = createGroupEntity('root', undefined, [ + 'team-a', + 'team-b', + ]); + const groupCMock = createGroupEntity( + 'team-c', + 'team-a', + ['team-a'], + ['mike'], + ); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['tom']); + const groupAMock = createGroupEntity('team-a', 'team-c', ['team-c']); + const groupBMock = createGroupEntity('team-b', 'root', ['team-d']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock] }; + } else if (hasMember && hasMember === 'user:default/tom') { + return { items: [groupDMock] }; + } + return { + items: [ + groupRootMock, + groupCMock, + groupDMock, + groupAMock, + groupBMock, + ], + }; + }); + + let result = await roleManager.hasLink( + 'user:default/mike', + 'group:default/team-a', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-c"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-c"]]', + ); + + result = await roleManager.hasLink( + 'user:default/tom', + 'group:default/team-b', + ); + expect(result).toBeTruthy(); + }); + + // user:default/mike should inherits role:default/team-a from group:default/team-a, but we have cycle dependency: team-a -> team-c. + // So return false on call hasLink. + // + // user:default/tom should inherits role:default/team-b from group:default/team-b. Cycle dependency in the neighbor subgraph, should + // not affect evaluation user:default/tom inheritance. + // + // Hierarchy: + // + // group:default/root + // ↓ ↓ + // group:default/team-a -> role:default/team-a group:default/team-b -> role:default/team-b + // ↓ ↑ ↓ + // group:default/team-c group:default/team-d + // ↓ ↓ + // user:default/mike user:default/tom + // This test passes now ? + it('should return false for hasLink for user:default/mike and role:default/team-a(cycle dependency), but should be true for user:default/tom and role:default/team-b', async () => { + const groupRootMock = createGroupEntity('root', undefined, [ + 'team-a', + 'team-b', + ]); + const groupCMock = createGroupEntity( + 'team-c', + 'team-a', + ['team-a'], + ['mike'], + ); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['tom']); + const groupAMock = createGroupEntity('team-a', 'team-c', ['team-c']); + const groupBMock = createGroupEntity('team-b', 'root', ['team-d']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock] }; + } else if (hasMember && hasMember === 'user:default/tom') { + return { items: [groupDMock] }; + } + return { + items: [ + groupRootMock, + groupCMock, + groupDMock, + groupAMock, + groupBMock, + ], + }; + }); + + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + roleManager.addLink('group:default/team-b', 'role:default/team-b'); + + let result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-a', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-c"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-c"]]', + ); + + result = await roleManager.hasLink( + 'user:default/tom', + 'role:default/team-b', + ); + expect(result).toBeTruthy(); + }); + }); + + describe('hasLink with database', () => { + let tracker: Tracker; + + beforeEach(() => { + tracker = createTracker(rbacDBClient); + }); + + afterEach(() => { + tracker.reset(); + }); + + // user:default/mike should inherits from role:default/somerole + // + // Hierarchy: + // + // user:default/mike -> role:default/somerole + // + it('should return true for hasLink when user:default/mike inherits from role:default/somerole when using the database', async () => { + const user = [{ v0: 'user:default/mike', v1: 'role:default/somerole' }]; + + roleManager.isPGClient = jest.fn().mockImplementation(() => true); + + tracker.on.select('casbin_rule').response(user); + + roleManager.addLink('user:default/mike', 'role:default/somerole'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/somerole', + ); + + expect(result).toBeTruthy(); + }); + + // user:default/mike should not inherits role:default/team-c from group:default/team-c + // + // Hierarchy: + // + // group:default/team-a group:default/team-c -> role:default/team-c + // | + // group:default/team-b + // | + // user:default/mike + // + it('should return false for hasLink, when user:default/mike does not inherits role:default/team-c from group:default/team-c with database', async () => { + roleManager.isPGClient = jest.fn().mockImplementation(() => true); + + tracker.on.select('casbin_rule').response([]); + + const groupBMock = createGroupEntity('team-b', 'team-a', ['mike']); + const groupAMock = createGroupEntity('team-a', undefined, ['team-b']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupBMock] }; + } + return { items: [groupAMock, groupBMock] }; + }); + + roleManager.addLink('group:default/team-c', 'role:default/team-c'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-c', + ); + expect(result).toBeFalsy(); + }); + + // user:default/mike should inherits role from group:default/team-e, and we have a complex graph + // So return true on call hasLink. + // + // Hierarchy: + // role:default/team-e + // ↓ + // |----------------- group:default/team-e ---------| + // ↓ | + // | ----------------- group:default/team-f ----| | + // ↓ ↓ | + // group:default/team-a -> role:default/team-a group:default/team-b | + // ↓ ↓ ↓ + // group:default/team-c -> role:default/team-c group:default/team-d group:default/team-g -> role:default/team-g + // ↓ ↓ ↓ + // user:default/mike -------------------|---------------------------------| + // + it('should return true for hasLink, when user:default/mike inherits role from group tree with group:default/team-e, complex tree with database', async () => { + const data = [{ v0: 'group:default/team-e', v1: 'role:default/team-e' }]; + roleManager.isPGClient = jest.fn().mockImplementation(() => true); + + tracker.on.select('casbin_rule').response(data); + + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); + const groupAMock = createGroupEntity('team-a', 'team-f', ['team-c']); + const groupBMock = createGroupEntity('team-b', 'team-f', ['team-d']); + const groupFMock = createGroupEntity('team-f', 'team-e', [ + 'team-a', + 'team-b', + ]); + const groupEMock = createGroupEntity('team-e', undefined, [ + 'team-f', + 'team-g', + ]); + const groupGMock = createGroupEntity('team-g', 'team-e', [], ['mike']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock, groupGMock] }; + } + return { + items: [ + groupCMock, + groupDMock, + groupAMock, + groupBMock, + groupFMock, + groupEMock, + groupGMock, + ], + }; + }); + + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + roleManager.addLink('group:default/team-c', 'role:default/team-c'); + roleManager.addLink('group:default/team-e', 'role:default/team-e'); + roleManager.addLink('group:default/team-g', 'role:default/team-g'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-e', + ); + expect(result).toBeTruthy(); + }); + + // user:default/mike should inherits role from group:default/team-a, but we have cycle dependency: team-a -> team-c. + // So return false on call hasLink. + // + // Hierarchy: + // + // group:default/team-a -> role:default/team-a group:default/team-b + // ↓ ↑ ↓ + // group:default/team-c -> role:default/team-c group:default/team-d + // ↓ ↓ + // user:default/mike -------------------| + // + it('should return false for hasLink, when user:default/mike inherits role from group tree with group:default/team-a, but we cycle dependency with database', async () => { + const data = [{ v0: 'group:default/team-a', v1: 'role:default/team-a' }]; + + tracker.on.select('casbin_rule').response(data); + + tracker.on + .select('select "v0" from "casbin_rule" where "v1" = ?') + .response(['group:default/team-a']); + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); + const groupAMock = createGroupEntity('team-a', 'team-c', ['team-c']); + const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock] }; + } + return { items: [groupCMock, groupDMock, groupAMock, groupBMock] }; + }); + + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + roleManager.addLink('group:default/team-c', 'role:default/team-c'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-a', + ); + expect(result).toBeFalsy(); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'Detected cycle dependencies in the Group graph: [["group:default/team-a","group:default/team-c"]]. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: [["group:default/team-a","group:default/team-c"]]', + ); + }); + + // user:default/mike should inherits role:default/team-a from group:default/team-a + // + // Hierarchy: + // + // group:default/team-a -x-> role:default/team-a group:default/team-b + // | | + // group:default/team-c group:default/team-d + // | | + // |--------user:default/mike -------------------| + // + it('should return false for hasLink, when user:default/mike originally inherits role:default/team-a group tree with group:default/team-a but connection has been removed in database', async () => { + roleManager.isPGClient = jest.fn().mockImplementation(() => true); + + tracker.on.select('casbin_rule').response([]); + + const groupCMock = createGroupEntity('team-c', 'team-a', [], ['mike']); + const groupDMock = createGroupEntity('team-d', 'team-b', [], ['mike']); + const groupAMock = createGroupEntity('team-a', undefined, ['team-c']); + const groupBMock = createGroupEntity('team-b', undefined, ['team-d']); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + if (hasMember && hasMember === 'user:default/mike') { + return { items: [groupCMock, groupDMock] }; + } + return { items: [groupAMock, groupBMock, groupCMock, groupDMock] }; + }); + + roleManager.addLink('group:default/team-a', 'role:default/team-a'); + + const result = await roleManager.hasLink( + 'user:default/mike', + 'role:default/team-a', + ); + expect(result).toBeFalsy(); + }); + }); + + describe('getRoles returns roles per user', () => { + it('should returns role per user', async () => { + roleManager.addLink('user:default/test', 'role:default/rbac_admin'); + roleManager.addLink('user:default/test-two', 'role:default/rbac_admin'); + roleManager.addLink( + 'user:default/test-three', + 'role:default/rbac_admin_test', + ); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + + if (hasMember && hasMember[0] === 'user:default/test') { + return { items: [] }; + } + if (hasMember && hasMember[0] === 'user:default/test-two') { + return { items: [] }; + } + if (hasMember && hasMember[0] === 'user:default/test-three') { + return { items: [] }; + } + return { items: [] }; + }); + + let roles = await roleManager.getRoles('user:default/test'); + expect(roles.length).toBe(1); + expect(roles[0]).toEqual('role:default/rbac_admin'); + + roles = await roleManager.getRoles('user:default/test-two'); + expect(roles.length).toBe(1); + expect(roles[0]).toEqual('role:default/rbac_admin'); + + roles = await roleManager.getRoles('user:default/test-three'); + expect(roles.length).toBe(1); + expect(roles[0]).toEqual('role:default/rbac_admin_test'); + }); + + it('getRoles returns role for user inherited from group', async () => { + const teamAGroup = createGroupEntity('team-a', undefined, [], ['test']); + + roleManager.addLink('group:default/team-a', 'role:default/rbac_admin'); + + catalogApiMock.getEntities.mockImplementation((_arg: any) => { + return { items: [teamAGroup] }; + }); + + let roles = await roleManager.getRoles('user:default/test'); + expect(roles.length).toBe(1); + expect(roles[0]).toEqual('role:default/rbac_admin'); + + // should return empty array for group + roles = await roleManager.getRoles('group:default/team-a'); + expect(roles.length).toBe(0); + + // should return empty array for role + roles = await roleManager.getRoles('role:default/rbac_admin'); + expect(roles.length).toBe(0); + }); + }); + + describe('getRoles returns roles per user with database', () => { + let tracker: Tracker; + + beforeEach(() => { + tracker = createTracker(rbacDBClient); + }); + + afterEach(() => { + tracker.reset(); + }); + + it('should returns role per user', async () => { + roleManager.isPGClient = jest.fn().mockImplementation(() => true); + + catalogApiMock.getEntities.mockImplementation((arg: any) => { + const hasMember = arg.filter['relations.hasMember']; + + if (hasMember && hasMember[0] === 'user:default/test') { + return { items: [] }; + } + if (hasMember && hasMember[0] === 'user:default/test-two') { + return { items: [] }; + } + if (hasMember && hasMember[0] === 'user:default/test-three') { + return { items: [] }; + } + return { items: [] }; + }); + + roleManager.addLink('user:default/test', 'role:default/rbac_admin'); + + let data = [{ v0: 'user:default/test', v1: 'role:default/rbac_admin' }]; + + tracker.on.select('casbin_rule').response(data); + + let roles = await roleManager.getRoles('user:default/test'); + expect(roles.length).toBe(1); + expect(roles[0]).toEqual('role:default/rbac_admin'); + + roleManager.addLink('user:default/test-two', 'role:default/rbac_admin'); + + tracker.resetHandlers(); + + data = [{ v0: 'user:default/test-two', v1: 'role:default/rbac_admin' }]; + + tracker.on.select('casbin_rule').response(data); + + roles = await roleManager.getRoles('user:default/test-two'); + expect(roles.length).toBe(1); + expect(roles[0]).toEqual('role:default/rbac_admin'); + + roleManager.addLink( + 'user:default/test-three', + 'role:default/rbac_admin_test', + ); + + tracker.resetHandlers(); + + data = [ + { v0: 'user:default/test-three', v1: 'role:default/rbac_admin_test' }, + ]; + + tracker.on.select('casbin_rule').response(data); + + roles = await roleManager.getRoles('user:default/test-three'); + expect(roles.length).toBe(1); + expect(roles[0]).toEqual('role:default/rbac_admin_test'); + }); + + it('getRoles returns role for user inherited from group', async () => { + roleManager.isPGClient = jest.fn().mockImplementation(() => true); + + const teamAGroup = createGroupEntity('team-a', undefined, [], ['test']); + + roleManager.addLink('group:default/team-a', 'role:default/rbac_admin'); + + catalogApiMock.getEntities.mockImplementation((_arg: any) => { + return { items: [teamAGroup] }; + }); + + const data = [ + { v0: 'group:default/team-a', v1: 'role:default/rbac_admin' }, + ]; + + tracker.on.select('casbin_rule').response(data); + + let roles = await roleManager.getRoles('user:default/test'); + expect(roles.length).toBe(1); + expect(roles[0]).toEqual('role:default/rbac_admin'); + + tracker.on + .select('select "v1" from "casbin_rule" where "v0" = ?') + .response([]); + + // should return empty array for group + roles = await roleManager.getRoles('group:default/team-a'); + expect(roles.length).toBe(0); + + tracker.on + .select('select "v1" from "casbin_rule" where "v0" = ?') + .response([]); + + // should return empty array for role + roles = await roleManager.getRoles('role:default/rbac_admin'); + expect(roles.length).toBe(0); + }); + }); + + function createGroupEntity( + name: string, + parent?: string, + children?: string[], + members?: string[], + ): Entity { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name, + namespace: 'default', + }, + spec: {}, + }; + + if (children) { + entity.spec!.children = children; + } + + if (members) { + entity.spec!.members = members; + } + + if (parent) { + entity.spec!.parent = parent; + } + + return entity; + } +}); + +function newConfig( + maxDepth?: number, + users?: Array<{ name: string }>, + superUsers?: Array<{ name: string }>, +): Config { + const testUsers = [ + { + name: 'user:default/guest', + }, + { + name: 'group:default/guests', + }, + ]; + + return mockServices.rootConfig({ + data: { + permission: { + rbac: { + admin: { + users: users || testUsers, + superUsers: superUsers, + }, + maxDepth, + }, + }, + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + }, + }); +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/role-manager/role-manager.ts b/workspaces/rbac/plugins/rbac-backend/src/role-manager/role-manager.ts new file mode 100644 index 0000000000..f0db48deed --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/role-manager/role-manager.ts @@ -0,0 +1,372 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { AuthService, LoggerService } from '@backstage/backend-plugin-api'; +import type { CatalogApi } from '@backstage/catalog-client'; +import { parseEntityRef } from '@backstage/catalog-model'; +import type { Config } from '@backstage/config'; + +import { RoleManager } from 'casbin'; +import { Knex } from 'knex'; + +import { AncestorSearchMemo } from './ancestor-search-memo'; +import { RoleMemberList } from './member-list'; + +export class BackstageRoleManager implements RoleManager { + private allRoles: Map; + private maxDepth?: number; + constructor( + private readonly catalogApi: CatalogApi, + private readonly logger: LoggerService, + private readonly catalogDBClient: Knex, + private readonly rbacDBClient: Knex, + private readonly config: Config, + private readonly auth: AuthService, + ) { + this.allRoles = new Map(); + const rbacConfig = this.config.getOptionalConfig('permission.rbac'); + this.maxDepth = rbacConfig?.getOptionalNumber('maxDepth'); + if (this.maxDepth !== undefined && this.maxDepth! < 0) { + throw new Error( + 'Max Depth for RBAC group hierarchy must be greater than or equal to zero', + ); + } + } + + /** + * clear clears all stored data and resets the role manager to the initial state. + */ + async clear(): Promise { + // do nothing + } + + /** + * addLink adds the inheritance link between name1 and role: name2. + * aka name1 inherits role: name2. + * The link that is established is based on the defined grouping policies that are added by the enforcer. + * + * ex. `g, name1, name2`. + * @param name1 User or group that will be assigned to a role. + * @param name2 The role that will be created or updated. + * @param _domain Unimplemented prefix to the role. + */ + async addLink( + name1: string, + name2: string, + ..._domain: string[] + ): Promise { + if (!this.isPGClient()) { + const role1 = this.getOrCreateRole(name2); + role1.addMember(name1); + } + } + + /** + * deleteLink deletes the inheritance link between name1 and role: name2. + * aka name1 does not inherit role: name2 any more. + * The link that is deleted is based on the defined grouping policies that are removed by the enforcer. + * + * ex. `g, name1, name2`. + * @param name1 User or group that will be removed from assignment of a role. + * @param name2 The role that will be deleted or updated. + * @param _domain Unimplemented. + */ + async deleteLink( + name1: string, + name2: string, + ..._domain: string[] + ): Promise { + if (!this.isPGClient()) { + const role1 = this.getOrCreateRole(name2); + role1.deleteMember(name1); + + // Clean up in the event that there are no more members in the role + if (role1.getMembers().length === 0) { + this.allRoles.delete(name2); + } + } + } + + /** + * hasLink determines whether name1 inherits role: name2. + * During this check we build the group hierarchy graph to determine if the particular user is directly or indirectly + * attached to the role that we are receiving. + * In the event that there is a postgres database connection, we will attempt to query the roles from the database. + * Otherwise we will use the cached allRoles to determine if there is a link. + * @param name1 The user that we are authorizing. + * @param name2 The name of the role that we are checking against. + * @param domain Unimplemented. + * @returns True if the user is directly or indirectly attached to the role. + */ + async hasLink( + name1: string, + name2: string, + ...domain: string[] + ): Promise { + let currentRole: RoleMemberList; + if (domain.length > 0) { + throw new Error('domain argument is not supported.'); + } + + // Name2 can be an empty string in the event that there is not a role associated with the user + // This happens because of the filtering of the roles reduces the number of roles that we iterate through. + if (name2.length === 0) { + return false; + } + + if (name1 === name2) { + return true; + } + + if (this.isPGClient()) { + currentRole = new RoleMemberList(name2); + await currentRole.buildMembers(currentRole, this.rbacDBClient); + } else { + currentRole = this.allRoles.get(name2)!; + } + + // Check for direct declaration of user to role + const directDeclaration = await this.checkForUserToRole( + name1, + name2, + currentRole, + ); + if (directDeclaration) { + return true; + } + + // name1 is always user in our case. + // name2 is user or group. + // user(name1) couldn't inherit user(name2). + // We can use this fact for optimization. + const { kind } = parseEntityRef(name2); + if (kind.toLocaleLowerCase() === 'user') { + return false; + } + + const memo = new AncestorSearchMemo( + name1, + this.catalogApi, + this.catalogDBClient, + this.auth, + this.maxDepth, + ); + await memo.buildUserGraph(memo); + + memo.debugNodesAndEdges(this.logger, name1); + if (!memo.isAcyclic()) { + const cycles = memo.findCycles(); + + this.logger.warn( + `Detected cycle dependencies in the Group graph: ${JSON.stringify( + cycles, + )}. Admin/(catalog owner) have to fix it to make RBAC permission evaluation correct for groups: ${JSON.stringify( + cycles, + )}`, + ); + + return false; + } + + if ( + this.parseEntityKind(name2) === 'role' && + this.hasMember(currentRole, memo) + ) { + return true; + } + return memo.hasEntityRef(name2); + } + + /** + * syncedHasLink determines whether role: name1 inherits role: name2. + * domain is a prefix to the roles. + */ + syncedHasLink?( + _name1: string, + _name2: string, + ..._domain: string[] + ): boolean { + throw new Error('Method "syncedHasLink" not implemented.'); + } + + /** + * getRoles gets the roles that a subject inherits. + * + * name - is a string entity reference, for example: user:default/tom, role:default/dev, + * so format is :/. + * GetRoles method supports only two kind values: 'user' and 'role'. + * + * domain - is a prefix to the roles, unused parameter. + * + * If name's kind === 'user' we return all inherited roles from groups and roles directly assigned to the user. + * if name's kind === 'role' we return empty array, because we don't support role inheritance. + * Case kind === 'group' - should not happen, because: + * 1) Method getRoles returns only role entity references, so casbin engine doesn't call this + * method again to ask about name with kind "group". + * 2) We implemented getRoles method only to use: + * 'await enforcer.getImplicitPermissionsForUser(userEntityRef)', + * so name argument can be only with kind 'user' or 'role'. + * + * Info: when we call 'await enforcer.getImplicitPermissionsForUser(userEntityRef)', + * then casbin engine executes 'getRoles' method few times. + * Firstly casbin asks about roles for 'userEntityRef'. + * Let's imagine, that 'getRoles' returned two roles for userEntityRef. + * Then casbin calls 'getRoles' two more times to + * find parent roles. But we return empty array for each such call, + * because we don't support role inheritance and we notify casbin about end of the role sub-tree. + */ + async getRoles(name: string, ..._domain: string[]): Promise { + const { kind } = parseEntityRef(name); + if (kind === 'user') { + const memo = new AncestorSearchMemo( + name, + this.catalogApi, + this.catalogDBClient, + this.auth, + this.maxDepth, + ); + await memo.buildUserGraph(memo); + memo.debugNodesAndEdges(this.logger, name); + + if (this.isPGClient()) { + const currentRole = new RoleMemberList(name); + await currentRole.buildRoles( + currentRole, + memo.getNodes(), + this.rbacDBClient, + ); + return Promise.resolve(currentRole.getRoles()); + } + + const allRoles: string[] = []; + // Account for the user not being in the graph + memo.setNode(name); + for (const value of this.allRoles.values()) { + if (this.hasMember(value, memo)) { + allRoles.push(value.name); + } + } + + return Promise.resolve(allRoles); + } + + return []; + } + + /** + * getUsers gets the users that inherits a subject. + * domain is an unreferenced parameter here, may be used in other implementations. + */ + async getUsers(_name: string, ..._domain: string[]): Promise { + throw new Error('Method "getUsers" not implemented.'); + } + + /** + * printRoles prints all the roles to log. + */ + async printRoles(): Promise { + // do nothing + } + + /** + * getOrCreateRole will get a role if it has already been cached + * or it will create a new role to be cached. + * This cache is a simple tree that is used to quickly compare + * users and groups to roles. + * @param name The user or group whose cache we will be getting / creating. + * @returns The cached role as a RoleList. + */ + private getOrCreateRole(name: string): RoleMemberList { + const role = this.allRoles.get(name); + if (role) { + return role; + } + const newRole = new RoleMemberList(name); + this.allRoles.set(name, newRole); + + return newRole; + } + + // parse the entity to find out if it is a user / group / or role + private parseEntityKind(name: string): string { + const parsed = name.split(':'); + return parsed[0]; + } + + /** + * isPGClient checks what the current database client is at them time. + * This is to ensure that we are querying the database in the event of postgres + * or using in memory cache for better sqlite3. + * @returns True if the database client is pg. + */ + isPGClient(): boolean { + const client = this.rbacDBClient.client.config.client; + return client === 'pg'; + } + + /** + * checkForUserToRole checks if there exists a direct declaration of a user to a role. Used to exit out of + * hasLink faster in the event to reduce the time it would take to build the user graph. + * @param name1 The user that we are checking for. + * @param name2 The role that we are checking for. + * @returns True if there is a user that is directly attached to a particular role. + */ + private async checkForUserToRole( + name1: string, + name2: string, + currentRole: RoleMemberList | undefined, + ): Promise { + const tempRole = this.getOrCreateRole(name2); + + // Immediately check if the our temporary role has a link with the role that we are comparing it to + if (this.parseEntityKind(name2) === 'role' && tempRole.hasMember(name1)) { + return true; + } + + // Clean up the temp role + if (tempRole.getMembers().length === 0) { + this.allRoles.delete(name2); + } + + if (currentRole && currentRole.hasMember(name1)) { + return true; + } + + return undefined; + } + + /** + * hasMember checks if the members from a particular role is associated with the user + * that the AncestorSearchMemo graph is built for. + * @param role The role that we are getting the members from. + * @param memo The user graph that we are comparing members with. + * @returns True if a member from the role is also associated with the user. + */ + private hasMember( + role: RoleMemberList | undefined, + memo: AncestorSearchMemo, + ): boolean { + if (role === undefined) { + return false; + } + + for (const member of role.getMembers()) { + if (memo.hasEntityRef(member)) { + return true; + } + } + return false; + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/service/enforcer-delegate.test.ts b/workspaces/rbac/plugins/rbac-backend/src/service/enforcer-delegate.test.ts new file mode 100644 index 0000000000..e3cb1281a5 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/service/enforcer-delegate.test.ts @@ -0,0 +1,1162 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { mockServices } from '@backstage/backend-test-utils'; + +import { newEnforcer, newModelFromString } from 'casbin'; +import * as Knex from 'knex'; +import { MockClient } from 'knex-mock-client'; + +import { CasbinDBAdapterFactory } from '../database/casbin-adapter-factory'; +import { + RoleMetadataDao, + RoleMetadataStorage, +} from '../database/role-metadata'; +import { BackstageRoleManager } from '../role-manager/role-manager'; +import { EnforcerDelegate } from './enforcer-delegate'; +import { MODEL } from './permission-model'; + +// TODO: Move to 'catalogServiceMock' from '@backstage/plugin-catalog-node/testUtils' +// once '@backstage/plugin-catalog-node' is upgraded +const catalogApiMock = { + 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 = { + filterRoleMetadata: jest.fn().mockImplementation(() => []), + findRoleMetadata: jest.fn().mockImplementation(), + createRoleMetadata: jest.fn().mockImplementation(), + updateRoleMetadata: jest.fn().mockImplementation(), + removeRoleMetadata: jest.fn().mockImplementation(), +}; + +const mockClientKnex = Knex.knex({ client: MockClient }); + +const mockAuthService = mockServices.auth(); + +const config = mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + permission: { + rbac: {}, + }, + }, +}); +const policy = ['user:default/tom', 'policy-entity', 'read', 'allow']; +const secondPolicy = ['user:default/tim', 'catalog-entity', 'write', 'allow']; + +const groupingPolicy = ['user:default/tom', 'role:default/dev-team']; +const secondGroupingPolicy = ['user:default/tim', 'role:default/qa-team']; + +describe('EnforcerDelegate', () => { + let enfRemovePolicySpy: jest.SpyInstance, string[], any>; + let enfRemovePoliciesSpy: jest.SpyInstance< + Promise, + [rules: string[][]], + any + >; + let enfRemoveGroupingPolicySpy: jest.SpyInstance< + Promise, + string[], + any + >; + let enfFilterGroupingPolicySpy: jest.SpyInstance< + Promise, + [fieldIndex: number, ...fieldValues: string[]], + any + >; + let enfRemoveGroupingPoliciesSpy: jest.SpyInstance< + Promise, + [rules: string[][]], + any + >; + let enfAddPolicySpy: jest.SpyInstance< + Promise, + [...policy: string[]], + any + >; + let enfAddGroupingPolicySpy: jest.SpyInstance< + Promise, + [...policy: string[]], + any + >; + let enfAddGroupingPoliciesSpy: jest.SpyInstance< + Promise, + [policy: string[][]], + any + >; + let enfAddPoliciesSpy: jest.SpyInstance< + Promise, + [rules: string[][]], + any + >; + + const modifiedBy = 'user:default/some-admin'; + + beforeEach(() => { + (roleMetadataStorageMock.createRoleMetadata as jest.Mock).mockReset(); + (roleMetadataStorageMock.updateRoleMetadata as jest.Mock).mockReset(); + (roleMetadataStorageMock.findRoleMetadata as jest.Mock).mockReset(); + (roleMetadataStorageMock.removeRoleMetadata as jest.Mock).mockReset(); + }); + + const knex = Knex.knex({ client: MockClient }); + + async function createEnfDelegate( + policies?: string[][], + groupingPolicies?: string[][], + ): Promise { + const theModel = newModelFromString(MODEL); + const logger = mockServices.logger.mock(); + + const sqliteInMemoryAdapter = await new CasbinDBAdapterFactory( + config, + mockClientKnex, + ).createAdapter(); + + const catalogDBClient = Knex.knex({ client: MockClient }); + const rbacDBClient = Knex.knex({ client: MockClient }); + const enf = await newEnforcer(theModel, sqliteInMemoryAdapter); + enfRemovePolicySpy = jest.spyOn(enf, 'removePolicy'); + enfRemovePoliciesSpy = jest.spyOn(enf, 'removePolicies'); + enfRemoveGroupingPolicySpy = jest.spyOn(enf, 'removeGroupingPolicy'); + enfFilterGroupingPolicySpy = jest.spyOn(enf, 'getFilteredGroupingPolicy'); + enfRemoveGroupingPoliciesSpy = jest.spyOn(enf, 'removeGroupingPolicies'); + enfAddPolicySpy = jest.spyOn(enf, 'addPolicy'); + enfAddGroupingPolicySpy = jest.spyOn(enf, 'addGroupingPolicy'); + enfAddGroupingPoliciesSpy = jest.spyOn(enf, 'addGroupingPolicies'); + enfAddPoliciesSpy = jest.spyOn(enf, 'addPolicies'); + + const rm = new BackstageRoleManager( + catalogApiMock, + logger, + catalogDBClient, + rbacDBClient, + config, + mockAuthService, + ); + enf.setRoleManager(rm); + enf.enableAutoBuildRoleLinks(false); + await enf.buildRoleLinks(); + + if (policies && policies.length > 0) { + await enf.addPolicies(policies); + } + if (groupingPolicies && groupingPolicies.length > 0) { + await enf.addGroupingPolicies(groupingPolicies); + } + + return new EnforcerDelegate(enf, roleMetadataStorageMock, knex); + } + + describe('hasPolicy', () => { + it('has policy should return false', async () => { + const enfDelegate = await createEnfDelegate(); + const result = await enfDelegate.hasPolicy(...policy); + + expect(result).toBeFalsy(); + }); + + it('has policy should return true', async () => { + const enfDelegate = await createEnfDelegate([policy]); + + const result = await enfDelegate.hasPolicy(...policy); + + expect(result).toBeTruthy(); + }); + }); + + describe('hasGroupingPolicy', () => { + it('has policy should return false', async () => { + const enfDelegate = await createEnfDelegate([policy]); + const result = await enfDelegate.hasGroupingPolicy(...groupingPolicy); + + expect(result).toBeFalsy(); + }); + + it('has policy should return true', async () => { + const enfDelegate = await createEnfDelegate([], [groupingPolicy]); + + const result = await enfDelegate.hasGroupingPolicy(...groupingPolicy); + + expect(result).toBeTruthy(); + }); + }); + + describe('getPolicy', () => { + it('should return empty array', async () => { + const enfDelegate = await createEnfDelegate(); + const policies = await enfDelegate.getPolicy(); + + expect(policies.length).toEqual(0); + }); + + it('should return policy', async () => { + const enfDelegate = await createEnfDelegate([policy]); + + const policies = await enfDelegate.getPolicy(); + + expect(policies.length).toEqual(1); + expect(policies[0]).toEqual(policy); + }); + }); + + describe('getGroupingPolicy', () => { + it('should return empty array', async () => { + const enfDelegate = await createEnfDelegate(); + const groupingPolicies = await enfDelegate.getGroupingPolicy(); + + expect(groupingPolicies.length).toEqual(0); + }); + + it('should return grouping policy', async () => { + const enfDelegate = await createEnfDelegate([], [groupingPolicy]); + + const policies = await enfDelegate.getGroupingPolicy(); + + expect(policies.length).toEqual(1); + expect(policies[0]).toEqual(groupingPolicy); + }); + }); + + describe('getFilteredPolicy', () => { + it('should return empty array', async () => { + const enfDelegate = await createEnfDelegate(); + // filter by policy assignment person + const policies = await enfDelegate.getFilteredPolicy(0, policy[0]); + + expect(policies.length).toEqual(0); + }); + + it('should return filteredPolicy', async () => { + const enfDelegate = await createEnfDelegate([policy, secondPolicy]); + + // filter by policy assignment person + const policies = await enfDelegate.getFilteredPolicy( + 0, + 'user:default/tim', + ); + + expect(policies.length).toEqual(1); + expect(policies[0]).toEqual(secondPolicy); + }); + }); + + describe('getFilteredGroupingPolicy', () => { + it('should return empty array', async () => { + const enfDelegate = await createEnfDelegate(); + // filter by policy assignment person + const policies = await enfDelegate.getFilteredGroupingPolicy( + 0, + 'user:default/tim', + ); + + expect(policies.length).toEqual(0); + }); + + it('should return filteredPolicy', async () => { + const enfDelegate = await createEnfDelegate( + [], + [groupingPolicy, secondGroupingPolicy], + ); + + // filter by policy assignment person + const policies = await enfDelegate.getFilteredGroupingPolicy( + 0, + 'user:default/tim', + ); + + expect(policies.length).toEqual(1); + expect(policies[0]).toEqual(secondGroupingPolicy); + }); + }); + + describe('addPolicy', () => { + it('should add policy', async () => { + const enfDelegate = await createEnfDelegate(); + enfAddPolicySpy.mockClear(); + + await enfDelegate.addPolicy(policy); + + expect(enfAddPolicySpy).toHaveBeenCalledWith(...policy); + + expect(await enfDelegate.getPolicy()).toEqual([policy]); + }); + }); + + describe('addPolicies', () => { + it('should be added single policy', async () => { + const enfDelegate = await createEnfDelegate(); + + await enfDelegate.addPolicies([policy]); + + const storePolicies = await enfDelegate.getPolicy(); + + expect(storePolicies).toEqual([policy]); + expect(enfAddPoliciesSpy).toHaveBeenCalledWith([policy]); + }); + + it('should be added few policies', async () => { + const enfDelegate = await createEnfDelegate(); + + await enfDelegate.addPolicies([policy, secondPolicy]); + + const storePolicies = await enfDelegate.getPolicy(); + + expect(storePolicies.length).toEqual(2); + expect(storePolicies).toEqual( + expect.arrayContaining([ + expect.objectContaining(policy), + expect.objectContaining(secondPolicy), + ]), + ); + expect(enfAddPoliciesSpy).toHaveBeenCalledWith([policy, secondPolicy]); + }); + + it('should not fail, when argument is empty array', async () => { + const enfDelegate = await createEnfDelegate(); + + enfDelegate.addPolicies([]); + + expect(enfAddPoliciesSpy).not.toHaveBeenCalled(); + expect((await enfDelegate.getPolicy()).length).toEqual(0); + }); + }); + + describe('addGroupingPolicy', () => { + it('should add grouping policy and create role metadata', async () => { + (roleMetadataStorageMock.findRoleMetadata as jest.Mock).mockReturnValue( + Promise.resolve(undefined), + ); + + const enfDelegate = await createEnfDelegate(); + + const roleEntityRef = 'role:default/dev-team'; + await enfDelegate.addGroupingPolicy(groupingPolicy, { + source: 'rest', + roleEntityRef: roleEntityRef, + author: modifiedBy, + modifiedBy, + }); + + expect(enfAddGroupingPolicySpy).toHaveBeenCalledWith(...groupingPolicy); + expect(roleMetadataStorageMock.createRoleMetadata).toHaveBeenCalled(); + expect( + (roleMetadataStorageMock.createRoleMetadata as jest.Mock).mock.calls + .length, + ).toEqual(1); + const metadata: RoleMetadataDao = ( + roleMetadataStorageMock.createRoleMetadata as jest.Mock + ).mock.calls[0][0]; + const createdAtData = new Date(`${metadata.createdAt}`); + const lastModified = new Date(`${metadata.lastModified}`); + expect(lastModified).toEqual(createdAtData); + + expect(metadata.source).toEqual('rest'); + expect(metadata.roleEntityRef).toEqual('role:default/dev-team'); + }); + + it('should fail to add policy, caused role metadata storage error', async () => { + const enfDelegate = await createEnfDelegate(); + + roleMetadataStorageMock.createRoleMetadata = jest + .fn() + .mockImplementation(() => { + throw new Error('some unexpected error'); + }); + + await expect( + enfDelegate.addGroupingPolicy(groupingPolicy, { + source: 'rest', + roleEntityRef: 'role:default/dev-team', + author: modifiedBy, + modifiedBy, + }), + ).rejects.toThrow('some unexpected error'); + }); + + it('should update role metadata on addGroupingPolicy, because metadata has been created', async () => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async ( + _roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + return { + source: 'csv-file', + roleEntityRef: 'role:default/dev-team', + createdAt: '2024-03-01 00:23:41+00', + author: modifiedBy, + modifiedBy, + }; + }, + ); + + const enfDelegate = await createEnfDelegate(); + + const roleEntityRef = 'role:default/dev-team'; + await enfDelegate.addGroupingPolicy(groupingPolicy, { + source: 'rest', + roleEntityRef, + author: modifiedBy, + modifiedBy, + }); + + expect(enfAddGroupingPolicySpy).toHaveBeenCalledWith(...groupingPolicy); + + expect(roleMetadataStorageMock.createRoleMetadata).not.toHaveBeenCalled(); + const metadata: RoleMetadataDao = ( + roleMetadataStorageMock.updateRoleMetadata as jest.Mock + ).mock.calls[0][0]; + const createdAtData = new Date(`${metadata.createdAt}`); + const lastModified = new Date(`${metadata.lastModified}`); + expect(lastModified > createdAtData).toBeTruthy(); + + expect(metadata.source).toEqual('rest'); + expect(metadata.roleEntityRef).toEqual('role:default/dev-team'); + }); + }); + + describe('addGroupingPolicies', () => { + it('should add grouping policies and create role metadata', async () => { + const enfDelegate = await createEnfDelegate(); + + const roleMetadataDao: RoleMetadataDao = { + roleEntityRef: 'role:default/security', + source: 'rest', + author: modifiedBy, + modifiedBy, + }; + await enfDelegate.addGroupingPolicies( + [groupingPolicy, secondGroupingPolicy], + roleMetadataDao, + ); + + const storedPolicies = await enfDelegate.getGroupingPolicy(); + expect(storedPolicies).toEqual([groupingPolicy, secondGroupingPolicy]); + + expect(enfAddGroupingPoliciesSpy).toHaveBeenCalledWith([ + groupingPolicy, + secondGroupingPolicy, + ]); + + expect(roleMetadataStorageMock.createRoleMetadata).toHaveBeenCalledWith( + roleMetadataDao, + expect.anything(), + ); + + const metadata: RoleMetadataDao = ( + roleMetadataStorageMock.createRoleMetadata as jest.Mock + ).mock.calls[0][0]; + + const createdAtData = new Date(`${metadata.createdAt}`); + const lastModified = new Date(`${metadata.lastModified}`); + expect(lastModified).toEqual(createdAtData); + expect(metadata.author).toEqual(modifiedBy); + expect(metadata.roleEntityRef).toEqual('role:default/security'); + expect(metadata.source).toEqual('rest'); + expect(metadata.description).toBeUndefined(); + }); + + it('should add grouping policies and create role metadata with description', async () => { + const enfDelegate = await createEnfDelegate(); + + const description = 'Role for security engineers'; + const roleMetadataDao: RoleMetadataDao = { + roleEntityRef: 'role:default/security', + source: 'rest', + description, + author: modifiedBy, + modifiedBy, + }; + await enfDelegate.addGroupingPolicies( + [groupingPolicy, secondGroupingPolicy], + roleMetadataDao, + ); + + const storedPolicies = await enfDelegate.getGroupingPolicy(); + expect(storedPolicies).toEqual([groupingPolicy, secondGroupingPolicy]); + + expect(enfAddGroupingPoliciesSpy).toHaveBeenCalledWith([ + groupingPolicy, + secondGroupingPolicy, + ]); + + expect(roleMetadataStorageMock.createRoleMetadata).toHaveBeenCalledWith( + roleMetadataDao, + expect.anything(), + ); + + const metadata: RoleMetadataDao = ( + roleMetadataStorageMock.createRoleMetadata as jest.Mock + ).mock.calls[0][0]; + + const createdAtData = new Date(`${metadata.createdAt}`); + const lastModified = new Date(`${metadata.lastModified}`); + expect(lastModified).toEqual(createdAtData); + expect(metadata.roleEntityRef).toEqual('role:default/security'); + expect(metadata.source).toEqual('rest'); + expect(metadata.description).toEqual('Role for security engineers'); + }); + + it('should fail to add grouping policy, because fail to create role metadata', async () => { + roleMetadataStorageMock.createRoleMetadata = jest + .fn() + .mockImplementation(() => { + throw new Error('some unexpected error'); + }); + + const enfDelegate = await createEnfDelegate(); + + const roleMetadataDao: RoleMetadataDao = { + roleEntityRef: 'role:default/security', + source: 'rest', + author: 'user:default/some-user', + modifiedBy: 'user:default/some-user', + }; + await expect( + enfDelegate.addGroupingPolicies( + [groupingPolicy, secondGroupingPolicy], + roleMetadataDao, + ), + ).rejects.toThrow('some unexpected error'); + + // shouldn't store group policies + const storedPolicies = await enfDelegate.getGroupingPolicy(); + expect(storedPolicies).toEqual([]); + }); + + it('should update role metadata, because metadata has been created', async () => { + (roleMetadataStorageMock.findRoleMetadata as jest.Mock) = jest + .fn() + .mockReturnValueOnce({ + source: 'csv-file', + roleEntityRef: 'role:default/dev-team', + author: 'user:default/some-user', + description: 'Role for dev engineers', + createdAt: '2024-03-01 00:23:41+00', + }); + + const enfDelegate = await createEnfDelegate(); + + const roleMetadataDao: RoleMetadataDao = { + roleEntityRef: 'role:default/dev-team', + source: 'rest', + author: 'user:default/some-user', + modifiedBy, + }; + await enfDelegate.addGroupingPolicies( + [ + ['user:default/tom', 'role:default/dev-team'], + ['user:default/tim', 'role:default/dev-team'], + ], + roleMetadataDao, + ); + const storedPolicies = await enfDelegate.getGroupingPolicy(); + + expect(storedPolicies).toEqual([ + ['user:default/tom', 'role:default/dev-team'], + ['user:default/tim', 'role:default/dev-team'], + ]); + + expect(enfAddGroupingPoliciesSpy).toHaveBeenCalledWith([ + ['user:default/tom', 'role:default/dev-team'], + ['user:default/tim', 'role:default/dev-team'], + ]); + + expect(roleMetadataStorageMock.createRoleMetadata).not.toHaveBeenCalled(); + + const metadata = (roleMetadataStorageMock.updateRoleMetadata as jest.Mock) + .mock.calls[0][0]; + + const createdAtData = new Date(`${metadata.createdAt}`); + const lastModified = new Date(`${metadata.lastModified}`); + expect(lastModified > createdAtData).toBeTruthy(); + expect(metadata.author).toEqual('user:default/some-user'); + expect(metadata.description).toEqual('Role for dev engineers'); + expect(metadata.modifiedBy).toEqual(modifiedBy); + expect(metadata.roleEntityRef).toEqual('role:default/dev-team'); + expect(metadata.source).toEqual('rest'); + }); + }); + + describe('updateGroupingPolicies', () => { + it('should update grouping policies: add one more policy and update roleMetadata with new modifiedBy', async () => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation(async (): Promise => { + return { + source: 'rest', + roleEntityRef: 'role:default/dev-team', + author: 'user:default/tom', + modifiedBy: 'user:default/tom', + description: 'Role for dev engineers', + createdAt: '2024-03-01 00:23:41+00', + }; + }); + + const enfDelegate = await createEnfDelegate([], [groupingPolicy]); + + const roleMetadataDao: RoleMetadataDao = { + roleEntityRef: 'role:default/dev-team', + source: 'rest', + author: modifiedBy, + modifiedBy: 'user:default/system-admin', + }; + + await enfDelegate.updateGroupingPolicies( + [groupingPolicy], + [groupingPolicy, secondGroupingPolicy], + roleMetadataDao, + ); + + const storedPolicies = await enfDelegate.getGroupingPolicy(); + expect(storedPolicies.length).toEqual(2); + + expect(enfRemoveGroupingPoliciesSpy).toHaveBeenCalledWith([ + groupingPolicy, + ]); + expect(enfAddGroupingPoliciesSpy).toHaveBeenCalledWith([ + groupingPolicy, + secondGroupingPolicy, + ]); + + const metadata = (roleMetadataStorageMock.updateRoleMetadata as jest.Mock) + .mock.calls[0][0]; + + const createdAtData = new Date(`${metadata.createdAt}`); + const lastModified = new Date(`${metadata.lastModified}`); + expect(lastModified > createdAtData).toBeTruthy(); + expect(metadata.author).toEqual('user:default/tom'); + expect(metadata.description).toEqual('Role for dev engineers'); + expect(metadata.modifiedBy).toEqual('user:default/system-admin'); + expect(metadata.roleEntityRef).toEqual('role:default/dev-team'); + expect(metadata.source).toEqual('rest'); + }); + + it('should update grouping policies: one policy should be removed for updateGroupingPolicies', async () => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation(async (): Promise => { + return { + source: 'rest', + roleEntityRef: 'role:default/dev-team', + author: modifiedBy, + modifiedBy, + description: 'Role for dev engineers', + createdAt: '2024-03-01 00:23:41+00', + }; + }); + + const enfDelegate = await createEnfDelegate( + [], + [groupingPolicy, secondGroupingPolicy], + ); + + const roleMetadataDao: RoleMetadataDao = { + roleEntityRef: 'role:default/dev-team', + source: 'rest', + author: modifiedBy, + modifiedBy: 'user:default/system-admin', + }; + await enfDelegate.updateGroupingPolicies( + [groupingPolicy, secondGroupingPolicy], + [groupingPolicy], + roleMetadataDao, + ); + + const storedPolicies = await enfDelegate.getGroupingPolicy(); + expect(storedPolicies.length).toEqual(1); + + expect(enfRemoveGroupingPoliciesSpy).toHaveBeenCalledWith([ + groupingPolicy, + secondGroupingPolicy, + ]); + expect(enfAddGroupingPoliciesSpy).toHaveBeenCalledWith([groupingPolicy]); + + const metadata = (roleMetadataStorageMock.updateRoleMetadata as jest.Mock) + .mock.calls[0][0]; + + const createdAtData = new Date(`${metadata.createdAt}`); + const lastModified = new Date(`${metadata.lastModified}`); + expect(lastModified > createdAtData).toBeTruthy(); + expect(metadata.author).toEqual(modifiedBy); + expect(metadata.description).toEqual('Role for dev engineers'); + expect(metadata.modifiedBy).toEqual('user:default/system-admin'); + expect(metadata.roleEntityRef).toEqual('role:default/dev-team'); + expect(metadata.source).toEqual('rest'); + }); + + it('should update grouping policies: one policy should be removed and description updated', async () => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation(async (): Promise => { + return { + source: 'rest', + roleEntityRef: 'role:default/dev-team', + author: 'user:default/some-user', + modifiedBy: 'user:default/some-user', + description: 'Role for dev engineers', + createdAt: '2024-03-01 00:23:41+00', + }; + }); + + const enfDelegate = await createEnfDelegate( + [], + [groupingPolicy, secondGroupingPolicy], + ); + + const roleMetadataDao: RoleMetadataDao = { + roleEntityRef: 'role:default/dev-team', + source: 'rest', + author: modifiedBy, + modifiedBy: 'user:default/system-admin', + description: 'updated description', + }; + await enfDelegate.updateGroupingPolicies( + [groupingPolicy, secondGroupingPolicy], + [groupingPolicy], + roleMetadataDao, + ); + + const storedPolicies = await enfDelegate.getGroupingPolicy(); + expect(storedPolicies.length).toEqual(1); + + expect(enfRemoveGroupingPoliciesSpy).toHaveBeenCalledWith([ + groupingPolicy, + secondGroupingPolicy, + ]); + expect(enfAddGroupingPoliciesSpy).toHaveBeenCalledWith([groupingPolicy]); + + const metadata = (roleMetadataStorageMock.updateRoleMetadata as jest.Mock) + .mock.calls[0][0]; + + const createdAtData = new Date(`${metadata.createdAt}`); + const lastModified = new Date(`${metadata.lastModified}`); + expect(lastModified > createdAtData).toBeTruthy(); + expect(metadata.author).toEqual('user:default/some-user'); + expect(metadata.description).toEqual('updated description'); + expect(metadata.modifiedBy).toEqual('user:default/system-admin'); + expect(metadata.roleEntityRef).toEqual('role:default/dev-team'); + expect(metadata.source).toEqual('rest'); + }); + + it('should update grouping policies: role should be renamed', async () => { + const oldRoleName = 'role:default/dev-team'; + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation(async (): Promise => { + return { + source: 'rest', + roleEntityRef: oldRoleName, + author: modifiedBy, + modifiedBy, + description: 'Role for dev engineers', + createdAt: '2024-03-01 00:23:41+00', + }; + }); + + const enfDelegate = await createEnfDelegate( + [], + [groupingPolicy, secondGroupingPolicy], + ); + + const newRoleName = 'role:default/new-team-name'; + const groupingPolicyWithRenamedRole = ['user:default/tom', newRoleName]; + const secondGroupingPolicyWithRenamedRole = [ + 'user:default/tim', + newRoleName, + ]; + + const roleMetadataDao: RoleMetadataDao = { + roleEntityRef: newRoleName, + source: 'rest', + modifiedBy, + }; + await enfDelegate.updateGroupingPolicies( + [groupingPolicy, secondGroupingPolicy], + [groupingPolicyWithRenamedRole, secondGroupingPolicyWithRenamedRole], + roleMetadataDao, + ); + + const storedPolicies = await enfDelegate.getGroupingPolicy(); + expect(storedPolicies.length).toEqual(2); + expect(storedPolicies[0]).toEqual(groupingPolicyWithRenamedRole); + expect(storedPolicies[1]).toEqual(secondGroupingPolicyWithRenamedRole); + + expect(enfRemoveGroupingPoliciesSpy).toHaveBeenCalledWith([ + groupingPolicy, + secondGroupingPolicy, + ]); + expect(enfAddGroupingPoliciesSpy).toHaveBeenCalledWith([ + groupingPolicyWithRenamedRole, + secondGroupingPolicyWithRenamedRole, + ]); + + const metadata = (roleMetadataStorageMock.updateRoleMetadata as jest.Mock) + .mock.calls[0][0]; + + const createdAtData = new Date(`${metadata.createdAt}`); + const lastModified = new Date(`${metadata.lastModified}`); + expect(lastModified > createdAtData).toBeTruthy(); + expect(metadata.author).toEqual(modifiedBy); + expect(metadata.description).toEqual('Role for dev engineers'); + expect(metadata.modifiedBy).toEqual(modifiedBy); + expect(metadata.roleEntityRef).toEqual(newRoleName); + expect(metadata.source).toEqual('rest'); + }); + + it('should update grouping policies: should be updated role description and source', async () => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation(async (): Promise => { + return { + source: 'legacy', + roleEntityRef: 'role:default/dev-team', + author: modifiedBy, + description: 'Role for dev engineers', + createdAt: '2024-03-01 00:23:41+00', + modifiedBy, + }; + }); + + const enfDelegate = await createEnfDelegate([], [groupingPolicy]); + + const roleMetadataDao: RoleMetadataDao = { + roleEntityRef: 'role:default/dev-team', + source: 'rest', + modifiedBy, + description: 'some-new-description', + }; + await enfDelegate.updateGroupingPolicies( + [groupingPolicy], + [groupingPolicy], + roleMetadataDao, + ); + + const storedPolicies = await enfDelegate.getGroupingPolicy(); + expect(storedPolicies.length).toEqual(1); + expect(storedPolicies).toEqual([groupingPolicy]); + + const metadata = (roleMetadataStorageMock.updateRoleMetadata as jest.Mock) + .mock.calls[0][0]; + + const createdAtData = new Date(`${metadata.createdAt}`); + const lastModified = new Date(`${metadata.lastModified}`); + expect(lastModified > createdAtData).toBeTruthy(); + expect(metadata.author).toEqual(modifiedBy); + expect(metadata.description).toEqual('some-new-description'); + expect(metadata.modifiedBy).toEqual(modifiedBy); + expect(metadata.roleEntityRef).toEqual('role:default/dev-team'); + expect(metadata.source).toEqual('rest'); + }); + }); + + describe('updatePolicies', () => { + it('should be updated single policy', async () => { + const enfDelegate = await createEnfDelegate([policy]); + enfAddPolicySpy.mockClear(); + enfRemovePoliciesSpy.mockClear(); + + const newPolicy = ['user:default/tom', 'policy-entity', 'read', 'deny']; + + await enfDelegate.updatePolicies([policy], [newPolicy]); + + expect(enfRemovePoliciesSpy).toHaveBeenCalledWith([policy]); + expect(enfAddPoliciesSpy).toHaveBeenCalledWith([newPolicy]); + }); + + it('should be added few policies', async () => { + const enfDelegate = await createEnfDelegate([policy, secondPolicy]); + enfAddPolicySpy.mockClear(); + enfRemovePoliciesSpy.mockClear(); + + const newPolicy1 = ['user:default/tom', 'policy-entity', 'read', 'deny']; + const newPolicy2 = [ + 'user:default/tim', + 'catalog-entity', + 'write', + 'allow', + ]; + + await enfDelegate.updatePolicies( + [policy, secondPolicy], + [newPolicy1, newPolicy2], + ); + + expect(enfRemovePoliciesSpy).toHaveBeenCalledWith([policy, secondPolicy]); + expect(enfAddPoliciesSpy).toHaveBeenCalledWith([newPolicy1, newPolicy2]); + }); + }); + + describe('removePolicy', () => { + const policyToDelete = [ + 'user:default/some-user', + 'catalog-entity', + 'read', + 'allow', + ]; + + it('policy should be removed', async () => { + const enfDelegate = await createEnfDelegate([policyToDelete]); + await enfDelegate.removePolicy(policyToDelete); + + expect(enfRemovePolicySpy).toHaveBeenCalledWith(...policyToDelete); + }); + }); + + describe('removePolicies', () => { + const policiesToDelete = [ + ['user:default/some-user', 'catalog-entity', 'read', 'allow'], + ['user:default/some-user-2', 'catalog-entity', 'read', 'allow'], + ]; + it('policies should be removed', async () => { + const enfDelegate = await createEnfDelegate(policiesToDelete); + await enfDelegate.removePolicies(policiesToDelete); + + expect(enfRemovePoliciesSpy).toHaveBeenCalledWith(policiesToDelete); + }); + }); + + describe('removeGroupingPolicy', () => { + const groupingPolicyToDelete = [ + 'user:default/some-user', + 'role:default/team-dev', + ]; + + beforeEach(() => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation(() => { + return { + source: 'rest', + roleEntityRef: 'role:default/team-dev', + createdAt: '2024-03-01 00:23:41+00', + }; + }); + }); + + it('should remove grouping policy and remove role metadata', async () => { + const enfDelegate = await createEnfDelegate([], [groupingPolicyToDelete]); + await enfDelegate.removeGroupingPolicy( + groupingPolicyToDelete, + { source: 'rest', roleEntityRef: 'role:default/team-dev', modifiedBy }, + false, + ); + + expect(roleMetadataStorageMock.findRoleMetadata).toHaveBeenCalledTimes(1); + expect(enfFilterGroupingPolicySpy).toHaveBeenCalledTimes(1); + + expect(roleMetadataStorageMock.removeRoleMetadata).toHaveBeenCalledWith( + 'role:default/team-dev', + expect.anything(), + ); + }); + + it('should remove grouping policy and update role metadata', async () => { + const enfDelegate = await createEnfDelegate( + [], + [ + groupingPolicyToDelete, + ['group:default/team-a', 'role:default/team-dev'], + ], + ); + await enfDelegate.removeGroupingPolicy( + groupingPolicyToDelete, + { source: 'rest', roleEntityRef: 'role:default/team-dev', modifiedBy }, + false, + ); + + expect(roleMetadataStorageMock.findRoleMetadata).toHaveBeenCalledTimes(1); + expect(enfFilterGroupingPolicySpy).toHaveBeenCalledTimes(1); + + const metadata = (roleMetadataStorageMock.updateRoleMetadata as jest.Mock) + .mock.calls[0][0]; + + const createdAtData = new Date(`${metadata.createdAt}`); + const lastModified = new Date(`${metadata.lastModified}`); + expect(lastModified > createdAtData).toBeTruthy(); + + expect(metadata.roleEntityRef).toEqual('role:default/team-dev'); + expect(metadata.source).toEqual('rest'); + }); + + it('should remove grouping policy and not update or remove role metadata, because isUpdate flag set to true', async () => { + const enfDelegate = await createEnfDelegate([], [groupingPolicyToDelete]); + await enfDelegate.removeGroupingPolicy( + groupingPolicyToDelete, + { + source: 'rest', + roleEntityRef: 'role:default/dev-team', + modifiedBy: 'user:default/some-user', + }, + true, + ); + + expect(enfRemoveGroupingPolicySpy).toHaveBeenCalledWith( + ...groupingPolicyToDelete, + ); + + expect(roleMetadataStorageMock.findRoleMetadata).not.toHaveBeenCalled(); + expect(enfFilterGroupingPolicySpy).not.toHaveBeenCalled(); + expect(roleMetadataStorageMock.removeRoleMetadata).not.toHaveBeenCalled(); + expect(roleMetadataStorageMock.updateRoleMetadata).not.toHaveBeenCalled(); + }); + }); + + describe('removeGroupingPolicies', () => { + const groupingPoliciesToDelete = [ + ['user:default/some-user', 'role:default/team-dev'], + ['group:default/team-a', 'role:default/team-dev'], + ]; + + it('should remove grouping policies and remove role metadata', async () => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation(() => { + return { + source: 'rest', + roleEntityRef: 'role:default/team-dev', + }; + }); + enfRemoveGroupingPoliciesSpy.mockReset(); + enfFilterGroupingPolicySpy.mockReset(); + + const enfDelegate = await createEnfDelegate([], groupingPoliciesToDelete); + await enfDelegate.removeGroupingPolicies( + groupingPoliciesToDelete, + { + roleEntityRef: 'role:default/team-dev', + source: 'rest', + modifiedBy, + }, + false, + ); + + expect(enfRemoveGroupingPoliciesSpy).toHaveBeenCalledWith( + groupingPoliciesToDelete, + ); + + expect(roleMetadataStorageMock.findRoleMetadata).toHaveBeenCalledTimes(1); + expect(enfFilterGroupingPolicySpy).toHaveBeenCalledTimes(1); + + expect(roleMetadataStorageMock.removeRoleMetadata).toHaveBeenCalledWith( + 'role:default/team-dev', + expect.anything(), + ); + }); + + it('should remove grouping policies and update role metadata', async () => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation(() => { + return { + source: 'rest', + roleEntityRef: 'role:default/team-dev', + createdAt: '2024-03-01 00:23:41+00', + }; + }); + enfRemoveGroupingPoliciesSpy.mockReset(); + enfFilterGroupingPolicySpy.mockReset(); + + const remainingGroupPolicy = [ + 'user:default/some-user-2', + 'role:default/team-dev', + ]; + const enfDelegate = await createEnfDelegate( + [], + [...groupingPoliciesToDelete, remainingGroupPolicy], + ); + await enfDelegate.removeGroupingPolicies( + groupingPoliciesToDelete, + { + roleEntityRef: 'role:default/team-dev', + source: 'rest', + modifiedBy, + }, + false, + ); + + expect(enfRemoveGroupingPoliciesSpy).toHaveBeenCalledWith( + groupingPoliciesToDelete, + ); + + expect(roleMetadataStorageMock.findRoleMetadata).toHaveBeenCalledTimes(1); + expect(enfFilterGroupingPolicySpy).toHaveBeenCalledTimes(1); + + const metadata = (roleMetadataStorageMock.updateRoleMetadata as jest.Mock) + .mock.calls[0][0]; + + const createdAtData = new Date(`${metadata.createdAt}`); + const lastModified = new Date(`${metadata.lastModified}`); + expect(lastModified > createdAtData).toBeTruthy(); + + expect(metadata.roleEntityRef).toEqual('role:default/team-dev'); + expect(metadata.source).toEqual('rest'); + }); + + it('should remove grouping policy and not update or remove role metadata, because isUpdate flag set to true', async () => { + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation(() => { + return { + source: 'rest', + roleEntityRef: 'role:default/team-dev', + }; + }); + enfRemoveGroupingPoliciesSpy.mockReset(); + enfFilterGroupingPolicySpy.mockReset(); + + const enfDelegate = await createEnfDelegate([], groupingPoliciesToDelete); + await enfDelegate.removeGroupingPolicies( + groupingPoliciesToDelete, + { + roleEntityRef: 'role:default/team-dev', + source: 'rest', + modifiedBy: 'user:default/test-user', + }, + true, + ); + + expect(enfRemoveGroupingPoliciesSpy).toHaveBeenCalledWith( + groupingPoliciesToDelete, + ); + + expect(roleMetadataStorageMock.findRoleMetadata).not.toHaveBeenCalled(); + expect(enfFilterGroupingPolicySpy).not.toHaveBeenCalled(); + expect(roleMetadataStorageMock.removeRoleMetadata).not.toHaveBeenCalled(); + expect(roleMetadataStorageMock.updateRoleMetadata).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/service/enforcer-delegate.ts b/workspaces/rbac/plugins/rbac-backend/src/service/enforcer-delegate.ts new file mode 100644 index 0000000000..ce77fc89de --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/service/enforcer-delegate.ts @@ -0,0 +1,480 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Enforcer, newModelFromString } from 'casbin'; +import { Knex } from 'knex'; + +import EventEmitter from 'events'; + +import { ADMIN_ROLE_NAME } from '../admin-permissions/admin-creation'; +import { + RoleMetadataDao, + RoleMetadataStorage, +} from '../database/role-metadata'; +import { mergeRoleMetadata, policiesToString, policyToString } from '../helper'; +import { MODEL } from './permission-model'; + +export type RoleEvents = 'roleAdded'; +export interface RoleEventEmitter { + on(event: T, listener: (roleEntityRef: string | string[]) => void): this; +} + +type EventMap = { + [event in RoleEvents]: any[]; +}; + +export class EnforcerDelegate implements RoleEventEmitter { + private readonly roleEventEmitter = new EventEmitter(); + + constructor( + private readonly enforcer: Enforcer, + private readonly roleMetadataStorage: RoleMetadataStorage, + private readonly knex: Knex, + ) {} + + on(event: RoleEvents, listener: (role: string) => void): this { + this.roleEventEmitter.on(event, listener); + return this; + } + + async hasPolicy(...policy: string[]): Promise { + return await this.enforcer.hasPolicy(...policy); + } + + async hasGroupingPolicy(...policy: string[]): Promise { + return await this.enforcer.hasGroupingPolicy(...policy); + } + + async getPolicy(): Promise { + return await this.enforcer.getPolicy(); + } + + async getGroupingPolicy(): Promise { + return await this.enforcer.getGroupingPolicy(); + } + + async getRolesForUser(userEntityRef: string): Promise { + return await this.enforcer.getRolesForUser(userEntityRef); + } + + async getFilteredPolicy( + fieldIndex: number, + ...filter: string[] + ): Promise { + return await this.enforcer.getFilteredPolicy(fieldIndex, ...filter); + } + + async getFilteredGroupingPolicy( + fieldIndex: number, + ...filter: string[] + ): Promise { + return await this.enforcer.getFilteredGroupingPolicy(fieldIndex, ...filter); + } + + async addPolicy( + policy: string[], + externalTrx?: Knex.Transaction, + ): Promise { + const trx = externalTrx ?? (await this.knex.transaction()); + + if (await this.enforcer.hasPolicy(...policy)) { + return; + } + try { + const ok = await this.enforcer.addPolicy(...policy); + if (!ok) { + throw new Error(`failed to create policy ${policyToString(policy)}`); + } + if (!externalTrx) { + await trx.commit(); + } + } catch (err) { + if (!externalTrx) { + await trx.rollback(err); + } + throw err; + } + } + + async addPolicies( + policies: string[][], + externalTrx?: Knex.Transaction, + ): Promise { + if (policies.length === 0) { + return; + } + + const trx = externalTrx || (await this.knex.transaction()); + + try { + const ok = await this.enforcer.addPolicies(policies); + if (!ok) { + throw new Error( + `Failed to store policies ${policiesToString(policies)}`, + ); + } + if (!externalTrx) { + await trx.commit(); + } + } catch (err) { + if (!externalTrx) { + await trx.rollback(err); + } + throw err; + } + } + + async addGroupingPolicy( + policy: string[], + roleMetadata: RoleMetadataDao, + externalTrx?: Knex.Transaction, + ): Promise { + const trx = externalTrx ?? (await this.knex.transaction()); + const entityRef = roleMetadata.roleEntityRef; + + if (await this.enforcer.hasGroupingPolicy(...policy)) { + return; + } + try { + let currentMetadata; + if (entityRef.startsWith(`role:`)) { + currentMetadata = await this.roleMetadataStorage.findRoleMetadata( + entityRef, + trx, + ); + } + + if (currentMetadata) { + await this.roleMetadataStorage.updateRoleMetadata( + mergeRoleMetadata(currentMetadata, roleMetadata), + entityRef, + trx, + ); + } else { + const currentDate: Date = new Date(); + roleMetadata.createdAt = currentDate.toUTCString(); + roleMetadata.lastModified = currentDate.toUTCString(); + await this.roleMetadataStorage.createRoleMetadata(roleMetadata, trx); + } + + const ok = await this.enforcer.addGroupingPolicy(...policy); + if (!ok) { + throw new Error(`failed to create policy ${policyToString(policy)}`); + } + if (!externalTrx) { + await trx.commit(); + } + if (!currentMetadata) { + this.roleEventEmitter.emit('roleAdded', roleMetadata.roleEntityRef); + } + } catch (err) { + if (!externalTrx) { + await trx.rollback(err); + } + throw err; + } + } + + async addGroupingPolicies( + policies: string[][], + roleMetadata: RoleMetadataDao, + externalTrx?: Knex.Transaction, + ): Promise { + if (policies.length === 0) { + return; + } + + const trx = externalTrx ?? (await this.knex.transaction()); + + try { + const currentRoleMetadata = + await this.roleMetadataStorage.findRoleMetadata( + roleMetadata.roleEntityRef, + trx, + ); + if (currentRoleMetadata) { + await this.roleMetadataStorage.updateRoleMetadata( + mergeRoleMetadata(currentRoleMetadata, roleMetadata), + roleMetadata.roleEntityRef, + trx, + ); + } else { + const currentDate: Date = new Date(); + roleMetadata.createdAt = currentDate.toUTCString(); + roleMetadata.lastModified = currentDate.toUTCString(); + await this.roleMetadataStorage.createRoleMetadata(roleMetadata, trx); + } + + const ok = await this.enforcer.addGroupingPolicies(policies); + if (!ok) { + throw new Error( + `Failed to store policies ${policiesToString(policies)}`, + ); + } + + if (!externalTrx) { + await trx.commit(); + } + if (!currentRoleMetadata) { + this.roleEventEmitter.emit('roleAdded', roleMetadata.roleEntityRef); + } + } catch (err) { + if (!externalTrx) { + await trx.rollback(err); + } + throw err; + } + } + + async updateGroupingPolicies( + oldRole: string[][], + newRole: string[][], + newRoleMetadata: RoleMetadataDao, + ): Promise { + const oldRoleName = oldRole.at(0)?.at(1)!; + + const trx = await this.knex.transaction(); + try { + const currentMetadata = await this.roleMetadataStorage.findRoleMetadata( + oldRoleName, + trx, + ); + if (!currentMetadata) { + throw new Error(`Role metadata ${oldRoleName} was not found`); + } + + await this.removeGroupingPolicies(oldRole, currentMetadata, true, trx); + await this.addGroupingPolicies(newRole, newRoleMetadata, trx); + await trx.commit(); + } catch (err) { + await trx.rollback(err); + throw err; + } + } + + async updatePolicies( + oldPolicies: string[][], + newPolicies: string[][], + ): Promise { + const trx = await this.knex.transaction(); + + try { + await this.removePolicies(oldPolicies, trx); + await this.addPolicies(newPolicies, trx); + await trx.commit(); + } catch (err) { + await trx.rollback(err); + throw err; + } + } + + async removePolicy(policy: string[], externalTrx?: Knex.Transaction) { + const trx = externalTrx ?? (await this.knex.transaction()); + + try { + const ok = await this.enforcer.removePolicy(...policy); + if (!ok) { + throw new Error(`fail to delete policy ${policy}`); + } + if (!externalTrx) { + await trx.commit(); + } + } catch (err) { + if (!externalTrx) { + await trx.rollback(err); + } + throw err; + } + } + + async removePolicies( + policies: string[][], + externalTrx?: Knex.Transaction, + ): Promise { + const trx = externalTrx ?? (await this.knex.transaction()); + + try { + const ok = await this.enforcer.removePolicies(policies); + if (!ok) { + throw new Error( + `Failed to delete policies ${policiesToString(policies)}`, + ); + } + + if (!externalTrx) { + await trx.commit(); + } + } catch (err) { + if (!externalTrx) { + await trx.rollback(err); + } + throw err; + } + } + + async removeGroupingPolicy( + policy: string[], + roleMetadata: RoleMetadataDao, + isUpdate?: boolean, + externalTrx?: Knex.Transaction, + ): Promise { + const trx = externalTrx ?? (await this.knex.transaction()); + const roleEntity = policy[1]; + + try { + const ok = await this.enforcer.removeGroupingPolicy(...policy); + if (!ok) { + throw new Error(`Failed to delete policy ${policyToString(policy)}`); + } + + if (!isUpdate) { + const currentRoleMetadata = + await this.roleMetadataStorage.findRoleMetadata(roleEntity, trx); + const remainingGroupPolicies = + await this.enforcer.getFilteredGroupingPolicy(1, roleEntity); + if ( + currentRoleMetadata && + remainingGroupPolicies.length === 0 && + roleEntity !== ADMIN_ROLE_NAME + ) { + await this.roleMetadataStorage.removeRoleMetadata(roleEntity, trx); + } else if (currentRoleMetadata) { + await this.roleMetadataStorage.updateRoleMetadata( + mergeRoleMetadata(currentRoleMetadata, roleMetadata), + roleEntity, + trx, + ); + } + } + + if (!externalTrx) { + await trx.commit(); + } + } catch (err) { + if (!externalTrx) { + await trx.rollback(err); + } + throw err; + } + } + + async removeGroupingPolicies( + policies: string[][], + roleMetadata: RoleMetadataDao, + isUpdate?: boolean, + externalTrx?: Knex.Transaction, + ): Promise { + const trx = externalTrx ?? (await this.knex.transaction()); + + const roleEntity = roleMetadata.roleEntityRef; + try { + const ok = await this.enforcer.removeGroupingPolicies(policies); + if (!ok) { + throw new Error( + `Failed to delete grouping policies: ${policiesToString(policies)}`, + ); + } + + if (!isUpdate) { + const currentRoleMetadata = + await this.roleMetadataStorage.findRoleMetadata(roleEntity, trx); + const remainingGroupPolicies = + await this.enforcer.getFilteredGroupingPolicy(1, roleEntity); + if ( + currentRoleMetadata && + remainingGroupPolicies.length === 0 && + roleEntity !== ADMIN_ROLE_NAME + ) { + await this.roleMetadataStorage.removeRoleMetadata(roleEntity, trx); + } else if (currentRoleMetadata) { + await this.roleMetadataStorage.updateRoleMetadata( + mergeRoleMetadata(currentRoleMetadata, roleMetadata), + roleEntity, + trx, + ); + } + } + + if (!externalTrx) { + await trx.commit(); + } + } catch (err) { + if (!externalTrx) { + await trx.rollback(err); + } + throw err; + } + } + + /** + * enforce aims to enforce a particular permission policy based on the user that it receives. + * Under the hood, enforce uses the `enforce` method from the enforcer`. + * + * Before enforcement, a filter is set up to reduce the number of permission policies that will + * be loaded in. + * This will reduce the amount of checks that need to be made to determine if a user is authorize + * to perform an action + * + * A temporary enforcer will also be used while enforcing. + * This is to ensure that the filter does not interact with the base enforcer. + * The temporary enforcer has lazy loading of the permission policies enabled to reduce the amount + * of time it takes to initialize the temporary enforcer. + * The justification for lazy loading is because permission policies are already present in the + * role manager / database and it will be filtered and loaded whenever `loadFilteredPolicy` is called. + * @param entityRef The user to enforce + * @param resourceType The resource type / name of the permission policy + * @param action The action of the permission policy + * @param roles Any roles that the user is directly or indirectly attached to. + * Used for filtering permission policies. + * @returns True if the user is allowed based on the particular permission + */ + async enforce( + entityRef: string, + resourceType: string, + action: string, + roles: string[], + ): Promise { + const filter = []; + if (roles.length > 0) { + roles.forEach(role => { + filter.push({ ptype: 'p', v0: role, v1: resourceType, v2: action }); + }); + } else { + filter.push({ ptype: 'p', v1: resourceType, v2: action }); + } + + const adapt = this.enforcer.getAdapter(); + const roleManager = this.enforcer.getRoleManager(); + const tempEnforcer = new Enforcer(); + await tempEnforcer.initWithModelAndAdapter( + newModelFromString(MODEL), + adapt, + true, + ); + tempEnforcer.setRoleManager(roleManager); + + await tempEnforcer.loadFilteredPolicy(filter); + + return await tempEnforcer.enforce(entityRef, resourceType, action); + } + + async getImplicitPermissionsForUser(user: string): Promise { + return this.enforcer.getImplicitPermissionsForUser(user); + } + + async getAllRoles(): Promise { + return this.enforcer.getAllRoles(); + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/service/permission-model.ts b/workspaces/rbac/plugins/rbac-backend/src/service/permission-model.ts new file mode 100644 index 0000000000..e322a3f63d --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/service/permission-model.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const MODEL = ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act, eft + +[policy_effect] +e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) + +[role_definition] +g = _, _ + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act +`; diff --git a/workspaces/rbac/plugins/rbac-backend/src/service/plugin-endpoint.test.ts b/workspaces/rbac/plugins/rbac-backend/src/service/plugin-endpoint.test.ts new file mode 100644 index 0000000000..da0959c98d --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/service/plugin-endpoint.test.ts @@ -0,0 +1,434 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { UrlReaderServiceReadUrlResponse } from '@backstage/backend-plugin-api'; +import { mockServices } from '@backstage/backend-test-utils'; +import { NotFoundError } from '@backstage/errors'; + +import { PluginPermissionMetadataCollector } from './plugin-endpoints'; + +const backendPluginIDsProviderMock = { + getPluginIds: jest.fn().mockImplementation(() => { + return []; + }), +}; + +describe('plugin-endpoint', () => { + const mockPluginEndpointDiscovery = mockServices.discovery.mock({ + getBaseUrl: async (pluginId: string) => { + return `https://localhost:7007/api/${pluginId}`; + }, + }); + + describe('Test list plugin policies', () => { + it('should return empty plugin policies list', async () => { + const collector = new PluginPermissionMetadataCollector({ + deps: { + discovery: mockPluginEndpointDiscovery, + pluginIdProvider: backendPluginIDsProviderMock, + logger: mockServices.logger.mock(), + config: mockServices.rootConfig(), + }, + }); + const policiesMetadata = await collector.getPluginPolicies( + mockServices.auth(), + ); + + expect(policiesMetadata.length).toEqual(0); + }); + + it('should return non empty plugin policies list with resourced permission', async () => { + backendPluginIDsProviderMock.getPluginIds.mockReturnValue(['permission']); + + const mockUrlReaderService = mockServices.urlReader.mock({ + readUrl: async () => { + return { + buffer: async () => { + return Buffer.from( + '{"permissions":[{"type":"resource","name":"policy.entity.read","attributes":{"action":"read"},"resourceType":"policy-entity"}]}', + ); + }, + } as UrlReaderServiceReadUrlResponse; + }, + }); + + const collector = new PluginPermissionMetadataCollector({ + deps: { + discovery: mockPluginEndpointDiscovery, + pluginIdProvider: backendPluginIDsProviderMock, + logger: mockServices.logger.mock(), + config: mockServices.rootConfig(), + }, + optional: { urlReader: mockUrlReaderService }, + }); + const policiesMetadata = await collector.getPluginPolicies( + mockServices.auth(), + ); + + expect(policiesMetadata.length).toEqual(1); + expect(policiesMetadata[0].pluginId).toEqual('permission'); + expect(policiesMetadata[0].policies).toEqual([ + { + name: 'policy.entity.read', + resourceType: 'policy-entity', + policy: 'read', + }, + ]); + }); + + it('should return non empty plugin policies list with non resourced permission', async () => { + backendPluginIDsProviderMock.getPluginIds.mockReturnValue(['permission']); + + const mockUrlReaderService = mockServices.urlReader.mock({ + readUrl: async () => { + return { + buffer: async () => { + return Buffer.from( + '{"permissions":[{"type":"basic","name":"catalog.entity.create","attributes":{"action":"create"}}]}', + ); + }, + } as UrlReaderServiceReadUrlResponse; + }, + }); + + const collector = new PluginPermissionMetadataCollector({ + deps: { + discovery: mockPluginEndpointDiscovery, + pluginIdProvider: backendPluginIDsProviderMock, + logger: mockServices.logger.mock(), + config: mockServices.rootConfig(), + }, + optional: { urlReader: mockUrlReaderService }, + }); + const policiesMetadata = await collector.getPluginPolicies( + mockServices.auth(), + ); + + expect(policiesMetadata.length).toEqual(1); + expect(policiesMetadata[0].pluginId).toEqual('permission'); + expect(policiesMetadata[0].policies).toEqual([ + { + name: 'catalog.entity.create', + policy: 'create', + }, + ]); + }); + + it('should log warning for not found endpoint', async () => { + backendPluginIDsProviderMock.getPluginIds.mockReturnValue([ + 'permission', + 'unknown-plugin-id', + ]); + + const mockUrlReaderService = mockServices.urlReader.mock({ + readUrl: async (wellKnownURL: string) => { + if ( + wellKnownURL === + 'https://localhost:7007/api/permission/.well-known/backstage/permissions/metadata' + ) { + return { + buffer: async () => { + return Buffer.from( + '{"permissions":[{"type":"resource","resourceType":"policy-entity","name":"policy.entity.read","attributes":{"action":"read"}}]}', + ); + }, + } as UrlReaderServiceReadUrlResponse; + } + throw new NotFoundError(); + }, + }); + + const logger = mockServices.logger.mock(); + const errorSpy = jest.spyOn(logger, 'warn').mockClear(); + const collector = new PluginPermissionMetadataCollector({ + deps: { + discovery: mockPluginEndpointDiscovery, + pluginIdProvider: backendPluginIDsProviderMock, + logger, + config: mockServices.rootConfig(), + }, + optional: { urlReader: mockUrlReaderService }, + }); + const policiesMetadata = await collector.getPluginPolicies( + mockServices.auth(), + ); + + expect(policiesMetadata.length).toEqual(1); + expect(policiesMetadata[0].pluginId).toEqual('permission'); + expect(policiesMetadata[0].policies).toEqual([ + { + name: 'policy.entity.read', + resourceType: 'policy-entity', + policy: 'read', + }, + ]); + + expect(errorSpy).toHaveBeenCalledWith( + 'No permission metadata found for unknown-plugin-id. NotFoundError', + ); + }); + + it('should log error when it is not possible to retrieve permission metadata for known endpoint', async () => { + backendPluginIDsProviderMock.getPluginIds.mockReturnValue([ + 'permission', + 'catalog', + ]); + + const mockUrlReaderService = mockServices.urlReader.mock({ + readUrl: async (wellKnownURL: string) => { + if ( + wellKnownURL === + 'https://localhost:7007/api/permission/.well-known/backstage/permissions/metadata' + ) { + return { + buffer: async () => { + return Buffer.from( + '{"permissions":[{"type":"resource","resourceType":"policy-entity","name":"policy.entity.read","attributes":{"action":"read"}}]}', + ); + }, + } as UrlReaderServiceReadUrlResponse; + } + throw new Error('Unexpected error'); + }, + }); + + const logger = mockServices.logger.mock(); + const errorSpy = jest.spyOn(logger, 'error').mockClear(); + const collector = new PluginPermissionMetadataCollector({ + deps: { + discovery: mockPluginEndpointDiscovery, + pluginIdProvider: backendPluginIDsProviderMock, + logger, + config: mockServices.rootConfig(), + }, + optional: { urlReader: mockUrlReaderService }, + }); + + const policiesMetadata = await collector.getPluginPolicies( + mockServices.auth(), + ); + + expect(policiesMetadata.length).toEqual(1); + expect(policiesMetadata[0].pluginId).toEqual('permission'); + expect(policiesMetadata[0].policies).toEqual([ + { + name: 'policy.entity.read', + resourceType: 'policy-entity', + policy: 'read', + }, + ]); + + expect(errorSpy).toHaveBeenCalledWith( + 'Failed to retrieve permission metadata for catalog. Error: Unexpected error', + ); + }); + + it('should not log error caused by non json permission metadata for known endpoint', async () => { + backendPluginIDsProviderMock.getPluginIds.mockReturnValue([ + 'permission', + 'catalog', + ]); + + const mockUrlReaderService = mockServices.urlReader.mock({ + readUrl: async (wellKnownURL: string) => { + if ( + wellKnownURL === + 'https://localhost:7007/api/permission/.well-known/backstage/permissions/metadata' + ) { + return { + buffer: async () => { + return Buffer.from( + '{"permissions":[{"type":"resource","resourceType":"policy-entity","name":"policy.entity.read","attributes":{"action":"read"}}]}', + ); + }, + } as UrlReaderServiceReadUrlResponse; + } else if ( + wellKnownURL === + 'https://localhost:7007/api/catalog/.well-known/backstage/permissions/metadata' + ) { + return { + buffer: async () => { + return Buffer.from('non json data'); + }, + } as UrlReaderServiceReadUrlResponse; + } + throw new Error('Unexpected error'); + }, + }); + + const errorSpy = jest + .spyOn(mockServices.logger.mock(), 'error') + .mockClear(); + + const collector = new PluginPermissionMetadataCollector({ + deps: { + discovery: mockPluginEndpointDiscovery, + pluginIdProvider: backendPluginIDsProviderMock, + logger: mockServices.logger.mock(), + config: mockServices.rootConfig(), + }, + optional: { urlReader: mockUrlReaderService }, + }); + const policiesMetadata = await collector.getPluginPolicies( + mockServices.auth(), + ); + + expect(policiesMetadata.length).toEqual(1); + expect(policiesMetadata[0].pluginId).toEqual('permission'); + expect(policiesMetadata[0].policies).toEqual([ + { + name: 'policy.entity.read', + resourceType: 'policy-entity', + policy: 'read', + }, + ]); + + // workaround for https://issues.redhat.com/browse/RHIDP-1456 + expect(errorSpy).not.toHaveBeenCalled(); + }); + }); + + describe('Test list plugin condition rules', () => { + it('should return empty condition rule list', async () => { + backendPluginIDsProviderMock.getPluginIds.mockReturnValue([]); + + const collector = new PluginPermissionMetadataCollector({ + deps: { + discovery: mockPluginEndpointDiscovery, + pluginIdProvider: backendPluginIDsProviderMock, + logger: mockServices.logger.mock(), + config: mockServices.rootConfig(), + }, + }); + const conditionRulesMetadata = await collector.getPluginConditionRules( + mockServices.auth(), + ); + + expect(conditionRulesMetadata.length).toEqual(0); + }); + + it('should return non empty condition rule list', async () => { + backendPluginIDsProviderMock.getPluginIds.mockReturnValue(['catalog']); + + const mockUrlReaderService = mockServices.urlReader.mock({ + readUrl: async () => { + return { + buffer: async () => { + return Buffer.from( + '{"rules": [{"description":"Allow entities with the specified label","name":"HAS_LABEL","paramsSchema":{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"label":{"description":"Name of the label to match on","type":"string"}},"required":["label"],"type":"object"},"resourceType":"catalog-entity"}]}', + ); + }, + } as UrlReaderServiceReadUrlResponse; + }, + }); + + const collector = new PluginPermissionMetadataCollector({ + deps: { + discovery: mockPluginEndpointDiscovery, + pluginIdProvider: backendPluginIDsProviderMock, + logger: mockServices.logger.mock(), + config: mockServices.rootConfig(), + }, + optional: { urlReader: mockUrlReaderService }, + }); + const conditionRulesMetadata = await collector.getPluginConditionRules( + mockServices.auth(), + ); + + expect(conditionRulesMetadata.length).toEqual(1); + expect(conditionRulesMetadata[0].pluginId).toEqual('catalog'); + expect(conditionRulesMetadata[0].rules).toEqual([ + { + description: 'Allow entities with the specified label', + name: 'HAS_LABEL', + paramsSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: { + label: { + description: 'Name of the label to match on', + type: 'string', + }, + }, + required: ['label'], + type: 'object', + }, + resourceType: 'catalog-entity', + }, + ]); + }); + }); + + describe('Test get plugin metadata by id', () => { + it('should return metadata by id', async () => { + backendPluginIDsProviderMock.getPluginIds.mockReturnValue(['catalog']); + + const mockUrlReaderService = mockServices.urlReader.mock({ + readUrl: async () => { + return { + buffer: async () => { + return Buffer.from( + '{"permissions":[{"type":"resource","name":"catalog.entity.read","attributes":{"action":"read"},"resourceType":"catalog-entity"}], "rules": [{"description":"Allow entities with the specified label","name":"HAS_LABEL","paramsSchema":{"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false,"properties":{"label":{"description":"Name of the label to match on","type":"string"}},"required":["label"],"type":"object"},"resourceType":"catalog-entity"}]}', + ); + }, + } as UrlReaderServiceReadUrlResponse; + }, + }); + + const collector = new PluginPermissionMetadataCollector({ + deps: { + discovery: mockPluginEndpointDiscovery, + pluginIdProvider: backendPluginIDsProviderMock, + logger: mockServices.logger.mock(), + config: mockServices.rootConfig(), + }, + optional: { urlReader: mockUrlReaderService }, + }); + const metadata = await collector.getMetadataByPluginId( + 'catalog', + undefined, + ); + + expect(metadata).not.toBeUndefined(); + expect(metadata?.permissions).toEqual([ + { + name: 'catalog.entity.read', + attributes: { action: 'read' }, + type: 'resource', + resourceType: 'catalog-entity', + }, + ]); + expect(metadata?.rules).toEqual([ + { + description: 'Allow entities with the specified label', + name: 'HAS_LABEL', + paramsSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: { + label: { + description: 'Name of the label to match on', + type: 'string', + }, + }, + required: ['label'], + type: 'object', + }, + resourceType: 'catalog-entity', + }, + ]); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/service/plugin-endpoints.ts b/workspaces/rbac/plugins/rbac-backend/src/service/plugin-endpoints.ts new file mode 100644 index 0000000000..84918dd967 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/service/plugin-endpoints.ts @@ -0,0 +1,208 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + FetchUrlReader, + ReaderFactory, + UrlReaders, +} from '@backstage/backend-defaults/urlReader'; +import type { + AuthService, + DiscoveryService, + LoggerService, + UrlReaderService, +} from '@backstage/backend-plugin-api'; +import type { Config } from '@backstage/config'; +import { isError } from '@backstage/errors'; +import { + isResourcePermission, + Permission, +} from '@backstage/plugin-permission-common'; +import type { + MetadataResponse, + MetadataResponseSerializedRule, +} from '@backstage/plugin-permission-node'; + +import type { + PluginPermissionMetaData, + PolicyDetails, +} from '@backstage-community/plugin-rbac-common'; +import type { PluginIdProvider } from '@backstage-community/plugin-rbac-node'; + +type PluginMetadataResponse = { + pluginId: string; + metaDataResponse: MetadataResponse; +}; + +export type PluginMetadataResponseSerializedRule = { + pluginId: string; + rules: MetadataResponseSerializedRule[]; +}; + +export class PluginPermissionMetadataCollector { + private readonly pluginIds: string[]; + private readonly discovery: DiscoveryService; + private readonly logger: LoggerService; + private readonly urlReader: UrlReaderService; + + constructor({ + deps, + optional, + }: { + deps: { + discovery: DiscoveryService; + pluginIdProvider: PluginIdProvider; + logger: LoggerService; + config: Config; + }; + optional?: { + urlReader?: UrlReaderService; + }; + }) { + const { discovery, pluginIdProvider, logger, config } = deps; + this.pluginIds = pluginIdProvider.getPluginIds(); + this.discovery = discovery; + this.logger = logger; + this.urlReader = + optional?.urlReader ?? + UrlReaders.default({ + config, + logger, + factories: [PluginPermissionMetadataCollector.permissionFactory], + }); + } + + async getPluginConditionRules( + auth: AuthService, + ): Promise { + const pluginMetadata = await this.getPluginMetaData(auth); + + return pluginMetadata + .filter(metadata => metadata.metaDataResponse.rules.length > 0) + .map(metadata => { + return { + pluginId: metadata.pluginId, + rules: metadata.metaDataResponse.rules, + }; + }); + } + + async getPluginPolicies( + auth: AuthService, + ): Promise { + const pluginMetadata = await this.getPluginMetaData(auth); + + return pluginMetadata + .filter(metadata => metadata.metaDataResponse.permissions !== undefined) + .map(metadata => { + return { + pluginId: metadata.pluginId, + policies: permissionsToCasbinPolicies( + metadata.metaDataResponse.permissions!, + ), + }; + }); + } + + private static permissionFactory: ReaderFactory = () => { + return [{ reader: new FetchUrlReader(), predicate: (_url: URL) => true }]; + }; + + private async getPluginMetaData( + auth: AuthService, + ): Promise { + let pluginResponses: PluginMetadataResponse[] = []; + + for (const pluginId of this.pluginIds) { + try { + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: pluginId, + }); + + const permMetaData = await this.getMetadataByPluginId(pluginId, token); + if (permMetaData) { + pluginResponses = [ + ...pluginResponses, + { + metaDataResponse: permMetaData, + pluginId, + }, + ]; + } + } catch (error) { + this.logger.error( + `Failed to retrieve permission metadata for ${pluginId}. ${error}`, + ); + } + } + + return pluginResponses; + } + + async getMetadataByPluginId( + pluginId: string, + token: string | undefined, + ): Promise { + let permMetaData: MetadataResponse | undefined; + try { + const baseEndpoint = await this.discovery.getBaseUrl(pluginId); + const wellKnownURL = `${baseEndpoint}/.well-known/backstage/permissions/metadata`; + + const permResp = await this.urlReader.readUrl(wellKnownURL, { token }); + const permMetaDataRaw = (await permResp.buffer()).toString(); + + try { + permMetaData = JSON.parse(permMetaDataRaw); + } catch (err) { + // workaround for https://issues.redhat.com/browse/RHIDP-1456 + return undefined; + } + } catch (err) { + if (isError(err) && err.name === 'NotFoundError') { + this.logger.warn( + `No permission metadata found for ${pluginId}. ${err}`, + ); + return undefined; + } + this.logger.error( + `Failed to retrieve permission metadata for ${pluginId}. ${err}`, + ); + } + return permMetaData; + } +} + +function permissionsToCasbinPolicies( + permissions: Permission[], +): PolicyDetails[] { + const policies: PolicyDetails[] = []; + for (const permission of permissions) { + if (isResourcePermission(permission)) { + policies.push({ + resourceType: permission.resourceType, + name: permission.name, + policy: permission.attributes.action || 'use', + }); + } else { + policies.push({ + name: permission.name, + policy: permission.attributes.action || 'use', + }); + } + } + + return policies; +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/service/policies-rest-api.test.ts b/workspaces/rbac/plugins/rbac-backend/src/service/policies-rest-api.test.ts new file mode 100644 index 0000000000..79353b6f70 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/service/policies-rest-api.test.ts @@ -0,0 +1,4046 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter'; +import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; +import { InputError } from '@backstage/errors'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import type { MetadataResponse } from '@backstage/plugin-permission-node'; + +import express from 'express'; +import * as Knex from 'knex'; +import { MockClient } from 'knex-mock-client'; +import request from 'supertest'; + +import { + PermissionAction, + PermissionInfo, + PluginPermissionMetaData, + policyEntityCreatePermission, + policyEntityDeletePermission, + policyEntityReadPermission, + policyEntityUpdatePermission, + Role, + RoleConditionalPolicyDecision, + Source, +} from '@backstage-community/plugin-rbac-common'; +import type { RBACProvider } from '@backstage-community/plugin-rbac-node'; + +import { + RoleMetadataDao, + RoleMetadataStorage, +} from '../database/role-metadata'; +import { RBACPermissionPolicy } from '../policies/permission-policy'; +import { EnforcerDelegate } from './enforcer-delegate'; +import { + PluginMetadataResponseSerializedRule, + PluginPermissionMetadataCollector, +} from './plugin-endpoints'; +import { PoliciesServer } from './policies-rest-api'; +import { RBACRouterOptions } from './policy-builder'; + +jest.setTimeout(60000); + +const pluginPermissionMetadataCollectorMock: Partial = + { + getPluginPolicies: jest.fn().mockImplementation(), + getPluginConditionRules: jest.fn().mockImplementation(), + getMetadataByPluginId: jest.fn().mockImplementation(), + }; + +jest.mock('@backstage/plugin-auth-node', () => ({ + getBearerTokenFromAuthorizationHeader: () => 'token', +})); + +const enforcerDelegateMock: Partial = { + hasPolicy: jest.fn().mockImplementation(), + + hasGroupingPolicy: jest.fn().mockImplementation(), + + getPolicy: jest.fn().mockImplementation((): Promise => { + return Promise.resolve([[]]); + }), + + getGroupingPolicy: jest.fn().mockImplementation((): Promise => { + return Promise.resolve([[]]); + }), + + getFilteredPolicy: jest.fn().mockImplementation(), + getFilteredGroupingPolicy: jest.fn().mockImplementation(), + + addPolicy: jest.fn().mockImplementation(), + + addPolicies: jest.fn().mockImplementation(), + + addGroupingPolicies: jest.fn().mockImplementation(), + + removePolicy: jest.fn().mockImplementation(), + + removePolicies: jest.fn().mockImplementation(), + + removeGroupingPolicy: jest.fn().mockImplementation(), + + removeGroupingPolicies: jest.fn().mockImplementation(), + + updatePolicies: jest.fn().mockImplementation(), + + updateGroupingPolicies: jest.fn().mockImplementation(), +}; + +const roleMetadataStorageMock: RoleMetadataStorage = { + filterRoleMetadata: jest.fn().mockImplementation(() => []), + findRoleMetadata: jest.fn().mockImplementation(), + createRoleMetadata: jest.fn().mockImplementation(), + updateRoleMetadata: jest.fn().mockImplementation(), + removeRoleMetadata: jest.fn().mockImplementation(), +}; + +const conditionalStorageMock = { + filterConditions: jest.fn().mockImplementation(), + createCondition: jest.fn().mockImplementation(), + checkConflictedConditions: jest.fn().mockImplementation(), + getCondition: jest.fn().mockImplementation(), + deleteCondition: jest.fn().mockImplementation(), + updateCondition: jest.fn().mockImplementation(), +}; + +const validateRoleConditionMock = jest.fn().mockImplementation(); +jest.mock('../validation/condition-validation', () => { + return { + validateRoleCondition: jest + .fn() + .mockImplementation( + (condition: RoleConditionalPolicyDecision) => { + validateRoleConditionMock(condition); + }, + ), + }; +}); + +const auditLoggerMock = { + getActorId: jest.fn().mockImplementation(), + createAuditLogDetails: jest.fn().mockImplementation(), + auditLog: jest.fn().mockImplementation(() => Promise.resolve()), +}; + +const mockHttpAuth = mockServices.httpAuth({ + pluginId: 'permission', + defaultCredentials: mockCredentials.user('user:default/guest'), +}); +const mockAuthService = mockServices.auth(); +const credentials = mockCredentials.user('user:default/guest'); +const mockUserInfo = mockServices.userInfo(); + +const conditions: RoleConditionalPolicyDecision[] = [ + { + id: 1, + pluginId: 'catalog', + roleEntityRef: 'role:default/test', + resourceType: 'catalog-entity', + permissionMapping: [{ name: 'catalog.entity.read', action: 'read' }], + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { claims: ['group:default/team-a'] }, + }, + }, +]; + +const expectedConditions: RoleConditionalPolicyDecision[] = [ + { + id: 1, + pluginId: 'catalog', + roleEntityRef: 'role:default/test', + resourceType: 'catalog-entity', + permissionMapping: ['read'], + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { claims: ['group:default/team-a'] }, + }, + }, +]; + +const providerMock: RBACProvider = { + getProviderName: jest.fn().mockImplementation(() => `testProvider`), + connect: jest.fn().mockImplementation(), + refresh: jest.fn().mockImplementation(), +}; + +const modifiedBy = 'user:default/some-admin'; + +describe('REST policies api', () => { + let app: express.Express; + + const mockedAuthorize = jest.fn().mockImplementation(async () => [ + { + result: AuthorizeResult.ALLOW, + }, + ]); + + const mockedAuthorizeConditional = jest.fn().mockImplementation(async () => [ + { + result: AuthorizeResult.ALLOW, + }, + ]); + + const mockPermissionEvaluator = { + authorize: mockedAuthorize, + authorizeConditional: mockedAuthorizeConditional, + }; + + const logger = mockServices.logger.mock(); + const mockDiscovery = mockServices.discovery.mock(); + + const knex = Knex.knex({ client: MockClient }); + + let config = mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + permission: { + enabled: true, + }, + }, + }); + + let server: PoliciesServer; + + beforeEach(async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation(async () => { + return conditions; + }); + + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return false; + }); + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return false; + }); + enforcerDelegateMock.getFilteredPolicy = jest + .fn() + .mockImplementation( + async (_fieldIndex: number, ..._fieldValues: string[]) => { + return [ + [ + 'user:default/permission_admin', + 'policy-entity', + 'create', + 'allow', + ], + ]; + }, + ); + enforcerDelegateMock.getFilteredGroupingPolicy = jest + .fn() + .mockImplementation( + async (_fieldIndex: number, ..._fieldValues: string[]) => { + return [['user:default/permission_admin', 'role:default/rbac_admin']]; + }, + ); + enforcerDelegateMock.removeGroupingPolicies = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + enforcerDelegateMock.addGroupingPolicies = jest.fn().mockImplementation(); + + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async (roleEntityRef: string): Promise => { + return { + source: 'rest', + roleEntityRef: roleEntityRef, + modifiedBy: 'user:default/some-user', + }; + }, + ); + + mockHttpAuth.credentials = jest.fn().mockImplementation(() => credentials); + + const options: RBACRouterOptions = { + config: config, + logger, + discovery: mockDiscovery, + httpAuth: mockHttpAuth, + auth: mockAuthService, + policy: await RBACPermissionPolicy.build( + logger, + auditLoggerMock, + config, + conditionalStorageMock, + enforcerDelegateMock as EnforcerDelegate, + roleMetadataStorageMock, + knex, + pluginPermissionMetadataCollectorMock as PluginPermissionMetadataCollector, + mockAuthService, + ), + userInfo: mockUserInfo, + }; + + server = new PoliciesServer( + mockPermissionEvaluator, + options, + enforcerDelegateMock as EnforcerDelegate, + conditionalStorageMock, + pluginPermissionMetadataCollectorMock as PluginPermissionMetadataCollector, + roleMetadataStorageMock, + auditLoggerMock, + ); + const router = await server.serve(); + app = express().use(router); + app.use(MiddlewareFactory.create({ logger, config }).error()); + conditionalStorageMock.getCondition.mockReset(); + validateRoleConditionMock.mockReset(); + auditLoggerMock.auditLog.mockClear(); + jest.clearAllMocks(); + }); + + it('should build', () => { + expect(app).toBeTruthy(); + }); + + describe('GET /', () => { + it('should return a status of Authorized', async () => { + const result = await request(app).get('/').send(); + + expect(result.status).toBe(200); + expect(result.body).toEqual({ status: 'Authorized' }); + }); + + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app).get('/').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityReadPermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + }); + + describe('POST /policies', () => { + afterEach(() => { + (enforcerDelegateMock.addPolicies as jest.Mock).mockReset(); + }); + + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app).post('/policies').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityCreatePermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should return a status of Unauthorized - non user request', async () => { + mockHttpAuth.credentials = jest + .fn() + .mockImplementationOnce(() => mockCredentials.service()); + const result = await request(app).post('/policies').send(); + + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: `Only creadential principal with type 'user' permitted to modify permissions`, + }); + }); + + it('should not be created permission policy - req body is an empty', async () => { + const result = await request(app).post('/policies').send(); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `permission policy must be present`, + }); + }); + + it('should not be created permission policy - entityReference is empty', async () => { + const result = await request(app).post('/policies').send([{}]); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid policy definition. Cause: 'entityReference' must not be empty`, + }); + }); + + it('should not be created permission policy - entityReference is invalid', async () => { + const result = await request(app) + .post('/policies') + .send([{ entityReference: 'user' }]); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid policy definition. Cause: Entity reference "user" had missing or empty kind (e.g. did not start with "component:" or similar)`, + }); + }); + + it('should not be created permission policy - permission is an empty', async () => { + const result = await request(app) + .post('/policies') + .send([ + { + entityReference: 'user:default/permission_admin', + }, + ]); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid policy definition. Cause: 'permission' field must not be empty`, + }); + }); + + it('should not be created permission policy - policy is an empty', async () => { + const result = await request(app) + .post('/policies') + .send([ + { + entityReference: 'user:default/permission_admin', + permission: 'policy-entity', + }, + ]); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid policy definition. Cause: 'policy' field must not be empty`, + }); + }); + + it('should not be created permission policy - effect is an empty', async () => { + const result = await request(app) + .post('/policies') + .send([ + { + entityReference: 'user:default/permission_admin', + permission: 'policy-entity', + policy: 'read', + }, + ]); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid policy definition. Cause: 'effect' field must not be empty`, + }); + }); + + it('should be created permission policy', async () => { + const result = await request(app) + .post('/policies') + .send([ + { + entityReference: 'user:default/permission_admin', + permission: 'policy-entity', + policy: 'delete', + effect: 'deny', + }, + ]); + + expect(result.statusCode).toBe(201); + }); + + it('should fail to create permission policy, because of source mismatch', async () => { + const roleMeta: RoleMetadataDao = { + roleEntityRef: 'user:default/permission_admin', + source: 'csv-file', + modifiedBy, + }; + + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async (roleEntityRef: string): Promise => { + if (roleEntityRef === roleMeta.roleEntityRef) { + return roleMeta; + } + return { source: 'rest', roleEntityRef: roleEntityRef, modifiedBy }; + }, + ); + + const result = await request(app) + .post('/policies') + .send([ + { + entityReference: 'user:default/permission_admin', + permission: 'policy-entity', + policy: 'delete', + effect: 'deny', + }, + ]); + + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: `Unable to add policy user:default/permission_admin,policy-entity,delete,deny: source does not match originating role ${ + roleMeta.roleEntityRef + }, consider making changes to the '${roleMeta.source.toLocaleUpperCase()}'`, + }); + }); + + it('should fail to add permission policy, with original source of configuration', async () => { + const roleMeta: RoleMetadataDao = { + roleEntityRef: 'user:default/permission_admin', + source: 'configuration', + modifiedBy, + }; + + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async (roleEntityRef: string): Promise => { + if (roleEntityRef === roleMeta.roleEntityRef) { + return roleMeta; + } + return { source: 'rest', roleEntityRef: roleEntityRef, modifiedBy }; + }, + ); + + const result = await request(app) + .post('/policies') + .send([ + { + entityReference: 'user:default/permission_admin', + permission: 'policy-entity', + policy: 'delete', + effect: 'deny', + }, + ]); + + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: `Unable to add policy user:default/permission_admin,policy-entity,delete,deny: source does not match originating role ${ + roleMeta.roleEntityRef + }, consider making changes to the '${roleMeta.source.toLocaleUpperCase()}'`, + }); + }); + + it('should not be created permission policy, because it is has been already present', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param.at(2) === 'read') { + return Promise.resolve(true); + } + return Promise.resolve(false); + }); + + const result = await request(app) + .post('/policies') + .send([ + { + entityReference: 'user:default/permission_admin', + permission: 'policy-entity', + policy: 'read', + effect: 'deny', + }, + { + entityReference: 'user:default/permission_admin', + permission: 'policy-entity', + policy: 'delete', + effect: 'deny', + }, + ]); + + expect(result.statusCode).toBe(409); + }); + + it('should not be created permission policy caused some unexpected error', async () => { + enforcerDelegateMock.addPolicies = jest + .fn() + .mockImplementation(async (): Promise => { + throw new Error(`Failed to add policies`); + }); + + const result = await request(app) + .post('/policies') + .send([ + { + entityReference: 'user:default/permission_admin', + permission: 'policy-entity', + policy: 'delete', + effect: 'deny', + }, + ]); + + expect(result.statusCode).toBe(500); + }); + + it('should fail to create permission policy - duplication in req body', async () => { + const result = await request(app) + .post('/policies') + .send([ + { + entityReference: 'user:default/permission_admin', + permission: 'policy-entity', + policy: 'delete', + effect: 'deny', + }, + { + entityReference: 'user:default/permission_admin', + permission: 'policy-entity', + policy: 'delete', + effect: 'deny', + }, + ]); + + expect(result.statusCode).toBe(409); + expect(result.body.error).toEqual({ + name: 'ConflictError', + message: `Duplicate polices found; user:default/permission_admin, policy-entity, delete, deny is a duplicate`, + }); + }); + }); + + describe('GET /policies/:kind/:namespace/:name', () => { + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app) + .get('/policies/user/default/permission_admin') + .send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityReadPermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should be returned permission policies by user reference', async () => { + enforcerDelegateMock.getFilteredPolicy = jest + .fn() + .mockImplementation( + async (_fieldIndex: number, ..._fieldValues: string[]) => { + return [ + [ + 'user:default/permission_admin', + 'policy-entity', + 'create', + 'allow', + ], + ]; + }, + ); + const result = await request(app) + .get('/policies/user/default/permission_admin') + .send(); + expect(result.statusCode).toBe(200); + expect(result.body).toEqual([ + { + entityReference: 'user:default/permission_admin', + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + metadata: { + source: 'rest', + }, + }, + ]); + }); + it('should be returned policies by user reference not found', async () => { + enforcerDelegateMock.getFilteredPolicy = jest + .fn() + .mockImplementation( + async (_fieldIndex: number, ..._fieldValues: string[]) => { + return []; + }, + ); + + const result = await request(app) + .get('/policies/user/default/permission_admin') + .send(); + expect(result.statusCode).toBe(404); + expect(result.body).toEqual({ + error: { message: '', name: 'NotFoundError' }, + request: { + method: 'GET', + url: '/policies/user/default/permission_admin', + }, + response: { statusCode: 404 }, + }); + }); + }); + + describe('GET /policies', () => { + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app).get('/policies').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityReadPermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should be returned list all policies', async () => { + enforcerDelegateMock.getPolicy = jest + .fn() + .mockImplementation(async () => { + return [ + [ + 'user:default/permission_admin', + 'policy-entity', + 'create', + 'allow', + 'rest', + ], + ['user:default/guest', 'policy-entity', 'read', 'allow', 'rest'], + ]; + }); + const result = await request(app).get('/policies').send(); + expect(result.statusCode).toBe(200); + expect(result.body).toEqual([ + { + entityReference: 'user:default/permission_admin', + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + metadata: { + source: 'rest', + }, + }, + { + entityReference: 'user:default/guest', + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + metadata: { + source: 'rest', + }, + }, + ]); + }); + it('should be returned list filtered policies', async () => { + enforcerDelegateMock.getFilteredPolicy = jest + .fn() + .mockImplementation( + async (_fieldIndex: number, ..._fieldValues: string[]) => { + return [ + ['user:default/guest', 'policy-entity', 'read', 'allow', 'rest'], + ]; + }, + ); + const result = await request(app) + .get( + '/policies?entityRef=user:default/guest&permission=policy-entity&policy=read&effect=allow', + ) + .send(); + expect(result.statusCode).toBe(200); + expect(result.body).toEqual([ + { + entityReference: 'user:default/guest', + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + metadata: { + source: 'rest', + }, + }, + ]); + }); + }); + + describe('DELETE /policies/:kind/:namespace/:name', () => { + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app) + .delete('/policies/user/default/permission_admin') + .send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityDeletePermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should fail to delete, request is empty', async () => { + const result = await request(app) + .delete('/policies/user/default/permission_admin') + .send(); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `permission policy must be present`, + }); + }); + + it('should fail to delete, because permission field is absent', async () => { + const result = await request(app) + .delete('/policies/user/default/permission_admin') + .send([{}]); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid policy definition. Cause: 'permission' field must not be empty`, + }); + }); + + it('should fail to delete, because policy field is absent', async () => { + const result = await request(app) + .delete('/policies/user/default/permission_admin') + .send([ + { + permission: 'policy-entity', + }, + ]); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid policy definition. Cause: 'policy' field must not be empty`, + }); + }); + + it('should fail to delete, because effect field is absent', async () => { + const result = await request(app) + .delete('/policies/user/default/permission_admin') + .send([ + { + permission: 'policy-entity', + policy: 'read', + }, + ]); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid policy definition. Cause: 'effect' field must not be empty`, + }); + }); + + it('should fail to delete, because policy not found', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return false; + }); + + const result = await request(app) + .delete('/policies/user/default/permission_admin') + .send([ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ]); + + expect(result.statusCode).toEqual(404); + expect(result.body.error).toEqual({ + name: 'NotFoundError', + message: `Policy '[user:default/permission_admin, policy-entity, read, allow]' not found`, + }); + }); + + it('should fail to delete, because unexpected error', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + enforcerDelegateMock.removePolicies = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + throw new Error('Fail to delete policy'); + }); + + const result = await request(app) + .delete('/policies/user/default/permission_admin') + .send([ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ]); + + expect(result.statusCode).toEqual(500); + expect(result.body.error).toEqual({ + name: 'Error', + message: 'Fail to delete policy', + }); + }); + + it('should fail to delete, because source mismatch', async () => { + const roleMeta: RoleMetadataDao = { + roleEntityRef: 'user:default/permission_admin', + source: 'csv-file', + modifiedBy, + }; + + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async (roleEntityRef: string): Promise => { + if (roleEntityRef === roleMeta.roleEntityRef) { + return roleMeta; + } + return { source: 'rest', roleEntityRef: roleEntityRef, modifiedBy }; + }, + ); + + const result = await request(app) + .delete('/policies/user/default/permission_admin') + .send([ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ]); + + const policy = [ + 'user:default/permission_admin', + 'policy-entity', + 'read', + 'allow', + ]; + + expect(result.statusCode).toEqual(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: `Unable to delete policy ${policy}: source does not match originating role ${ + roleMeta.roleEntityRef + }, consider making changes to the '${roleMeta.source.toLocaleUpperCase()}'`, + }); + }); + + it('should fail to delete policy, with original source of configuration', async () => { + const roleMeta: RoleMetadataDao = { + roleEntityRef: 'user:default/permission_admin', + source: 'configuration', + modifiedBy, + }; + + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async (roleEntityRef: string): Promise => { + if (roleEntityRef === roleMeta.roleEntityRef) { + return roleMeta; + } + return { source: 'rest', roleEntityRef: roleEntityRef, modifiedBy }; + }, + ); + + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + enforcerDelegateMock.removePolicies = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + + const result = await request(app) + .delete('/policies/user/default/permission_admin') + .send([ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ]); + + const policy = [ + 'user:default/permission_admin', + 'policy-entity', + 'read', + 'allow', + ]; + + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: `Unable to delete policy ${policy}: source does not match originating role ${ + roleMeta.roleEntityRef + }, consider making changes to the '${roleMeta.source.toLocaleUpperCase()}'`, + }); + }); + + it('should delete policy', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + enforcerDelegateMock.removePolicies = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + + const result = await request(app) + .delete( + '/policies/user/default/permission_admin?permission=policy-entity&policy=read&effect=allow', + ) + .send([ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ]); + + expect(result.statusCode).toEqual(204); + }); + }); + + describe('PUT /policies/:kind/:namespace/:name', () => { + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityUpdatePermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should fail to update policy - old policy is absent', async () => { + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send([{}]); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `'oldPolicy' object must be present`, + }); + }); + + it('should fail to update policy - new policy is absent', async () => { + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ oldPolicy: [{}] }); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `'newPolicy' object must be present`, + }); + }); + + it('should fail to update policy - oldPolicy permission is absent', async () => { + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ oldPolicy: [{}], newPolicy: [{}] }); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid old policy definition. Cause: 'permission' field must not be empty`, + }); + }); + + it('should fail to update policy - oldPolicy policy is absent', async () => { + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [{ permission: 'policy-entity' }], + newPolicy: [{}], + }); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid old policy definition. Cause: 'policy' field must not be empty`, + }); + }); + + it('should fail to update policy - oldPolicy effect is absent', async () => { + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [{ permission: 'policy-entity', policy: 'read' }], + newPolicy: [{}], + }); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid old policy definition. Cause: 'effect' field must not be empty`, + }); + }); + + it('should fail to update policy - newPolicy permission is absent', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [{}], + }); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid new policy definition. Cause: 'permission' field must not be empty`, + }); + }); + + it('should fail to update policy - newPolicy policy is absent', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [{ permission: 'policy-entity' }], + }); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid new policy definition. Cause: 'policy' field must not be empty`, + }); + }); + + it('should fail to update policy - newPolicy effect is absent', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [{ permission: 'policy-entity', policy: 'create' }], + }); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid new policy definition. Cause: 'effect' field must not be empty`, + }); + }); + + it('should fail to update policy - newPolicy effect has invalid value', async () => { + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'unknown', + }, + ], + newPolicy: [{ permission: 'policy-entity', policy: 'create' }], + }); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid old policy definition. Cause: 'effect' has invalid value: 'unknown'. It should be: '${AuthorizeResult.ALLOW.toLocaleLowerCase()}' or '${AuthorizeResult.DENY.toLocaleLowerCase()}'`, + }); + }); + + it('should fail to update policy - old policy not found', async () => { + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toEqual(404); + expect(result.body.error).toEqual({ + name: 'NotFoundError', + message: `Policy '[user:default/permission_admin, policy-entity, read, allow]' not found`, + }); + }); + + it('should fail to update policy - old policy not found but old and new policies match', async () => { + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toEqual(404); + expect(result.body.error).toEqual({ + name: 'NotFoundError', + message: `Policy '[user:default/permission_admin, policy-entity, read, allow]' not found`, + }); + }); + + it('should fail to update policy - newPolicy is already present', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toEqual(409); + expect(result.body.error).toEqual({ + name: 'ConflictError', + message: `Policy '[user:default/permission_admin, policy-entity, create, allow]' has been already stored`, + }); + }); + + it('should nothing to update', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toEqual(204); + }); + + it('should nothing to update - same permissions with different policy in a different order', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + { + permission: 'policy-entity', + policy: 'delete', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'delete', + effect: 'allow', + }, + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toEqual(204); + }); + + it('should nothing to update - same permissions with different permission type in a different order', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + { + permission: 'catalog-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'catalog-entity', + policy: 'read', + effect: 'allow', + }, + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toEqual(204); + }); + + it('should fail to update policy - unable to remove oldPolicy', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[2] === 'create') { + return false; + } + return true; + }); + enforcerDelegateMock.updatePolicies = jest + .fn() + .mockImplementation(async (): Promise => { + throw new Error('Fail to remove policy'); + }); + + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toEqual(500); + expect(result.body.error).toEqual({ + name: 'Error', + message: 'Fail to remove policy', + }); + }); + + it('should fail to update policy - unable to add newPolicy', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[2] === 'create') { + return false; + } + return true; + }); + enforcerDelegateMock.updatePolicies = jest + .fn() + .mockImplementation( + async (_param: string[][], _source: Source): Promise => { + throw new Error('Fail to add policy'); + }, + ); + + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toEqual(500); + expect(result.body.error).toEqual({ + name: 'Error', + message: 'Fail to add policy', + }); + }); + + it('should update policy', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[2] === 'create') { + return false; + } + return true; + }); + enforcerDelegateMock.updatePolicies = jest.fn().mockImplementation(); + + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toEqual(200); + }); + + it('should fail to update permission policy - duplication in old policy', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[2] === 'create') { + return false; + } + return true; + }); + + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + { + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toBe(409); + expect(result.body.error).toEqual({ + name: 'ConflictError', + message: `Duplicate polices found; user:default/permission_admin, policy-entity, read, allow is a duplicate`, + }); + }); + + it('should fail to update permission policy - duplication in new policy', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[2] === 'update') { + return false; + } + return true; + }); + + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + { + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'update', + effect: 'allow', + }, + { + permission: 'policy-entity', + policy: 'update', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toBe(409); + expect(result.body.error).toEqual({ + name: 'ConflictError', + message: `Duplicate polices found; user:default/permission_admin, policy-entity, update, allow is a duplicate`, + }); + }); + + it('should fail to update permission policy - oldPolicy has an additional permission', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + { + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'delete', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `'oldPolicy' object has more permission policies compared to 'newPolicy' object`, + }); + }); + + it('should fail to update permission policy, because of source mismatch', async () => { + const roleMeta: RoleMetadataDao = { + roleEntityRef: 'user:default/permission_admin', + source: 'csv-file', + modifiedBy, + }; + + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async (roleEntityRef: string): Promise => { + if (roleEntityRef === roleMeta.roleEntityRef) { + return roleMeta; + } + return { source: 'rest', roleEntityRef: roleEntityRef, modifiedBy }; + }, + ); + + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + { + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'delete', + effect: 'allow', + }, + ], + }); + + const policy = [ + 'user:default/permission_admin', + 'policy-entity', + 'read', + 'allow', + ]; + + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: `Unable to edit policy ${policy}: source does not match originating role ${ + roleMeta.roleEntityRef + }, consider making changes to the '${roleMeta.source.toLocaleUpperCase()}'`, + }); + }); + + it('should fail to update permission policy, with original source of configuration', async () => { + const roleMeta: RoleMetadataDao = { + roleEntityRef: 'user:default/permission_admin', + source: 'configuration', + modifiedBy, + }; + + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async (roleEntityRef: string): Promise => { + if (roleEntityRef === roleMeta.roleEntityRef) { + return roleMeta; + } + return { source: 'rest', roleEntityRef: roleEntityRef, modifiedBy }; + }, + ); + + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[2] === 'delete') { + return false; + } + return true; + }); + enforcerDelegateMock.updatePolicies = jest.fn().mockImplementation(); + + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'delete', + effect: 'allow', + }, + ], + }); + + const policy = [ + 'user:default/permission_admin', + 'policy-entity', + 'read', + 'allow', + ]; + + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: `Unable to edit policy ${policy}: source does not match originating role ${ + roleMeta.roleEntityRef + }, consider making changes to the '${roleMeta.source.toLocaleUpperCase()}'`, + }); + }); + }); + + describe('GET /roles', () => { + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app).get('/roles').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityReadPermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should be returned list all roles', async () => { + enforcerDelegateMock.getGroupingPolicy = jest + .fn() + .mockImplementation(async () => { + return [ + ['group:default/test', 'role:default/test'], + ['group:default/team_a', 'role:default/team_a'], + ]; + }); + const result = await request(app).get('/roles').send(); + expect(result.statusCode).toBe(200); + expect(result.body).toEqual([ + { + memberReferences: ['group:default/test'], + name: 'role:default/test', + metadata: { + source: 'rest', + modifiedBy: 'user:default/some-user', + }, + }, + { + memberReferences: ['group:default/team_a'], + name: 'role:default/team_a', + metadata: { + source: 'rest', + modifiedBy: 'user:default/some-user', + }, + }, + ]); + }); + }); + + describe('GET /roles/:kind/:namespace/:name', () => { + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app) + .get('/roles/role/default/rbac_admin') + .send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityReadPermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should return an input error when kind is wrong', async () => { + const result = await request(app) + .get('/roles/test/default/rbac_admin') + .send(); + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Unsupported kind test. Supported value should be "role"`, + }); + }); + + it('should be returned role by role reference', async () => { + const result = await request(app) + .get('/roles/role/default/rbac_admin') + .send(); + expect(result.statusCode).toBe(200); + expect(result.body).toEqual([ + { + memberReferences: ['user:default/permission_admin'], + name: 'role:default/rbac_admin', + metadata: { + source: 'rest', + modifiedBy: 'user:default/some-user', + }, + }, + ]); + }); + + it('should be returned not found error by role reference', async () => { + enforcerDelegateMock.getFilteredGroupingPolicy = jest + .fn() + .mockImplementation( + async (_fieldIndex: number, ..._fieldValues: string[]) => { + return []; + }, + ); + + const result = await request(app) + .get('/roles/role/default/rbac_admin') + .send(); + expect(result.statusCode).toBe(404); + expect(result.body).toEqual({ + error: { message: '', name: 'NotFoundError' }, + request: { + method: 'GET', + url: '/roles/role/default/rbac_admin', + }, + response: { statusCode: 404 }, + }); + }); + }); + + describe('POST /roles', () => { + beforeEach(() => { + mockedAuthorize.mockImplementation(async () => [ + { result: AuthorizeResult.ALLOW }, + ]); + }); + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app).post('/roles').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityCreatePermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should not be created role - req body is an empty', async () => { + const result = await request(app).post('/roles').send(); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid role definition. Cause: 'name' field must not be empty`, + }); + }); + + it('should not be created role - memberReferences is missing', async () => { + const result = await request(app).post('/roles').send({ + name: 'role:default/test', + }); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid role definition. Cause: 'memberReferences' field must not be empty`, + }); + }); + + it('should not be created role - memberReferences is empty', async () => { + const result = await request(app).post('/roles').send({ + memberReferences: [], + name: 'role:default/test', + }); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid role definition. Cause: 'memberReferences' field must not be empty`, + }); + }); + + it('should not be created role - memberReferences is invalid', async () => { + const result = await request(app) + .post('/roles') + .send({ + memberReferences: ['user'], + name: 'role:default/test', + }); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid role definition. Cause: Entity reference "user" had missing or empty kind (e.g. did not start with "component:" or similar)`, + }); + }); + + it('should not be created role - name is empty', async () => { + const result = await request(app) + .post('/roles') + .send({ + memberReferences: ['user:default/permission_admin'], + }); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid role definition. Cause: 'name' field must not be empty`, + }); + }); + + it('should not create a role - name is invalid', async () => { + const result = await request(app) + .post('/roles') + .send({ + memberReferences: ['user:default/permission_admin'], + name: 'x:default/rbac_admin', + }); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid role definition. Cause: Unsupported kind x. Supported value should be "role"`, + }); + }); + + it('should be created role', async () => { + const result = await request(app) + .post('/roles') + .send({ + memberReferences: ['user:default/permission_admin'], + name: 'role:default/rbac_admin', + }); + + expect(result.statusCode).toBe(201); + expect(enforcerDelegateMock.addGroupingPolicies).toHaveBeenCalledWith( + [['user:default/permission_admin', 'role:default/rbac_admin']], + { + author: 'user:default/guest', + roleEntityRef: 'role:default/rbac_admin', + source: 'rest', + description: '', + modifiedBy: 'user:default/guest', + }, + ); + }); + + it('should be created role with description', async () => { + const result = await request(app) + .post('/roles') + .send({ + memberReferences: ['user:default/permission_admin'], + name: 'role:default/rbac_admin', + metadata: { + description: 'some test description', + }, + }); + + expect(result.statusCode).toBe(201); + expect(enforcerDelegateMock.addGroupingPolicies).toHaveBeenCalledWith( + [['user:default/permission_admin', 'role:default/rbac_admin']], + { + roleEntityRef: 'role:default/rbac_admin', + source: 'rest', + author: 'user:default/guest', + description: 'some test description', + modifiedBy: 'user:default/guest', + }, + ); + }); + + it('should not be created role, because it is has been already present', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + + const result = await request(app) + .post('/roles') + .send({ + memberReferences: ['user:default/permission_admin'], + name: 'role:default/rbac_admin', + }); + + expect(result.statusCode).toBe(409); + }); + + it('should not be created role caused some unexpected error', async () => { + enforcerDelegateMock.addGroupingPolicies = jest + .fn() + .mockImplementation(async (): Promise => { + throw new Error('Fail to create new policy'); + }); + + const result = await request(app) + .post('/roles') + .send({ + memberReferences: ['user:default/permission_admin'], + name: 'role:default/rbac_admin', + }); + + expect(result.statusCode).toBe(500); + expect(result.body.error).toEqual({ + name: 'Error', + message: 'Fail to create new policy', + }); + }); + + it('should fail to create role - duplicate', async () => { + const result = await request(app) + .post('/roles') + .send({ + memberReferences: [ + 'user:default/permission_admin', + 'user:default/permission_admin', + ], + name: 'role:default/rbac_admin', + }); + + expect(result.statusCode).toBe(409); + expect(result.body.error).toEqual({ + name: 'ConflictError', + message: `Duplicate role members found; user:default/permission_admin, role:default/rbac_admin is a duplicate`, + }); + }); + + it('should fail to add role, because source mismatch', async () => { + const roleMeta: RoleMetadataDao = { + roleEntityRef: 'role:default/rbac_admin', + source: 'configuration', + modifiedBy, + }; + + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async (roleEntityRef: string): Promise => { + if (roleEntityRef === roleMeta.roleEntityRef) { + return roleMeta; + } + return { source: 'rest', roleEntityRef: roleEntityRef, modifiedBy }; + }, + ); + + const result = await request(app) + .post('/roles') + .send({ + memberReferences: ['user:default/permission_admin'], + name: 'role:default/rbac_admin', + }); + + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: `Unable to add role: source does not match originating role ${ + roleMeta.roleEntityRef + }, consider making changes to the '${roleMeta.source.toLocaleUpperCase()}'`, + }); + }); + }); + + describe('PUT /roles/:kind/:namespace/:name', () => { + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityUpdatePermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should fail to update role - old role is absent', async () => { + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send(); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `'oldRole' object must be present`, + }); + }); + + it('should fail to update role - new role is absent', async () => { + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ oldRole: {} }); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `'newRole' object must be present`, + }); + }); + + it('should fail to update role - oldRole entity is absent', async () => { + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ oldRole: {}, newRole: {} }); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid old role object. Cause: 'memberReferences' field must not be empty`, + }); + }); + + it('should fail to update role - newRole entity is absent', async () => { + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { memberReferences: ['user:default/permission_admin'] }, + newRole: {}, + }); + + expect(result.statusCode).toEqual(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid new role object. Cause: 'name' field must not be empty`, + }); + }); + + it('should fail to update role - old role not found', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._policy: string[]): Promise => { + return false; + }); + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test'], + name: 'role:default/rbac_admin', + }, + }); + + expect(result.statusCode).toEqual(404); + expect(result.body.error).toEqual({ + name: 'NotFoundError', + message: + 'Member reference: user:default/permission_admin was not found for role role:default/rbac_admin', + }); + }); + + it('should fail to update role - newRole is already present', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test'], + name: 'role:default/rbac_admin', + }, + }); + + expect(result.statusCode).toEqual(409); + expect(result.body.error).toEqual({ + name: 'ConflictError', + message: '', + }); + }); + + it('should nothing to update', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/permission_admin'], + name: 'role:default/rbac_admin', + }, + }); + + expect(result.statusCode).toEqual(204); + }); + + it('should nothing to update, because role and metadata are the same', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + metadata: { + source: 'rest', + }, + }, + newRole: { + memberReferences: ['user:default/permission_admin'], + name: 'role:default/rbac_admin', + metadata: { + source: 'rest', + }, + }, + }); + + expect(result.statusCode).toEqual(204); + }); + + it('should nothing to update, because role and metadata are the same, but old role metadata was not send', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/permission_admin'], + name: 'role:default/rbac_admin', + metadata: { + source: 'rest', + }, + }, + }); + + expect(result.statusCode).toEqual(204); + }); + + it('should update description and set author', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/permission_admin'], + name: 'role:default/rbac_admin', + metadata: { + source: 'rest', + description: 'some admin role.', + }, + }, + }); + + expect(result.statusCode).toEqual(200); + expect(enforcerDelegateMock.updateGroupingPolicies).toHaveBeenCalledWith( + [['user:default/permission_admin', 'role:default/rbac_admin']], + [['user:default/permission_admin', 'role:default/rbac_admin']], + { + description: 'some admin role.', + modifiedBy: 'user:default/guest', + roleEntityRef: 'role:default/rbac_admin', + source: 'rest', + }, + ); + }); + + it('should update role and role description', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[0] === 'user:default/permission_admin') { + return true; + } + return false; + }); + + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test', 'user:default/dev'], + name: 'role:default/rbac_admin', + metadata: { + source: 'rest', + description: 'some admin role.', + }, + }, + }); + + expect(result.statusCode).toEqual(200); + + expect(enforcerDelegateMock.updateGroupingPolicies).toHaveBeenCalledWith( + [['user:default/permission_admin', 'role:default/rbac_admin']], + [ + ['user:default/test', 'role:default/rbac_admin'], + ['user:default/dev', 'role:default/rbac_admin'], + ], + { + description: 'some admin role.', + modifiedBy: 'user:default/guest', + roleEntityRef: 'role:default/rbac_admin', + source: 'rest', + }, + ); + }); + + it('should fail to update policy - role metadata could not be found', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[0] === 'user:default/test') { + return false; + } + return true; + }); + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation(async (): Promise => { + return undefined; + }); + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test'], + name: 'role:default/rbac_admin', + }, + }); + + expect(result.statusCode).toEqual(404); + expect(result.body.error).toEqual({ + name: 'NotFoundError', + message: `Unable to find metadata for role:default/rbac_admin`, + }); + }); + + it('should fail to update role - unable to remove oldRole', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[0] === 'user:default/test') { + return false; + } + return true; + }); + enforcerDelegateMock.updateGroupingPolicies = jest + .fn() + .mockImplementation(async (): Promise => { + throw new Error('Unexpected error'); + }); + + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test'], + name: 'role:default/rbac_admin', + }, + }); + + expect(result.statusCode).toEqual(500); + expect(result.body.error).toEqual({ + name: 'Error', + message: 'Unexpected error', + }); + }); + + it('should fail to update role - unable to add newRole', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[0] === 'user:default/test') { + return false; + } + return true; + }); + enforcerDelegateMock.updateGroupingPolicies = jest + .fn() + .mockImplementation( + async (_param: string[][], _source: Source): Promise => { + throw new Error('Unexpected error'); + }, + ); + + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test'], + name: 'role:default/rbac_admin', + }, + }); + + expect(result.statusCode).toEqual(500); + expect(result.body.error).toEqual({ + name: 'Error', + message: 'Unexpected error', + }); + }); + + it('should update role', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[0] === 'user:default/test') { + return false; + } + return true; + }); + enforcerDelegateMock.updateGroupingPolicies = jest + .fn() + .mockImplementation(); + + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test'], + name: 'role:default/rbac_admin', + }, + }); + + expect(result.statusCode).toEqual(200); + }); + + it('should update role where newRole has multiple roles', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if ( + param[0] === 'user:default/test' || + param[0] === 'user:default/test2' + ) { + return false; + } + return true; + }); + enforcerDelegateMock.updateGroupingPolicies = jest + .fn() + .mockImplementation(); + + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test', 'user:default/test2'], + name: 'role:default/rbac_admin', + }, + }); + + expect(result.statusCode).toEqual(200); + }); + + it('should update role where newRole has multiple roles with one being from oldRole', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[0] === 'user:default/test') { + return false; + } + return true; + }); + enforcerDelegateMock.updateGroupingPolicies = jest + .fn() + .mockImplementation(); + + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: [ + 'user:default/permission_admin', + 'user:default/test', + ], + name: 'role:default/rbac_admin', + }, + }); + + expect(result.statusCode).toEqual(200); + }); + + it('should update role name', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[0] === 'user:default/test') { + return false; + } + return true; + }); + enforcerDelegateMock.updateGroupingPolicies = jest + .fn() + .mockImplementation(); + + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test'], + name: 'role:default/test', + }, + }); + + expect(result.statusCode).toEqual(200); + }); + + it('should fail to update role - duplicate roles in oldRole', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[0] === 'user:default/test') { + return false; + } + return true; + }); + + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: [ + 'user:default/permission_admin', + 'user:default/permission_admin', + ], + }, + newRole: { + memberReferences: ['user:default/test'], + name: 'role:default/rbac_admin', + }, + }); + + expect(result.statusCode).toBe(409); + expect(result.body.error).toEqual({ + name: 'ConflictError', + message: `Duplicate role members found; user:default/permission_admin, role:default/rbac_admin is a duplicate`, + }); + }); + + it('should fail to update role - duplicate roles in newRole', async () => { + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test', 'user:default/test'], + name: 'role:default/rbac_admin', + }, + }); + + expect(result.statusCode).toBe(409); + expect(result.body.error).toEqual({ + name: 'ConflictError', + message: `Duplicate role members found; user:default/test, role:default/rbac_admin is a duplicate`, + }); + }); + + it('should fail to update role name when role name is invalid', async () => { + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test'], + name: 'role:default/', + }, + }); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Invalid new role object. Cause: Entity reference "role:default/" was not on the form [:][/]`, + }); + }); + + it('should fail to update - oldRole name is invalid', async () => { + const result = await request(app) + .put('/roles/x/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test'], + name: 'role:default/', + }, + }); + + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + name: 'InputError', + message: `Unsupported kind x. Supported value should be "role"`, + }); + }); + + it('should fail to update role, because source mismatch', async () => { + const roleMeta: RoleMetadataDao = { + roleEntityRef: 'role:default/rbac_admin', + source: 'configuration', + modifiedBy, + }; + + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async (roleEntityRef: string): Promise => { + if (roleEntityRef === roleMeta.roleEntityRef) { + return roleMeta; + } + return { source: 'rest', roleEntityRef: roleEntityRef, modifiedBy }; + }, + ); + + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test'], + name: 'role:default/rbac_admin', + }, + }); + + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: `Unable to edit role: source does not match originating role ${ + roleMeta.roleEntityRef + }, consider making changes to the '${roleMeta.source.toLocaleUpperCase()}'`, + }); + }); + }); + + describe('DELETE /roles/:kind/:namespace/:name', () => { + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app) + .delete('/roles/role/default/rbac_admin') + .send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityDeletePermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should fail to delete, because unexpected error', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + enforcerDelegateMock.removeGroupingPolicies = jest + .fn() + .mockImplementation( + async (_param: string[][], _source: Source): Promise => { + throw new Error('Unexpected error'); + }, + ); + enforcerDelegateMock.getFilteredGroupingPolicy = jest + .fn() + .mockImplementation( + async (_index: number, ..._filter: string[]): Promise => { + return ['group:default/test', 'role/default/rbac_admin', 'rest']; + }, + ); + + const result = await request(app) + .delete( + '/roles/role/default/rbac_admin?memberReferences=group:default/test', + ) + .send(); + + expect(result.statusCode).toEqual(500); + expect(result.body.error).toEqual({ + name: 'Error', + message: 'Unexpected error', + }); + }); + + it('should fail to delete, because not found error', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return false; + }); + enforcerDelegateMock.getFilteredGroupingPolicy = jest + .fn() + .mockImplementation( + async (_index: number, ..._filter: string[]): Promise => { + return []; + }, + ); + + const result = await request(app) + .delete( + '/roles/role/default/rbac_admin?memberReferences=group:default/test', + ) + .send(); + + expect(result.statusCode).toEqual(404); + expect(result.body.error).toEqual({ + name: 'NotFoundError', + message: `role member 'group:default/test' was not found`, + }); + }); + + it('should delete a user / group from a role', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + enforcerDelegateMock.removeGroupingPolicies = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + enforcerDelegateMock.getFilteredGroupingPolicy = jest + .fn() + .mockImplementation( + async (_index: number, ..._filter: string[]): Promise => { + return ['group:default/test', 'role/default/rbac_admin', 'rest']; + }, + ); + + const result = await request(app) + .delete( + '/roles/role/default/rbac_admin?memberReferences=group:default/test', + ) + .send(); + + expect(result.statusCode).toEqual(204); + }); + + it('should delete a role', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + enforcerDelegateMock.removeGroupingPolicies = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + + const result = await request(app) + .delete('/roles/role/default/rbac_admin') + .send(); + + expect(result.statusCode).toEqual(204); + }); + + it('should fail to delete role, because source mismatch', async () => { + const roleMeta: RoleMetadataDao = { + roleEntityRef: 'role:default/rbac_admin', + source: 'configuration', + modifiedBy, + }; + + roleMetadataStorageMock.findRoleMetadata = jest + .fn() + .mockImplementation( + async (roleEntityRef: string): Promise => { + if (roleEntityRef === roleMeta.roleEntityRef) { + return roleMeta; + } + return { source: 'rest', roleEntityRef: roleEntityRef, modifiedBy }; + }, + ); + enforcerDelegateMock.getFilteredGroupingPolicy = jest + .fn() + .mockImplementation( + async (_index: number, ..._filter: string[]): Promise => { + return ['group:default/test', 'role/default/rbac_admin', 'rest']; + }, + ); + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + + const result = await request(app) + .delete('/roles/role/default/rbac_admin') + .send(); + + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: `Unable to delete role: source does not match originating role ${ + roleMeta.roleEntityRef + }, consider making changes to the '${roleMeta.source.toLocaleUpperCase()}'`, + }); + }); + }); + + describe('GetFirstQuery', () => { + it('should return an empty string for undefined query value', () => { + const result = server.getFirstQuery(undefined); + expect(result).toBe(''); + }); + + it('should return the first string value from a string array', async () => { + const queryValue = ['value1', 'value2']; + const result = server.getFirstQuery(queryValue); + expect(result).toBe('value1'); + }); + + it('should throw an InputError for an array of ParsedQs', () => { + const queryValue = [{ key: 'value' }, { key: 'value2' }]; + expect(() => server.getFirstQuery(queryValue)).toThrow(InputError); + }); + + it('should return the string value when query value is a string', () => { + const queryValue = 'singleValue'; + const result = server.getFirstQuery(queryValue); + expect(result).toBe('singleValue'); + }); + + it('should throw an InputError for ParsedQs', () => { + const queryValue = { key: 'value' }; + expect(() => server.getFirstQuery(queryValue)).toThrow(InputError); + }); + }); + + describe('transformRoleArray', () => { + it('should combine two roles together that are similar', async () => { + const roles = [ + ['group:default/test', 'role:default/test'], + ['user:default/test', 'role:default/test'], + ]; + + const expectedResult: Role[] = [ + { + memberReferences: ['group:default/test', 'user:default/test'], + name: 'role:default/test', + metadata: { + author: undefined, + createdAt: undefined, + description: undefined, + lastModified: undefined, + modifiedBy: 'user:default/some-user', + source: 'rest', + }, + }, + ]; + + const transformedRoles = await server.transformRoleArray(...roles); + expect(transformedRoles).toStrictEqual(expectedResult); + }); + }); + + // Define a test suite for the GET /conditions endpoint + describe('GET /roles/conditions', () => { + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + + // Perform the GET request to the endpoint + const result = await request(app).get('/roles/conditions').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityReadPermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + + // Assert the response status code and error message + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should be returned list all condition decisions', async () => { + const result = await request(app).get('/roles/conditions').send(); + expect(result.statusCode).toBe(200); + expect(result.body).toEqual(expectedConditions); + expect(mockHttpAuth.credentials).toHaveBeenCalledTimes(1); + }); + + it('should be returned condition decision by pluginId', async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation( + async ( + _roleEntityRef: string, + pluginId: string, + _resourceType: string, + ) => { + if (pluginId === 'catalog') { + return conditions; + } + return []; + }, + ); + const result = await request(app) + .get('/roles/conditions?pluginId=catalog') + .send(); + expect(result.statusCode).toBe(200); + expect(result.body).toEqual(expectedConditions); + }); + + it('should be returned empty condition decision list by pluginId', async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation( + async ( + _roleEntityRef: string, + pluginId: string, + _resourceType: string, + ) => { + if (pluginId === 'catalog') { + return conditions; + } + return []; + }, + ); + const result = await request(app) + .get('/roles/conditions?pluginId=scaffolder') + .send(); + expect(result.statusCode).toBe(200); + expect(result.body).toEqual([]); + }); + + it('should be returned condition decision by resourceType', async () => { + conditionalStorageMock.filterConditions = jest + .fn() + .mockImplementation( + async ( + _roleEntityRef: string, + _pluginId: string, + resourceType: string, + ) => { + if (resourceType === 'catalog-entity') { + return conditions; + } + return []; + }, + ); + const result = await request(app) + .get('/roles/conditions?resourceType=catalog-entity') + .send(); + expect(result.statusCode).toBe(200); + expect(result.body).toEqual(expectedConditions); + }); + }); + + describe('DELETE /roles/conditions/:id', () => { + beforeEach(() => { + conditionalStorageMock.getCondition = jest + .fn() + .mockImplementation(async () => { + return expectedConditions[0]; + }); + }); + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + + const result = await request(app).delete('/roles/conditions/1').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityDeletePermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + + // Assert the response status code and error message + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should delete condition decision by id', async () => { + const result = await request(app).delete('/roles/conditions/1').send(); + + expect(result.statusCode).toEqual(204); + expect(mockHttpAuth.credentials).toHaveBeenCalledTimes(1); + expect(conditionalStorageMock.deleteCondition).toHaveBeenCalled(); + }); + + it('should fail to delete condition decision by id', async () => { + conditionalStorageMock.deleteCondition = jest.fn(() => { + throw new Error('Failed to delete condition decision by id'); + }); + + const result = await request(app).delete('/roles/conditions/1').send(); + + expect(result.statusCode).toEqual(500); + expect(result.body.error.message).toEqual( + 'Failed to delete condition decision by id', + ); + }); + + it('should return return 400', async () => { + const result = await request(app) + .delete('/roles/conditions/non-number') + .send(); + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + message: 'Id is not a valid number.', + name: 'InputError', + }); + }); + }); + + describe('GET /roles/condition/:id', () => { + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + + const result = await request(app).get('/roles/conditions/1').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityReadPermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + + // Assert the response status code and error message + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should return condition decision by id', async () => { + conditionalStorageMock.getCondition = jest + .fn() + .mockImplementation(async (id: number) => { + if (id === 1) { + return conditions[0]; + } + return undefined; + }); + + const result = await request(app).get('/roles/conditions/1').send(); + expect(result.statusCode).toBe(200); + expect(result.body).toEqual(expectedConditions[0]); + expect(mockHttpAuth.credentials).toHaveBeenCalledTimes(1); + }); + + it('should return return 404', async () => { + const result = await request(app).get('/roles/conditions/1').send(); + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual({ + message: '', + name: 'NotFoundError', + }); + }); + + it('should return return 400', async () => { + const result = await request(app) + .get('/roles/conditions/non-number') + .send(); + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + message: 'Id is not a valid number.', + name: 'InputError', + }); + }); + }); + + describe('POST /roles/conditions', () => { + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + + const result = await request(app).post('/roles/conditions').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityCreatePermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + + // Assert the response status code and error message + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should be created condition', async () => { + conditionalStorageMock.createCondition = jest + .fn() + .mockImplementation(() => { + return 1; + }); + pluginPermissionMetadataCollectorMock.getMetadataByPluginId = jest + .fn() + .mockImplementation(() => { + const response: MetadataResponse = { + permissions: [ + { + name: 'catalog.entity.read', + attributes: { + action: 'read', + }, + type: 'resource', + resourceType: 'catalog-entity', + }, + ], + rules: [], + }; + return response; + }); + + const roleCondition: RoleConditionalPolicyDecision = { + id: 1, + pluginId: 'catalog', + roleEntityRef: 'role:default/test', + resourceType: 'catalog-entity', + permissionMapping: ['read'], + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { claims: ['group:default/team-a'] }, + }, + }; + const result = await request(app) + .post('/roles/conditions') + .send(roleCondition); + + expect(result.statusCode).toBe(201); + expect(validateRoleConditionMock).toHaveBeenCalledWith(roleCondition); + expect(result.body).toEqual({ id: 1 }); + expect(mockHttpAuth.credentials).toHaveBeenCalledTimes(1); + }); + }); + + describe('PUT /roles/conditions', () => { + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + + const result = await request(app).put('/roles/conditions/1').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityUpdatePermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + + // Assert the response status code and error message + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should return return 400', async () => { + const result = await request(app) + .put('/roles/conditions/non-number') + .send(); + expect(result.statusCode).toBe(400); + expect(result.body.error).toEqual({ + message: 'Id is not a valid number.', + name: 'InputError', + }); + }); + + it('should update condition decision', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.ALLOW }, + ]); + const conditionDecision: RoleConditionalPolicyDecision = + { + id: 1, + pluginId: 'catalog', + roleEntityRef: 'role:default/test', + resourceType: 'catalog-entity', + permissionMapping: ['read'], + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { claims: ['group:default/team-a'] }, + }, + }; + const result = await request(app) + .put('/roles/conditions/1') + .send(conditionDecision); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityUpdatePermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(validateRoleConditionMock).toHaveBeenCalledWith(conditionDecision); + + expect(result.statusCode).toBe(200); + expect(conditionalStorageMock.updateCondition).toHaveBeenCalledWith(1, { + id: 1, + pluginId: 'catalog', + roleEntityRef: 'role:default/test', + resourceType: 'catalog-entity', + permissionMapping: [ + { + action: 'read', + name: 'catalog.entity.read', + }, + ], + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { claims: ['group:default/team-a'] }, + }, + }); + expect(mockHttpAuth.credentials).toHaveBeenCalledTimes(1); + }); + }); + + describe('POST /refresh/:id', () => { + let appWithProvider: express.Express; + + beforeEach(async () => { + mockedAuthorize.mockImplementation(async () => [ + { result: AuthorizeResult.ALLOW }, + ]); + + const options: RBACRouterOptions = { + config: config, + logger, + discovery: mockDiscovery, + httpAuth: mockHttpAuth, + auth: mockAuthService, + policy: await RBACPermissionPolicy.build( + logger, + auditLoggerMock, + config, + conditionalStorageMock, + enforcerDelegateMock as EnforcerDelegate, + roleMetadataStorageMock, + knex, + pluginPermissionMetadataCollectorMock as PluginPermissionMetadataCollector, + mockAuthService, + ), + userInfo: mockUserInfo, + }; + + server = new PoliciesServer( + mockPermissionEvaluator, + options, + enforcerDelegateMock as EnforcerDelegate, + conditionalStorageMock, + pluginPermissionMetadataCollectorMock as PluginPermissionMetadataCollector, + roleMetadataStorageMock, + auditLoggerMock, + [providerMock], + ); + const router = await server.serve(); + appWithProvider = express().use(router); + appWithProvider.use(MiddlewareFactory.create({ logger, config }).error()); + }); + + it('should return a status of Unauthorized', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app).post('/refresh/test').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityCreatePermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should return a 200 for successful refresh set', async () => { + const result = await request(appWithProvider) + .post('/refresh/testProvider') + .send(); + expect(result.statusCode).toBe(200); + }); + + it('should return a 404 when there are no rbac providers', async () => { + const result = await request(app).post('/refresh/test').send(); + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual({ + message: 'No RBAC providers were found', + name: 'NotFoundError', + }); + }); + + it('should return a 404 when the rbac provider does not exist', async () => { + const result = await request(appWithProvider) + .post('/refresh/test') + .send(); + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual({ + message: 'The RBAC provider test was not found', + name: 'NotFoundError', + }); + }); + }); + + describe('list plugin permissions and condition rules', () => { + it('should return list plugins permission', async () => { + const pluginMetadata: PluginPermissionMetaData[] = [ + { + pluginId: 'permissions', + policies: [ + { + name: 'catalog.entity.read', + resourceType: 'policy-entity', + policy: 'read', + }, + ], + }, + ]; + pluginPermissionMetadataCollectorMock.getPluginPolicies = jest + .fn() + .mockImplementation(async () => { + return pluginMetadata; + }); + const result = await request(app).get('/plugins/policies').send(); + expect(result.statusCode).toEqual(200); + expect(result.body).toEqual(pluginMetadata); + }); + + it('should return a status of Unauthorized for /plugins/policies', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app).get('/plugins/policies').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityReadPermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + + it('should return list plugins condition rules', async () => { + const rules: PluginMetadataResponseSerializedRule[] = [ + { + pluginId: 'catalog', + rules: [ + { + description: 'Allow entities with the specified label', + name: 'HAS_LABEL', + paramsSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: { + label: { + description: 'Name of the label to match on', + type: 'string', + }, + }, + required: ['label'], + type: 'object', + }, + resourceType: 'catalog-entity', + }, + ], + }, + ]; + pluginPermissionMetadataCollectorMock.getPluginConditionRules = jest + .fn() + .mockImplementation(async () => { + return rules; + }); + const result = await request(app).get('/plugins/condition-rules').send(); + expect(result.statusCode).toEqual(200); + expect(result.body).toEqual(rules); + }); + + it('should return a status of Unauthorized for /plugins/condition-rules', async () => { + mockedAuthorize.mockImplementationOnce(async () => [ + { result: AuthorizeResult.DENY }, + ]); + const result = await request(app).get('/plugins/condition-rules').send(); + + expect(mockedAuthorize).toHaveBeenCalledWith( + [ + { + permission: policyEntityReadPermission, + resourceRef: 'policy-entity', + }, + ], + { + credentials: credentials, + }, + ); + expect(result.statusCode).toBe(403); + expect(result.body.error).toEqual({ + name: 'NotAllowedError', + message: '', + }); + }); + }); + + describe('test rest API when permission framework disabled', () => { + beforeAll(() => { + config = mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + permission: { + enabled: false, + }, + }, + }); + }); + + it('should not delete policy, because permission framework was disabled', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + enforcerDelegateMock.removePolicies = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + + const result = await request(app) + .delete( + '/policies/user/default/permission_admin?permission=policy-entity&policy=read&effect=allow', + ) + .send([ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ]); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not create policies, because permission framework was disabled', async () => { + const result = await request(app).post('/policies').send(); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not return policies, because permission framework was disabled', async () => { + const result = await request(app) + .get('/policies/user/default/permission_admin') + .send(); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not update policy, because permission framework was disabled', async () => { + enforcerDelegateMock.hasPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[2] === 'create') { + return false; + } + return true; + }); + enforcerDelegateMock.updatePolicies = jest.fn().mockImplementation(); + + const result = await request(app) + .put('/policies/user/default/permission_admin') + .send({ + oldPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + newPolicy: [ + { + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + ], + }); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not return list all policies, because permission framework was disabled', async () => { + enforcerDelegateMock.getPolicy = jest + .fn() + .mockImplementation(async () => { + return [ + [ + 'user:default/permission_admin', + 'policy-entity', + 'create', + 'allow', + ], + ['user:default/guest', 'policy-entity', 'read', 'allow'], + ]; + }); + const result = await request(app).get('/policies').send(); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not return list all roles, because permission framework was disabled', async () => { + enforcerDelegateMock.getGroupingPolicy = jest + .fn() + .mockImplementation(async () => { + return [ + ['group:default/test', 'role:default/test'], + ['group:default/team_a', 'role:default/team_a'], + ]; + }); + + const result = await request(app).get('/roles').send(); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not return role by role reference, because permission framework was disabled', async () => { + const result = await request(app) + .get('/roles/role/default/rbac_admin') + .send(); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not create role, because permission framework was disabled', async () => { + const result = await request(app) + .post('/roles') + .send({ + memberReferences: ['user:default/permission_admin'], + name: 'role:default/rbac_admin', + }); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not update role, because permission framework was disabled', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (...param: string[]): Promise => { + if (param[0] === 'user:default/permission_admin') { + return true; + } + return false; + }); + + const result = await request(app) + .put('/roles/role/default/rbac_admin') + .send({ + oldRole: { + memberReferences: ['user:default/permission_admin'], + }, + newRole: { + memberReferences: ['user:default/test', 'user:default/dev'], + name: 'role:default/rbac_admin', + metadata: { + source: 'rest', + description: 'some admin role.', + }, + }, + }); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not delete a role, because permission framework was disabled', async () => { + enforcerDelegateMock.hasGroupingPolicy = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + enforcerDelegateMock.removeGroupingPolicies = jest + .fn() + .mockImplementation(async (..._param: string[]): Promise => { + return true; + }); + + const result = await request(app) + .delete('/roles/role/default/rbac_admin') + .send(); + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not return list of all condition decisions, because permission framework was disabled', async () => { + const result = await request(app).get('/roles/conditions').send(); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not delete condition decision, because permission framework was disabled', async () => { + const result = await request(app).delete('/roles/conditions/1').send(); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not return condition decision by id, because permission framework was disabled', async () => { + conditionalStorageMock.getCondition = jest + .fn() + .mockImplementation(async (id: number) => { + if (id === 1) { + return conditions[0]; + } + return undefined; + }); + + const result = await request(app).get('/roles/conditions/1').send(); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not create condition, because permission framework was disabled', async () => { + conditionalStorageMock.createCondition = jest + .fn() + .mockImplementation(() => { + return 1; + }); + pluginPermissionMetadataCollectorMock.getMetadataByPluginId = jest + .fn() + .mockImplementation(() => { + const response: MetadataResponse = { + permissions: [ + { + name: 'catalog.entity.read', + attributes: { + action: 'read', + }, + type: 'resource', + resourceType: 'catalog-entity', + }, + ], + rules: [], + }; + return response; + }); + + const roleCondition: RoleConditionalPolicyDecision = { + id: 1, + pluginId: 'catalog', + roleEntityRef: 'role:default/test', + resourceType: 'catalog-entity', + permissionMapping: ['read'], + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { claims: ['group:default/team-a'] }, + }, + }; + + const result = await request(app) + .post('/roles/conditions') + .send(roleCondition); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not update condition decision, because permission framework was disabled', async () => { + mockedAuthorizeConditional.mockImplementationOnce(async () => [ + { result: AuthorizeResult.ALLOW }, + ]); + const conditionDecision: RoleConditionalPolicyDecision = + { + id: 1, + pluginId: 'catalog', + roleEntityRef: 'role:default/test', + resourceType: 'catalog-entity', + permissionMapping: ['read'], + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { claims: ['group:default/team-a'] }, + }, + }; + + const result = await request(app) + .put('/roles/conditions/1') + .send(conditionDecision); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + + it('should not return list plugins condition rules, because permission framework was disabled', async () => { + const rules: PluginMetadataResponseSerializedRule[] = [ + { + pluginId: 'catalog', + rules: [ + { + description: 'Allow entities with the specified label', + name: 'HAS_LABEL', + paramsSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: { + label: { + description: 'Name of the label to match on', + type: 'string', + }, + }, + required: ['label'], + type: 'object', + }, + resourceType: 'catalog-entity', + }, + ], + }, + ]; + pluginPermissionMetadataCollectorMock.getPluginConditionRules = jest + .fn() + .mockImplementation(async () => { + return rules; + }); + + const result = await request(app).get('/plugins/condition-rules').send(); + + expect(result.statusCode).toBe(404); + expect(result.body.error).toEqual(undefined); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/service/policies-rest-api.ts b/workspaces/rbac/plugins/rbac-backend/src/service/policies-rest-api.ts new file mode 100644 index 0000000000..1e04e2c3a0 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/service/policies-rest-api.ts @@ -0,0 +1,1255 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { PermissionsService } from '@backstage/backend-plugin-api'; +import { + ConflictError, + InputError, + NotAllowedError, + NotFoundError, + ServiceUnavailableError, +} from '@backstage/errors'; +import { createRouter } from '@backstage/plugin-permission-backend'; +import { + AuthorizeResult, + PolicyDecision, + ResourcePermission, +} from '@backstage/plugin-permission-common'; +import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; + +import type { AuditLogger } from '@janus-idp/backstage-plugin-audit-log-node'; +import express from 'express'; +import type { Request } from 'express-serve-static-core'; +import { isEmpty, isEqual } from 'lodash'; +import type { ParsedQs } from 'qs'; + +import { + PermissionAction, + policyEntityCreatePermission, + policyEntityDeletePermission, + policyEntityPermissions, + policyEntityReadPermission, + policyEntityUpdatePermission, + RESOURCE_TYPE_POLICY_ENTITY, + Role, + RoleBasedPolicy, + RoleConditionalPolicyDecision, +} from '@backstage-community/plugin-rbac-common'; +import type { RBACProvider } from '@backstage-community/plugin-rbac-node'; + +import { + ConditionAuditInfo, + ConditionEvents, + ListConditionEvents, + ListPluginPoliciesEvents, + PermissionAuditInfo, + PermissionEvents, + RoleAuditInfo, + RoleEvents, + SEND_RESPONSE_STAGE, +} from '../audit-log/audit-logger'; +import { auditError as logAuditError } from '../audit-log/rest-errors-interceptor'; +import { ConditionalStorage } from '../database/conditional-storage'; +import { + daoToMetadata, + RoleMetadataDao, + RoleMetadataStorage, +} from '../database/role-metadata'; +import { + buildRoleSourceMap, + deepSortedEqual, + isPermissionAction, + policyToString, + processConditionMapping, +} from '../helper'; +import { validateRoleCondition } from '../validation/condition-validation'; +import { + validateEntityReference, + validatePolicy, + validateRole, + validateSource, +} from '../validation/policies-validation'; +import { EnforcerDelegate } from './enforcer-delegate'; +import { PluginPermissionMetadataCollector } from './plugin-endpoints'; +import { RBACRouterOptions } from './policy-builder'; + +export class PoliciesServer { + constructor( + private readonly permissions: PermissionsService, + private readonly options: RBACRouterOptions, + private readonly enforcer: EnforcerDelegate, + private readonly conditionalStorage: ConditionalStorage, + private readonly pluginPermMetaData: PluginPermissionMetadataCollector, + private readonly roleMetadata: RoleMetadataStorage, + private readonly aLog: AuditLogger, + private readonly rbacProviders?: RBACProvider[], + ) {} + + private async authorize( + request: Request, + permission: ResourcePermission, + ): Promise { + const credentials = await this.options.httpAuth.credentials(request, { + allow: ['user', 'service'], + }); + + // allow service to service communication, but only with read permission + if ( + this.options.auth.isPrincipal(credentials, 'service') && + permission !== policyEntityReadPermission + ) { + throw new NotAllowedError( + `Only creadential principal with type 'user' permitted to modify permissions`, + ); + } + + const decision = ( + await this.permissions.authorize( + [{ permission: permission, resourceRef: permission.resourceType }], + { credentials }, + ) + )[0]; + + return decision; + } + + async serve(): Promise { + const router = await createRouter(this.options); + + const { httpAuth } = this.options; + + if (!httpAuth) { + throw new ServiceUnavailableError( + 'httpAuth not found, ensure the correct configuration for the RBAC plugin', + ); + } + + const permissionsIntegrationRouter = createPermissionIntegrationRouter({ + resourceType: RESOURCE_TYPE_POLICY_ENTITY, + permissions: policyEntityPermissions, + }); + router.use(permissionsIntegrationRouter); + + const isPluginEnabled = + this.options.config.getOptionalBoolean('permission.enabled'); + if (!isPluginEnabled) { + return router; + } + + router.get('/', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityReadPermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + response.send({ status: 'Authorized' }); + }); + + // Policy CRUD + + router.get('/policies', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityReadPermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + let policies: string[][]; + if (this.isPolicyFilterEnabled(request)) { + const entityRef = this.getFirstQuery(request.query.entityRef); + const permission = this.getFirstQuery(request.query.permission); + const policy = this.getFirstQuery(request.query.policy); + const effect = this.getFirstQuery(request.query.effect); + + const filter: string[] = [entityRef, permission, policy, effect]; + policies = await this.enforcer.getFilteredPolicy(0, ...filter); + } else { + policies = await this.enforcer.getPolicy(); + } + + const body = await this.transformPolicyArray(...policies); + + await this.aLog.auditLog({ + message: `Return list permission policies`, + eventName: PermissionEvents.GET_POLICY, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 200, body }, + }); + + response.json(body); + }); + + router.get( + '/policies/:kind/:namespace/:name', + async (request, response) => { + const decision = await this.authorize( + request, + policyEntityReadPermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const entityRef = this.getEntityReference(request); + + const policy = await this.enforcer.getFilteredPolicy(0, entityRef); + if (policy.length !== 0) { + const body = await this.transformPolicyArray(...policy); + + await this.aLog.auditLog({ + message: `Return permission policy`, + eventName: PermissionEvents.GET_POLICY, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 200, body }, + }); + + response.json(body); + } else { + throw new NotFoundError(); // 404 + } + }, + ); + + router.delete( + '/policies/:kind/:namespace/:name', + async (request, response) => { + const decision = await this.authorize( + request, + policyEntityDeletePermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const entityRef = this.getEntityReference(request); + + const policyRaw: RoleBasedPolicy[] = request.body; + if (isEmpty(policyRaw)) { + throw new InputError(`permission policy must be present`); // 400 + } + + policyRaw.forEach(element => { + element.entityReference = entityRef; + }); + + const processedPolicies = await this.processPolicies(policyRaw, true); + + await this.enforcer.removePolicies(processedPolicies); + + await this.aLog.auditLog({ + message: `Deleted permission policies`, + eventName: PermissionEvents.DELETE_POLICY, + metadata: { policies: processedPolicies, source: 'rest' }, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 204 }, + }); + + response.status(204).end(); + }, + ); + + router.post('/policies', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityCreatePermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const policyRaw: RoleBasedPolicy[] = request.body; + + if (isEmpty(policyRaw)) { + throw new InputError(`permission policy must be present`); // 400 + } + + const processedPolicies = await this.processPolicies(policyRaw); + + const entityRef = processedPolicies[0][0]; + const roleMetadata = await this.roleMetadata.findRoleMetadata(entityRef); + if (entityRef.startsWith('role:default') && !roleMetadata) { + throw new Error(`Corresponding role ${entityRef} was not found`); + } + + await this.enforcer.addPolicies(processedPolicies); + + await this.aLog.auditLog({ + message: `Created permission policies`, + eventName: PermissionEvents.CREATE_POLICY, + metadata: { policies: processedPolicies, source: 'rest' }, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 201 }, + }); + + response.status(201).end(); + }); + + router.put( + '/policies/:kind/:namespace/:name', + async (request, response) => { + const decision = await this.authorize( + request, + policyEntityUpdatePermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const entityRef = this.getEntityReference(request); + + const oldPolicyRaw: RoleBasedPolicy[] = request.body.oldPolicy; + if (isEmpty(oldPolicyRaw)) { + throw new InputError(`'oldPolicy' object must be present`); // 400 + } + const newPolicyRaw: RoleBasedPolicy[] = request.body.newPolicy; + if (isEmpty(newPolicyRaw)) { + throw new InputError(`'newPolicy' object must be present`); // 400 + } + + [...oldPolicyRaw, ...newPolicyRaw].forEach(element => { + element.entityReference = entityRef; + }); + + const processedOldPolicy = await this.processPolicies( + oldPolicyRaw, + true, + 'old policy', + ); + + oldPolicyRaw.sort((a, b) => + a.permission === b.permission + ? this.nameSort(a.policy!, b.policy!) + : this.nameSort(a.permission!, b.permission!), + ); + + newPolicyRaw.sort((a, b) => + a.permission === b.permission + ? this.nameSort(a.policy!, b.policy!) + : this.nameSort(a.permission!, b.permission!), + ); + + if ( + isEqual(oldPolicyRaw, newPolicyRaw) && + !oldPolicyRaw.some(isEmpty) + ) { + response.status(204).end(); + } else if (oldPolicyRaw.length > newPolicyRaw.length) { + throw new InputError( + `'oldPolicy' object has more permission policies compared to 'newPolicy' object`, + ); + } + + const processedNewPolicy = await this.processPolicies( + newPolicyRaw, + false, + 'new policy', + ); + + const roleMetadata = await this.roleMetadata.findRoleMetadata( + entityRef, + ); + if (entityRef.startsWith('role:default') && !roleMetadata) { + throw new Error(`Corresponding role ${entityRef} was not found`); + } + + await this.enforcer.updatePolicies( + processedOldPolicy, + processedNewPolicy, + ); + + await this.aLog.auditLog({ + message: `Updated permission policies`, + eventName: PermissionEvents.UPDATE_POLICY, + metadata: { policies: processedNewPolicy, source: 'rest' }, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 200 }, + }); + + response.status(200).end(); + }, + ); + + // Role CRUD + + router.get('/roles', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityReadPermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const roles = await this.enforcer.getGroupingPolicy(); + + const body = await this.transformRoleArray(...roles); + + await this.aLog.auditLog({ + message: `Return list roles`, + eventName: RoleEvents.GET_ROLE, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 200, body }, + }); + + response.json(body); + }); + + router.get('/roles/:kind/:namespace/:name', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityReadPermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + const roleEntityRef = this.getEntityReference(request, true); + + const role = await this.enforcer.getFilteredGroupingPolicy( + 1, + roleEntityRef, + ); + + if (role.length !== 0) { + const body = await this.transformRoleArray(...role); + + await this.aLog.auditLog({ + message: `Return ${body[0].name}`, + eventName: RoleEvents.GET_ROLE, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 200, body }, + }); + + response.json(body); + } else { + throw new NotFoundError(); // 404 + } + }); + + router.post('/roles', async (request, response) => { + const uniqueItems = new Set(); + const decision = await this.authorize( + request, + policyEntityCreatePermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + const roleRaw: Role = request.body; + let err = validateRole(roleRaw); + if (err) { + throw new InputError( // 400 + `Invalid role definition. Cause: ${err.message}`, + ); + } + + const rMetadata = await this.roleMetadata.findRoleMetadata(roleRaw.name); + + err = await validateSource('rest', rMetadata); + if (err) { + throw new NotAllowedError(`Unable to add role: ${err.message}`); + } + + const roles = this.transformRoleToArray(roleRaw); + + for (const role of roles) { + if (await this.enforcer.hasGroupingPolicy(...role)) { + throw new ConflictError(); // 409 + } + const roleString = JSON.stringify(role); + + if (uniqueItems.has(roleString)) { + throw new ConflictError( + `Duplicate role members found; ${role.at(0)}, ${role.at( + 1, + )} is a duplicate`, + ); + } else { + uniqueItems.add(roleString); + } + } + + const credentials = await httpAuth.credentials(request, { + allow: ['user'], + }); + const modifiedBy = credentials.principal.userEntityRef; + const metadata: RoleMetadataDao = { + roleEntityRef: roleRaw.name, + source: 'rest', + description: roleRaw.metadata?.description ?? '', + author: modifiedBy, + modifiedBy, + }; + + await this.enforcer.addGroupingPolicies(roles, metadata); + + await this.aLog.auditLog({ + message: `Created ${metadata.roleEntityRef}`, + eventName: RoleEvents.CREATE_ROLE, + metadata: { + ...metadata, + members: roles.map(gp => gp[0]), + }, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 201 }, + }); + + response.status(201).end(); + }); + + router.put('/roles/:kind/:namespace/:name', async (request, response) => { + const uniqueItems = new Set(); + const decision = await this.authorize( + request, + policyEntityUpdatePermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + const roleEntityRef = this.getEntityReference(request, true); + + const oldRoleRaw: Role = request.body.oldRole; + + if (!oldRoleRaw) { + throw new InputError(`'oldRole' object must be present`); // 400 + } + const newRoleRaw: Role = request.body.newRole; + if (!newRoleRaw) { + throw new InputError(`'newRole' object must be present`); // 400 + } + + oldRoleRaw.name = roleEntityRef; + let err = validateRole(oldRoleRaw); + if (err) { + throw new InputError( // 400 + `Invalid old role object. Cause: ${err.message}`, + ); + } + err = validateRole(newRoleRaw); + if (err) { + throw new InputError( // 400 + `Invalid new role object. Cause: ${err.message}`, + ); + } + + const oldRole = this.transformRoleToArray(oldRoleRaw); + const newRole = this.transformRoleToArray(newRoleRaw); + // todo shell we allow newRole with an empty array?... + + const credentials = await httpAuth.credentials(request, { + allow: ['user'], + }); + + const newMetadata: RoleMetadataDao = { + ...newRoleRaw.metadata, + source: newRoleRaw.metadata?.source ?? 'rest', + roleEntityRef: newRoleRaw.name, + modifiedBy: credentials.principal.userEntityRef, + }; + + const oldMetadata = await this.roleMetadata.findRoleMetadata( + roleEntityRef, + ); + if (!oldMetadata) { + throw new NotFoundError(`Unable to find metadata for ${roleEntityRef}`); + } + + err = await validateSource('rest', oldMetadata); + if (err) { + throw new NotAllowedError(`Unable to edit role: ${err.message}`); + } + + if ( + isEqual(oldRole, newRole) && + deepSortedEqual(oldMetadata, newMetadata, [ + 'author', + 'modifiedBy', + 'createdAt', + 'lastModified', + ]) + ) { + // no content: old role and new role are equal and their metadata too + response.status(204).end(); + return; + } + + for (const role of newRole) { + const hasRole = oldRole.some(element => { + return isEqual(element, role); + }); + // if the role is already part of old role and is a grouping policy we want to skip returning a conflict error + // to allow for other roles to be checked and added + if (await this.enforcer.hasGroupingPolicy(...role)) { + if (!hasRole) { + throw new ConflictError(); // 409 + } + } + const roleString = JSON.stringify(role); + + if (uniqueItems.has(roleString)) { + throw new ConflictError( + `Duplicate role members found; ${role.at(0)}, ${role.at( + 1, + )} is a duplicate`, + ); + } else { + uniqueItems.add(roleString); + } + } + + uniqueItems.clear(); + for (const role of oldRole) { + if (!(await this.enforcer.hasGroupingPolicy(...role))) { + throw new NotFoundError( + `Member reference: ${role[0]} was not found for role ${roleEntityRef}`, + ); // 404 + } + const roleString = JSON.stringify(role); + + if (uniqueItems.has(roleString)) { + throw new ConflictError( + `Duplicate role members found; ${role.at(0)}, ${role.at( + 1, + )} is a duplicate`, + ); + } else { + uniqueItems.add(roleString); + } + } + + await this.enforcer.updateGroupingPolicies(oldRole, newRole, newMetadata); + + let message = `Updated ${oldMetadata.roleEntityRef}.`; + if (newMetadata.roleEntityRef !== oldMetadata.roleEntityRef) { + message = `${message}. Role entity reference renamed to ${newMetadata.roleEntityRef}`; + } + await this.aLog.auditLog({ + message, + eventName: RoleEvents.UPDATE_ROLE, + metadata: { + ...newMetadata, + members: newRole.map(gp => gp[0]), + }, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 200 }, + }); + + response.status(200).end(); + }); + + router.delete( + '/roles/:kind/:namespace/:name', + async (request, response) => { + const decision = await this.authorize( + request, + policyEntityDeletePermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const roleEntityRef = this.getEntityReference(request, true); + + let roleMembers = []; + if (request.query.memberReferences) { + const memberReference = this.getFirstQuery( + request.query.memberReferences!, + ); + const gp = await this.enforcer.getFilteredGroupingPolicy( + 0, + memberReference, + roleEntityRef, + ); + if (gp.length > 0) { + roleMembers.push(gp[0]); + } else { + throw new NotFoundError( + `role member '${memberReference}' was not found`, + ); // 404 + } + } else { + roleMembers = await this.enforcer.getFilteredGroupingPolicy( + 1, + roleEntityRef, + ); + } + + for (const role of roleMembers) { + if (!(await this.enforcer.hasGroupingPolicy(...role))) { + throw new NotFoundError(`role member '${role[0]}' was not found`); + } + } + + const currentMetadata = await this.roleMetadata.findRoleMetadata( + roleEntityRef, + ); + const err = await validateSource('rest', currentMetadata); + if (err) { + throw new NotAllowedError(`Unable to delete role: ${err.message}`); + } + + const credentials = await httpAuth.credentials(request, { + allow: ['user'], + }); + + const metadata: RoleMetadataDao = { + roleEntityRef, + source: 'rest', + modifiedBy: credentials.principal.userEntityRef, + }; + + await this.enforcer.removeGroupingPolicies( + roleMembers, + metadata, + false, + ); + + await this.aLog.auditLog({ + message: `Deleted ${metadata.roleEntityRef}`, + eventName: RoleEvents.DELETE_ROLE, + metadata: { + ...metadata, + members: roleMembers.map(gp => gp[0]), + }, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 204 }, + }); + + response.status(204).end(); + }, + ); + + router.get('/plugins/policies', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityReadPermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const body = await this.pluginPermMetaData.getPluginPolicies( + this.options.auth, + ); + + await this.aLog.auditLog({ + message: `Return list plugin policies`, + eventName: ListPluginPoliciesEvents.GET_PLUGINS_POLICIES, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 200, body }, + }); + + response.json(body); + }); + + router.get('/plugins/condition-rules', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityReadPermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const body = await this.pluginPermMetaData.getPluginConditionRules( + this.options.auth, + ); + + await this.aLog.auditLog({ + message: `Return list conditional rules and schemas`, + eventName: ListConditionEvents.GET_CONDITION_RULES, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 200, body }, + }); + + response.json(body); + }); + + router.get('/roles/conditions', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityReadPermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const conditions = await this.conditionalStorage.filterConditions( + this.getFirstQuery(request.query.roleEntityRef), + this.getFirstQuery(request.query.pluginId), + this.getFirstQuery(request.query.resourceType), + this.getActionQueries(request.query.actions), + ); + + const body: RoleConditionalPolicyDecision[] = + conditions.map(condition => { + return { + ...condition, + permissionMapping: condition.permissionMapping.map(pm => pm.action), + }; + }); + + await this.aLog.auditLog({ + message: `Return list conditional permission policies`, + eventName: ConditionEvents.GET_CONDITION, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 200, body }, + }); + + response.json(body); + }); + + router.post('/roles/conditions', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityCreatePermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const roleConditionPolicy: RoleConditionalPolicyDecision = + request.body; + validateRoleCondition(roleConditionPolicy); + + const conditionToCreate = await processConditionMapping( + roleConditionPolicy, + this.pluginPermMetaData, + this.options.auth, + ); + + const id = await this.conditionalStorage.createCondition( + conditionToCreate, + ); + + const body = { id: id }; + + await this.aLog.auditLog({ + message: `Created conditional permission policy`, + eventName: ConditionEvents.CREATE_CONDITION, + metadata: { condition: roleConditionPolicy }, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 201, body }, + }); + + response.status(201).json(body); + }); + + router.get('/roles/conditions/:id', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityReadPermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const id: number = parseInt(request.params.id, 10); + if (isNaN(id)) { + throw new InputError('Id is not a valid number.'); + } + + const condition = await this.conditionalStorage.getCondition(id); + if (!condition) { + throw new NotFoundError(); + } + + const body: RoleConditionalPolicyDecision = { + ...condition, + permissionMapping: condition.permissionMapping.map(pm => pm.action), + }; + + await this.aLog.auditLog({ + message: `Return conditional permission policy by id`, + eventName: ConditionEvents.GET_CONDITION, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 200, body }, + }); + + response.json(body); + }); + + router.delete('/roles/conditions/:id', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityDeletePermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const id: number = parseInt(request.params.id, 10); + if (isNaN(id)) { + throw new InputError('Id is not a valid number.'); + } + + const condition = await this.conditionalStorage.getCondition(id); + if (!condition) { + throw new NotFoundError(`Condition with id ${id} was not found`); + } + const conditionToDelete: RoleConditionalPolicyDecision = + { + ...condition, + permissionMapping: condition.permissionMapping.map(pm => pm.action), + }; + + await this.conditionalStorage.deleteCondition(id); + + await this.aLog.auditLog({ + message: `Deleted conditional permission policy`, + eventName: ConditionEvents.DELETE_CONDITION, + metadata: { condition: conditionToDelete }, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 204 }, + }); + + response.status(204).end(); + }); + + router.put('/roles/conditions/:id', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityUpdatePermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + const id: number = parseInt(request.params.id, 10); + if (isNaN(id)) { + throw new InputError('Id is not a valid number.'); + } + + const roleConditionPolicy: RoleConditionalPolicyDecision = + request.body; + + validateRoleCondition(roleConditionPolicy); + + const conditionToUpdate = await processConditionMapping( + roleConditionPolicy, + this.pluginPermMetaData, + this.options.auth, + ); + + await this.conditionalStorage.updateCondition(id, conditionToUpdate); + + await this.aLog.auditLog({ + message: `Updated conditional permission policy`, + eventName: ConditionEvents.UPDATE_CONDITION, + metadata: { condition: roleConditionPolicy }, + stage: SEND_RESPONSE_STAGE, + status: 'succeeded', + request, + response: { status: 200 }, + }); + + response.status(200).end(); + }); + + router.post('/refresh/:id', async (request, response) => { + const decision = await this.authorize( + request, + policyEntityCreatePermission, + ); + + if (decision.result === AuthorizeResult.DENY) { + throw new NotAllowedError(); // 403 + } + + if (!this.rbacProviders) { + throw new NotFoundError(`No RBAC providers were found`); + } + + const idProvider = this.rbacProviders.find(provider => { + const id = provider.getProviderName(); + return id === request.params.id; + }); + + if (!idProvider) { + throw new NotFoundError( + `The RBAC provider ${request.params.id} was not found`, + ); + } + + await idProvider.refresh(); + response.status(200).end(); + }); + + router.use(logAuditError(this.aLog)); + + return router; + } + + getEntityReference(request: Request, role?: boolean): string { + const kind = request.params.kind; + const namespace = request.params.namespace; + const name = request.params.name; + const entityRef = `${kind}:${namespace}/${name}`; + + const err = validateEntityReference(entityRef, role); + if (err) { + throw new InputError(err.message); + } + + return entityRef; + } + + async transformPolicyArray( + ...policies: string[][] + ): Promise { + const roleToSourceMap = await buildRoleSourceMap( + policies, + this.roleMetadata, + ); + + const roleBasedPolices: RoleBasedPolicy[] = []; + for (const p of policies) { + const [entityReference, permission, policy, effect] = p; + roleBasedPolices.push({ + entityReference, + permission, + policy, + effect, + metadata: { source: roleToSourceMap.get(entityReference)! }, + }); + } + + return roleBasedPolices; + } + + async transformRoleArray(...roles: string[][]): Promise { + const combinedRoles: { [key: string]: string[] } = {}; + + roles.forEach(([value, role]) => { + if (combinedRoles.hasOwnProperty(role)) { + combinedRoles[role].push(value); + } else { + combinedRoles[role] = [value]; + } + }); + + const result: Role[] = await Promise.all( + Object.entries(combinedRoles).map(async ([role, value]) => { + const metadataDao = await this.roleMetadata.findRoleMetadata(role); + const metadata = metadataDao ? daoToMetadata(metadataDao) : undefined; + return Promise.resolve({ + memberReferences: value, + name: role, + metadata, + }); + }), + ); + return result; + } + + transformPolicyToArray(policy: RoleBasedPolicy): string[] { + return [ + policy.entityReference!, + policy.permission!, + policy.policy!, + policy.effect!, + ]; + } + + transformRoleToArray(role: Role): string[][] { + const roles: string[][] = []; + for (const entity of role.memberReferences) { + roles.push([entity, role.name]); + } + return roles; + } + + getActionQueries( + queryValue: string | string[] | ParsedQs | ParsedQs[] | undefined, + ): PermissionAction[] | undefined { + if (!queryValue) { + return undefined; + } + if (Array.isArray(queryValue)) { + const permissionNames: PermissionAction[] = []; + for (const permissionQuery of queryValue) { + if ( + typeof permissionQuery === 'string' && + isPermissionAction(permissionQuery) + ) { + permissionNames.push(permissionQuery); + } else { + throw new InputError( + `Invalid permission action query value: ${permissionQuery}. Permission name should be string.`, + ); + } + } + return permissionNames; + } + + if (typeof queryValue === 'string' && isPermissionAction(queryValue)) { + return [queryValue]; + } + throw new InputError( + `Invalid permission action query value: ${queryValue}. Permission name should be string.`, + ); + } + + getFirstQuery( + queryValue: string | string[] | ParsedQs | ParsedQs[] | undefined, + ): string { + if (!queryValue) { + return ''; + } + if (Array.isArray(queryValue)) { + if (typeof queryValue[0] === 'string') { + return queryValue[0].toString(); + } + throw new InputError(`This api doesn't support nested query`); + } + + if (typeof queryValue === 'string') { + return queryValue; + } + throw new InputError(`This api doesn't support nested query`); + } + + isPolicyFilterEnabled(request: Request): boolean { + return ( + !!request.query.entityRef || + !!request.query.permission || + !!request.query.policy || + !!request.query.effect + ); + } + + async processPolicies( + policyArray: RoleBasedPolicy[], + isOld?: boolean, + errorMessage?: string, + ): Promise { + const policies: string[][] = []; + const uniqueItems = new Set(); + for (const policy of policyArray) { + let err = validatePolicy(policy); + if (err) { + throw new InputError( + `Invalid ${errorMessage ?? 'policy'} definition. Cause: ${ + err.message + }`, + ); // 400 + } + + const metadata = await this.roleMetadata.findRoleMetadata( + policy.entityReference!, + ); + + let action = errorMessage ? 'edit' : 'delete'; + action = isOld ? action : 'add'; + + err = await validateSource('rest', metadata); + if (err) { + throw new NotAllowedError( + `Unable to ${action} policy ${policy.entityReference},${policy.permission},${policy.policy},${policy.effect}: ${err.message}`, + ); + } + + const transformedPolicy = this.transformPolicyToArray(policy); + if (isOld && !(await this.enforcer.hasPolicy(...transformedPolicy))) { + throw new NotFoundError( + `Policy '${policyToString(transformedPolicy)}' not found`, + ); // 404 + } + + if (!isOld && (await this.enforcer.hasPolicy(...transformedPolicy))) { + throw new ConflictError( + `Policy '${policyToString( + transformedPolicy, + )}' has been already stored`, + ); // 409 + } + + // We want to ensure that there are not duplicate permission policies + const rowString = JSON.stringify(transformedPolicy); + if (uniqueItems.has(rowString)) { + throw new ConflictError( + `Duplicate polices found; ${policy.entityReference}, ${policy.permission}, ${policy.policy}, ${policy.effect} is a duplicate`, + ); + } else { + uniqueItems.add(rowString); + policies.push(transformedPolicy); + } + } + return policies; + } + + nameSort(nameA: string, nameB: string): number { + if (nameA.toLocaleUpperCase('en-US') < nameB.toLocaleUpperCase('en-US')) { + return -1; + } + if (nameA.toLocaleUpperCase('en-US') > nameB.toLocaleUpperCase('en-US')) { + return 1; + } + return 0; + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/service/policy-builder.test.ts b/workspaces/rbac/plugins/rbac-backend/src/service/policy-builder.test.ts new file mode 100644 index 0000000000..846161331d --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/service/policy-builder.test.ts @@ -0,0 +1,384 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { mockServices } from '@backstage/backend-test-utils'; + +import type { Adapter, Enforcer } from 'casbin'; +import type { Router } from 'express'; +import type TypeORMAdapter from 'typeorm-adapter'; + +import type { + PluginIdProvider, + RBACProvider, +} from '@backstage-community/plugin-rbac-node'; + +import { CasbinDBAdapterFactory } from '../database/casbin-adapter-factory'; +import { RBACPermissionPolicy } from '../policies/permission-policy'; +import { PluginPermissionMetadataCollector } from './plugin-endpoints'; +import { PoliciesServer } from './policies-rest-api'; +import { PolicyBuilder } from './policy-builder'; + +const enforcerMock: Partial = { + loadPolicy: jest.fn().mockImplementation(async () => {}), + enableAutoSave: jest.fn().mockImplementation(() => {}), + setRoleManager: jest.fn().mockImplementation(() => {}), + enableAutoBuildRoleLinks: jest.fn().mockImplementation(() => {}), + buildRoleLinks: jest.fn().mockImplementation(() => {}), +}; + +jest.mock('casbin', () => { + const actualCasbin = jest.requireActual('casbin'); + return { + ...actualCasbin, + newEnforcer: jest.fn((): Promise> => { + return Promise.resolve(enforcerMock); + }), + FileAdapter: jest.fn((): Adapter => { + return {} as Adapter; + }), + }; +}); + +const dataBaseAdapterFactoryMock: Partial = { + createAdapter: jest.fn((): Promise => { + return Promise.resolve({} as TypeORMAdapter); + }), +}; + +jest.mock('../database/casbin-adapter-factory', () => { + return { + CasbinDBAdapterFactory: jest.fn((): Partial => { + return dataBaseAdapterFactoryMock; + }), + }; +}); + +const pluginMetadataCollectorMock: Partial = + { + getPluginConditionRules: jest.fn().mockImplementation(), + getPluginPolicies: jest.fn().mockImplementation(), + getMetadataByPluginId: jest.fn().mockImplementation(), + }; + +jest.mock('./plugin-endpoints', () => { + return { + PluginPermissionMetadataCollector: jest + .fn() + .mockImplementation(() => pluginMetadataCollectorMock), + }; +}); + +const mockRouter: Router = {} as Router; +const policiesServerMock: Partial = { + serve: jest.fn().mockImplementation(async () => { + return mockRouter; + }), +}; + +jest.mock('./policies-rest-api', () => { + return { + PoliciesServer: jest.fn().mockImplementation(() => policiesServerMock), + }; +}); + +jest.mock('../policies/permission-policy', () => { + return { + RBACPermissionPolicy: { + build: jest.fn((): Promise => { + return Promise.resolve({} as RBACPermissionPolicy); + }), + }, + }; +}); + +const providerMock: RBACProvider = { + getProviderName: jest.fn().mockImplementation(), + connect: jest.fn().mockImplementation(), + refresh: jest.fn().mockImplementation(), +}; + +describe('PolicyBuilder', () => { + const backendPluginIDsProviderMock = { + getPluginIds: jest.fn().mockImplementation(() => { + return []; + }), + }; + + const mockLoggerService = mockServices.logger.mock(); + + beforeEach(async () => { + jest.clearAllMocks(); + }); + + it('should build policy server', async () => { + const router = await PolicyBuilder.build( + { + config: mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + permission: { + enabled: true, + rbac: {}, + }, + }, + }), + logger: mockLoggerService, + discovery: mockServices.discovery.mock(), + permissions: mockServices.permissions.mock(), + userInfo: mockServices.userInfo.mock(), + auth: mockServices.auth.mock(), + httpAuth: mockServices.httpAuth.mock(), + lifecycle: mockServices.lifecycle.mock(), + }, + backendPluginIDsProviderMock, + ); + expect(CasbinDBAdapterFactory).toHaveBeenCalled(); + expect(enforcerMock.loadPolicy).toHaveBeenCalled(); + expect(enforcerMock.enableAutoSave).toHaveBeenCalled(); + expect(RBACPermissionPolicy.build).toHaveBeenCalled(); + + expect(PoliciesServer).toHaveBeenCalled(); + expect(policiesServerMock.serve).toHaveBeenCalled(); + expect(router).toBeTruthy(); + expect(router).toBe(mockRouter); + expect(mockLoggerService.info).toHaveBeenCalledWith( + 'RBAC backend plugin was enabled', + ); + }); + + it('should build policy server with rbac providers', async () => { + const router = await PolicyBuilder.build( + { + config: mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + permission: { + enabled: true, + rbac: {}, + }, + }, + }), + logger: mockLoggerService, + discovery: mockServices.discovery.mock(), + permissions: mockServices.permissions.mock(), + userInfo: mockServices.userInfo.mock(), + auth: mockServices.auth.mock(), + httpAuth: mockServices.httpAuth.mock(), + lifecycle: mockServices.lifecycle.mock(), + }, + backendPluginIDsProviderMock, + [providerMock], + ); + expect(CasbinDBAdapterFactory).toHaveBeenCalled(); + expect(enforcerMock.loadPolicy).toHaveBeenCalled(); + expect(enforcerMock.enableAutoSave).toHaveBeenCalled(); + expect(RBACPermissionPolicy.build).toHaveBeenCalled(); + expect(providerMock.connect).toHaveBeenCalled(); + + expect(PoliciesServer).toHaveBeenCalled(); + expect(policiesServerMock.serve).toHaveBeenCalled(); + expect(router).toBeTruthy(); + expect(router).toBe(mockRouter); + expect(mockLoggerService.info).toHaveBeenCalledWith( + 'RBAC backend plugin was enabled', + ); + }); + + it('should build policy server, but log warning that permission framework disabled', async () => { + const router = await PolicyBuilder.build( + { + config: mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + permission: { + enabled: false, + rbac: {}, + }, + }, + }), + logger: mockLoggerService, + discovery: mockServices.discovery.mock(), + permissions: mockServices.permissions.mock(), + userInfo: mockServices.userInfo.mock(), + auth: mockServices.auth.mock(), + httpAuth: mockServices.httpAuth.mock(), + lifecycle: mockServices.lifecycle.mock(), + }, + backendPluginIDsProviderMock, + ); + expect(CasbinDBAdapterFactory).toHaveBeenCalled(); + expect(enforcerMock.loadPolicy).toHaveBeenCalled(); + expect(enforcerMock.enableAutoSave).toHaveBeenCalled(); + expect(RBACPermissionPolicy.build).not.toHaveBeenCalled(); + + expect(PoliciesServer).toHaveBeenCalled(); + expect(policiesServerMock.serve).toHaveBeenCalled(); + expect(router).toBeTruthy(); + expect(router).toBe(mockRouter); + expect(mockLoggerService.warn).toHaveBeenCalledWith( + 'RBAC backend plugin was disabled by application config permission.enabled: false', + ); + }); + + it('should get list plugin ids from application configuration', async () => { + const pluginIdProvider: PluginIdProvider = { getPluginIds: () => [] }; + const router = await PolicyBuilder.build( + { + config: mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + permission: { + enabled: true, + rbac: { + pluginsWithPermission: ['catalog'], + }, + }, + }, + }), + logger: mockLoggerService, + discovery: mockServices.discovery.mock(), + permissions: mockServices.permissions.mock(), + userInfo: mockServices.userInfo.mock(), + auth: mockServices.auth.mock(), + httpAuth: mockServices.httpAuth.mock(), + lifecycle: mockServices.lifecycle.mock(), + }, + pluginIdProvider, + ); + expect(CasbinDBAdapterFactory).toHaveBeenCalled(); + expect(enforcerMock.loadPolicy).toHaveBeenCalled(); + expect(enforcerMock.enableAutoSave).toHaveBeenCalled(); + expect(RBACPermissionPolicy.build).toHaveBeenCalled(); + + expect(PoliciesServer).toHaveBeenCalled(); + expect(policiesServerMock.serve).toHaveBeenCalled(); + expect(router).toBeTruthy(); + expect(router).toBe(mockRouter); + expect(mockLoggerService.info).toHaveBeenCalledWith( + 'RBAC backend plugin was enabled', + ); + + expect(pluginIdProvider.getPluginIds()).toEqual(['catalog']); + }); + + it('should merge list plugin ids from application configuration and build method', async () => { + const pluginIdProvider: PluginIdProvider = { getPluginIds: () => ['rbac'] }; + const router = await PolicyBuilder.build( + { + config: mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + permission: { + enabled: true, + rbac: { + pluginsWithPermission: ['catalog'], + }, + }, + }, + }), + logger: mockLoggerService, + discovery: mockServices.discovery.mock(), + permissions: mockServices.permissions.mock(), + userInfo: mockServices.userInfo.mock(), + auth: mockServices.auth.mock(), + httpAuth: mockServices.httpAuth.mock(), + lifecycle: mockServices.lifecycle.mock(), + }, + pluginIdProvider, + ); + expect(CasbinDBAdapterFactory).toHaveBeenCalled(); + expect(enforcerMock.loadPolicy).toHaveBeenCalled(); + expect(enforcerMock.enableAutoSave).toHaveBeenCalled(); + expect(RBACPermissionPolicy.build).toHaveBeenCalled(); + + expect(PoliciesServer).toHaveBeenCalled(); + expect(policiesServerMock.serve).toHaveBeenCalled(); + expect(router).toBeTruthy(); + expect(router).toBe(mockRouter); + expect(mockLoggerService.info).toHaveBeenCalledWith( + 'RBAC backend plugin was enabled', + ); + + expect(pluginIdProvider.getPluginIds()).toEqual(['catalog', 'rbac']); + }); + + it('should get list plugin ids from application configuration, but provider should be created by default', async () => { + const router = await PolicyBuilder.build({ + config: mockServices.rootConfig({ + data: { + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + permission: { + enabled: true, + rbac: { + pluginsWithPermission: ['catalog'], + }, + }, + }, + }), + logger: mockLoggerService, + discovery: mockServices.discovery.mock(), + permissions: mockServices.permissions.mock(), + userInfo: mockServices.userInfo.mock(), + auth: mockServices.auth.mock(), + httpAuth: mockServices.httpAuth.mock(), + lifecycle: mockServices.lifecycle.mock(), + }); + expect(CasbinDBAdapterFactory).toHaveBeenCalled(); + expect(enforcerMock.loadPolicy).toHaveBeenCalled(); + expect(enforcerMock.enableAutoSave).toHaveBeenCalled(); + expect(RBACPermissionPolicy.build).toHaveBeenCalled(); + + expect(policiesServerMock.serve).toHaveBeenCalled(); + expect(router).toBeTruthy(); + expect(router).toBe(mockRouter); + expect(mockLoggerService.info).toHaveBeenCalledWith( + 'RBAC backend plugin was enabled', + ); + const pIdProvider = ( + PluginPermissionMetadataCollector as unknown as jest.Mock + ).mock.calls[0][0].deps.pluginIdProvider; + expect(pIdProvider.getPluginIds()).toEqual(['catalog']); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/service/policy-builder.ts b/workspaces/rbac/plugins/rbac-backend/src/service/policy-builder.ts new file mode 100644 index 0000000000..7bd226182b --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/service/policy-builder.ts @@ -0,0 +1,208 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DatabaseManager } from '@backstage/backend-defaults/database'; +import type { + AuthService, + DiscoveryService, + HttpAuthService, + LifecycleService, + LoggerService, + UserInfoService, +} from '@backstage/backend-plugin-api'; +import { CatalogClient } from '@backstage/catalog-client'; +import type { Config } from '@backstage/config'; +import type { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import { PermissionPolicy } from '@backstage/plugin-permission-node'; + +import { DefaultAuditLogger } from '@janus-idp/backstage-plugin-audit-log-node'; +import { newEnforcer, newModelFromString } from 'casbin'; +import type { Router } from 'express'; + +import type { + PluginIdProvider, + RBACProvider, +} from '@backstage-community/plugin-rbac-node'; + +import { CasbinDBAdapterFactory } from '../database/casbin-adapter-factory'; +import { DataBaseConditionalStorage } from '../database/conditional-storage'; +import { migrate } from '../database/migration'; +import { DataBaseRoleMetadataStorage } from '../database/role-metadata'; +import { AllowAllPolicy } from '../policies/allow-all-policy'; +import { RBACPermissionPolicy } from '../policies/permission-policy'; +import { connectRBACProviders } from '../providers/connect-providers'; +import { BackstageRoleManager } from '../role-manager/role-manager'; +import { EnforcerDelegate } from './enforcer-delegate'; +import { MODEL } from './permission-model'; +import { PluginPermissionMetadataCollector } from './plugin-endpoints'; +import { PoliciesServer } from './policies-rest-api'; + +export type EnvOptions = { + config: Config; + logger: LoggerService; + discovery: DiscoveryService; + permissions: PermissionEvaluator; + auth: AuthService; + httpAuth: HttpAuthService; + userInfo: UserInfoService; + lifecycle: LifecycleService; +}; + +export type RBACRouterOptions = { + config: Config; + logger: LoggerService; + discovery: DiscoveryService; + policy: PermissionPolicy; + auth: AuthService; + httpAuth: HttpAuthService; + userInfo: UserInfoService; +}; + +export class PolicyBuilder { + public static async build( + env: EnvOptions, + pluginIdProvider: PluginIdProvider = { getPluginIds: () => [] }, + rbacProviders?: Array, + ): Promise { + let policy: PermissionPolicy; + + const databaseManager = DatabaseManager.fromConfig(env.config).forPlugin( + 'permission', + { logger: env.logger, lifecycle: env.lifecycle }, + ); + + const databaseClient = await databaseManager.getClient(); + + const adapter = await new CasbinDBAdapterFactory( + env.config, + databaseClient, + ).createAdapter(); + + const enf = await newEnforcer(newModelFromString(MODEL), adapter); + await enf.loadPolicy(); + enf.enableAutoSave(true); + + const catalogClient = new CatalogClient({ discoveryApi: env.discovery }); + const catalogDBClient = await DatabaseManager.fromConfig(env.config) + .forPlugin('catalog', { logger: env.logger, lifecycle: env.lifecycle }) + .getClient(); + + const rm = new BackstageRoleManager( + catalogClient, + env.logger, + catalogDBClient, + databaseClient, + env.config, + env.auth, + ); + enf.setRoleManager(rm); + enf.enableAutoBuildRoleLinks(false); + await enf.buildRoleLinks(); + + await migrate(databaseManager); + + const conditionStorage = new DataBaseConditionalStorage(databaseClient); + + const roleMetadataStorage = new DataBaseRoleMetadataStorage(databaseClient); + const enforcerDelegate = new EnforcerDelegate( + enf, + roleMetadataStorage, + databaseClient, + ); + + const defAuditLog = new DefaultAuditLogger({ + logger: env.logger, + authService: env.auth, + httpAuthService: env.httpAuth, + }); + + if (rbacProviders) { + await connectRBACProviders( + rbacProviders, + enforcerDelegate, + roleMetadataStorage, + env.logger, + defAuditLog, + ); + } + + const pluginIdsConfig = env.config.getOptionalStringArray( + 'permission.rbac.pluginsWithPermission', + ); + if (pluginIdsConfig) { + const pluginIds = new Set([ + ...pluginIdsConfig, + ...pluginIdProvider.getPluginIds(), + ]); + pluginIdProvider.getPluginIds = () => { + return [...pluginIds]; + }; + } + + const pluginPermMetaData = new PluginPermissionMetadataCollector({ + deps: { + discovery: env.discovery, + pluginIdProvider: pluginIdProvider, + logger: env.logger, + config: env.config, + }, + }); + + const isPluginEnabled = env.config.getOptionalBoolean('permission.enabled'); + if (isPluginEnabled) { + env.logger.info('RBAC backend plugin was enabled'); + + policy = await RBACPermissionPolicy.build( + env.logger, + defAuditLog, + env.config, + conditionStorage, + enforcerDelegate, + roleMetadataStorage, + databaseClient, + pluginPermMetaData, + env.auth, + ); + } else { + env.logger.warn( + 'RBAC backend plugin was disabled by application config permission.enabled: false', + ); + + policy = new AllowAllPolicy(); + } + + const options: RBACRouterOptions = { + config: env.config, + logger: env.logger, + discovery: env.discovery, + policy, + auth: env.auth, + httpAuth: env.httpAuth, + userInfo: env.userInfo, + }; + + const server = new PoliciesServer( + env.permissions, + options, + enforcerDelegate, + conditionStorage, + pluginPermMetaData, + roleMetadataStorage, + defAuditLog, + rbacProviders, + ); + return server.serve(); + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/service/router.test.ts b/workspaces/rbac/plugins/rbac-backend/src/service/router.test.ts new file mode 100644 index 0000000000..f3a7d25afd --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/service/router.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { mockServices } from '@backstage/backend-test-utils'; + +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter({ + logger: mockServices.logger.mock(), + config: mockServices.rootConfig(), + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/service/router.ts b/workspaces/rbac/plugins/rbac-backend/src/service/router.ts new file mode 100644 index 0000000000..0d60187307 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/service/router.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter'; +import type { LoggerService } from '@backstage/backend-plugin-api'; +import type { Config } from '@backstage/config'; + +import express from 'express'; + +export interface RouterOptions { + logger: LoggerService; + config: Config; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger, config } = options; + + const router = express.Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.json({ status: 'ok' }); + }); + + const middleware = MiddlewareFactory.create({ logger, config }); + + router.use(middleware.error()); + return router; +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/setupTests.ts b/workspaces/rbac/plugins/rbac-backend/src/setupTests.ts new file mode 100644 index 0000000000..c7ce5c0988 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export {}; diff --git a/workspaces/rbac/plugins/rbac-backend/src/validation/condition-validation.test.ts b/workspaces/rbac/plugins/rbac-backend/src/validation/condition-validation.test.ts new file mode 100644 index 0000000000..e425a292e2 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/validation/condition-validation.test.ts @@ -0,0 +1,966 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { AuthorizeResult } from '@backstage/plugin-permission-common'; + +import type { + PermissionAction, + RoleConditionalPolicyDecision, +} from '@backstage-community/plugin-rbac-common'; + +import { validateRoleCondition } from './condition-validation'; + +describe('condition-validation', () => { + describe('validation common fields', () => { + it('should fail validation role condition without pluginId', () => { + const condition: any = { + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'pluginId' must be specified in the role condition`, + ); + }); + + it('should fail validation role condition without resourceType', () => { + const condition: any = { + pluginId: 'catalog', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'resourceType' must be specified in the role condition`, + ); + }); + + it('should fail validation role condition without permissionMapping', () => { + const condition: any = { + resourceType: 'catalog-entity', + pluginId: 'catalog', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'permissionMapping' must be non empty array in the role condition`, + ); + }); + + it('should fail validation role condition with empty array permissionMapping', () => { + const condition: any = { + resourceType: 'catalog-entity', + pluginId: 'catalog', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: [], + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'permissionMapping' must be non empty array in the role condition`, + ); + }); + + it('should fail validation role condition with array permissionMapping, but with wrong action value', () => { + const condition: any = { + resourceType: 'catalog-entity', + pluginId: 'catalog', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['wrong-value'], + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'permissionMapping' array contains non action value: 'wrong-value'`, + ); + }); + + it('should fail validation role condition without role entity reference', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'roleEntityRef' must be specified in the role condition`, + ); + }); + + it('should fail validation role condition without result', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + permissionMapping: ['read'], + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'result' must be specified in the role condition`, + ); + }); + + it('should fail validation role condition without conditions', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'conditions' must be specified in the role condition`, + ); + }); + }); + + describe('validate simple condition', () => { + it('should fail validation role-condition.conditions without rule', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'rule' must be specified in the roleCondition.conditions.condition`, + ); + }); + + it('should fail validation role-condition.conditions without resourceType', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + rule: 'IS_ENTITY_OWNER', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'resourceType' must be specified in the roleCondition.conditions.condition`, + ); + }); + + it('should validate role-condition.conditions without errors', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + }; + let unexpectedErr; + try { + validateRoleCondition(condition); + } catch (err) { + unexpectedErr = err; + } + expect(unexpectedErr).toBeUndefined(); + }); + + it('should validate role-condition.conditions with permission policy action of use without errors', () => { + const condition: any = { + pluginId: 'scaffolder', + resourceType: 'scaffolder-action', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['use'], + conditions: { + rule: 'HAS_ACTION_ID', + resourceType: 'scaffolder-action', + params: { + actionId: 'quay:create-repository', + }, + }, + }; + let unexpectedErr; + try { + validateRoleCondition(condition); + } catch (err) { + unexpectedErr = err; + } + expect(unexpectedErr).toBeUndefined(); + }); + }); + + describe('validate "not" criteria', () => { + it('should fail validation role-condition.conditions.not without rule', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + permissionMapping: ['read'], + result: AuthorizeResult.CONDITIONAL, + conditions: { + not: { + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'rule' must be specified in the roleCondition.conditions.not.condition`, + ); + }); + + it('should fail validation role-condition.conditions.not without resourceType', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + not: { + rule: 'IS_ENTITY_OWNER', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'resourceType' must be specified in the roleCondition.conditions.not.condition`, + ); + }); + + it('should validate role-condition.conditions.not without errors', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + not: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + }, + }; + let unexpectedErr; + try { + validateRoleCondition(condition); + } catch (err) { + unexpectedErr = err; + } + + expect(unexpectedErr).toBeUndefined(); + }); + }); + + describe('validate anyOf criteria', () => { + it('should fail validation role-condition.conditions.anyOf with an empty array value', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + anyOf: [], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `roleCondition.conditions.anyOf criteria must be non empty array`, + ); + }); + + it('should fail validation role-condition.conditions.anyOf with non array value', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + anyOf: { + rule: 'IS_ENTITY_OWNER', + params: { + claims: ['group:default/team-a'], + }, + }, + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `roleCondition.conditions.anyOf criteria must be non empty array`, + ); + }); + + it('should fail validation role-condition.conditions.anyOf without resourceType in the first param', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'resourceType' must be specified in the roleCondition.conditions.anyOf[0].condition`, + ); + }); + + it('should fail validation role-condition.conditions.anyOf without resourceType in the second param', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'resourceType' must be specified in the roleCondition.conditions.anyOf[1].condition`, + ); + }); + + it('should fail validation role-condition.conditions.anyOf without rule in the first param', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + anyOf: [ + { + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'rule' must be specified in the roleCondition.conditions.anyOf[0].condition`, + ); + }); + + it('should fail validation role-condition.conditions.anyOf without rule in the second param', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'rule' must be specified in the roleCondition.conditions.anyOf[1].condition`, + ); + }); + + it('should validate role-condition.conditions.anyOf without errors', () => { + const condition: RoleConditionalPolicyDecision = { + id: 1, + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + let unexpectedErr; + try { + validateRoleCondition(condition); + } catch (err) { + unexpectedErr = err; + } + expect(unexpectedErr).toBeUndefined(); + }); + }); + + describe('validate allOf criteria', () => { + it('should fail validation role-condition.conditions.allOf with an empty array value', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + allOf: [], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `roleCondition.conditions.allOf criteria must be non empty array`, + ); + }); + + it('should fail validation role-condition.conditions.allOf with non array value', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + allOf: { + rule: 'IS_ENTITY_OWNER', + params: { + claims: ['group:default/team-a'], + }, + }, + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `roleCondition.conditions.allOf criteria must be non empty array`, + ); + }); + + it('should fail validation role-condition.conditions.allOf without resourceType in the first param', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'resourceType' must be specified in the roleCondition.conditions.allOf[0].condition`, + ); + }); + + it('should fail validation role-condition.conditions.allOf without resourceType in the second param', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'resourceType' must be specified in the roleCondition.conditions.allOf[1].condition`, + ); + }); + + it('should fail validation role-condition.conditions.allOf without rule in the first param', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + allOf: [ + { + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'rule' must be specified in the roleCondition.conditions.allOf[0].condition`, + ); + }); + + it('should fail validation role-condition.conditions.allOf without rule in the second param', () => { + const condition: any = { + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `'rule' must be specified in the roleCondition.conditions.allOf[1].condition`, + ); + }); + + it('should success validation role-condition.conditions.allOf', () => { + const condition: RoleConditionalPolicyDecision = { + id: 1, + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + let unexpectedErr; + try { + validateRoleCondition(condition); + } catch (err) { + unexpectedErr = err; + } + expect(unexpectedErr).toBeUndefined(); + }); + }); + + describe('complex conditions', () => { + it('should fail validation of role-condition.conditions in parallel with condition rule', () => { + const condition: RoleConditionalPolicyDecision = { + id: 1, + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `RBAC plugin does not support parallel conditions alongside rules, consider reworking request to include nested condition criteria. Conditional criteria causing the error allOf, 'rule: IS_ENTITY_OWNER'.`, + ); + }); + + it('should fail validation of role-condition.conditions criteria (allOf, not) in parallel', () => { + const condition: RoleConditionalPolicyDecision = { + id: 1, + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + not: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `RBAC plugin does not support parallel conditions, consider reworking request to include nested condition criteria. Conditional criteria causing the error allOf,not.`, + ); + }); + + it('should fail validation of role-condition.conditions criteria (allOf, anyOf) in parallel', () => { + const condition: RoleConditionalPolicyDecision = { + id: 1, + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `RBAC plugin does not support parallel conditions, consider reworking request to include nested condition criteria. Conditional criteria causing the error allOf,anyOf.`, + ); + }); + + it('should fail validation of role-condition.conditions criteria (not, anyOf) in parallel', () => { + const condition: RoleConditionalPolicyDecision = { + id: 1, + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + not: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + expect(() => validateRoleCondition(condition)).toThrow( + `RBAC plugin does not support parallel conditions, consider reworking request to include nested condition criteria. Conditional criteria causing the error anyOf,not.`, + ); + }); + + it('should validate role-condition.conditions that are nested', () => { + const condition: RoleConditionalPolicyDecision = { + id: 1, + pluginId: 'catalog', + resourceType: 'catalog-entity', + roleEntityRef: 'role:default/test', + result: AuthorizeResult.CONDITIONAL, + permissionMapping: ['read'], + conditions: { + anyOf: [ + { + not: { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + }, + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/logarifm', 'group:default/team-a'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + ], + }, + }; + + let unexpectedErr; + try { + validateRoleCondition(condition); + } catch (err) { + unexpectedErr = err; + } + expect(unexpectedErr).toBeUndefined(); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/validation/condition-validation.ts b/workspaces/rbac/plugins/rbac-backend/src/validation/condition-validation.ts new file mode 100644 index 0000000000..14f2a9b83d --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/validation/condition-validation.ts @@ -0,0 +1,187 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + PermissionCondition, + PermissionCriteria, + PermissionRuleParams, +} from '@backstage/plugin-permission-common'; + +import type { + PermissionAction, + RoleConditionalPolicyDecision, +} from '@backstage-community/plugin-rbac-common'; + +import { isPermissionAction } from '../helper'; + +export function validateRoleCondition( + condition: RoleConditionalPolicyDecision, +) { + if (!condition.roleEntityRef) { + throw new Error(`'roleEntityRef' must be specified in the role condition`); + } + if (!condition.result) { + throw new Error(`'result' must be specified in the role condition`); + } + if (!condition.pluginId) { + throw new Error(`'pluginId' must be specified in the role condition`); + } + if (!condition.resourceType) { + throw new Error(`'resourceType' must be specified in the role condition`); + } + + if ( + !condition.permissionMapping || + condition.permissionMapping.length === 0 + ) { + throw new Error( + `'permissionMapping' must be non empty array in the role condition`, + ); + } + const nonActionValue = condition.permissionMapping.find( + action => !isPermissionAction(action), + ); + if (nonActionValue) { + throw new Error( + `'permissionMapping' array contains non action value: '${nonActionValue}'`, + ); + } + + if (!condition.conditions) { + throw new Error(`'conditions' must be specified in the role condition`); + } + if (condition.conditions) { + validatePermissionCondition( + condition.conditions, + 'roleCondition.conditions', + ); + } +} + +/** + * validatePermissionCondition validate conditional permission policies using validateCriteria and validateRule. + * @param conditionOrCriteria The Permission Criteria of the conditional permission. + * @param jsonPathLocator The location in the JSON of the current check. + * @returns undefined. + */ +function validatePermissionCondition( + conditionOrCriteria: PermissionCriteria< + PermissionCondition + >, + jsonPathLocator: string, +) { + validateCriteria(conditionOrCriteria, jsonPathLocator); + + if ('not' in conditionOrCriteria) { + validatePermissionCondition( + conditionOrCriteria.not, + `${jsonPathLocator}.not`, + ); + return; + } + + if ('allOf' in conditionOrCriteria) { + if ( + !Array.isArray(conditionOrCriteria.allOf) || + conditionOrCriteria.allOf.length === 0 + ) { + throw new Error( + `${jsonPathLocator}.allOf criteria must be non empty array`, + ); + } + for (const [index, elem] of conditionOrCriteria.allOf.entries()) { + validatePermissionCondition(elem, `${jsonPathLocator}.allOf[${index}]`); + } + return; + } + + if ('anyOf' in conditionOrCriteria) { + if ( + !Array.isArray(conditionOrCriteria.anyOf) || + conditionOrCriteria.anyOf.length === 0 + ) { + throw new Error( + `${jsonPathLocator}.anyOf criteria must be non empty array`, + ); + } + for (const [index, elem] of conditionOrCriteria.anyOf.entries()) { + validatePermissionCondition(elem, `${jsonPathLocator}.anyOf[${index}]`); + } + } +} + +/** + * validateRule ensures that there is a rule and resource type associated with each conditional permission. + * @param conditionOrCriteria The Permission Criteria of the conditional permission. + * @param jsonPathLocator The location in the JSON of the current check. + */ +function validateRule( + conditionOrCriteria: PermissionCriteria< + PermissionCondition + >, + jsonPathLocator: string, +) { + if (!('resourceType' in conditionOrCriteria)) { + throw new Error( + `'resourceType' must be specified in the ${jsonPathLocator}.condition`, + ); + } + if (!('rule' in conditionOrCriteria)) { + throw new Error( + `'rule' must be specified in the ${jsonPathLocator}.condition`, + ); + } +} + +/** + * validateCriteria ensures that there is only one of the following criteria: allOf, anyOf, and not, at any given level. + * We want to make sure that there are no parallel conditional criteria for conditional permission policies as this is + * not support by the permission framework. + * + * If more than one criteria are at a given level, we throw an error about the inability to support parallel conditions. + * If no criteria are found, we validate the rule. + * + * @param conditionOrCriteria The Permission Criteria of the conditional permission. + * @param jsonPathLocator The location in the JSON of the current check. + */ +function validateCriteria( + conditionOrCriteria: PermissionCriteria< + PermissionCondition + >, + jsonPathLocator: string, +) { + const criteriaList = ['allOf', 'anyOf', 'not']; + const found: string[] = []; + + for (const crit of criteriaList) { + if (crit in conditionOrCriteria) { + found.push(crit); + } + } + + if (found.length > 1) { + throw new Error( + `RBAC plugin does not support parallel conditions, consider reworking request to include nested condition criteria. Conditional criteria causing the error ${found}.`, + ); + } else if (found.length === 0) { + validateRule(conditionOrCriteria, jsonPathLocator); + } + + if (found.length === 1 && 'rule' in conditionOrCriteria) { + throw new Error( + `RBAC plugin does not support parallel conditions alongside rules, consider reworking request to include nested condition criteria. Conditional criteria causing the error ${found}, 'rule: ${conditionOrCriteria.rule}'.`, + ); + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/src/validation/policies-validation.test.ts b/workspaces/rbac/plugins/rbac-backend/src/validation/policies-validation.test.ts new file mode 100644 index 0000000000..1b52272f95 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/validation/policies-validation.test.ts @@ -0,0 +1,445 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Knex from 'knex'; + +import type { + RoleBasedPolicy, + Source, +} from '@backstage-community/plugin-rbac-common'; + +import { + RoleMetadataDao, + RoleMetadataStorage, +} from '../database/role-metadata'; +import { + validateEntityReference, + validateGroupingPolicy, + validatePolicy, + validateRole, + validateSource, +} from './policies-validation'; + +const modifiedBy = 'user:default/some-admin'; + +const roleMetadataStorageMock: RoleMetadataStorage = { + filterRoleMetadata: jest.fn().mockImplementation(() => []), + findRoleMetadata: jest + .fn() + .mockImplementation( + async ( + _roleEntityRef: string, + _trx: Knex.Knex.Transaction, + ): Promise => { + return { + roleEntityRef: 'role:default/catalog-reader', + source: 'rest', + modifiedBy, + }; + }, + ), + createRoleMetadata: jest.fn().mockImplementation(), + updateRoleMetadata: jest.fn().mockImplementation(), + removeRoleMetadata: jest.fn().mockImplementation(), +}; + +describe('rest data validation', () => { + describe('validate entity referenced policy', () => { + it('should return an error when entity reference is empty', () => { + const policy: RoleBasedPolicy = {}; + const err = validatePolicy(policy); + expect(err).toBeTruthy(); + expect(err?.message).toEqual(`'entityReference' must not be empty`); + }); + + it('should return an error when permission is empty', () => { + const policy: RoleBasedPolicy = { + entityReference: 'user:default/guest', + }; + const err = validatePolicy(policy); + expect(err).toBeTruthy(); + expect(err?.message).toEqual(`'permission' field must not be empty`); + }); + + it('should return an error when policy is empty', () => { + const policy: RoleBasedPolicy = { + entityReference: 'user:default/guest', + permission: 'catalog-entity', + }; + const err = validatePolicy(policy); + expect(err).toBeTruthy(); + expect(err?.message).toEqual(`'policy' field must not be empty`); + }); + + it('should return an error when policy has an invalid value', () => { + const policy: RoleBasedPolicy = { + entityReference: 'user:default/guest', + permission: 'catalog-entity', + policy: 'invalid-policy', + effect: 'allow', + }; + const err = validatePolicy(policy); + expect(err).toBeTruthy(); + expect(err?.message).toEqual( + `'policy' has invalid value: 'invalid-policy'. It should be one of: create, read, update, delete, use`, + ); + }); + + it('should return an error when effect is empty', () => { + const policy: RoleBasedPolicy = { + entityReference: 'user:default/guest', + permission: 'catalog-entity', + policy: 'read', + }; + const err = validatePolicy(policy); + expect(err).toBeTruthy(); + expect(err?.message).toEqual(`'effect' field must not be empty`); + }); + + it('should return an error when effect has an invalid value', () => { + const policy: RoleBasedPolicy = { + entityReference: 'user:default/guest', + permission: 'catalog-entity', + policy: 'read', + effect: 'invalid-effect', + }; + const err = validatePolicy(policy); + expect(err).toBeTruthy(); + expect(err?.message).toEqual( + `'effect' has invalid value: 'invalid-effect'. It should be: 'allow' or 'deny'`, + ); + }); + + it(`pass validation when all fields are valid. Effect 'allow' should be valid`, () => { + const policy: RoleBasedPolicy = { + entityReference: 'user:default/guest', + permission: 'catalog-entity', + policy: 'read', + effect: 'allow', + }; + const err = validatePolicy(policy); + expect(err).toBeUndefined(); + }); + + it(`pass validation when all fields are valid. Effect 'deny' should be valid`, () => { + const policy: RoleBasedPolicy = { + entityReference: 'user:default/guest', + permission: 'catalog-entity', + policy: 'read', + effect: 'deny', + }; + const err = validatePolicy(policy); + expect(err).toBeUndefined(); + }); + }); + + describe('validate entity reference', () => { + it('should return an error when entity reference is an empty', () => { + const err = validateEntityReference(''); + expect(err).toBeTruthy(); + expect(err?.message).toEqual(`'entityReference' must not be empty`); + }); + + it('should return an error when entity reference is not full or invalid', () => { + const invalidOrUnsupportedEntityRefs = [ + { + ref: 'admin', + expectedError: `Entity reference "admin" had missing or empty kind (e.g. did not start with "component:" or similar)`, + }, + { + ref: 'admin:default', + expectedError: `entity reference 'admin:default' does not match the required format [:][/]. Provide, please, full entity reference.`, + }, + { + ref: 'admin/guest', + expectedError: `Entity reference "admin/guest" had missing or empty kind (e.g. did not start with "component:" or similar)`, + }, + { + ref: 'admin/guest/somewhere', + expectedError: `Entity reference "admin/guest/somewhere" had missing or empty kind (e.g. did not start with "component:" or similar)`, + }, + { + ref: ':default/admin', + expectedError: `Entity reference ":default/admin" was not on the form [:][/]`, + }, + { + ref: 'user:/admin', + expectedError: `Entity reference "user:/admin" was not on the form [:][/]`, + }, + { + ref: 'user:default/', + expectedError: `Entity reference "user:default/" was not on the form [:][/]`, + }, + { + ref: 'user:/', + expectedError: `Entity reference "user:/" was not on the form [:][/]`, + }, + { + ref: ':default/', + expectedError: `Entity reference ":default/" was not on the form [:][/]`, + }, + { + ref: ':/guest', + expectedError: `Entity reference ":/guest" was not on the form [:][/]`, + }, + { + ref: ':/', + expectedError: `Entity reference ":/" was not on the form [:][/]`, + }, + { + ref: '/admin', + expectedError: `Entity reference "/admin" was not on the form [:][/]`, + }, + { + ref: 'user/', + expectedError: `Entity reference "user/" was not on the form [:][/]`, + }, + { + ref: ':default', + expectedError: `Entity reference ":default" was not on the form [:][/]`, + }, + { + ref: 'user:', + expectedError: `Entity reference "user:" was not on the form [:][/]`, + }, + { + ref: 'admin:default/test', + expectedError: `Unsupported kind admin. List supported values ["user", "group", "role"]`, + }, + ]; + for (const entityRef of invalidOrUnsupportedEntityRefs) { + const err = validateEntityReference(entityRef.ref); + expect(err).toBeTruthy(); + expect(err?.message).toEqual(entityRef.expectedError); + } + }); + + it('should return an error when entity reference name is invalid', () => { + const invalidEntityNames = [ + 'john@doe', + 'John Doe', + 'John/Doe', + 'invalid-', + 'invalid_', + '.invalid', + `too-long${'1'.repeat(60)}`, + ]; + + for (const invalidName of invalidEntityNames) { + const expectedError = `The name '${invalidName}' in the entity reference must be a string that is sequences of [a-zA-Z0-9] separated by any of [-_.], at most 63 characters in total`; + const entityRef = `user:default/${invalidName}`; + const err = validateEntityReference(entityRef); + expect(err).toBeTruthy(); + expect(err?.message).toEqual(expectedError); + } + }); + + it('should return an error when entity reference namespace is invalid', () => { + const invalidEntityNamespaces = [ + 'INVALID', + 'invalid-', + '-invalid', + 'invalid$namespace', + `too-long${'1'.repeat(60)}`, + ]; + + for (const invalidNamespace of invalidEntityNamespaces) { + const expectedError = `The namespace '${invalidNamespace}' in the entity reference must be a string that is sequences of [a-z0-9] separated by [-], at most 63 characters in total`; + const entityRef = `user:${invalidNamespace}/doe`; + const err = validateEntityReference(entityRef); + expect(err).toBeTruthy(); + expect(err?.message).toEqual(expectedError); + } + }); + + it('should pass entity reference validation', () => { + const validEntityRefs = [ + 'user:default/guest', + 'role:default/team-a', + 'role:default/team_1', + 'role:default/team.A', + 'role:custom-1/doe', + ]; + for (const entityRef of validEntityRefs) { + const err = validateEntityReference(entityRef); + expect(err).toBeFalsy(); + } + }); + }); + + describe('validateRole', () => { + it('should return an error when "memberReferences" query param is missing', () => { + const request = { name: 'role:default/user' } as any; + const err = validateRole(request); + expect(err).toBeTruthy(); + expect(err?.message).toEqual( + `'memberReferences' field must not be empty`, + ); + }); + + it('should pass validation when all required query params are present', () => { + const request = { + memberReferences: ['user:default/guest'], + name: 'role:default/user', + } as any; + const err = validateRole(request); + expect(err).toBeUndefined(); + }); + }); + + describe('validateSource', () => { + const roleMeta: RoleMetadataDao = { + roleEntityRef: 'role:default/catalog-reader', + source: 'rest', + modifiedBy, + }; + + it('should not return an error whenever the source that is passed matches the source of the role', async () => { + const source: Source = 'rest'; + + const err = await validateSource(source, roleMeta); + + expect(err).toBeUndefined(); + }); + + it('should not return an error whenever the source that is passed does not match a legacy source role', async () => { + const roleMetaLegacy: RoleMetadataDao = { + roleEntityRef: 'role:default/legacy-reader', + source: 'legacy', + modifiedBy, + }; + + const source: Source = 'rest'; + + const err = await validateSource(source, roleMetaLegacy); + + expect(err).toBeUndefined(); + }); + + it('should return an error whenever the source that is passed does not match the source of the role', async () => { + const source: Source = 'csv-file'; + + const err = await validateSource(source, roleMeta); + + expect(err).toBeTruthy(); + expect(err?.message).toEqual( + `source does not match originating role ${ + roleMeta.roleEntityRef + }, consider making changes to the '${roleMeta.source.toLocaleUpperCase()}'`, + ); + }); + }); + + describe('validateGroupingPolicy', () => { + let groupPolicy = ['user:default/test', 'role:default/catalog-reader']; + let source: Source = 'rest'; + const roleMeta: RoleMetadataDao = { + roleEntityRef: 'role:default/catalog-reader', + source: 'rest', + modifiedBy, + }; + + it('should not return an error during validation', async () => { + const err = await validateGroupingPolicy( + groupPolicy, + roleMetadataStorageMock, + source, + ); + + expect(err).toBeUndefined(); + }); + + it('should return an error if the grouping policy is too long', async () => { + groupPolicy = [ + 'user:default/test', + 'role:default/catalog-reader', + 'extra', + ]; + + const err = await validateGroupingPolicy( + groupPolicy, + roleMetadataStorageMock, + source, + ); + + expect(err).toBeTruthy(); + expect(err?.message).toEqual(`Group policy should have length 2`); + }); + + it('should return an error if a member starts with role:', async () => { + groupPolicy = ['role:default/test', 'role:default/catalog-reader']; + + const err = await validateGroupingPolicy( + groupPolicy, + roleMetadataStorageMock, + source, + ); + + expect(err).toBeTruthy(); + expect(err?.message).toEqual( + `Group policy is invalid: ${groupPolicy}. rbac-backend plugin doesn't support role inheritance.`, + ); + }); + + it('should return an error for group inheritance (user to group)', async () => { + groupPolicy = ['user:default/test', 'group:default/catalog-reader']; + + const err = await validateGroupingPolicy( + groupPolicy, + roleMetadataStorageMock, + source, + ); + + expect(err).toBeTruthy(); + expect(err?.message).toEqual( + `Group policy is invalid: ${groupPolicy}. User membership information could be provided only with help of Catalog API.`, + ); + }); + + it('should return an error for group inheritance (group to group)', async () => { + groupPolicy = ['group:default/test', 'group:default/catalog-reader']; + + const err = await validateGroupingPolicy( + groupPolicy, + roleMetadataStorageMock, + source, + ); + + expect(err).toBeTruthy(); + expect(err?.message).toEqual( + `Group policy is invalid: ${groupPolicy}. Group inheritance information could be provided only with help of Catalog API.`, + ); + }); + + it('should return an error for mismatch source', async () => { + groupPolicy = ['user:default/test', 'role:default/catalog-reader']; + source = 'csv-file'; + + const err = await validateGroupingPolicy( + groupPolicy, + roleMetadataStorageMock, + source, + ); + + expect(err).toBeTruthy(); + expect(err?.name).toEqual('NotAllowedError'); + expect(err?.message).toEqual( + `Unable to validate role ${groupPolicy}. Cause: source does not match originating role ${ + roleMeta.roleEntityRef + }, consider making changes to the '${roleMeta.source.toLocaleUpperCase()}'`, + ); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac-backend/src/validation/policies-validation.ts b/workspaces/rbac/plugins/rbac-backend/src/validation/policies-validation.ts new file mode 100644 index 0000000000..ce089de0ee --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/src/validation/policies-validation.ts @@ -0,0 +1,291 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CompoundEntityRef, parseEntityRef } from '@backstage/catalog-model'; +import { NotAllowedError } from '@backstage/errors'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; + +import { Enforcer } from 'casbin'; + +import { + isValidPermissionAction, + PermissionActionValues, + Role, + RoleBasedPolicy, + Source, +} from '@backstage-community/plugin-rbac-common'; + +import { + RoleMetadataDao, + RoleMetadataStorage, +} from '../database/role-metadata'; + +/** + * validateSource validates the source to the role that is being modified. This includes comparing the source from the + * originating role to the source that the modification is coming from. + * We do this to ensure consistency between permissions and roles and where they are originally defined. + * This is a strict comparison where the source of all new roles (grouping policies) and permissions must match + * the source of the first role that was created. + * We are not strict for permission policies defined with an originating role source of configuration. + * @param source The source in which the modification is coming from + * @param roleMetadata The original role that was created + * @returns An error in the event that the source does not match the originating role + */ +export const validateSource = async ( + source: Source, + roleMetadata: RoleMetadataDao | undefined, +): Promise => { + if (!roleMetadata) { + return undefined; // Role does not exist yet, there is no conflict with the source + } + + if (roleMetadata.source !== source && roleMetadata.source !== 'legacy') { + return new Error( + `source does not match originating role ${ + roleMetadata.roleEntityRef + }, consider making changes to the '${roleMetadata.source.toLocaleUpperCase()}'`, + ); + } + + return undefined; +}; + +// This should be called on add and edit and delete +export function validatePolicy(policy: RoleBasedPolicy): Error | undefined { + const err = validateEntityReference(policy.entityReference); + if (err) { + return err; + } + + if (!policy.permission) { + return new Error(`'permission' field must not be empty`); + } + + if (!policy.policy) { + return new Error(`'policy' field must not be empty`); + } else if (!isValidPermissionAction(policy.policy)) { + return new Error( + `'policy' has invalid value: '${ + policy.policy + }'. It should be one of: ${PermissionActionValues.join(', ')}`, + ); + } + + if (!policy.effect) { + return new Error(`'effect' field must not be empty`); + } else if (!isValidEffectValue(policy.effect)) { + return new Error( + `'effect' has invalid value: '${ + policy.effect + }'. It should be: '${AuthorizeResult.ALLOW.toLocaleLowerCase()}' or '${AuthorizeResult.DENY.toLocaleLowerCase()}'`, + ); + } + + return undefined; +} + +export function validateRole(role: Role): Error | undefined { + if (!role.name) { + return new Error(`'name' field must not be empty`); + } + + let err = validateEntityReference(role.name, true); + if (err) { + return err; + } + + if (!role.memberReferences || role.memberReferences.length === 0) { + return new Error(`'memberReferences' field must not be empty`); + } + + for (const member of role.memberReferences) { + err = validateEntityReference(member); + if (err) { + return err; + } + } + return undefined; +} + +function isValidEffectValue(effect: string): boolean { + return ( + effect === AuthorizeResult.ALLOW.toLocaleLowerCase() || + effect === AuthorizeResult.DENY.toLocaleLowerCase() + ); +} + +function isValidEntityName(name: string): boolean { + const validNamePattern = /^[a-zA-Z0-9]+([._-][a-zA-Z0-9]+)*$/; + return validNamePattern.test(name) && name.length <= 63; +} + +function isValidEntityNamespace(namespace: string): boolean { + const validNamespacePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/; + return validNamespacePattern.test(namespace) && namespace.length <= 63; +} + +// We supports only full form entity reference: [:][/] +export function validateEntityReference( + entityRef?: string, + role?: boolean, +): Error | undefined { + if (!entityRef) { + return new Error(`'entityReference' must not be empty`); + } + + let entityRefCompound: CompoundEntityRef; + try { + entityRefCompound = parseEntityRef(entityRef); + } catch (err) { + return err as Error; + } + + const entityRefFull = `${entityRefCompound.kind}:${entityRefCompound.namespace}/${entityRefCompound.name}`; + if (entityRefFull !== entityRef) { + return new Error( + `entity reference '${entityRef}' does not match the required format [:][/]. Provide, please, full entity reference.`, + ); + } + + if (role && entityRefCompound.kind !== 'role') { + return new Error( + `Unsupported kind ${entityRefCompound.kind}. Supported value should be "role"`, + ); + } + + if ( + entityRefCompound.kind !== 'user' && + entityRefCompound.kind !== 'group' && + entityRefCompound.kind !== 'role' + ) { + return new Error( + `Unsupported kind ${entityRefCompound.kind}. List supported values ["user", "group", "role"]`, + ); + } + + if (!isValidEntityName(entityRefCompound.name)) { + return new Error( + `The name '${entityRefCompound.name}' in the entity reference must be a string that is sequences of [a-zA-Z0-9] separated by any of [-_.], at most 63 characters in total`, + ); + } + + if (!isValidEntityNamespace(entityRefCompound.namespace)) { + return new Error( + `The namespace '${entityRefCompound.namespace}' in the entity reference must be a string that is sequences of [a-z0-9] separated by [-], at most 63 characters in total`, + ); + } + + return undefined; +} + +export async function validateGroupingPolicy( + groupPolicy: string[], + roleMetadataStorage: RoleMetadataStorage, + source: Source, +): Promise { + if (groupPolicy.length !== 2) { + return new Error(`Group policy should have length 2`); + } + + const member = groupPolicy[0]; + let err = validateEntityReference(member); + if (err) { + return new Error( + `Failed to validate group policy ${groupPolicy}. Cause: ${err.message}`, + ); + } + const parent = groupPolicy[1]; + err = validateEntityReference(parent); + if (err) { + return new Error( + `Failed to validate group policy ${groupPolicy}. Cause: ${err.message}`, + ); + } + if (member.startsWith(`role:`)) { + return new Error( + `Group policy is invalid: ${groupPolicy}. rbac-backend plugin doesn't support role inheritance.`, + ); + } + if (member.startsWith(`group:`) && parent.startsWith(`group:`)) { + return new Error( + `Group policy is invalid: ${groupPolicy}. Group inheritance information could be provided only with help of Catalog API.`, + ); + } + if (member.startsWith(`user:`) && parent.startsWith(`group:`)) { + return new Error( + `Group policy is invalid: ${groupPolicy}. User membership information could be provided only with help of Catalog API.`, + ); + } + + const metadata = await roleMetadataStorage.findRoleMetadata(parent); + + err = await validateSource(source, metadata); + if (metadata && err) { + return new NotAllowedError( + `Unable to validate role ${groupPolicy}. Cause: ${err.message}`, + ); + } + + return undefined; +} + +export const checkForDuplicatePolicies = async ( + fileEnf: Enforcer, + policy: string[], + policyFile: string, +): Promise => { + const duplicates = await fileEnf.getFilteredPolicy(0, ...policy); + if (duplicates.length > 1) { + return new Error( + `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; +}; + +export const checkForDuplicateGroupPolicies = async ( + fileEnf: Enforcer, + policy: string[], + policyFile: string, +): Promise => { + const duplicates = await fileEnf.getFilteredGroupingPolicy(0, ...policy); + + if (duplicates.length > 1) { + return new Error( + `Duplicate role: ${policy} found in the file ${policyFile}`, + ); + } + return undefined; +}; diff --git a/workspaces/rbac/plugins/rbac-backend/tsconfig.json b/workspaces/rbac/plugins/rbac-backend/tsconfig.json new file mode 100644 index 0000000000..a7f7d926e3 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src", "migrations"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "../../dist-types/plugins/rbac-backend", + "rootDir": ".", + "downlevelIteration": true + } +} diff --git a/workspaces/rbac/plugins/rbac-backend/turbo.json b/workspaces/rbac/plugins/rbac-backend/turbo.json new file mode 100644 index 0000000000..9fe704e3fc --- /dev/null +++ b/workspaces/rbac/plugins/rbac-backend/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "tsc": { + "outputs": ["../../dist-types/plugins/rbac-backend/**"] + } + } +} diff --git a/workspaces/rbac/plugins/rbac-common/.eslintignore b/workspaces/rbac/plugins/rbac-common/.eslintignore new file mode 100644 index 0000000000..6a77e2728b --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/.eslintignore @@ -0,0 +1,4 @@ +dist-dynamic +dist-scalprum +!.eslintrc.js +!.prettierrc.js \ No newline at end of file diff --git a/workspaces/rbac/plugins/rbac-common/.eslintrc.js b/workspaces/rbac/plugins/rbac-common/.eslintrc.js new file mode 100644 index 0000000000..8bd689af97 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/.eslintrc.js @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/workspaces/rbac/plugins/rbac-common/.lintstagedrc.json b/workspaces/rbac/plugins/rbac-common/.lintstagedrc.json new file mode 100644 index 0000000000..14b2263def --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/.lintstagedrc.json @@ -0,0 +1,4 @@ +{ + "*": "prettier --ignore-unknown --write", + "*.{js,jsx,ts,tsx,mjs,cjs}": "backstage-cli package lint --fix" +} diff --git a/workspaces/rbac/plugins/rbac-common/.prettierignore b/workspaces/rbac/plugins/rbac-common/.prettierignore new file mode 100644 index 0000000000..fc8357d99e --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/.prettierignore @@ -0,0 +1,12 @@ +dist +dist-types +coverage +.vscode +CHANGELOG.md +generated +templates +*.hbs +renovate.json +dist-dynamic +dist-scalprum +playwright-report diff --git a/workspaces/rbac/plugins/rbac-common/.prettierrc.js b/workspaces/rbac/plugins/rbac-common/.prettierrc.js new file mode 100644 index 0000000000..5b35247f3f --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/.prettierrc.js @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */ +module.exports = { + ...require('@spotify/prettier-config'), + plugins: ['@ianvs/prettier-plugin-sort-imports'], + importOrder: [ + '^react(.*)$', + '', + '^@backstage/(.*)$', + '', + '', + '', + '^@backstage-community/(.*)$', + '', + '', + '', + '^[.]', + ], +}; diff --git a/workspaces/rbac/plugins/rbac-common/.versionhistory.md b/workspaces/rbac/plugins/rbac-common/.versionhistory.md new file mode 100644 index 0000000000..3f93bc2eae --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/.versionhistory.md @@ -0,0 +1 @@ +- Bumped to 1.10.0 in main branch for next release 1.3.0 diff --git a/workspaces/rbac/plugins/rbac-common/CHANGELOG.md b/workspaces/rbac/plugins/rbac-common/CHANGELOG.md new file mode 100644 index 0000000000..82fd2446ff --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/CHANGELOG.md @@ -0,0 +1,120 @@ +## @backstage-community/plugin-rbac-common [1.8.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.8.1...@backstage-community/plugin-rbac-common@1.8.2) (2024-08-06) + +## 1.12.0 + +### Minor Changes + +- 8244f28: chore(deps): update to backstage 1.32 + +## 1.11.0 + +### Minor Changes + +- d9551ae: feat(deps): update to backstage 1.31 + +### Patch Changes + +- d9551ae: change deps to peer deps in common packages +- d9551ae: upgrade to yarn v3 + +### Bug Fixes + +- **rbac:** implement conditional aliases ([#1847](https://github.com/janus-idp/backstage-plugins/issues/1847)) ([dbc9a0b](https://github.com/janus-idp/backstage-plugins/commit/dbc9a0bc92f19a4382e406f83b4889905dc6e33d)) + +## @backstage-community/plugin-rbac-common [1.8.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.8.0...@backstage-community/plugin-rbac-common@1.8.1) (2024-08-05) + +### Bug Fixes + +- **rbac:** add additional validation for permission policies ([#1908](https://github.com/janus-idp/backstage-plugins/issues/1908)) ([592498f](https://github.com/janus-idp/backstage-plugins/commit/592498f34a3b605162d3c242184aa6877b0360e8)), closes [#1939](https://github.com/janus-idp/backstage-plugins/issues/1939) + +## @backstage-community/plugin-rbac-common [1.8.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.7.2...@backstage-community/plugin-rbac-common@1.8.0) (2024-07-26) + +### Features + +- **deps:** update to backstage 1.29 ([#1900](https://github.com/janus-idp/backstage-plugins/issues/1900)) ([f53677f](https://github.com/janus-idp/backstage-plugins/commit/f53677fb02d6df43a9de98c43a9f101a6db76802)) + +## @backstage-community/plugin-rbac-common [1.7.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.7.1...@backstage-community/plugin-rbac-common@1.7.2) (2024-07-24) + +### Bug Fixes + +- **deps:** rollback unreleased plugins ([#1951](https://github.com/janus-idp/backstage-plugins/issues/1951)) ([8b77969](https://github.com/janus-idp/backstage-plugins/commit/8b779694f02f8125587296305276b84cdfeeaebe)) + +## @backstage-community/plugin-rbac-common [1.7.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.7.0...@backstage-community/plugin-rbac-common@1.7.1) (2024-07-19) + +## @backstage-community/plugin-rbac-common [1.7.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.6.1...@backstage-community/plugin-rbac-common@1.7.0) (2024-07-17) + +### Features + +- **deps:** update to backstage 1.28 ([#1891](https://github.com/janus-idp/backstage-plugins/issues/1891)) ([1ba1108](https://github.com/janus-idp/backstage-plugins/commit/1ba11088e0de60e90d138944267b83600dc446e5)) + +## @backstage-community/plugin-rbac-common [1.6.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.6.0...@backstage-community/plugin-rbac-common@1.6.1) (2024-06-28) + +### Bug Fixes + +- **rbac:** correct plugin ID matching to permission policy ([#1795](https://github.com/janus-idp/backstage-plugins/issues/1795)) ([6dc4b1c](https://github.com/janus-idp/backstage-plugins/commit/6dc4b1c23d22252f394eecd8b795ac15507ecc50)) + +## @backstage-community/plugin-rbac-common [1.6.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.5.0...@backstage-community/plugin-rbac-common@1.6.0) (2024-06-13) + +### Features + +- **deps:** update to backstage 1.27 ([#1683](https://github.com/janus-idp/backstage-plugins/issues/1683)) ([a14869c](https://github.com/janus-idp/backstage-plugins/commit/a14869c3f4177049cb8d6552b36c3ffd17e7997d)) + +## @backstage-community/plugin-rbac-common [1.5.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.4.2...@backstage-community/plugin-rbac-common@1.5.0) (2024-06-04) + +### Features + +- **rbac:** add audit log for RBAC backend ([#1726](https://github.com/janus-idp/backstage-plugins/issues/1726)) ([e50464b](https://github.com/janus-idp/backstage-plugins/commit/e50464bcb38e9897ddfe208fdeef699e4bfeda3a)) + +## @backstage-community/plugin-rbac-common [1.4.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.4.1...@backstage-community/plugin-rbac-common@1.4.2) (2024-05-09) + +## @backstage-community/plugin-rbac-common [1.4.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.4.0...@backstage-community/plugin-rbac-common@1.4.1) (2024-04-17) + +### Bug Fixes + +- **rbac:** reduce the number of permissions returned, add isResourced flag ([#1474](https://github.com/janus-idp/backstage-plugins/issues/1474)) ([e5dda95](https://github.com/janus-idp/backstage-plugins/commit/e5dda95bfc87d1d5d404726cbbe05c8bfdb73845)) + +## @backstage-community/plugin-rbac-common [1.4.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.3.2...@backstage-community/plugin-rbac-common@1.4.0) (2024-04-05) + +### Features + +- **rbac:** save role modification information to the metadata ([#1280](https://github.com/janus-idp/backstage-plugins/issues/1280)) ([0454509](https://github.com/janus-idp/backstage-plugins/commit/0454509e41db2ae332d1b2bf8f72d34241483efd)) + +## @backstage-community/plugin-rbac-common [1.3.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.3.1...@backstage-community/plugin-rbac-common@1.3.2) (2024-04-04) + +### Bug Fixes + +- **rbac:** rework condition policies to bound them to RBAC roles ([#1330](https://github.com/janus-idp/backstage-plugins/issues/1330)) ([55c00b2](https://github.com/janus-idp/backstage-plugins/commit/55c00b21b27b449cb0e5100c7b64a6ae742536ac)) + +## @backstage-community/plugin-rbac-common [1.3.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.3.0...@backstage-community/plugin-rbac-common@1.3.1) (2024-03-29) + +## @backstage-community/plugin-rbac-common [1.3.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.2.1...@backstage-community/plugin-rbac-common@1.3.0) (2024-02-21) + +### Features + +- **rbac:** backend part - store role description to the database ([#1178](https://github.com/janus-idp/backstage-plugins/issues/1178)) ([ec8b1c2](https://github.com/janus-idp/backstage-plugins/commit/ec8b1c27cce5c36997f84a068dc4cc5cc542f428)) + +## @backstage-community/plugin-rbac-common [1.2.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.2.0...@backstage-community/plugin-rbac-common@1.2.1) (2024-02-02) + +### Bug Fixes + +- **rbac:** split policies and roles by source ([#1042](https://github.com/janus-idp/backstage-plugins/issues/1042)) ([03a678d](https://github.com/janus-idp/backstage-plugins/commit/03a678d96deeb1d42448e94ac95d735e61393a40)), closes [#1103](https://github.com/janus-idp/backstage-plugins/issues/1103) + +## @backstage-community/plugin-rbac-common [1.2.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.1.0...@backstage-community/plugin-rbac-common@1.2.0) (2023-12-05) + +### Features + +- **rbac:** role overview ([#972](https://github.com/janus-idp/backstage-plugins/issues/972)) ([43c1906](https://github.com/janus-idp/backstage-plugins/commit/43c19064e9477a5449ff5d56b00efe27cf640c27)) + +## @backstage-community/plugin-rbac-common [1.1.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.0.1...@backstage-community/plugin-rbac-common@1.1.0) (2023-10-27) + +### Features + +- **rbac:** implement the concept of roles in rbac ([#867](https://github.com/janus-idp/backstage-plugins/issues/867)) ([4d878a2](https://github.com/janus-idp/backstage-plugins/commit/4d878a29babd86bd7896d69e6b2b63392b6e6cc8)) + +## @backstage-community/plugin-rbac-common [1.0.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-common@1.0.0...@backstage-community/plugin-rbac-common@1.0.1) (2023-10-19) + +## @backstage-community/plugin-rbac-common 1.0.0 (2023-09-29) + +### Bug Fixes + +- **rbac:** remove private package ([#809](https://github.com/janus-idp/backstage-plugins/issues/809)) ([cf59d6d](https://github.com/janus-idp/backstage-plugins/commit/cf59d6d1c5a65363a7ccdd7490d3148d665e7d46)) diff --git a/workspaces/rbac/plugins/rbac-common/README.md b/workspaces/rbac/plugins/rbac-common/README.md new file mode 100644 index 0000000000..1e9f3e9ec2 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/README.md @@ -0,0 +1,5 @@ +# @backstage-community/plugin-rbac-common + +The RBAC plugin provides your Backstage instance with role based access control and policy management. + +For more information about RBAC plugin, see the [RBAC plugin documentation](https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac-backend) on GitHub. diff --git a/workspaces/rbac/plugins/rbac-common/catalog-info.yaml b/workspaces/rbac/plugins/rbac-common/catalog-info.yaml new file mode 100644 index 0000000000..75c41f628f --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/catalog-info.yaml @@ -0,0 +1,28 @@ +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-community-rbac-common + title: '@backstage-community/backstage-plugin-rbac-common' + description: RBAC plugin shared code + annotations: + backstage.io/source-location: url:https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac-common + backstage.io/view-url: https://github.com/backstage/community-plugins/blob/main/workspaces/rbac/plugins/rbac-common/catalog-info.yaml + backstage.io/edit-url: https://github.com/backstage/community-plugins/edit/main/workspaces/rbac/plugins/rbac-common/catalog-info.yaml + github.com/project-slug: backstage-community/backstage-plugins + github.com/team-slug: backstage/maintainers-plugins + sonarqube.org/project-key: backstage-community_plugins + tags: + - security + - rbac + links: + - url: https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac-common + title: GitHub Source + icon: source + type: source +spec: + type: backstage-common-library + lifecycle: production + owner: backstage-team + system: backstage + subcomponentOf: backstage-community-rbac diff --git a/workspaces/rbac/plugins/rbac-common/package.json b/workspaces/rbac/plugins/rbac-common/package.json new file mode 100644 index 0000000000..c4d27177f2 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/package.json @@ -0,0 +1,69 @@ +{ + "name": "@backstage-community/plugin-rbac-common", + "description": "Common functionalities for the rbac-common plugin", + "version": "1.12.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library", + "supported-versions": "1.32.5", + "pluginId": "rbac", + "pluginPackages": [ + "@backstage-community/plugin-rbac", + "@backstage-community/plugin-rbac-backend", + "@backstage-community/plugin-rbac-common", + "@backstage-community/plugin-rbac-node" + ] + }, + "sideEffects": false, + "scripts": { + "build": "backstage-cli package build", + "tsc": "tsc", + "prettier:check": "prettier --ignore-unknown --check .", + "prettier:fix": "prettier --ignore-unknown --write .", + "lint:check": "backstage-cli package lint", + "lint:fix": "backstage-cli package lint --fix", + "test": "backstage-cli package test --passWithNoTests --coverage", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "peerDependencies": { + "@backstage/errors": "^1.2.4", + "@backstage/plugin-permission-common": "^0.8.1" + }, + "devDependencies": { + "@backstage/cli": "0.28.2", + "@backstage/errors": "^1.2.4", + "@backstage/plugin-permission-common": "^0.8.1", + "@spotify/prettier-config": "^15.0.0", + "prettier": "3.3.3" + }, + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "https://github.com/backstage/community-plugins", + "directory": "workspaces/rbac/plugins/rbac-common" + }, + "keywords": [ + "support:production", + "lifecycle:active", + "backstage", + "plugin" + ], + "homepage": "https://red.ht/rhdh", + "bugs": "https://github.com/backstage/community-plugins/issues", + "maintainers": [ + "@PatAKnight" + ], + "author": "Red Hat" +} diff --git a/workspaces/rbac/plugins/rbac-common/report.api.md b/workspaces/rbac/plugins/rbac-common/report.api.md new file mode 100644 index 0000000000..cd7e14ba75 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/report.api.md @@ -0,0 +1,228 @@ +## API Report File for "@backstage-community/plugin-rbac-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ConditionalPolicyDecision } from '@backstage/plugin-permission-common'; +import { NotAllowedError } from '@backstage/errors'; +import { PermissionAttributes } from '@backstage/plugin-permission-common'; +import { ResourcePermission } from '@backstage/plugin-permission-common'; + +// Warning: (ae-missing-release-tag) "CONDITION_ALIAS_SIGN" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const CONDITION_ALIAS_SIGN = "$"; + +// Warning: (ae-missing-release-tag) "ConditionalAliases" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const ConditionalAliases: { + readonly CURRENT_USER: "currentUser"; + readonly OWNER_REFS: "ownerRefs"; +}; + +// Warning: (ae-missing-release-tag) "isResourcedPolicy" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function isResourcedPolicy(policy: PolicyDetails): policy is ResourcedPolicy; + +// Warning: (ae-missing-release-tag) "isValidPermissionAction" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function isValidPermissionAction(action: string): action is PermissionAction; + +// Warning: (ae-missing-release-tag) "NamedPolicy" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type NamedPolicy = { + name: string; + policy: string; +}; + +// Warning: (ae-missing-release-tag) "NonEmptyArray" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type NonEmptyArray = [T, ...T[]]; + +// Warning: (ae-missing-release-tag) "PermissionAction" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PermissionAction = (typeof PermissionActionValues)[number]; + +// Warning: (ae-missing-release-tag) "PermissionActionValues" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const PermissionActionValues: readonly ["create", "read", "update", "delete", "use"]; + +// Warning: (ae-missing-release-tag) "PermissionInfo" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PermissionInfo = { + name: string; + action: PermissionAction; +}; + +// Warning: (ae-missing-release-tag) "PermissionPolicyMetadata" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PermissionPolicyMetadata = { + source: Source; +}; + +// Warning: (ae-missing-release-tag) "PluginPermissionMetaData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PluginPermissionMetaData = { + pluginId: string; + policies: PolicyDetails[]; +}; + +// Warning: (ae-missing-release-tag) "Policy" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type Policy = { + permission?: string; + policy?: string; +}; + +// Warning: (ae-missing-release-tag) "PolicyDetails" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PolicyDetails = NamedPolicy | ResourcedPolicy; + +// Warning: (ae-missing-release-tag) "policyEntityCreatePermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const policyEntityCreatePermission: ResourcePermission<"policy-entity">; + +// Warning: (ae-missing-release-tag) "policyEntityDeletePermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const policyEntityDeletePermission: ResourcePermission<"policy-entity">; + +// Warning: (ae-missing-release-tag) "PolicyEntityPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type PolicyEntityPermission = ResourcePermission; + +// Warning: (ae-missing-release-tag) "policyEntityPermissions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const policyEntityPermissions: ResourcePermission<"policy-entity">[]; + +// Warning: (ae-missing-release-tag) "policyEntityReadPermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const policyEntityReadPermission: ResourcePermission<"policy-entity">; + +// Warning: (ae-missing-release-tag) "policyEntityUpdatePermission" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const policyEntityUpdatePermission: ResourcePermission<"policy-entity">; + +// Warning: (ae-missing-release-tag) "RESOURCE_TYPE_POLICY_ENTITY" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const RESOURCE_TYPE_POLICY_ENTITY = "policy-entity"; + +// Warning: (ae-missing-release-tag) "ResourcedPolicy" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ResourcedPolicy = NamedPolicy & { + resourceType: string; +}; + +// Warning: (ae-missing-release-tag) "Role" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type Role = { + memberReferences: string[]; + name: string; + metadata?: RoleMetadata; +}; + +// Warning: (ae-missing-release-tag) "RoleBasedPolicy" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type RoleBasedPolicy = Policy & { + entityReference?: string; + effect?: string; + metadata?: PermissionPolicyMetadata; +}; + +// Warning: (ae-missing-release-tag) "RoleConditionalPolicyDecision" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type RoleConditionalPolicyDecision = ConditionalPolicyDecision & { + id: number; + roleEntityRef: string; + permissionMapping: T[]; +}; + +// Warning: (ae-missing-release-tag) "RoleMetadata" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type RoleMetadata = { + description?: string; + source?: Source; + modifiedBy?: string; + author?: string; + lastModified?: string; + createdAt?: string; +}; + +// Warning: (ae-missing-release-tag) "Source" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type Source = string; + +// Warning: (ae-missing-release-tag) "toPermissionAction" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const toPermissionAction: (attr: PermissionAttributes) => PermissionAction; + +// Warning: (ae-missing-release-tag) "UnauthorizedError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class UnauthorizedError extends NotAllowedError { + constructor(); +} + +// Warning: (ae-missing-release-tag) "UpdatePolicy" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type UpdatePolicy = { + oldPolicy: Policy; + newPolicy: Policy; +}; + +// Warnings were encountered during analysis: +// +// src/permissions.d.ts:2:22 - (ae-undocumented) Missing documentation for "RESOURCE_TYPE_POLICY_ENTITY". +// src/types.d.ts:3:1 - (ae-undocumented) Missing documentation for "Source". +// src/types.d.ts:4:1 - (ae-undocumented) Missing documentation for "PermissionPolicyMetadata". +// src/types.d.ts:7:1 - (ae-undocumented) Missing documentation for "RoleMetadata". +// src/types.d.ts:15:1 - (ae-undocumented) Missing documentation for "Policy". +// src/types.d.ts:19:1 - (ae-undocumented) Missing documentation for "RoleBasedPolicy". +// src/types.d.ts:24:1 - (ae-undocumented) Missing documentation for "Role". +// src/types.d.ts:29:1 - (ae-undocumented) Missing documentation for "UpdatePolicy". +// src/types.d.ts:33:1 - (ae-undocumented) Missing documentation for "NamedPolicy". +// src/types.d.ts:37:1 - (ae-undocumented) Missing documentation for "ResourcedPolicy". +// src/types.d.ts:40:1 - (ae-undocumented) Missing documentation for "PolicyDetails". +// src/types.d.ts:41:1 - (ae-undocumented) Missing documentation for "isResourcedPolicy". +// src/types.d.ts:42:1 - (ae-undocumented) Missing documentation for "PluginPermissionMetaData". +// src/types.d.ts:46:1 - (ae-undocumented) Missing documentation for "NonEmptyArray". +// src/types.d.ts:47:22 - (ae-undocumented) Missing documentation for "PermissionActionValues". +// src/types.d.ts:48:1 - (ae-undocumented) Missing documentation for "PermissionAction". +// src/types.d.ts:49:22 - (ae-undocumented) Missing documentation for "toPermissionAction". +// src/types.d.ts:50:1 - (ae-undocumented) Missing documentation for "isValidPermissionAction". +// src/types.d.ts:51:1 - (ae-undocumented) Missing documentation for "PermissionInfo". +// src/types.d.ts:55:1 - (ae-undocumented) Missing documentation for "RoleConditionalPolicyDecision". +// src/types.d.ts:60:22 - (ae-undocumented) Missing documentation for "ConditionalAliases". +// src/types.d.ts:64:22 - (ae-undocumented) Missing documentation for "CONDITION_ALIAS_SIGN". +// src/types.d.ts:65:1 - (ae-undocumented) Missing documentation for "UnauthorizedError". + +// (No @packageDocumentation comment for this package) + +``` diff --git a/workspaces/rbac/plugins/rbac-common/src/index.ts b/workspaces/rbac/plugins/rbac-common/src/index.ts new file mode 100644 index 0000000000..3eba88f409 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; +export * from './permissions'; diff --git a/workspaces/rbac/plugins/rbac-common/src/permissions.ts b/workspaces/rbac/plugins/rbac-common/src/permissions.ts new file mode 100644 index 0000000000..651ff7e066 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/src/permissions.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createPermission, + ResourcePermission, +} from '@backstage/plugin-permission-common'; + +export const RESOURCE_TYPE_POLICY_ENTITY = 'policy-entity'; + +/** + * Convenience type for permission entity + */ +export type PolicyEntityPermission = ResourcePermission< + typeof RESOURCE_TYPE_POLICY_ENTITY +>; + +/** + * This permission is used to authorize actions that involve reading + * permission policies. + */ +export const policyEntityReadPermission = createPermission({ + name: 'policy.entity.read', + attributes: { + action: 'read', + }, + resourceType: RESOURCE_TYPE_POLICY_ENTITY, +}); + +/** + * This permission is used to authorize the creation of new permission policies. + */ +export const policyEntityCreatePermission = createPermission({ + name: 'policy.entity.create', + attributes: { + action: 'create', + }, + resourceType: RESOURCE_TYPE_POLICY_ENTITY, +}); + +/** + * This permission is used to authorize actions that involve removing permission + * policies. + */ +export const policyEntityDeletePermission = createPermission({ + name: 'policy.entity.delete', + attributes: { + action: 'delete', + }, + resourceType: RESOURCE_TYPE_POLICY_ENTITY, +}); + +/** + * This permission is used to authorize updating permission policies + */ +export const policyEntityUpdatePermission = createPermission({ + name: 'policy.entity.update', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_POLICY_ENTITY, +}); + +/** + * List of all permissions on permission polices. + */ +export const policyEntityPermissions = [ + policyEntityReadPermission, + policyEntityCreatePermission, + policyEntityDeletePermission, + policyEntityUpdatePermission, +]; diff --git a/workspaces/rbac/plugins/rbac-common/src/setupTests.ts b/workspaces/rbac/plugins/rbac-common/src/setupTests.ts new file mode 100644 index 0000000000..c7ce5c0988 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export {}; diff --git a/workspaces/rbac/plugins/rbac-common/src/types.ts b/workspaces/rbac/plugins/rbac-common/src/types.ts new file mode 100644 index 0000000000..b7bd4d2fac --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/src/types.ts @@ -0,0 +1,136 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { NotAllowedError } from '@backstage/errors'; +import { + ConditionalPolicyDecision, + PermissionAttributes, +} from '@backstage/plugin-permission-common'; + +// 'rest' created via REST API +// 'csv-file' created via policies-csv-file with defined path in the application configuration +// 'configuration' created from application configuration +// 'legacy'; preexisting policies +export type Source = string; + +export type PermissionPolicyMetadata = { + source: Source; +}; + +export type RoleMetadata = { + description?: string; + source?: Source; + modifiedBy?: string; + author?: string; + lastModified?: string; + createdAt?: string; +}; + +export type Policy = { + permission?: string; + policy?: string; +}; + +export type RoleBasedPolicy = Policy & { + entityReference?: string; + effect?: string; + metadata?: PermissionPolicyMetadata; +}; + +export type Role = { + memberReferences: string[]; + name: string; + metadata?: RoleMetadata; +}; + +export type UpdatePolicy = { + oldPolicy: Policy; + newPolicy: Policy; +}; + +export type NamedPolicy = { + name: string; + + policy: string; +}; + +export type ResourcedPolicy = NamedPolicy & { + resourceType: string; +}; + +export type PolicyDetails = NamedPolicy | ResourcedPolicy; + +export function isResourcedPolicy( + policy: PolicyDetails, +): policy is ResourcedPolicy { + return 'resourceType' in policy; +} + +export type PluginPermissionMetaData = { + pluginId: string; + policies: PolicyDetails[]; +}; + +export type NonEmptyArray = [T, ...T[]]; + +// Permission framework attributes action has values: 'create' | 'read' | 'update' | 'delete' | undefined. +// But we are introducing an action named "use" when action does not exist('undefined') to avoid +// a more complicated model with multiple policy and request shapes. +export const PermissionActionValues = [ + 'create', + 'read', + 'update', + 'delete', + 'use', +] as const; +export type PermissionAction = (typeof PermissionActionValues)[number]; +export const toPermissionAction = ( + attr: PermissionAttributes, +): PermissionAction => attr.action ?? 'use'; + +export function isValidPermissionAction( + action: string, +): action is PermissionAction { + return (PermissionActionValues as readonly string[]).includes(action); +} + +export type PermissionInfo = { + name: string; + action: PermissionAction; +}; + +// Frontend should use RoleConditionalPolicyDecision +export type RoleConditionalPolicyDecision< + T extends PermissionAction | PermissionInfo, +> = ConditionalPolicyDecision & { + id: number; + roleEntityRef: string; + + permissionMapping: T[]; +}; + +export const ConditionalAliases = { + CURRENT_USER: 'currentUser', + OWNER_REFS: 'ownerRefs', +} as const; + +export const CONDITION_ALIAS_SIGN = '$'; + +// UnauthorizedError should be uniformely used for authorization errors. +export class UnauthorizedError extends NotAllowedError { + constructor() { + super('Unauthorized'); + } +} diff --git a/workspaces/rbac/plugins/rbac-common/tsconfig.json b/workspaces/rbac/plugins/rbac-common/tsconfig.json new file mode 100644 index 0000000000..a5fd04c967 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src", "dev", "migrations"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "../../dist-types/plugins/rbac-common", + "rootDir": "." + } +} diff --git a/workspaces/rbac/plugins/rbac-common/turbo.json b/workspaces/rbac/plugins/rbac-common/turbo.json new file mode 100644 index 0000000000..3d6471db4f --- /dev/null +++ b/workspaces/rbac/plugins/rbac-common/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "tsc": { + "outputs": ["../../dist-types/plugins/rbac-common/**"] + } + } +} diff --git a/workspaces/rbac/plugins/rbac-node/.eslintignore b/workspaces/rbac/plugins/rbac-node/.eslintignore new file mode 100644 index 0000000000..6a77e2728b --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/.eslintignore @@ -0,0 +1,4 @@ +dist-dynamic +dist-scalprum +!.eslintrc.js +!.prettierrc.js \ No newline at end of file diff --git a/workspaces/rbac/plugins/rbac-node/.eslintrc.js b/workspaces/rbac/plugins/rbac-node/.eslintrc.js new file mode 100644 index 0000000000..8bd689af97 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/.eslintrc.js @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/workspaces/rbac/plugins/rbac-node/.lintstagedrc.json b/workspaces/rbac/plugins/rbac-node/.lintstagedrc.json new file mode 100644 index 0000000000..14b2263def --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/.lintstagedrc.json @@ -0,0 +1,4 @@ +{ + "*": "prettier --ignore-unknown --write", + "*.{js,jsx,ts,tsx,mjs,cjs}": "backstage-cli package lint --fix" +} diff --git a/workspaces/rbac/plugins/rbac-node/.prettierignore b/workspaces/rbac/plugins/rbac-node/.prettierignore new file mode 100644 index 0000000000..fc8357d99e --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/.prettierignore @@ -0,0 +1,12 @@ +dist +dist-types +coverage +.vscode +CHANGELOG.md +generated +templates +*.hbs +renovate.json +dist-dynamic +dist-scalprum +playwright-report diff --git a/workspaces/rbac/plugins/rbac-node/.prettierrc.js b/workspaces/rbac/plugins/rbac-node/.prettierrc.js new file mode 100644 index 0000000000..5b35247f3f --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/.prettierrc.js @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */ +module.exports = { + ...require('@spotify/prettier-config'), + plugins: ['@ianvs/prettier-plugin-sort-imports'], + importOrder: [ + '^react(.*)$', + '', + '^@backstage/(.*)$', + '', + '', + '', + '^@backstage-community/(.*)$', + '', + '', + '', + '^[.]', + ], +}; diff --git a/workspaces/rbac/plugins/rbac-node/.versionhistory.md b/workspaces/rbac/plugins/rbac-node/.versionhistory.md new file mode 100644 index 0000000000..6c0616baff --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/.versionhistory.md @@ -0,0 +1,2 @@ +- Bumped to 1.1.0 in main branch for next release 1.2.0 +- Bumped to 1.6.0 in main branch for next release 1.3.0 diff --git a/workspaces/rbac/plugins/rbac-node/CHANGELOG.md b/workspaces/rbac/plugins/rbac-node/CHANGELOG.md new file mode 100644 index 0000000000..70187c4b58 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/CHANGELOG.md @@ -0,0 +1,67 @@ +## @backstage-community/plugin-rbac-node [1.4.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.3.1...@backstage-community/plugin-rbac-node@1.4.0) (2024-07-26) + +## 1.8.0 + +### Minor Changes + +- 8244f28: chore(deps): update to backstage 1.32 + +## 1.7.0 + +### Minor Changes + +- d9551ae: feat(deps): update to backstage 1.31 + +### Patch Changes + +- d9551ae: upgrade to yarn v3 + +### Features + +- **deps:** update to backstage 1.29 ([#1900](https://github.com/janus-idp/backstage-plugins/issues/1900)) ([f53677f](https://github.com/janus-idp/backstage-plugins/commit/f53677fb02d6df43a9de98c43a9f101a6db76802)) + +## @backstage-community/plugin-rbac-node [1.3.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.3.0...@backstage-community/plugin-rbac-node@1.3.1) (2024-07-24) + +### Bug Fixes + +- **deps:** rollback unreleased plugins ([#1951](https://github.com/janus-idp/backstage-plugins/issues/1951)) ([8b77969](https://github.com/janus-idp/backstage-plugins/commit/8b779694f02f8125587296305276b84cdfeeaebe)) + +## @backstage-community/plugin-rbac-node [1.3.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.2.0...@backstage-community/plugin-rbac-node@1.3.0) (2024-07-23) + +### Features + +- **deps:** update to backstage 1.28 ([#1891](https://github.com/janus-idp/backstage-plugins/issues/1891)) ([1ba1108](https://github.com/janus-idp/backstage-plugins/commit/1ba11088e0de60e90d138944267b83600dc446e5)) + +## @backstage-community/plugin-rbac-node [1.2.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.1.2...@backstage-community/plugin-rbac-node@1.2.0) (2024-06-13) + +### Features + +- **deps:** update to backstage 1.27 ([#1683](https://github.com/janus-idp/backstage-plugins/issues/1683)) ([a14869c](https://github.com/janus-idp/backstage-plugins/commit/a14869c3f4177049cb8d6552b36c3ffd17e7997d)) + +## @backstage-community/plugin-rbac-node [1.1.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.1.1...@backstage-community/plugin-rbac-node@1.1.2) (2024-06-04) + +## @backstage-community/plugin-rbac-node [1.1.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.1.0...@backstage-community/plugin-rbac-node@1.1.1) (2024-05-09) + +## @backstage-community/plugin-rbac-node [1.1.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.0.6...@backstage-community/plugin-rbac-node@1.1.0) (2024-04-15) + +### Features + +- checkPluginVersion.sh bump plugins for 1.2.0 release ([#1511](https://github.com/janus-idp/backstage-plugins/issues/1511)) ([73c6588](https://github.com/janus-idp/backstage-plugins/commit/73c6588adb7e8c20907b06f2a8ef248cfd4332e4)) + +## @backstage-community/plugin-rbac-node [1.0.6](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.0.5...@backstage-community/plugin-rbac-node@1.0.6) (2024-04-09) + +## @backstage-community/plugin-rbac-node [1.0.5](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.0.4...@backstage-community/plugin-rbac-node@1.0.5) (2024-04-08) + +## @backstage-community/plugin-rbac-node [1.0.4](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.0.3...@backstage-community/plugin-rbac-node@1.0.4) (2024-03-29) + +## @backstage-community/plugin-rbac-node [1.0.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.0.2...@backstage-community/plugin-rbac-node@1.0.3) (2024-03-04) + +## @backstage-community/plugin-rbac-node [1.0.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.0.1...@backstage-community/plugin-rbac-node@1.0.2) (2024-02-27) + +## @backstage-community/plugin-rbac-node [1.0.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac-node@1.0.0...@backstage-community/plugin-rbac-node@1.0.1) (2024-02-21) + +## @backstage-community/plugin-rbac-node 1.0.0 (2024-02-08) + +### Features + +- add support for the new backend system to the `rbac-backend` plugin ([#1179](https://github.com/janus-idp/backstage-plugins/issues/1179)) ([d625cb2](https://github.com/janus-idp/backstage-plugins/commit/d625cb2470513862027e048c70944275043ce70a)) diff --git a/workspaces/rbac/plugins/rbac-node/README.md b/workspaces/rbac/plugins/rbac-node/README.md new file mode 100644 index 0000000000..7e70b8f8c0 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/README.md @@ -0,0 +1,9 @@ +# @backstage/plugin-rbac-node + +Welcome to the Node.js library package for the rbac plugin! + +For more information about RBAC plugin, see the [RBAC plugin documentation](https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac-backend) on GitHub. + +## Version bump history + +- Bumped to 1.0.6 to re-release changes meant for 1.0.5, which failed due to timeout - see https://github.com/janus-idp/backstage-plugins/issues/1467 diff --git a/workspaces/rbac/plugins/rbac-node/catalog-info.yaml b/workspaces/rbac/plugins/rbac-node/catalog-info.yaml new file mode 100644 index 0000000000..60152fa991 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/catalog-info.yaml @@ -0,0 +1,28 @@ +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-community-rbac-node + title: '@backstage-community/backstage-plugin-rbac-node' + description: Node.js library for the rbac plugin + annotations: + backstage.io/source-location: url:https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac-node + backstage.io/view-url: https://github.com/backstage/community-plugins/blob/main/workspaces/rbac/plugins/rbac-node/catalog-info.yaml + backstage.io/edit-url: https://github.com/backstage/community-plugins/edit/main/workspaces/rbac/plugins/rbac-node/catalog-info.yaml + github.com/project-slug: backstage-community/backstage-plugins + github.com/team-slug: backstage/maintainers-plugins + sonarqube.org/project-key: backstage-community_plugins + tags: + - security + - rbac + links: + - url: https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac-node + title: GitHub Source + icon: source + type: source +spec: + type: backstage-node-library + lifecycle: production + owner: backstage-core-team + system: backstage + subcomponentOf: backstage-community-rbac diff --git a/workspaces/rbac/plugins/rbac-node/package.json b/workspaces/rbac/plugins/rbac-node/package.json new file mode 100644 index 0000000000..fd039081c1 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/package.json @@ -0,0 +1,62 @@ +{ + "name": "@backstage-community/plugin-rbac-node", + "description": "Node.js library for the rbac plugin", + "version": "1.8.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library", + "supported-versions": "1.32.5", + "pluginId": "rbac", + "pluginPackages": [ + "@backstage-community/plugin-rbac", + "@backstage-community/plugin-rbac-backend", + "@backstage-community/plugin-rbac-common", + "@backstage-community/plugin-rbac-node" + ] + }, + "scripts": { + "build": "backstage-cli package build", + "tsc": "tsc", + "prettier:check": "prettier --ignore-unknown --check .", + "prettier:fix": "prettier --ignore-unknown --write .", + "lint:check": "backstage-cli package lint", + "lint:fix": "backstage-cli package lint --fix", + "test": "backstage-cli package test --passWithNoTests --coverage", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "0.28.2", + "@spotify/prettier-config": "^15.0.0", + "prettier": "3.3.3" + }, + "files": [ + "dist" + ], + "dependencies": { + "@backstage/backend-plugin-api": "^1.0.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/community-plugins", + "directory": "workspaces/rbac/plugins/rbac-node" + }, + "maintainers": [ + "@PatAKnight" + ], + "author": "Red Hat", + "homepage": "https://red.ht/rhdh", + "bugs": "https://github.com/backstage/community-plugins/issues", + "keywords": [ + "support:production", + "lifecycle:active" + ] +} diff --git a/workspaces/rbac/plugins/rbac-node/report.api.md b/workspaces/rbac/plugins/rbac-node/report.api.md new file mode 100644 index 0000000000..177ad82393 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/report.api.md @@ -0,0 +1,70 @@ +## API Report File for "@backstage-community/plugin-rbac-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ExtensionPoint } from '@backstage/backend-plugin-api'; + +// @public +export interface PluginIdProvider { + // (undocumented) + getPluginIds: () => string[]; +} + +// @public +export type PluginIdProviderExtensionPoint = { + addPluginIdProvider(pluginIdProvider: PluginIdProvider): void; +}; + +// @public +export const pluginIdProviderExtensionPoint: ExtensionPoint; + +// Warning: (ae-missing-release-tag) "RBACProvider" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface RBACProvider { + // (undocumented) + connect(connection: RBACProviderConnection): Promise; + // (undocumented) + getProviderName(): string; + // (undocumented) + refresh(): Promise; +} + +// Warning: (ae-missing-release-tag) "RBACProviderConnection" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface RBACProviderConnection { + // (undocumented) + applyPermissions(permissions: string[][]): Promise; + // (undocumented) + applyRoles(roles: string[][]): Promise; +} + +// Warning: (ae-missing-release-tag) "RBACProviderExtensionPoint" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type RBACProviderExtensionPoint = { + addRBACProvider(...providers: Array>): void; +}; + +// Warning: (ae-missing-release-tag) "rbacProviderExtensionPoint" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const rbacProviderExtensionPoint: ExtensionPoint; + +// Warnings were encountered during analysis: +// +// src/extensions.d.ts:16:22 - (ae-undocumented) Missing documentation for "rbacProviderExtensionPoint". +// src/extensions.d.ts:17:1 - (ae-undocumented) Missing documentation for "RBACProviderExtensionPoint". +// src/types/types.d.ts:6:5 - (ae-undocumented) Missing documentation for "getPluginIds". +// src/types/types.d.ts:8:1 - (ae-undocumented) Missing documentation for "RBACProvider". +// src/types/types.d.ts:9:5 - (ae-undocumented) Missing documentation for "getProviderName". +// src/types/types.d.ts:10:5 - (ae-undocumented) Missing documentation for "connect". +// src/types/types.d.ts:11:5 - (ae-undocumented) Missing documentation for "refresh". +// src/types/types.d.ts:13:1 - (ae-undocumented) Missing documentation for "RBACProviderConnection". +// src/types/types.d.ts:14:5 - (ae-undocumented) Missing documentation for "applyRoles". +// src/types/types.d.ts:15:5 - (ae-undocumented) Missing documentation for "applyPermissions". + +``` diff --git a/workspaces/rbac/plugins/rbac-node/src/extensions.ts b/workspaces/rbac/plugins/rbac-node/src/extensions.ts new file mode 100644 index 0000000000..cc00cf0111 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/src/extensions.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createExtensionPoint } from '@backstage/backend-plugin-api'; + +import { PluginIdProvider, RBACProvider } from './types'; + +/** + * An extension point the exposes the ability to configure additional PluginIDProviders. + * + * @public + */ +export const pluginIdProviderExtensionPoint = + createExtensionPoint({ + id: 'permission.rbac.pluginIdProvider', + }); + +/** + * The interface for {@link pluginIdProviderExtensionPoint}. + * + * @public + */ +export type PluginIdProviderExtensionPoint = { + addPluginIdProvider(pluginIdProvider: PluginIdProvider): void; +}; + +export const rbacProviderExtensionPoint = + createExtensionPoint({ + id: 'permission.rbac.rbacProvider', + }); + +export type RBACProviderExtensionPoint = { + addRBACProvider( + ...providers: Array> + ): void; +}; diff --git a/workspaces/rbac/plugins/rbac-node/src/index.ts b/workspaces/rbac/plugins/rbac-node/src/index.ts new file mode 100644 index 0000000000..0ed3cad833 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Node.js library for the rbac plugin. + * + * @packageDocumentation + */ + +export * from './extensions'; +export * from './types'; diff --git a/workspaces/rbac/plugins/rbac-node/src/setupTests.ts b/workspaces/rbac/plugins/rbac-node/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export {}; diff --git a/workspaces/rbac/plugins/rbac-node/src/types/index.ts b/workspaces/rbac/plugins/rbac-node/src/types/index.ts new file mode 100644 index 0000000000..34ed8a9604 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/src/types/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; diff --git a/workspaces/rbac/plugins/rbac-node/src/types/types.ts b/workspaces/rbac/plugins/rbac-node/src/types/types.ts new file mode 100644 index 0000000000..3f377c39e5 --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/src/types/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Used to provide a list of pluginIDs on which a permission well-known endpoint is to be searched. + * @public + */ +export interface PluginIdProvider { + getPluginIds: () => string[]; +} + +export interface RBACProvider { + getProviderName(): string; + connect(connection: RBACProviderConnection): Promise; + refresh(): Promise; +} + +export interface RBACProviderConnection { + applyRoles(roles: string[][]): Promise; + applyPermissions(permissions: string[][]): Promise; +} diff --git a/workspaces/rbac/plugins/rbac-node/tsconfig.json b/workspaces/rbac/plugins/rbac-node/tsconfig.json new file mode 100644 index 0000000000..563742902f --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src", "dev", "migrations"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "../../dist-types/plugins/rbac-node", + "rootDir": "." + } +} diff --git a/workspaces/rbac/plugins/rbac-node/turbo.json b/workspaces/rbac/plugins/rbac-node/turbo.json new file mode 100644 index 0000000000..e3603c8dab --- /dev/null +++ b/workspaces/rbac/plugins/rbac-node/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "tsc": { + "outputs": ["../../dist-types/plugins/rbac-node/**"] + } + } +} diff --git a/workspaces/rbac/plugins/rbac/.eslintignore b/workspaces/rbac/plugins/rbac/.eslintignore new file mode 100644 index 0000000000..5bdf8c7b12 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/.eslintignore @@ -0,0 +1,5 @@ +**/templates/** +dist-dynamic +dist-scalprum +!.eslintrc.js +!.prettierrc.js \ No newline at end of file diff --git a/workspaces/rbac/plugins/rbac/.eslintrc.js b/workspaces/rbac/plugins/rbac/.eslintrc.js new file mode 100644 index 0000000000..8bd689af97 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/.eslintrc.js @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/workspaces/rbac/plugins/rbac/.lintstagedrc.json b/workspaces/rbac/plugins/rbac/.lintstagedrc.json new file mode 100644 index 0000000000..14b2263def --- /dev/null +++ b/workspaces/rbac/plugins/rbac/.lintstagedrc.json @@ -0,0 +1,4 @@ +{ + "*": "prettier --ignore-unknown --write", + "*.{js,jsx,ts,tsx,mjs,cjs}": "backstage-cli package lint --fix" +} diff --git a/workspaces/rbac/plugins/rbac/.prettierignore b/workspaces/rbac/plugins/rbac/.prettierignore new file mode 100644 index 0000000000..fc8357d99e --- /dev/null +++ b/workspaces/rbac/plugins/rbac/.prettierignore @@ -0,0 +1,12 @@ +dist +dist-types +coverage +.vscode +CHANGELOG.md +generated +templates +*.hbs +renovate.json +dist-dynamic +dist-scalprum +playwright-report diff --git a/workspaces/rbac/plugins/rbac/.prettierrc.js b/workspaces/rbac/plugins/rbac/.prettierrc.js new file mode 100644 index 0000000000..5b35247f3f --- /dev/null +++ b/workspaces/rbac/plugins/rbac/.prettierrc.js @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */ +module.exports = { + ...require('@spotify/prettier-config'), + plugins: ['@ianvs/prettier-plugin-sort-imports'], + importOrder: [ + '^react(.*)$', + '', + '^@backstage/(.*)$', + '', + '', + '', + '^@backstage-community/(.*)$', + '', + '', + '', + '^[.]', + ], +}; diff --git a/workspaces/rbac/plugins/rbac/.versionhistory.md b/workspaces/rbac/plugins/rbac/.versionhistory.md new file mode 100644 index 0000000000..40f1d873e5 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/.versionhistory.md @@ -0,0 +1 @@ +- Bumped to 1.30.0 in main branch for next release 1.3.0 diff --git a/workspaces/rbac/plugins/rbac/CHANGELOG.md b/workspaces/rbac/plugins/rbac/CHANGELOG.md new file mode 100644 index 0000000000..b8bfdd0ee7 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/CHANGELOG.md @@ -0,0 +1,747 @@ +### Dependencies + +## 1.32.0 + +### Minor Changes + +- 8244f28: chore(deps): update to backstage 1.32 + +### Patch Changes + +- Updated dependencies [8244f28] + - @janus-idp/shared-react@2.13.0 + - @backstage-community/plugin-rbac-common@1.12.0 + +## 1.31.1 + +### Patch Changes + +- 7342e9b: chore: remove @janus-idp/cli dep and relink local packages + + This update removes `@janus-idp/cli` from all plugins, as it’s no longer necessary. Additionally, packages are now correctly linked with a specified version. + +## 1.31.0 + +### Minor Changes + +- d9551ae: feat(deps): update to backstage 1.31 + +### Patch Changes + +- d9551ae: Change local package references to a `*` +- d9551ae: pin the @janus-idp/cli package +- d9551ae: upgrade to yarn v3 +- Updated dependencies [d9551ae] +- Updated dependencies [d9551ae] +- Updated dependencies [d9551ae] +- Updated dependencies [d9551ae] + - @janus-idp/shared-react@2.12.0 + - @backstage-community/plugin-rbac-common@1.11.0 + +* **@janus-idp/cli:** upgraded to 1.15.2 + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.15.1 + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.11.1 +- **@janus-idp/cli:** upgraded to 1.15.0 + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.10.0 +- **@janus-idp/shared-react:** upgraded to 2.11.0 +- **@janus-idp/cli:** upgraded to 1.14.0 + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.13.2 + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.10.3 + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.10.2 + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.10.1 + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.9.0 + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.13.1 + +## @backstage-community/plugin-rbac [1.27.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.27.0...@backstage-community/plugin-rbac@1.27.1) (2024-08-06) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.8.2 + +## @backstage-community/plugin-rbac [1.27.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.26.1...@backstage-community/plugin-rbac@1.27.0) (2024-08-06) + +### Features + +- **rbac:** nested condition ([#1814](https://github.com/janus-idp/backstage-plugins/issues/1814)) ([228f1a9](https://github.com/janus-idp/backstage-plugins/commit/228f1a986f851885bbdba49686449b97350097d2)) + +## @backstage-community/plugin-rbac [1.26.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.26.0...@backstage-community/plugin-rbac@1.26.1) (2024-08-05) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.8.1 + +## @backstage-community/plugin-rbac [1.26.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.25.0...@backstage-community/plugin-rbac@1.26.0) (2024-08-02) + +### Features + +- **rbac:** show list of accessible plugins in roles list page ([#1894](https://github.com/janus-idp/backstage-plugins/issues/1894)) ([62d9d6c](https://github.com/janus-idp/backstage-plugins/commit/62d9d6c30ab393755003c9a7387b67e25dd4c3d9)) + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.10.0 + +## @backstage-community/plugin-rbac [1.25.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.24.1...@backstage-community/plugin-rbac@1.25.0) (2024-07-26) + +### Features + +- **deps:** update to backstage 1.29 ([#1900](https://github.com/janus-idp/backstage-plugins/issues/1900)) ([f53677f](https://github.com/janus-idp/backstage-plugins/commit/f53677fb02d6df43a9de98c43a9f101a6db76802)) + +### Bug Fixes + +- **deps:** update rhdh dependencies (non-major) ([#1960](https://github.com/janus-idp/backstage-plugins/issues/1960)) ([8b6c249](https://github.com/janus-idp/backstage-plugins/commit/8b6c249f1d2e8097cac0260785c26496a5be1a06)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.8.0 +- **@janus-idp/shared-react:** upgraded to 2.9.0 + +## @backstage-community/plugin-rbac [1.24.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.24.0...@backstage-community/plugin-rbac@1.24.1) (2024-07-24) + +### Bug Fixes + +- **deps:** rollback unreleased plugins ([#1951](https://github.com/janus-idp/backstage-plugins/issues/1951)) ([8b77969](https://github.com/janus-idp/backstage-plugins/commit/8b779694f02f8125587296305276b84cdfeeaebe)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.7.2 + +## @backstage-community/plugin-rbac [1.24.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.23.2...@backstage-community/plugin-rbac@1.24.0) (2024-07-24) + +### Features + +- **deps:** update to backstage 1.28 ([#1891](https://github.com/janus-idp/backstage-plugins/issues/1891)) ([1ba1108](https://github.com/janus-idp/backstage-plugins/commit/1ba11088e0de60e90d138944267b83600dc446e5)) + +### Bug Fixes + +- **deps:** fix rbac dependencies ([#1918](https://github.com/janus-idp/backstage-plugins/issues/1918)) ([fcc4e1d](https://github.com/janus-idp/backstage-plugins/commit/fcc4e1dde55bc0fb2dd284d256330c7f9f928036)) + +### Documentation + +- fix rbac integration code samples ([#1905](https://github.com/janus-idp/backstage-plugins/issues/1905)) ([3a8da8d](https://github.com/janus-idp/backstage-plugins/commit/3a8da8d77797ab2bd7933c41764c9222c79b0016)) + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.8.0 + +## @backstage-community/plugin-rbac [1.23.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.23.1...@backstage-community/plugin-rbac@1.23.2) (2024-07-17) + +### Bug Fixes + +- **rbac:** background color of sidebar in dark theme ([#1859](https://github.com/janus-idp/backstage-plugins/issues/1859)) ([ec6a2d8](https://github.com/janus-idp/backstage-plugins/commit/ec6a2d83e134988a963b331a0e2cd3a9bb58e26a)) + +## @backstage-community/plugin-rbac [1.23.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.23.0...@backstage-community/plugin-rbac@1.23.1) (2024-07-11) + +### Other changes + +- **rbac:** reduce cognitive stress for create, update and delete role functions ([#1878](https://github.com/janus-idp/backstage-plugins/issues/1878)) ([38c3144](https://github.com/janus-idp/backstage-plugins/commit/38c314450b01ebe71463362685242956483fef3d)) + +## @backstage-community/plugin-rbac [1.23.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.22.1...@backstage-community/plugin-rbac@1.23.0) (2024-07-08) + +### Features + +- **rbac:** show rules count in overview page ([#1845](https://github.com/janus-idp/backstage-plugins/issues/1845)) ([a10dc36](https://github.com/janus-idp/backstage-plugins/commit/a10dc368cd4e0dc28d1a99dec6d06a5f578f49db)) + +## @backstage-community/plugin-rbac [1.22.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.22.0...@backstage-community/plugin-rbac@1.22.1) (2024-07-05) + +### Bug Fixes + +- **rbac:** correct plugin ID matching to permission policy ([#1795](https://github.com/janus-idp/backstage-plugins/issues/1795)) ([6dc4b1c](https://github.com/janus-idp/backstage-plugins/commit/6dc4b1c23d22252f394eecd8b795ac15507ecc50)) +- **rbac:** edit role page loads error page ([#1849](https://github.com/janus-idp/backstage-plugins/issues/1849)) ([6782b4b](https://github.com/janus-idp/backstage-plugins/commit/6782b4bda08019c6f81b211486ff47ba1fb2d6bf)) +- **rbac:** update rbac common to fix compilation ([#1858](https://github.com/janus-idp/backstage-plugins/issues/1858)) ([48f142b](https://github.com/janus-idp/backstage-plugins/commit/48f142b447f0d1677ba3f16b2a3c8972b22d0588)) + +## @backstage-community/plugin-rbac [1.22.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.21.1...@backstage-community/plugin-rbac@1.22.0) (2024-06-21) + +### Features + +- **rbac:** show total no. of rules in review and create step ([#1827](https://github.com/janus-idp/backstage-plugins/issues/1827)) ([e54c470](https://github.com/janus-idp/backstage-plugins/commit/e54c4705de0b318a759e928999b5dcdb29f9846f)) + +## @backstage-community/plugin-rbac [1.21.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.21.0...@backstage-community/plugin-rbac@1.21.1) (2024-06-19) + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.7.1 +- **@janus-idp/cli:** upgraded to 1.11.1 + +## @backstage-community/plugin-rbac [1.21.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.15...@backstage-community/plugin-rbac@1.21.0) (2024-06-13) + +### Features + +- **deps:** update to backstage 1.27 ([#1683](https://github.com/janus-idp/backstage-plugins/issues/1683)) ([a14869c](https://github.com/janus-idp/backstage-plugins/commit/a14869c3f4177049cb8d6552b36c3ffd17e7997d)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.6.0 +- **@janus-idp/shared-react:** upgraded to 2.7.0 +- **@janus-idp/cli:** upgraded to 1.11.0 + +## @backstage-community/plugin-rbac [1.20.15](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.14...@backstage-community/plugin-rbac@1.20.15) (2024-06-13) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.10.1 + +## @backstage-community/plugin-rbac [1.20.14](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.13...@backstage-community/plugin-rbac@1.20.14) (2024-06-05) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.10.0 + +## @backstage-community/plugin-rbac [1.20.13](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.12...@backstage-community/plugin-rbac@1.20.13) (2024-06-04) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.5.0 +- **@janus-idp/shared-react:** upgraded to 2.6.4 + +## @backstage-community/plugin-rbac [1.20.12](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.11...@backstage-community/plugin-rbac@1.20.12) (2024-06-04) + +### Bug Fixes + +- **rbac:** fix role list view permission policies column value ([#1714](https://github.com/janus-idp/backstage-plugins/issues/1714)) ([07200e4](https://github.com/janus-idp/backstage-plugins/commit/07200e42d62c51c2ff59e812521ad0c82cb62ea8)) + +## @backstage-community/plugin-rbac [1.20.11](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.10...@backstage-community/plugin-rbac@1.20.11) (2024-06-03) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.9.0 + +## @backstage-community/plugin-rbac [1.20.10](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.9...@backstage-community/plugin-rbac@1.20.10) (2024-06-03) + +### Bug Fixes + +- **rbac:** add proper empty page for RBAC plugin ([#1728](https://github.com/janus-idp/backstage-plugins/issues/1728)) ([79e62a6](https://github.com/janus-idp/backstage-plugins/commit/79e62a6f120a7390af2e2bdc1e6dc6962c0e3780)) + +## @backstage-community/plugin-rbac [1.20.9](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.8...@backstage-community/plugin-rbac@1.20.9) (2024-05-31) + +## @backstage-community/plugin-rbac [1.20.8](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.7...@backstage-community/plugin-rbac@1.20.8) (2024-05-31) + +### Bug Fixes + +- **rbac:** improve criteria toggle button readability on dark themes ([#1755](https://github.com/janus-idp/backstage-plugins/issues/1755)) ([345230b](https://github.com/janus-idp/backstage-plugins/commit/345230baa4188ce659b7c48c114fa98b68d41a0c)) + +## @backstage-community/plugin-rbac [1.20.7](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.6...@backstage-community/plugin-rbac@1.20.7) (2024-05-31) + +### Bug Fixes + +- **rbac:** show configure-access cta for existing simple permission policies in edit form ([#1702](https://github.com/janus-idp/backstage-plugins/issues/1702)) ([16b7e00](https://github.com/janus-idp/backstage-plugins/commit/16b7e00646153dffd9919f32e57853dbcbd2facb)) + +## @backstage-community/plugin-rbac [1.20.6](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.5...@backstage-community/plugin-rbac@1.20.6) (2024-05-30) + +### Bug Fixes + +- **rbac:** fix to enable create and edit role buttons on having correct permissions ([#1703](https://github.com/janus-idp/backstage-plugins/issues/1703)) ([19a9088](https://github.com/janus-idp/backstage-plugins/commit/19a908844f48b59116e92091169dd906c45f5621)) + +## @backstage-community/plugin-rbac [1.20.5](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.4...@backstage-community/plugin-rbac@1.20.5) (2024-05-30) + +### Bug Fixes + +- **rbac:** do not disable already selected rule for allOf/anyOf ([#1739](https://github.com/janus-idp/backstage-plugins/issues/1739)) ([dc73650](https://github.com/janus-idp/backstage-plugins/commit/dc73650587cd13e80923a36473a46e016fae3e81)) + +## @backstage-community/plugin-rbac [1.20.4](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.3...@backstage-community/plugin-rbac@1.20.4) (2024-05-29) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.8.10 + +## @backstage-community/plugin-rbac [1.20.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.2...@backstage-community/plugin-rbac@1.20.3) (2024-05-29) + +### Bug Fixes + +- **rbac:** conditional access form validation ([#1699](https://github.com/janus-idp/backstage-plugins/issues/1699)) ([d56f4af](https://github.com/janus-idp/backstage-plugins/commit/d56f4affd2538c5b9554e19b6ec2951d98d2b218)) +- **rbac:** enable save on remove-all button click ([#1712](https://github.com/janus-idp/backstage-plugins/issues/1712)) ([0502332](https://github.com/janus-idp/backstage-plugins/commit/0502332409b092ebc860c9a77d8b966ef920f7bf)) +- **rbac:** fix mui autocomplete related warnings ([#1707](https://github.com/janus-idp/backstage-plugins/issues/1707)) ([8e5c5ae](https://github.com/janus-idp/backstage-plugins/commit/8e5c5aef5e0472fdb876d81fc7f2356cfb4319f0)) + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.6.3 +- **@janus-idp/cli:** upgraded to 1.8.9 + +## @backstage-community/plugin-rbac [1.20.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.1...@backstage-community/plugin-rbac@1.20.2) (2024-05-16) + +## @backstage-community/plugin-rbac [1.20.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.20.0...@backstage-community/plugin-rbac@1.20.1) (2024-05-16) + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.6.2 +- **@janus-idp/cli:** upgraded to 1.8.7 + +## @backstage-community/plugin-rbac [1.20.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.19.0...@backstage-community/plugin-rbac@1.20.0) (2024-05-15) + +### Features + +- **rbac:** support for updating/deleting conditional permissions ([#1628](https://github.com/janus-idp/backstage-plugins/issues/1628)) ([2bb8308](https://github.com/janus-idp/backstage-plugins/commit/2bb8308d53e539023dd87573a66ad25501ada7d1)) + +## @backstage-community/plugin-rbac [1.19.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.18.2...@backstage-community/plugin-rbac@1.19.0) (2024-05-14) + +### Features + +- **deps:** use RHDH themes in the backstage app and dev pages ([#1480](https://github.com/janus-idp/backstage-plugins/issues/1480)) ([8263bf0](https://github.com/janus-idp/backstage-plugins/commit/8263bf099736cbb0d0f2316082d338ba81fa6927)) + +## @backstage-community/plugin-rbac [1.18.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.18.1...@backstage-community/plugin-rbac@1.18.2) (2024-05-09) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.4.2 +- **@janus-idp/shared-react:** upgraded to 2.6.1 +- **@janus-idp/cli:** upgraded to 1.8.6 + +## @backstage-community/plugin-rbac [1.18.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.18.0...@backstage-community/plugin-rbac@1.18.1) (2024-05-08) + +### Bug Fixes + +- **rbac:** hide frontend when permission framework was disabled ([#1493](https://github.com/janus-idp/backstage-plugins/issues/1493)) ([5aa012f](https://github.com/janus-idp/backstage-plugins/commit/5aa012f0a35c1ee1269c570e4b5c94032f592559)) + +## @backstage-community/plugin-rbac [1.18.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.17.11...@backstage-community/plugin-rbac@1.18.0) (2024-05-08) + +### Features + +- **rbac:** support for adding conditional permissions ([#1588](https://github.com/janus-idp/backstage-plugins/issues/1588)) ([2042244](https://github.com/janus-idp/backstage-plugins/commit/2042244dc31a557d62bfcf6f7eb556c12154430e)) + +## @backstage-community/plugin-rbac [1.17.11](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.17.10...@backstage-community/plugin-rbac@1.17.11) (2024-05-02) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.8.5 + +## @backstage-community/plugin-rbac [1.17.10](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.17.9...@backstage-community/plugin-rbac@1.17.10) (2024-05-02) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.8.4 + +## @backstage-community/plugin-rbac [1.17.9](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.17.8...@backstage-community/plugin-rbac@1.17.9) (2024-04-30) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.8.3 + +## @backstage-community/plugin-rbac [1.17.8](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.17.7...@backstage-community/plugin-rbac@1.17.8) (2024-04-30) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.8.2 + +## @backstage-community/plugin-rbac [1.17.7](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.17.6...@backstage-community/plugin-rbac@1.17.7) (2024-04-25) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.8.1 + +## @backstage-community/plugin-rbac [1.17.6](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.17.5...@backstage-community/plugin-rbac@1.17.6) (2024-04-17) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.4.1 + +## @backstage-community/plugin-rbac [1.17.5](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.17.4...@backstage-community/plugin-rbac@1.17.5) (2024-04-15) + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.6.0 +- **@janus-idp/cli:** upgraded to 1.8.0 + +## @backstage-community/plugin-rbac [1.17.4](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.17.3...@backstage-community/plugin-rbac@1.17.4) (2024-04-09) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.7.10 + +## @backstage-community/plugin-rbac [1.17.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.17.2...@backstage-community/plugin-rbac@1.17.3) (2024-04-09) + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.5.5 + +## @backstage-community/plugin-rbac [1.17.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.17.1...@backstage-community/plugin-rbac@1.17.2) (2024-04-09) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.7.9 + +## @backstage-community/plugin-rbac [1.17.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.17.0...@backstage-community/plugin-rbac@1.17.1) (2024-04-08) + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.5.4 + +## @backstage-community/plugin-rbac [1.17.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.16.2...@backstage-community/plugin-rbac@1.17.0) (2024-04-05) + +### Features + +- **rbac:** save role modification information to the metadata ([#1280](https://github.com/janus-idp/backstage-plugins/issues/1280)) ([0454509](https://github.com/janus-idp/backstage-plugins/commit/0454509e41db2ae332d1b2bf8f72d34241483efd)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.4.0 +- **@janus-idp/shared-react:** upgraded to 2.5.4 +- **@janus-idp/cli:** upgraded to 1.7.8 + +## @backstage-community/plugin-rbac [1.16.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.16.1...@backstage-community/plugin-rbac@1.16.2) (2024-04-04) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.3.2 + +## @backstage-community/plugin-rbac [1.16.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.16.0...@backstage-community/plugin-rbac@1.16.1) (2024-04-03) + +### Documentation + +- **rbac:** update to the rbac documentation ([#1433](https://github.com/janus-idp/backstage-plugins/issues/1433)) ([5d96db3](https://github.com/janus-idp/backstage-plugins/commit/5d96db3550690658341425786b6a382ea162faac)) + +## @backstage-community/plugin-rbac [1.16.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.15.7...@backstage-community/plugin-rbac@1.16.0) (2024-04-03) + +### Features + +- **rbac:** add conditional access button and sidebar ([#1359](https://github.com/janus-idp/backstage-plugins/issues/1359)) ([448267d](https://github.com/janus-idp/backstage-plugins/commit/448267d017247fbcb595452783e628467a3582fe)) + +## @backstage-community/plugin-rbac [1.15.7](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.15.6...@backstage-community/plugin-rbac@1.15.7) (2024-04-02) + +### Bug Fixes + +- **rbac:** align styles with UXD ([#1416](https://github.com/janus-idp/backstage-plugins/issues/1416)) ([1df3592](https://github.com/janus-idp/backstage-plugins/commit/1df3592e298f66db1eda5b21c80aeda99fb2d7ce)) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.7.7 + +## @backstage-community/plugin-rbac [1.15.6](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.15.5...@backstage-community/plugin-rbac@1.15.6) (2024-03-29) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.3.1 +- **@janus-idp/shared-react:** upgraded to 2.5.3 +- **@janus-idp/cli:** upgraded to 1.7.6 + +## @backstage-community/plugin-rbac [1.15.5](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.15.4...@backstage-community/plugin-rbac@1.15.5) (2024-03-26) + +### Bug Fixes + +- **rbac:** alert display issue after role creating/updating ([#1354](https://github.com/janus-idp/backstage-plugins/issues/1354)) ([2e04ccb](https://github.com/janus-idp/backstage-plugins/commit/2e04ccb0c2853b8b08de671723d421df64d51699)) + +## @backstage-community/plugin-rbac [1.15.4](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.15.3...@backstage-community/plugin-rbac@1.15.4) (2024-03-25) + +### Bug Fixes + +- **rbac:** yarn lint command ([#1361](https://github.com/janus-idp/backstage-plugins/issues/1361)) ([459b909](https://github.com/janus-idp/backstage-plugins/commit/459b90985013695fb9626ac9b547cf0627a385be)) + +### Other changes + +- **rbac:** add playwright tests for the plugin ([#1305](https://github.com/janus-idp/backstage-plugins/issues/1305)) ([16d0686](https://github.com/janus-idp/backstage-plugins/commit/16d0686ef8cc0d84d93e9e06d46f23b5bb7d5a1f)) + +## @backstage-community/plugin-rbac [1.15.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.15.2...@backstage-community/plugin-rbac@1.15.3) (2024-03-04) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.7.5 + +## @backstage-community/plugin-rbac [1.15.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.15.1...@backstage-community/plugin-rbac@1.15.2) (2024-02-27) + +### Bug Fixes + +- **rbac:** fixed autocomplete text input behavior on clear ([#1256](https://github.com/janus-idp/backstage-plugins/issues/1256)) ([cb70ff7](https://github.com/janus-idp/backstage-plugins/commit/cb70ff77fde0013eef58b233de226818617fcf6e)) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.7.4 + +## @backstage-community/plugin-rbac [1.15.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.15.0...@backstage-community/plugin-rbac@1.15.1) (2024-02-26) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.7.3 + +## @backstage-community/plugin-rbac [1.15.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.14.5...@backstage-community/plugin-rbac@1.15.0) (2024-02-26) + +### Features + +- **rbac:** save and display role description in the frontend ([#1206](https://github.com/janus-idp/backstage-plugins/issues/1206)) ([ff61266](https://github.com/janus-idp/backstage-plugins/commit/ff61266a729d472a0e4ff57cd9d2d6ea2389b820)) + +## @backstage-community/plugin-rbac [1.14.5](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.14.4...@backstage-community/plugin-rbac@1.14.5) (2024-02-21) + +### Bug Fixes + +- **rbac:** add test selectors ([#1229](https://github.com/janus-idp/backstage-plugins/issues/1229)) ([dca5f2e](https://github.com/janus-idp/backstage-plugins/commit/dca5f2e4e7db29e522752bd5743f41a83bcb6f32)) +- **rbac:** fix labels and dropdowns in dark theme by aligning/downgrading components to MUI v4 ([#1243](https://github.com/janus-idp/backstage-plugins/issues/1243)) ([ad44fa8](https://github.com/janus-idp/backstage-plugins/commit/ad44fa8a445234c1e2be0c6386dd1374feba03b0)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.3.0 +- **@janus-idp/shared-react:** upgraded to 2.5.2 +- **@janus-idp/cli:** upgraded to 1.7.2 + +## @backstage-community/plugin-rbac [1.14.4](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.14.3...@backstage-community/plugin-rbac@1.14.4) (2024-02-20) + +### Bug Fixes + +- **rbac:** add data-testid, names and aria-label to RBAC UI components ([#1224](https://github.com/janus-idp/backstage-plugins/issues/1224)) ([cabc76d](https://github.com/janus-idp/backstage-plugins/commit/cabc76ddf3a4b810b221de5982adbe403f3e5fac)) + +## @backstage-community/plugin-rbac [1.14.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.14.2...@backstage-community/plugin-rbac@1.14.3) (2024-02-16) + +### Bug Fixes + +- **rbac:** fix rbac tab route ([#1213](https://github.com/janus-idp/backstage-plugins/issues/1213)) ([218ab45](https://github.com/janus-idp/backstage-plugins/commit/218ab455b3cab5e95b235b4483c7d9bf53ca125e)) + +## @backstage-community/plugin-rbac [1.14.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.14.1...@backstage-community/plugin-rbac@1.14.2) (2024-02-08) + +### Bug Fixes + +- **rbac:** add test for 0 members in group ([#1189](https://github.com/janus-idp/backstage-plugins/issues/1189)) ([afebb56](https://github.com/janus-idp/backstage-plugins/commit/afebb566b314743fc2e1b2d7b12c74c96841ce26)) + +## @backstage-community/plugin-rbac [1.14.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.14.0...@backstage-community/plugin-rbac@1.14.1) (2024-02-08) + +### Bug Fixes + +- **rbac:** show 0 if no members in a group ([#1187](https://github.com/janus-idp/backstage-plugins/issues/1187)) ([0410800](https://github.com/janus-idp/backstage-plugins/commit/0410800f55f3fb43b75a144943ea70fc2ceca444)) + +## @backstage-community/plugin-rbac [1.14.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.13.2...@backstage-community/plugin-rbac@1.14.0) (2024-02-08) + +### Features + +- **rbac:** use relative links ([#1185](https://github.com/janus-idp/backstage-plugins/issues/1185)) ([9fcab95](https://github.com/janus-idp/backstage-plugins/commit/9fcab95869413f005c3246d0f9cd2b2b5acbe8cb)) + +## @backstage-community/plugin-rbac [1.13.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.13.1...@backstage-community/plugin-rbac@1.13.2) (2024-02-05) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.7.1 + +## @backstage-community/plugin-rbac [1.13.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.13.0...@backstage-community/plugin-rbac@1.13.1) (2024-02-02) + +### Bug Fixes + +- **rbac:** update the RBAC frontend plugin readme ([#1155](https://github.com/janus-idp/backstage-plugins/issues/1155)) ([8db80b9](https://github.com/janus-idp/backstage-plugins/commit/8db80b921ec83fce0d719f430bbdc77276a0e847)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.2.1 +- **@janus-idp/shared-react:** upgraded to 2.5.1 + +## @backstage-community/plugin-rbac [1.13.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.12.3...@backstage-community/plugin-rbac@1.13.0) (2024-01-31) + +### Features + +- **rbac:** turn rbac plugin into a dynamic plugin ([#1133](https://github.com/janus-idp/backstage-plugins/issues/1133)) ([b9b36d5](https://github.com/janus-idp/backstage-plugins/commit/b9b36d5b58b86eed457ffb347af785b3181a9de7)) + +## @backstage-community/plugin-rbac [1.12.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.12.2...@backstage-community/plugin-rbac@1.12.3) (2024-01-30) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.7.0 + +## @backstage-community/plugin-rbac [1.12.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.12.1...@backstage-community/plugin-rbac@1.12.2) (2024-01-30) + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.5.0 + +## @backstage-community/plugin-rbac [1.12.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.12.0...@backstage-community/plugin-rbac@1.12.1) (2024-01-30) + +### Bug Fixes + +- **rbac:** watch users and permission-policies ([#1102](https://github.com/janus-idp/backstage-plugins/issues/1102)) ([cec734b](https://github.com/janus-idp/backstage-plugins/commit/cec734b3998e37fce9b7291640beb7fc2d797939)) + +## @backstage-community/plugin-rbac [1.12.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.11.1...@backstage-community/plugin-rbac@1.12.0) (2024-01-29) + +### Features + +- **rbac:** disable selected permissions ([#1117](https://github.com/janus-idp/backstage-plugins/issues/1117)) ([00cd501](https://github.com/janus-idp/backstage-plugins/commit/00cd501d6cd587c8a7b151189da30dd8c9865803)) + +## @backstage-community/plugin-rbac [1.11.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.11.0...@backstage-community/plugin-rbac@1.11.1) (2024-01-25) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.6.0 + +## @backstage-community/plugin-rbac [1.11.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.10.2...@backstage-community/plugin-rbac@1.11.0) (2024-01-24) + +### Features + +- **rbac:** center align toast ([#1090](https://github.com/janus-idp/backstage-plugins/issues/1090)) ([697c96f](https://github.com/janus-idp/backstage-plugins/commit/697c96f25c220750ae290879e3020ecc1a5f03c5)) + +## @backstage-community/plugin-rbac [1.10.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.10.1...@backstage-community/plugin-rbac@1.10.2) (2024-01-23) + +### Bug Fixes + +- **rbac:** fix the roles table to also watch policies ([#1057](https://github.com/janus-idp/backstage-plugins/issues/1057)) ([ead78e2](https://github.com/janus-idp/backstage-plugins/commit/ead78e2e96e208ef394497d06452c3f3415af31b)) + +## @backstage-community/plugin-rbac [1.10.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.10.0...@backstage-community/plugin-rbac@1.10.1) (2024-01-18) + +### Bug Fixes + +- **rbac:** update the rbac ui readme ([#1079](https://github.com/janus-idp/backstage-plugins/issues/1079)) ([145e95b](https://github.com/janus-idp/backstage-plugins/commit/145e95bf47cead017872f130ee1c60f77809ff80)) + +## @backstage-community/plugin-rbac [1.10.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.9.0...@backstage-community/plugin-rbac@1.10.0) (2024-01-17) + +### Features + +- **rbac:** allow editing permission policies ([#1037](https://github.com/janus-idp/backstage-plugins/issues/1037)) ([c10347d](https://github.com/janus-idp/backstage-plugins/commit/c10347d1ecaa13d6d786ab51a05c6046530e457c)) +- **rbac:** show warning alert when user is not authorised to create roles ([#1064](https://github.com/janus-idp/backstage-plugins/issues/1064)) ([b5c46c8](https://github.com/janus-idp/backstage-plugins/commit/b5c46c8d19a092b8ecef653a48331d844cfb3c8c)) + +### Bug Fixes + +- **rbac:** disable edit when the user is unauthorized to read the catalog-entity ([#1049](https://github.com/janus-idp/backstage-plugins/issues/1049)) ([c4f2969](https://github.com/janus-idp/backstage-plugins/commit/c4f296960f450e29bd8cbd34f5ecbf1aae0f0837)) + +### Dependencies + +- **@janus-idp/shared-react:** upgraded to 2.4.0 + +## @backstage-community/plugin-rbac [1.9.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.8.0...@backstage-community/plugin-rbac@1.9.0) (2023-12-21) + +### Features + +- **rbac:** support for adding permission policies to roles ([#1021](https://github.com/janus-idp/backstage-plugins/issues/1021)) ([dd11c3a](https://github.com/janus-idp/backstage-plugins/commit/dd11c3a14eebaea9e8acc43b0c28b338d5fa14c1)) + +## @backstage-community/plugin-rbac [1.8.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.7.0...@backstage-community/plugin-rbac@1.8.0) (2023-12-20) + +### Features + +- **rbac:** cleanup policies when a role is deleted ([#1018](https://github.com/janus-idp/backstage-plugins/issues/1018)) ([fb0ee8c](https://github.com/janus-idp/backstage-plugins/commit/fb0ee8c269892f6c2ccaea69754a9dda653d4fcb)) + +## @backstage-community/plugin-rbac [1.7.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.6.0...@backstage-community/plugin-rbac@1.7.0) (2023-12-15) + +### Features + +- **rbac:** allow editing roles ([#1001](https://github.com/janus-idp/backstage-plugins/issues/1001)) ([2e81062](https://github.com/janus-idp/backstage-plugins/commit/2e810620ea5641df827dfe83bf7695cf16117033)) + +## @backstage-community/plugin-rbac [1.6.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.5.1...@backstage-community/plugin-rbac@1.6.0) (2023-12-12) + +### Features + +- **rbac:** add support for creation of role ([#974](https://github.com/janus-idp/backstage-plugins/issues/974)) ([7cb9cbd](https://github.com/janus-idp/backstage-plugins/commit/7cb9cbdba6076ffc5447e560de197ecd68ba6e40)) + +## @backstage-community/plugin-rbac [1.5.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.5.0...@backstage-community/plugin-rbac@1.5.1) (2023-12-07) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.4.7 + +## @backstage-community/plugin-rbac [1.5.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.4.0...@backstage-community/plugin-rbac@1.5.0) (2023-12-07) + +### Features + +- **rbac:** list roles with no permission policies ([#998](https://github.com/janus-idp/backstage-plugins/issues/998)) ([217b7b0](https://github.com/janus-idp/backstage-plugins/commit/217b7b0db3414788c8e77247f378a51cf0eeda0d)) + +## @backstage-community/plugin-rbac [1.4.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.3.1...@backstage-community/plugin-rbac@1.4.0) (2023-12-05) + +### Features + +- **rbac:** role overview ([#972](https://github.com/janus-idp/backstage-plugins/issues/972)) ([43c1906](https://github.com/janus-idp/backstage-plugins/commit/43c19064e9477a5449ff5d56b00efe27cf640c27)) + +### Dependencies + +- **@backstage-community/plugin-rbac-common:** upgraded to 1.2.0 + +## @backstage-community/plugin-rbac [1.3.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.3.0...@backstage-community/plugin-rbac@1.3.1) (2023-11-30) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.4.6 + +## @backstage-community/plugin-rbac [1.3.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.2.4...@backstage-community/plugin-rbac@1.3.0) (2023-11-28) + +### Features + +- **rbac:** list roles ([#937](https://github.com/janus-idp/backstage-plugins/issues/937)) ([8722056](https://github.com/janus-idp/backstage-plugins/commit/8722056088a3214f6267c621ecc10e3658484a07)) + +## @backstage-community/plugin-rbac [1.2.4](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.2.3...@backstage-community/plugin-rbac@1.2.4) (2023-11-22) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.4.5 + +## @backstage-community/plugin-rbac [1.2.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.2.2...@backstage-community/plugin-rbac@1.2.3) (2023-11-21) + +### Bug Fixes + +- sync versions in dynamic assets and publish derived packages as additional packages ([#963](https://github.com/janus-idp/backstage-plugins/issues/963)) ([7d0a386](https://github.com/janus-idp/backstage-plugins/commit/7d0a38609b4a18b54c75378a150e8b5c3ba8ff43)) + +## @backstage-community/plugin-rbac [1.2.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.2.1...@backstage-community/plugin-rbac@1.2.2) (2023-11-20) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.4.4 + +## @backstage-community/plugin-rbac [1.2.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.2.0...@backstage-community/plugin-rbac@1.2.1) (2023-11-16) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.4.3 + +## @backstage-community/plugin-rbac [1.2.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.1.5...@backstage-community/plugin-rbac@1.2.0) (2023-11-15) + +### Features + +- **rbac:** display administration to authorized users ([#895](https://github.com/janus-idp/backstage-plugins/issues/895)) ([70ae509](https://github.com/janus-idp/backstage-plugins/commit/70ae509e91e4967f4436a66c69be6040e235be0e)) + +## @backstage-community/plugin-rbac [1.1.5](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.1.4...@backstage-community/plugin-rbac@1.1.5) (2023-11-13) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.4.2 + +## @backstage-community/plugin-rbac [1.1.4](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.1.3...@backstage-community/plugin-rbac@1.1.4) (2023-11-13) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.4.1 + +## @backstage-community/plugin-rbac [1.1.3](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.1.2...@backstage-community/plugin-rbac@1.1.3) (2023-11-07) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.4.0 + +## @backstage-community/plugin-rbac [1.1.2](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.1.1...@backstage-community/plugin-rbac@1.1.2) (2023-11-06) + +### Bug Fixes + +- **cli:** add default scalprum config ([#909](https://github.com/janus-idp/backstage-plugins/issues/909)) ([d74fc72](https://github.com/janus-idp/backstage-plugins/commit/d74fc72ab7e0a843da047c7b6570d8a6fbc068e1)) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.3.3 + +## @backstage-community/plugin-rbac [1.1.1](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.1.0...@backstage-community/plugin-rbac@1.1.1) (2023-11-02) + +### Dependencies + +- **@janus-idp/cli:** upgraded to 1.3.2 + +## @backstage-community/plugin-rbac [1.1.0](https://github.com/janus-idp/backstage-plugins/compare/@backstage-community/plugin-rbac@1.0.0...@backstage-community/plugin-rbac@1.1.0) (2023-11-01) + +### Features + +- **dynamic-plugins:** publish dynamic assets for all frontend plugins ([#896](https://github.com/janus-idp/backstage-plugins/issues/896)) ([dcfb0ac](https://github.com/janus-idp/backstage-plugins/commit/dcfb0ac56769c82f6b8b2cef2726251e0b60c375)) + +## @backstage-community/plugin-rbac 1.0.0 (2023-10-23) + +### Features + +- **rbac:** add rbac frontend plugin ([#859](https://github.com/janus-idp/backstage-plugins/issues/859)) ([2a64b13](https://github.com/janus-idp/backstage-plugins/commit/2a64b137434ef3f9b685e16eb10b7a579f80cd3d)) diff --git a/workspaces/rbac/plugins/rbac/README.md b/workspaces/rbac/plugins/rbac/README.md new file mode 100644 index 0000000000..90dac8ca88 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/README.md @@ -0,0 +1,106 @@ +# RBAC frontend plugin for Backstage + +The RBAC UI plugin offers a streamlined user interface for effectively managing permissions in your Backstage instance. It allows you to assign permissions to users and groups, empowering them to view, create, modify and delete Roles, provided they have the necessary permissions. + +## For administrators + +### Installation + +#### Installing as a dynamic plugin? + +The sections below are relevant for static plugins. If the plugin is expected to be installed as a dynamic one: + +- follow https://github.com/janus-idp/backstage-showcase/blob/main/showcase-docs/dynamic-plugins.md#installing-a-dynamic-plugin-package-in-the-showcase +- add content of `app-config.yaml` into `app-config.local.yaml`. + +#### Prerequisites + +Follow the RBAC backend plugin [README](https://github.com/backstage/community-plugins/blob/main/workspaces/rbac/plugins/rbac-backend/README.md) to integrate rbac in your Backstage instance. + +--- + +**NOTE** + +- For non-admin users, to enable create/edit role button on Administration -> RBAC roles list page, the role associated with your user should have the following permission policies associated with it. Add the following in your permission policies configuration file: + +```CSV +p, role:default/team_a, catalog-entity, read, allow +p, role:default/team_a, policy-entity, read, allow +p, role:default/team_a, policy-entity, create, allow +g, user:default/, role:default/team_a +``` + +> Note: Make sure required users/groups are available in catalog as a role cannot be created without users/groups. + +> Note: Even after ingesting users/groups in catalog and applying above permissions if the create/edit button is still disabled then please contact your administrator as you might be conditionally restricted from accessing the create/edit button. + +- To fetch the permissions from other plugins like `Kubernetes` and `Jenkins` in the Role Form as mentioned [here](https://github.com/janus-idp/backstage-plugins/blob/main/plugins/rbac-backend/docs/permissions.md), add the following configuration in your `app-config.yaml`: + +```yaml title="app-config.yaml" +permission: + enabled: true + rbac: + pluginsWithPermission: + - kubernetes + - jenkins + admin: + users: + - name: user:default/ +``` + +--- + +#### Procedure + +1. Install the RBAC UI plugin executing the following command from the Backstage root directory : + + ```console + yarn workspace app add @backstage-community/plugin-rbac + ``` + +2. Add Route in `packages/app/src/App.tsx`: + + ```tsx title="packages/app/src/App.tsx" + /* highlight-add-next-line */ + import { RbacPage } from '@backstage-community/plugin-rbac'; + + } />; + ``` + +3. Add **Administration** Sidebar Item in `packages/app/src/components/Root/Root.tsx`: + + ```tsx title="packages/app/src/components/Root/Root.tsx" + /* highlight-add-next-line */ + import { Administration } from '@backstage-community/plugin-rbac'; + + export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + ... + + ... + + + ); + ``` + +4. For users with vanilla backstage instance, would need to integrate [`Auth`](https://backstage.io/docs/auth/) in to the instance: + + - ```yaml title="app-config.yaml" + # see https://backstage.io/docs/auth/ to learn about auth providers + environment: development + providers: + # Plugin: GitHub + github: + development: + clientId: ${GITHUB_BUCKET_CLIENT_ID} + clientSecret: ${GITHUB_BUCKET_SECRET} + # Plugin: BitBucket + bitbucket: + development: + clientId: ${BIT_BUCKET_CLIENT_ID} + clientSecret: ${BIT_BUCKET_SECRET} + ... + ``` + + - Integrate the [`SignIn`](https://backstage.io/docs/auth/#sign-in-configuration) component to be able to sign-in to the Backstage instance. diff --git a/workspaces/rbac/plugins/rbac/app-config.yaml b/workspaces/rbac/plugins/rbac/app-config.yaml new file mode 100644 index 0000000000..12ca33e070 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/app-config.yaml @@ -0,0 +1,19 @@ +dynamicPlugins: + frontend: + backstage-community.backstage-plugin-rbac: + appIcons: + - name: rbacIcon + importName: RbacIcon + module: RbacPlugin + dynamicRoutes: + - path: /rbac + module: RbacPlugin + importName: RbacPage + menuItem: + icon: rbacIcon + text: RBAC + menuItems: + rbac: + parent: admin + icon: rbacIcon + priority: 10 diff --git a/workspaces/rbac/plugins/rbac/catalog-info.yaml b/workspaces/rbac/plugins/rbac/catalog-info.yaml new file mode 100644 index 0000000000..f191025c31 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/catalog-info.yaml @@ -0,0 +1,57 @@ +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-community-rbac + title: RBAC plugin + description: RBAC for Backstage and RHDH + annotations: + backstage.io/source-location: url:https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac + backstage.io/view-url: https://github.com/backstage/community-plugins/blob/main/workspaces/rbac/plugins/rbac/catalog-info.yaml + backstage.io/edit-url: https://github.com/backstage/community-plugins/edit/main/workspaces/rbac/plugins/rbac/catalog-info.yaml + github.com/project-slug: backstage-community/backstage-plugins + github.com/team-slug: backstage/maintainers-plugins + sonarqube.org/project-key: backstage-community_plugins + tags: + - security + - rbac + links: + - url: https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac + title: GitHub Source + icon: source + type: source +spec: + type: backstage-plugin + lifecycle: production + owner: backstage-team + system: backstage + subcomponentOf: backstage-community-plugins +--- +# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-community-rbac-frontend + title: '@backstage-community/backstage-plugin-rbac' + description: RBAC frontend plugin for Backstage + annotations: + backstage.io/source-location: url:https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac + backstage.io/view-url: https://github.com/backstage/community-plugins/blob/main/workspaces/rbac/plugins/rbac/catalog-info.yaml + backstage.io/edit-url: https://github.com/backstage/community-plugins/edit/main/workspaces/rbac/plugins/rbac/catalog-info.yaml + github.com/project-slug: backstage-community/backstage-plugins + github.com/team-slug: backstage/maintainers-plugins + sonarqube.org/project-key: backstage-community_plugins + tags: + - security + - rbac + links: + - url: https://github.com/backstage/community-plugins/tree/main/workspaces/rbac/plugins/rbac + title: GitHub Source + icon: source + type: source +spec: + type: backstage-frontend-plugin + lifecycle: production + owner: backstage-ui-team + system: backstage + subcomponentOf: backstage-community-rbac diff --git a/workspaces/rbac/plugins/rbac/dev/index.tsx b/workspaces/rbac/plugins/rbac/dev/index.tsx new file mode 100644 index 0000000000..321ff1968c --- /dev/null +++ b/workspaces/rbac/plugins/rbac/dev/index.tsx @@ -0,0 +1,194 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { configApiRef } from '@backstage/core-plugin-api'; +import { createDevApp } from '@backstage/dev-utils'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; +import { + MockConfigApi, + MockPermissionApi, + TestApiProvider, +} from '@backstage/test-utils'; + +import { getAllThemes } from '@redhat-developer/red-hat-developer-hub-theme'; + +import { + PermissionAction, + PluginPermissionMetaData, + Role, + RoleBasedPolicy, + RoleConditionalPolicyDecision, +} from '@backstage-community/plugin-rbac-common'; + +import { mockConditionRules } from '../src/__fixtures__/mockConditionRules'; +import { mockConditions } from '../src/__fixtures__/mockConditions'; +import { mockMembers } from '../src/__fixtures__/mockMembers'; +import { mockPermissionPolicies } from '../src/__fixtures__/mockPermissionPolicies'; +import { mockPolicies } from '../src/__fixtures__/mockPolicies'; +import { RBACAPI, rbacApiRef } from '../src/api/RBACBackendClient'; +import { RbacPage, rbacPlugin } from '../src/plugin'; +import { MemberEntity, RoleBasedConditions, RoleError } from '../src/types'; + +class MockRBACApi implements RBACAPI { + readonly resources; + + constructor(fixtureData: Role[]) { + this.resources = fixtureData; + } + async isLicensePluginEnabled(): Promise { + return false; + } + async downloadStatistics(): Promise { + return { status: 200 } as Response; + } + + async getRoles(): Promise { + return this.resources; + } + + async getAssociatedPolicies( + entityReference: string, + ): Promise { + return mockPolicies.filter(pol => pol.entityReference === entityReference); + } + + async getPolicies(): Promise { + return mockPolicies; + } + + async getUserAuthorization(): Promise<{ status: string }> { + return { + status: 'Authorized', + }; + } + + async getRole(role: string): Promise { + const roleresource = this.resources.find(res => res.name === role); + return roleresource + ? [roleresource] + : ({ status: 404, statusText: 'Not Found' } as Response); + } + + async updateRole(_oldRole: Role, _newRole: Role): Promise { + return { status: 200 } as Response; + } + + async updatePolicies( + _entityReference: string, + _oldPolicies: RoleBasedPolicy[], + _newPolicies: RoleBasedPolicy[], + ): Promise { + return { status: 204 } as Response; + } + + async deleteRole(_roleName: string): Promise { + return { status: 204, statusText: 'Deleted Successfully' } as Response; + } + + async getMembers(): Promise { + return mockMembers; + } + + async listPermissions(): Promise { + return mockPermissionPolicies; + } + + async deletePolicies( + _entityRef: string, + _policies: RoleBasedPolicy[], + ): Promise { + return { + ok: true, + status: 204, + statusText: 'Deleted Successfully', + } as Response; + } + + async createRole(_role: Role): Promise { + return { status: 200 } as Response; + } + + async createPolicies(_policies: RoleBasedPolicy[]): Promise { + return { status: 200 } as Response; + } + + async getPluginsConditionRules(): Promise { + return mockConditionRules; + } + + async createConditionalPermission( + _conditionalPermission: RoleBasedConditions, + ): Promise { + return { status: 200 } as Response; + } + + async getRoleConditions( + roleRef: string, + ): Promise[] | Response> { + return mockConditions.filter(mc => mc.roleEntityRef === roleRef); + } + + async updateConditionalPolicies( + _conditionId: number, + _data: RoleBasedConditions, + ): Promise { + return { status: 200 } as Response; + } + + async deleteConditionalPolicies( + _conditionId: number, + ): Promise { + return { status: 204 } as Response; + } +} + +const mockPermissionApi = new MockPermissionApi(); +const mockRBACApi = new MockRBACApi([ + { + memberReferences: ['user:default/guest'], + name: 'role:default/guests', + }, + { + memberReferences: ['user:default/xyz', 'group:default/admins'], + name: 'role:default/rbac_admin', + }, +]); +const mockConfigApi = new MockConfigApi({ + permission: { + enabled: true, + }, +}); + +createDevApp() + .registerPlugin(rbacPlugin) + .addThemes(getAllThemes()) + .addPage({ + element: ( + + + + ), + title: 'Administration', + path: '/rbac', + }) + .render(); diff --git a/workspaces/rbac/plugins/rbac/package.json b/workspaces/rbac/plugins/rbac/package.json new file mode 100644 index 0000000000..b88715ff45 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/package.json @@ -0,0 +1,119 @@ +{ + "name": "@backstage-community/plugin-rbac", + "version": "1.32.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin", + "supported-versions": "1.32.5", + "pluginId": "rbac", + "pluginPackages": [ + "@backstage-community/plugin-rbac", + "@backstage-community/plugin-rbac-backend", + "@backstage-community/plugin-rbac-common", + "@backstage-community/plugin-rbac-node" + ] + }, + "sideEffects": false, + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint:check": "backstage-cli package lint", + "lint:fix": "backstage-cli package lint --fix", + "postpack": "backstage-cli package postpack", + "prepack": "backstage-cli package prepack", + "start": "backstage-cli package start", + "test": "backstage-cli package test --passWithNoTests --coverage", + "tsc": "tsc", + "prettier:check": "prettier --ignore-unknown --check .", + "prettier:fix": "prettier --ignore-unknown --write .", + "ui-test": "start-server-and-test start localhost:3000 'playwright test'" + }, + "dependencies": { + "@backstage-community/plugin-rbac-common": "^1.12.0", + "@backstage/catalog-model": "^1.7.0", + "@backstage/core-components": "^0.15.1", + "@backstage/core-plugin-api": "^1.10.0", + "@backstage/plugin-catalog": "^1.24.0", + "@backstage/plugin-catalog-common": "^1.1.0", + "@backstage/plugin-permission-common": "^0.8.1", + "@backstage/plugin-permission-react": "^0.4.27", + "@backstage/theme": "^0.6.0", + "@janus-idp/shared-react": "^2.13.0", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.11.3", + "@material-ui/lab": "^4.0.0-alpha.45", + "@mui/icons-material": "5.16.4", + "@mui/material": "^5.14.18", + "@rjsf/core": "^5.21.2", + "@rjsf/mui": "^5.21.2", + "@rjsf/utils": "^5.21.2", + "@rjsf/validator-ajv8": "^5.21.2", + "autosuggest-highlight": "^3.3.4", + "formik": "^2.4.5", + "react-use": "^17.4.0", + "yup": "^1.3.2" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-router-dom": "^6.0.0" + }, + "devDependencies": { + "@backstage/cli": "0.28.2", + "@backstage/core-app-api": "1.15.1", + "@backstage/dev-utils": "1.1.2", + "@backstage/test-utils": "1.7.0", + "@playwright/test": "1.45.3", + "@redhat-developer/red-hat-developer-hub-theme": "0.4.0", + "@spotify/prettier-config": "^15.0.0", + "@testing-library/dom": "^10.0.0", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^15.0.0", + "@testing-library/react-hooks": "8.0.1", + "@testing-library/user-event": "14.5.2", + "@types/autosuggest-highlight": "3.2.3", + "@types/node": "18.19.34", + "@types/react": "^18.2.58", + "canvas": "^2.11.2", + "msw": "1.3.3", + "prettier": "3.3.3", + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-router-dom": "^6.0.0", + "start-server-and-test": "2.0.8" + }, + "scalprum": { + "name": "janus-idp.backstage-plugin-rbac", + "exposedModules": { + "RbacPlugin": "./src/index.ts" + } + }, + "files": [ + "dist", + "dist-scalprum", + "app-config.yaml" + ], + "repository": { + "type": "git", + "url": "https://github.com/backstage/community-plugins", + "directory": "workspaces/rbac/plugins/rbac" + }, + "keywords": [ + "support:production", + "lifecycle:active", + "backstage", + "plugin" + ], + "homepage": "https://red.ht/rhdh", + "bugs": "https://github.com/backstage/community-plugins/issues", + "maintainers": [ + "@PatAKnight" + ], + "author": "Red Hat" +} diff --git a/workspaces/rbac/plugins/rbac/playwright.config.ts b/workspaces/rbac/plugins/rbac/playwright.config.ts new file mode 100644 index 0000000000..ae4cbb2799 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/playwright.config.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Run tests in sequence. */ + workers: 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? 'github' : 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + baseURL: process.env.PLUGIN_BASE_URL || 'http://localhost:3000', + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/workspaces/rbac/plugins/rbac/report.api.md b/workspaces/rbac/plugins/rbac/report.api.md new file mode 100644 index 0000000000..1aabd79f2a --- /dev/null +++ b/workspaces/rbac/plugins/rbac/report.api.md @@ -0,0 +1,50 @@ +## API Report File for "@backstage-community/plugin-rbac" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { default as AdminPanelSettingsOutlinedIcon } from '@mui/icons-material/AdminPanelSettingsOutlined'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { PathParams } from '@backstage/core-plugin-api'; +import { default as RbacIcon } from '@mui/icons-material/VpnKeyOutlined'; +import { RouteRef } from '@backstage/core-plugin-api'; +import { SubRouteRef } from '@backstage/core-plugin-api'; + +// Warning: (ae-missing-release-tag) "Administration" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const Administration: () => JSX_2.Element | null; + +export { AdminPanelSettingsOutlinedIcon } + +export { RbacIcon } + +// Warning: (ae-missing-release-tag) "RbacPage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const RbacPage: ({ useHeader }: { + useHeader?: boolean | undefined; +}) => JSX_2.Element; + +// Warning: (ae-missing-release-tag) "rbacPlugin" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const rbacPlugin: BackstagePlugin< { +root: RouteRef; +role: SubRouteRef>; +createRole: SubRouteRef; +}, {}, {}>; + +// Warnings were encountered during analysis: +// +// src/plugin.d.ts:2:22 - (ae-undocumented) Missing documentation for "rbacPlugin". +// src/plugin.d.ts:7:22 - (ae-undocumented) Missing documentation for "RbacPage". +// src/plugin.d.ts:10:22 - (ae-undocumented) Missing documentation for "Administration". + +// (No @packageDocumentation comment for this package) + +``` diff --git a/workspaces/rbac/plugins/rbac/src/__fixtures__/mockConditionRules.ts b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockConditionRules.ts new file mode 100644 index 0000000000..20284e64c7 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockConditionRules.ts @@ -0,0 +1,244 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const mockConditionRules = [ + { + pluginId: 'catalog', + rules: [ + { + name: 'HAS_ANNOTATION', + description: 'Allow entities with the specified annotation', + resourceType: 'catalog-entity', + paramsSchema: { + type: 'object', + properties: { + annotation: { + type: 'string', + description: 'Name of the annotation to match on', + }, + value: { + type: 'string', + description: 'Value of the annotation to match on', + }, + }, + required: ['annotation'], + additionalProperties: false, + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + { + name: 'HAS_LABEL', + description: 'Allow entities with the specified label', + resourceType: 'catalog-entity', + paramsSchema: { + type: 'object', + properties: { + label: { + type: 'string', + description: 'Name of the label to match on', + }, + }, + required: ['label'], + additionalProperties: false, + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + { + name: 'HAS_METADATA', + description: 'Allow entities with the specified metadata subfield', + resourceType: 'catalog-entity', + paramsSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: 'Property within the entities metadata to match on', + }, + value: { + type: 'string', + description: 'Value of the given property to match on', + }, + }, + required: ['key'], + additionalProperties: false, + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + { + name: 'HAS_SPEC', + description: 'Allow entities with the specified spec subfield', + resourceType: 'catalog-entity', + paramsSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: 'Property within the entities spec to match on', + }, + value: { + type: 'string', + description: 'Value of the given property to match on', + }, + }, + required: ['key'], + additionalProperties: false, + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + { + name: 'IS_ENTITY_KIND', + description: 'Allow entities matching a specified kind', + resourceType: 'catalog-entity', + paramsSchema: { + type: 'object', + properties: { + kinds: { + type: 'array', + items: { type: 'string' }, // TODO: should be enum? + minItems: 1, + description: 'List of kinds to match at least one of', + }, + }, + required: ['kinds'], + additionalProperties: false, + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + { + name: 'IS_ENTITY_OWNER', + description: 'Allow entities owned by a specified claim', + resourceType: 'catalog-entity', + paramsSchema: { + type: 'object', + properties: { + claims: { + type: 'array', + items: { type: 'string' }, + minItems: 1, + description: + 'List of claims to match at least one on within ownedBy', + }, + }, + required: ['claims'], + additionalProperties: false, + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + ], + }, + { + pluginId: 'scaffolder', + rules: [ + { + name: 'HAS_TAG', + description: 'Match parameters or steps with the given tag', + resourceType: 'scaffolder-template', + paramsSchema: { + type: 'object', + properties: { + tag: { + type: 'string', + description: 'Name of the tag to match on', + }, + }, + required: ['tag'], + additionalProperties: false, + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + { + name: 'HAS_ACTION_ID', + description: 'Match actions with the given actionId', + resourceType: 'scaffolder-action', + paramsSchema: { + type: 'object', + properties: { + actionId: { + type: 'string', + description: 'Name of the actionId to match on', + }, + }, + required: ['actionId'], + additionalProperties: false, + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + { + name: 'HAS_BOOLEAN_PROPERTY', + description: 'Allow actions with the specified property', + resourceType: 'scaffolder-action', + paramsSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: 'Property within the action parameters to match on', + }, + value: { + type: 'boolean', + description: 'Value of the given property to match on', + }, + }, + required: ['key', 'value'], + additionalProperties: false, + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + { + name: 'HAS_NUMBER_PROPERTY', + description: 'Allow actions with the specified property', + resourceType: 'scaffolder-action', + paramsSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: 'Property within the action parameters to match on', + }, + value: { + type: 'number', + description: 'Value of the given property to match on', + }, + }, + required: ['key', 'value'], + additionalProperties: false, + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + { + name: 'HAS_STRING_PROPERTY', + 'descriptio* Connection #0 to host localhost left intactn': + 'Allow actions with the specified property', + resourceType: 'scaffolder-action', + paramsSchema: { + type: 'object', + properties: { + key: { + type: 'string', + description: 'Property within the action parameters to match on', + }, + value: { + type: 'string', + description: 'Value of the given property to match on', + }, + }, + required: ['key', 'value'], + additionalProperties: false, + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + ], + }, +]; diff --git a/workspaces/rbac/plugins/rbac/src/__fixtures__/mockConditions.ts b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockConditions.ts new file mode 100644 index 0000000000..f026324544 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockConditions.ts @@ -0,0 +1,193 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { AuthorizeResult } from '@backstage/plugin-permission-common'; + +import { + PermissionAction, + RoleConditionalPolicyDecision, +} from '@backstage-community/plugin-rbac-common'; + +import { RoleBasedConditions } from '../types'; + +export const mockNewConditions: RoleBasedConditions[] = [ + { + result: AuthorizeResult.CONDITIONAL, + pluginId: 'catalog', + resourceType: 'catalog-entity', + conditions: { + rule: 'HAS_ANNOTATION', + resourceType: 'catalog-entity', + params: { annotation: 'temp' }, + }, + roleEntityRef: 'role:default/rbac_admin', + permissionMapping: ['read'], + }, + { + result: AuthorizeResult.CONDITIONAL, + pluginId: 'catalog', + resourceType: 'catalog-entity', + conditions: { + allOf: [ + { + rule: 'HAS_LABEL', + resourceType: 'catalog-entity', + params: { label: 'temp' }, + }, + { + rule: 'HAS_METADATA', + resourceType: 'catalog-entity', + params: { key: 'status' }, + }, + ], + }, + roleEntityRef: 'role:default/rbac_admin', + permissionMapping: ['delete', 'update'], + }, +]; + +export const mockConditions: RoleConditionalPolicyDecision[] = + [ + { + id: 1, + ...mockNewConditions[0], + }, + { + id: 2, + ...mockNewConditions[1], + }, + { + id: 3, + result: AuthorizeResult.CONDITIONAL, + pluginId: 'catalog', + resourceType: 'catalog-entity', + conditions: { + anyOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/ciiay'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { kinds: ['Group'] }, + }, + { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/ciiay'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { + kinds: ['User'], + }, + }, + { + not: { + rule: 'HAS_LABEL', + resourceType: 'catalog-entity', + params: { label: 'temp' }, + }, + }, + { + anyOf: [ + { + rule: 'HAS_TAG', + resourceType: 'catalog-entity', + params: { tag: 'dev' }, + }, + { + rule: 'HAS_TAG', + resourceType: 'catalog-entity', + params: { tag: 'test' }, + }, + ], + }, + ], + }, + { + not: { + allOf: [ + { + rule: 'IS_ENTITY_OWNER', + resourceType: 'catalog-entity', + params: { + claims: ['user:default/xyz'], + }, + }, + { + rule: 'IS_ENTITY_KIND', + resourceType: 'catalog-entity', + params: { + kinds: ['User'], + }, + }, + ], + }, + }, + ], + }, + roleEntityRef: 'role:default/rbac_admin', + permissionMapping: ['read', 'delete', 'update'], + }, + { + id: 4, + result: AuthorizeResult.CONDITIONAL, + pluginId: 'catalog', + resourceType: 'catalog-entity', + conditions: { + not: { + rule: 'HAS_LABEL', + resourceType: 'catalog-entity', + params: { label: 'temp' }, + }, + }, + roleEntityRef: 'role:default/rbac_admin', + permissionMapping: ['delete', 'update'], + }, + { + id: 5, + result: AuthorizeResult.CONDITIONAL, + pluginId: 'scaffolder', + resourceType: 'scaffolder-template', + conditions: { + not: { + anyOf: [ + { + rule: 'HAS_TAG', + resourceType: 'scaffolder-template', + params: { tag: 'dev' }, + }, + { + rule: 'HAS_TAG', + resourceType: 'scaffolder-template', + params: { tag: 'test' }, + }, + ], + }, + }, + roleEntityRef: 'role:default/rbac_admin', + permissionMapping: ['read'], + }, + ]; diff --git a/workspaces/rbac/plugins/rbac/src/__fixtures__/mockFormValues.tsx b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockFormValues.tsx new file mode 100644 index 0000000000..dcb6ed0b2e --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockFormValues.tsx @@ -0,0 +1,65 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const mockFormCurrentValues = { + kind: 'user', + name: 'div', + namespace: 'default', + selectedMembers: [], + permissionPoliciesRows: [ + { + permission: 'catalog-entity', + policies: [{ policy: 'read', effect: 'allow' }], + policyString: 'Read', + isResourced: true, + plugin: 'catalog', + conditions: { + condition: { + rule: 'HAS_LABEL', + params: { + label: 'temp', + }, + resourceType: 'catalog-entity', + }, + }, + }, + ], +}; + +export const mockFormInitialValues = { + kind: 'user', + name: 'div', + namespace: 'default', + selectedMembers: [], + permissionPoliciesRows: [ + { + id: 1, + permission: 'catalog-entity', + policies: [{ policy: 'read', effect: 'allow' }], + policyString: 'Read', + isResourced: true, + plugin: 'catalog', + conditions: { + condition: { + rule: 'HAS_LABEL', + params: { + label: 'temp', + }, + resourceType: 'catalog-entity', + }, + }, + }, + ], +}; diff --git a/workspaces/rbac/plugins/rbac/src/__fixtures__/mockMembers.ts b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockMembers.ts new file mode 100644 index 0000000000..9b599e8364 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockMembers.ts @@ -0,0 +1,274 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { MemberEntity } from '../types'; + +export const mockMembers: MemberEntity[] = [ + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'team-d', + description: 'Team D', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'team', + profile: { + displayName: 'Team D', + }, + parent: 'boxoffice', + children: [], + }, + relations: [ + { + type: 'childOf', + targetRef: 'group:default/boxoffice', + }, + { + type: 'hasMember', + targetRef: 'user:default/eva.macdowell', + }, + { + type: 'hasMember', + targetRef: 'user:default/lucy.sheehan', + }, + ], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'infrastructure', + description: 'The infra department', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'department', + parent: 'acme-corp', + children: ['backstage', 'boxoffice'], + }, + relations: [], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'guest', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + spec: { + profile: { + displayName: 'Guest User', + }, + memberOf: ['team-a'], + }, + relations: [ + { + type: 'memberOf', + targetRef: 'group:default/team-a', + }, + ], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'backstage-community-authors', + title: 'Backstage-Community Authors', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'team', + children: [], + }, + relations: [], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'team-a', + description: 'Team A', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'team', + profile: {}, + parent: 'backstage', + children: [], + }, + relations: [ + { + type: 'childOf', + targetRef: 'group:default/backstage', + }, + { + type: 'hasMember', + targetRef: 'user:default/breanna.davison', + }, + { + type: 'hasMember', + targetRef: 'user:default/guest', + }, + { + type: 'hasMember', + targetRef: 'user:default/janelle.dawe', + }, + { + type: 'hasMember', + targetRef: 'user:default/nigel.manning', + }, + ], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'backstage', + description: 'The backstage sub-department', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'sub-department', + profile: { + displayName: 'Backstage', + }, + parent: 'infrastructure', + children: ['team-a', 'team-b'], + }, + relations: [], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'team-b', + description: 'Team B', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'team', + profile: { + displayName: 'Team B', + }, + parent: 'backstage', + children: [], + }, + relations: [ + { + type: 'hasMember', + targetRef: 'user:default/amelia.park', + }, + { + type: 'hasMember', + targetRef: 'user:default/colette.brock', + }, + { + type: 'hasMember', + targetRef: 'user:default/jenny.doe', + }, + { + type: 'hasMember', + targetRef: 'user:default/jonathon.page', + }, + { + type: 'hasMember', + targetRef: 'user:default/justine.barrow', + }, + ], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'lucy.sheehan', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + spec: { + profile: { + displayName: 'Lucy Sheehan', + }, + memberOf: ['team-d'], + }, + relations: [ + { + type: 'memberOf', + targetRef: 'group:default/team-d', + }, + ], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'boxoffice', + description: 'The boxoffice sub-department', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'sub-department', + profile: { + displayName: 'Box Office', + }, + parent: 'infrastructure', + children: ['team-c', 'team-d'], + }, + relations: [ + { + type: 'childOf', + targetRef: 'group:default/infrastructure', + }, + { + type: 'parentOf', + targetRef: 'group:default/team-c', + }, + { + type: 'parentOf', + targetRef: 'group:default/team-d', + }, + ], + }, + { + metadata: { + namespace: 'default', + annotations: {}, + name: 'amelia.park', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + spec: { + profile: { + displayName: 'Amelia Park', + }, + memberOf: ['team-b'], + }, + relations: [ + { + type: 'memberOf', + targetRef: 'group:default/team-b', + }, + ], + }, +]; diff --git a/workspaces/rbac/plugins/rbac/src/__fixtures__/mockPermissionPolicies.ts b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockPermissionPolicies.ts new file mode 100644 index 0000000000..f86ee9c05b --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockPermissionPolicies.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PluginPermissionMetaData } from '@backstage-community/plugin-rbac-common'; + +export const mockPermissionPolicies: PluginPermissionMetaData[] = [ + { + pluginId: 'catalog', + policies: [ + { + resourceType: 'catalog-entity', + name: 'catalog.entity.read', + policy: 'read', + }, + { + name: 'catalog.entity.create', + policy: 'create', + }, + { + resourceType: 'catalog-entity', + name: 'catalog.entity.delete', + policy: 'delete', + }, + { + resourceType: 'catalog-entity', + name: 'catalog.entity.update', + policy: 'update', + }, + { + name: 'catalog.location.read', + policy: 'read', + }, + { + name: 'catalog.location.create', + policy: 'create', + }, + { + name: 'catalog.location.delete', + policy: 'delete', + }, + ], + }, + { + pluginId: 'scaffolder', + policies: [ + { + resourceType: 'scaffolder-template', + name: 'scaffolder.template.read', + policy: 'read', + }, + { + resourceType: 'scaffolder-template', + name: 'scaffolder.template.read', + policy: 'read', + }, + { + resourceType: 'scaffolder-action', + name: 'scaffolder.action.use', + policy: 'use', + }, + ], + }, + { + pluginId: 'permission', + policies: [ + { + name: 'policy-entity', + policy: 'read', + }, + { + name: 'policy-entity', + policy: 'create', + }, + { + name: 'policy-entity', + policy: 'delete', + }, + { + name: 'policy-entity', + policy: 'update', + }, + ], + }, +]; diff --git a/workspaces/rbac/plugins/rbac/src/__fixtures__/mockPolicies.ts b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockPolicies.ts new file mode 100644 index 0000000000..a13cf2cb6f --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockPolicies.ts @@ -0,0 +1,101 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { RoleBasedPolicy } from '@backstage-community/plugin-rbac-common'; + +export const mockAssociatedPolicies: RoleBasedPolicy[] = [ + { + entityReference: 'role:default/rbac_admin', + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'policy-entity', + policy: 'delete', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'catalog-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'catalog.entity.create', + policy: 'use', + effect: 'allow', + }, +]; + +export const mockPolicies: RoleBasedPolicy[] = [ + { + entityReference: 'role:default/guests', + permission: 'catalog-entity', + policy: 'read', + effect: 'deny', + }, + { + entityReference: 'role:default/guests', + permission: 'catalog.entity.create', + policy: 'use', + effect: 'deny', + }, + { + entityReference: 'role:default/guests', + permission: 'catalog-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'role:default/guests', + permission: 'catalog.entity.create', + policy: 'use', + effect: 'allow', + }, + { + entityReference: 'role:default/guests', + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + { + entityReference: 'role:default/guests', + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'role:default/guests', + permission: 'policy.entity.read', + policy: 'use', + effect: 'allow', + }, + { + entityReference: 'role:default/guests', + permission: 'policy-entity', + policy: 'delete', + effect: 'allow', + }, + ...mockAssociatedPolicies, +]; diff --git a/workspaces/rbac/plugins/rbac/src/__fixtures__/mockTransformedConditionRules.ts b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockTransformedConditionRules.ts new file mode 100644 index 0000000000..1bceb89081 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/__fixtures__/mockTransformedConditionRules.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConditionRulesData } from '../components/ConditionalAccess/types'; +import { mockConditionRules } from './mockConditionRules'; + +export const mockTransformedConditionRules: ConditionRulesData = { + catalog: { + 'catalog-entity': { + HAS_ANNOTATION: { + description: mockConditionRules[0].rules[0].description, + schema: mockConditionRules[0].rules[0].paramsSchema, + }, + HAS_LABEL: { + description: mockConditionRules[0].rules[1].description, + schema: mockConditionRules[0].rules[1].paramsSchema, + }, + rules: [ + mockConditionRules[0].rules[0].name, + mockConditionRules[0].rules[1].name, + ], + }, + }, + scaffolder: { + 'scaffolder-template': { + HAS_TAG: { + description: mockConditionRules[1].rules[0].description, + schema: mockConditionRules[1].rules[0].paramsSchema, + }, + rules: [mockConditionRules[1].rules[0].name], + }, + }, +}; diff --git a/workspaces/rbac/plugins/rbac/src/api/LicensedUsersClient.ts b/workspaces/rbac/plugins/rbac/src/api/LicensedUsersClient.ts new file mode 100644 index 0000000000..406301cf51 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/api/LicensedUsersClient.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + ConfigApi, + createApiRef, + IdentityApi, +} from '@backstage/core-plugin-api'; + +export type LicensedUsersAPI = { + isLicensePluginEnabled(): Promise; + downloadStatistics: () => Promise; +}; + +// @public +export const licensedUsersApiRef = createApiRef({ + id: 'plugin.licensed-users-info.service', +}); + +export type Options = { + configApi: ConfigApi; + identityApi: IdentityApi; +}; + +export class LicensedUsersAPIClient implements LicensedUsersAPI { + // @ts-ignore + private readonly configApi: ConfigApi; + private readonly identityApi: IdentityApi; + + constructor(options: Options) { + this.configApi = options.configApi; + this.identityApi = options.identityApi; + } + async isLicensePluginEnabled(): Promise { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch( + `${backendUrl}/api/licensed-users-info/health`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + }, + ); + + return jsonResponse.ok; + } + + async downloadStatistics(): Promise { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const response = await fetch( + `${backendUrl}/api/licensed-users-info/users`, + { + method: 'GET', + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'text/csv', + }, + }, + ); + return response; + } +} diff --git a/workspaces/rbac/plugins/rbac/src/api/RBACBackendClient.test.ts b/workspaces/rbac/plugins/rbac/src/api/RBACBackendClient.test.ts new file mode 100644 index 0000000000..89a12a7a61 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/api/RBACBackendClient.test.ts @@ -0,0 +1,669 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { IdentityApi } from '@backstage/core-plugin-api'; + +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +import { Role } from '@backstage-community/plugin-rbac-common'; + +import { RBACAPI, RBACBackendClient } from './RBACBackendClient'; + +const LOCAL_ADDR = 'https://localhost:7007'; +let lastRequest: any; +const handlers = [ + rest.get(`${LOCAL_ADDR}/api/permission`, (req, res, ctx) => { + lastRequest = { + url: req.url.toString(), + authorizationHeader: req.headers.get('authorization'), + }; + return res(ctx.status(200), ctx.json({ status: 'ok' })); + }), + rest.get(`${LOCAL_ADDR}/api/permission/roles`, (req, res, ctx) => { + const authorizationHeader = req.headers.get('Authorization'); + if (authorizationHeader === 'Bearer test-token') { + return res( + ctx.status(200), + ctx.json([ + { name: 'testrole:testns/name1' }, + { name: 'testrole:testns/name2' }, + ]), + ); + } + return res(ctx.status(404)); + }), + rest.get(`${LOCAL_ADDR}/api/permission/policies`, (req, res, ctx) => { + const authorizationHeader = req.headers.get('Authorization'); + if (authorizationHeader === 'Bearer test-token') { + return res( + ctx.status(200), + ctx.json([ + { policy: 'read', effect: 'allow' }, + { policy: 'create', effect: 'allow' }, + ]), + ); + } + return res(ctx.status(404)); + }), + rest.get( + `${LOCAL_ADDR}/api/permission/policies/:kind/:namespace/:name`, + (req, res, ctx) => { + const { kind, namespace, name } = req.params; + + if ( + kind === 'validKind' && + namespace === 'validNamespace' && + name === 'validName' + ) { + return res( + ctx.status(200), + ctx.json([ + { policy: 'read', effect: 'allow' }, + { policy: 'create', effect: 'allow' }, + ]), + ); + } + return res(ctx.status(404)); + }, + ), + rest.delete( + `${LOCAL_ADDR}/api/permission/roles/:kind/:namespace/:name`, + (req, res, ctx) => { + const authorizationHeader = req.headers.get('Authorization'); + lastRequest = { + url: req.url.toString(), + authorizationHeader: authorizationHeader, + }; + + if (authorizationHeader === 'Bearer test-token') { + return res(ctx.status(200)); + } + return res(ctx.status(404)); + }, + ), + rest.get( + `${LOCAL_ADDR}/api/permission/roles/:kind/:namespace/:name`, + (req, res, ctx) => { + const authorizationHeader = req.headers.get('Authorization'); + lastRequest = { + url: req.url.toString(), + authorizationHeader: authorizationHeader, + }; + + if (authorizationHeader === 'Bearer test-token') { + return res( + ctx.status(200), + ctx.json({ name: 'targetRole:targetNamespace/targetName' }), + ); + } + return res(ctx.status(404)); + }, + ), + rest.get( + `${LOCAL_ADDR}/api/catalog/entities?filter=kind=user&filter=kind=group`, + (req, res, ctx) => { + const authorizationHeader = req.headers.get('Authorization'); + lastRequest = { + url: req.url.toString(), + authorizationHeader: authorizationHeader, + }; + if (authorizationHeader === 'Bearer test-token') { + return res(ctx.status(200), ctx.json([{ kind: 'User', spec: {} }])); + } + return res(ctx.status(404)); + }, + ), + rest.get(`${LOCAL_ADDR}/api/permission/plugins/policies`, (req, res, ctx) => { + const authorizationHeader = req.headers.get('Authorization'); + lastRequest = { + url: req.url.toString(), + authorizationHeader: authorizationHeader, + }; + if (authorizationHeader === 'Bearer test-token') { + return res( + ctx.status(200), + ctx.json([ + { + pluginId: 'plugin1', + policies: [{ policy: 'read', effect: 'allow' }], + }, + { + pluginId: 'plugin2', + policies: [{ policy: 'create', effect: 'allow' }], + }, + ]), + ); + } + return res(ctx.status(404)); + }), + rest.post(`${LOCAL_ADDR}/api/permission/roles`, async (req, res, ctx) => { + const requestBody = await req.json(); + const { name } = requestBody; + if (name === '') { + return res(ctx.status(400), ctx.json({ message: 'Error creating role' })); + } + return res( + ctx.status(200), + ctx.json({ message: 'Role created successfully', ...requestBody }), + ); + }), + rest.put( + `${LOCAL_ADDR}/api/permission/roles/:kind/:namespace/:name`, + async (req, res, ctx) => { + const requestBody = await req.json(); + const { newRole } = requestBody; + + if (newRole.name === '') { + return res( + ctx.status(400), + ctx.json({ message: 'Error updating role' }), + ); + } + + return res( + ctx.status(200), + ctx.json({ message: 'Role updated successfully', ...requestBody }), + ); + }, + ), + rest.put( + `${LOCAL_ADDR}/api/permission/policies/:kind/:namespace/:name`, + async (req, res, ctx) => { + const requestBody = await req.json(); + const { newPolicy } = requestBody; + + if ( + newPolicy?.length === 0 || + newPolicy.some( + (policy: { entityReference: string }) => + policy.entityReference === '', + ) + ) { + return res( + ctx.status(400), + ctx.json({ message: 'Error updating policies' }), + ); + } + + return res( + ctx.status(200), + ctx.json({ message: 'Policies updated successfully', ...requestBody }), + ); + }, + ), + rest.delete( + `${LOCAL_ADDR}/api/permission/policies/:kind/:namespace/:name`, + (req, res, ctx) => { + const authorizationHeader = req.headers.get('Authorization'); + lastRequest = { + url: req.url.toString(), + authorizationHeader: authorizationHeader, + }; + if (authorizationHeader === 'Bearer test-token') { + return res(ctx.status(200)); + } + return res( + ctx.status(400), + ctx.json({ message: 'Error deleting policies' }), + ); + }, + ), + rest.post(`${LOCAL_ADDR}/api/permission/policies`, async (req, res, ctx) => { + const policies = await req.json(); + if ( + !policies || + policies.length === 0 || + policies.some( + (policy: { entityReference: string }) => policy.entityReference === '', + ) + ) { + return res( + ctx.status(400), + ctx.json({ message: 'Error creating policies' }), + ); + } + return res( + ctx.status(200), + ctx.json({ message: 'Policies created successfully', ...policies }), + ); + }), +]; +const server = setupServer(...handlers); + +beforeAll(() => server.listen()); +afterEach(() => server.restoreHandlers()); +afterAll(() => server.close()); + +describe('RBACBackendClient', () => { + let rbacApi: RBACAPI; + const getConfigApi = (getOptionalStringFn: any) => ({ + has: jest.fn(), + keys: jest.fn(), + get: jest.fn(), + getBoolean: jest.fn(), + getConfig: jest.fn(), + getConfigArray: jest.fn(), + getNumber: jest.fn(), + getString: jest.fn(key => { + if (key === 'backend.baseUrl') { + return LOCAL_ADDR; + } + return ''; + }), + getStringArray: jest.fn(), + getOptional: jest.fn(), + getOptionalStringArray: jest.fn(), + getOptionalBoolean: jest.fn(), + getOptionalConfig: jest.fn(), + getOptionalConfigArray: jest.fn(), + getOptionalNumber: jest.fn(), + getOptionalString: getOptionalStringFn, + }); + + const bearerToken = 'test-token'; + + const identityApi = { + async getCredentials() { + return { token: bearerToken }; + }, + } as IdentityApi; + + beforeEach(() => { + rbacApi = new RBACBackendClient({ + configApi: getConfigApi(() => { + return '/api'; + }), + identityApi: identityApi, + }); + }); + + it('getUserAuthorization should call fetch with correct URL and headers', async () => { + const result = await rbacApi.getUserAuthorization(); + + expect(result).toEqual({ status: 'ok' }); + expect(lastRequest).not.toBeNull(); + expect(lastRequest.url).toBe('https://localhost:7007/api/permission/'); + expect(lastRequest.authorizationHeader).toBe('Bearer test-token'); + }); + + describe('getRoles', () => { + it('getRoles should retrieve roles successfully', async () => { + const roles = await rbacApi.getRoles(); + expect(roles).toEqual([ + { name: 'testrole:testns/name1' }, + { name: 'testrole:testns/name2' }, + ]); + }); + + it('getRoles should handle non-200/204 responses correctly', async () => { + server.use( + rest.get(`${LOCAL_ADDR}/api/permission/roles`, (_req, res, ctx) => { + return res(ctx.status(404)); + }), + ); + + await expect(rbacApi.getRoles()).resolves.toEqual( + expect.objectContaining({ + status: 404, + }), + ); + }); + }); + + describe('getPolicies', () => { + it('getPolicies should retrieve policies successfully', async () => { + const policies = await rbacApi.getPolicies(); + expect(policies).toEqual([ + { policy: 'read', effect: 'allow' }, + { policy: 'create', effect: 'allow' }, + ]); + }); + + it('getPolicies should handle non-200/204 responses correctly', async () => { + server.use( + rest.get(`${LOCAL_ADDR}/api/permission/policies`, (_req, res, ctx) => { + return res(ctx.status(404)); + }), + ); + + await expect(rbacApi.getPolicies()).resolves.toEqual( + expect.objectContaining({ + status: 404, + }), + ); + }); + }); + + describe('getAssociatedPolicies', () => { + const entityReference = 'validKind:validNamespace/validName'; + it('getAssociatedPolicies should retrieve policies successfully', async () => { + const policies = await rbacApi.getAssociatedPolicies(entityReference); + expect(policies).toEqual([ + { policy: 'read', effect: 'allow' }, + { policy: 'create', effect: 'allow' }, + ]); + }); + + it('getAssociatedPolicies should handle non-200/204 responses correctly', async () => { + const invalidEntityReference = 'invalidKind:invalidNamespace/invalidName'; + + await expect( + rbacApi.getAssociatedPolicies(invalidEntityReference), + ).resolves.toEqual( + expect.objectContaining({ + status: 404, + }), + ); + }); + }); + + describe('deleteRole', () => { + it('deleteRole should send a DELETE request and handle successful response', async () => { + const targetRole = 'targetRole:targetNamespace/targetName'; + const response = await rbacApi.deleteRole(targetRole); + + expect(response.status).toBe(200); + expect(lastRequest).not.toBeNull(); + expect(lastRequest.url).toBe( + 'https://localhost:7007/api/permission/roles/targetRole/targetNamespace/targetName', + ); + expect(lastRequest.authorizationHeader).toBe('Bearer test-token'); + }); + + it('deleteRole should handle unauthorized response', async () => { + server.use( + rest.delete( + `${LOCAL_ADDR}/api/permission/roles/:kind/:namespace/:name`, + (_req, res, ctx) => { + return res(ctx.status(404)); + }, + ), + ); + + const targetRole = 'targetRole:targetNamespace/targetName'; + const response = await rbacApi.deleteRole(targetRole); + + expect(response.status).toBe(404); + }); + }); + + describe('getRole', () => { + it('getRole should send a GET request and handle successful response', async () => { + const targetRole = 'targetRole:targetNamespace/targetName'; + const response = await rbacApi.getRole(targetRole); + + expect(response).toEqual({ + name: 'targetRole:targetNamespace/targetName', + }); + expect(lastRequest).not.toBeNull(); + expect(lastRequest.url).toBe( + 'https://localhost:7007/api/permission/roles/targetRole/targetNamespace/targetName', + ); + expect(lastRequest.authorizationHeader).toBe('Bearer test-token'); + }); + it('getRole should handle unauthorized response', async () => { + server.use( + rest.get( + `${LOCAL_ADDR}/api/permission/roles/:kind/:namespace/:name`, + (_req, res, ctx) => { + return res(ctx.status(404)); + }, + ), + ); + + const targetRole = 'targetRole:targetNamespace/targetName'; + await expect(rbacApi.getRole(targetRole)).resolves.toEqual( + expect.objectContaining({ + status: 404, + }), + ); + }); + }); + + describe('getMembers', () => { + it('getMembers should send a GET request and handle successful response', async () => { + const response = await rbacApi.getMembers(); + + expect(response).toEqual([{ kind: 'User', spec: {} }]); + expect(lastRequest).not.toBeNull(); + expect(lastRequest.url).toBe( + 'https://localhost:7007/api/catalog/entities?filter=kind=user&filter=kind=group', + ); + expect(lastRequest.authorizationHeader).toBe('Bearer test-token'); + }); + it('getMembers should handle unauthorized response', async () => { + server.use( + rest.get( + `${LOCAL_ADDR}/api/catalog/entities?filter=kind=user&filter=kind=group`, + (_req, res, ctx) => { + return res(ctx.status(404)); + }, + ), + ); + + await expect(rbacApi.getMembers()).resolves.toEqual( + expect.objectContaining({ + status: 404, + }), + ); + }); + }); + + describe('listPermissions', () => { + it('listPermissions should send a GET request and handle successful response', async () => { + const response = await rbacApi.listPermissions(); + + expect(response).toEqual([ + { + pluginId: 'plugin1', + policies: [{ policy: 'read', effect: 'allow' }], + }, + { + pluginId: 'plugin2', + policies: [{ policy: 'create', effect: 'allow' }], + }, + ]); + expect(lastRequest).not.toBeNull(); + expect(lastRequest.url).toBe( + 'https://localhost:7007/api/permission/plugins/policies', + ); + expect(lastRequest.authorizationHeader).toBe('Bearer test-token'); + }); + it('listPermissions should handle unauthorized response', async () => { + server.use( + rest.get( + `${LOCAL_ADDR}/api/permission/plugins/policies`, + (_req, res, ctx) => { + return res(ctx.status(404)); + }, + ), + ); + + await expect(rbacApi.listPermissions()).resolves.toEqual( + expect.objectContaining({ + status: 404, + }), + ); + }); + }); + + describe('createRole', () => { + it('createRole should send a POST request and handle successful response', async () => { + const newRole: Role = { + name: 'testRole', + memberReferences: ['testUser1', 'testUser2'], + }; + const response = await rbacApi.createRole(newRole); + expect(response).toHaveProperty('status', 200); + }); + it('createRole should handle error response', async () => { + const newRole: Role = { name: '', memberReferences: [] }; + const response = await rbacApi.createRole(newRole); + expect(response).toEqual( + expect.objectContaining({ message: 'Error creating role' }), + ); + }); + }); + + describe('updateRole', () => { + it('updateRole should send a PUT request and handle successful response', async () => { + const oldRole: Role = { + name: 'testRole:testNamespace/testName', + memberReferences: ['testUser1', 'testUser2'], + }; + const newRole: Role = { + name: 'testRole:testNamespace/testName', + memberReferences: ['testUser1', 'testUser2', 'testUser3'], + }; + const response = await rbacApi.updateRole(oldRole, newRole); + expect(response).toHaveProperty('status', 200); + }); + it('updateRole should handle error response', async () => { + const oldRole: Role = { + name: 'testRole:testNamespace/testName', + memberReferences: ['testUser1', 'testUser2'], + }; + const newRole: Role = { + name: '', + memberReferences: ['testUser1', 'testUser2', 'testUser3'], + }; + const response = await rbacApi.updateRole(oldRole, newRole); + expect(response).toEqual( + expect.objectContaining({ message: 'Error updating role' }), + ); + }); + }); + + describe('updatePolicies', () => { + it('updatePolicies should send a POST request and handle successful response', async () => { + const response = await rbacApi.updatePolicies( + 'testRole:testNamespace/testName', + [ + { + entityReference: 'testRole:testNamespace/testName', + policy: 'read', + effect: 'allow', + }, + ], + [ + { + entityReference: 'testRole:testNamespace/testName', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'testRole:testNamespace/testName', + policy: 'update', + effect: 'allow', + }, + ], + ); + expect(response).toHaveProperty('status', 200); + }); + it('updatePolicies should handle error response', async () => { + const oldPolicies = [ + { + entityReference: 'testRole:testNamespace/testName', + policy: 'read', + effect: 'allow', + }, + ]; + const newPolicies = [ + { entityReference: '', policy: 'read', effect: 'allow' }, + { + entityReference: 'testRole:testNamespace/testName', + policy: 'update', + effect: 'allow', + }, + ]; + const response = await rbacApi.updatePolicies( + 'testRole:testNamespace/testName', + oldPolicies, + newPolicies, + ); + expect(response).toEqual( + expect.objectContaining({ message: 'Error updating policies' }), + ); + }); + }); + + describe('deletePolicies', () => { + it('deletePolicies should send a DELETE request and handle successful response', async () => { + const response = await rbacApi.deletePolicies( + 'testRole:testNamespace/testName', + [ + { + entityReference: 'testRole:testNamespace/testName', + policy: 'read', + effect: 'allow', + }, + ], + ); + expect(response).toHaveProperty('status', 200); + }); + it('deletePolicies should handle error response', async () => { + server.use( + rest.delete( + `${LOCAL_ADDR}/api/permission/policies/:kind/:namespace/:name`, + (_req, res, ctx) => { + return res( + ctx.status(404), + ctx.json({ message: 'Error deleting policies' }), + ); + }, + ), + ); + + const targetEntityReference = 'testRole:testNamespace/testName'; + const targetPolicies = [ + { + entityReference: 'testRole:testNamespace/testName', + policy: 'read', + effect: 'allow', + }, + ]; + const response = await rbacApi.deletePolicies( + targetEntityReference, + targetPolicies, + ); + expect(response).toEqual( + expect.objectContaining({ message: 'Error deleting policies' }), + ); + }); + }); + + describe('createPolicies', () => { + it('createPolicies should send a POST request and handle successful response', async () => { + const response = await rbacApi.createPolicies([ + { + entityReference: 'testRole:testNamespace/testName', + policy: 'read', + effect: 'allow', + }, + ]); + expect(response).toHaveProperty('status', 200); + }); + it('createPolicies should handle error response', async () => { + const newPolicies = [ + { entityReference: '', policy: 'read', effect: 'allow' }, + ]; + const response = await rbacApi.createPolicies(newPolicies); + expect(response).toEqual( + expect.objectContaining({ message: 'Error creating policies' }), + ); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/api/RBACBackendClient.ts b/workspaces/rbac/plugins/rbac/src/api/RBACBackendClient.ts new file mode 100644 index 0000000000..456aff87c1 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/api/RBACBackendClient.ts @@ -0,0 +1,459 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { parseEntityRef } from '@backstage/catalog-model'; +import { + ConfigApi, + createApiRef, + IdentityApi, +} from '@backstage/core-plugin-api'; + +import { + PermissionAction, + PluginPermissionMetaData, + Role, + RoleBasedPolicy, + RoleConditionalPolicyDecision, +} from '@backstage-community/plugin-rbac-common'; + +import { + MemberEntity, + PluginConditionRules, + RoleBasedConditions, + RoleError, +} from '../types'; + +// @public +export type RBACAPI = { + getUserAuthorization: () => Promise<{ status: string }>; + getRoles: () => Promise; + getPolicies: () => Promise; + getAssociatedPolicies: ( + entityReference: string, + ) => Promise; + deleteRole: (role: string) => Promise; + getRole: (role: string) => Promise; + getMembers: () => Promise; + listPermissions: () => Promise; + createRole: (role: Role) => Promise; + updateRole: (oldRole: Role, newRole: Role) => Promise; + updatePolicies: ( + entityReference: string, + oldPolicy: RoleBasedPolicy[], + newPolicy: RoleBasedPolicy[], + ) => Promise; + createPolicies: (polices: RoleBasedPolicy[]) => Promise; + deletePolicies: ( + entityReference: string, + polices: RoleBasedPolicy[], + ) => Promise; + getPluginsConditionRules: () => Promise; + createConditionalPermission: ( + conditionalPermission: RoleBasedConditions, + ) => Promise; + getRoleConditions: ( + roleRef: string, + ) => Promise[] | Response>; + updateConditionalPolicies: ( + conditionId: number, + data: RoleBasedConditions, + ) => Promise; + deleteConditionalPolicies: ( + conditionId: number, + ) => Promise; +}; + +export type Options = { + configApi: ConfigApi; + identityApi: IdentityApi; +}; + +// @public +export const rbacApiRef = createApiRef({ + id: 'plugin.rbac.service', +}); + +export class RBACBackendClient implements RBACAPI { + // @ts-ignore + private readonly configApi: ConfigApi; + private readonly identityApi: IdentityApi; + + constructor(options: Options) { + this.configApi = options.configApi; + this.identityApi = options.identityApi; + } + + async getUserAuthorization() { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch(`${backendUrl}/api/permission/`, { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + }); + return jsonResponse.json(); + } + + async getRoles() { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch(`${backendUrl}/api/permission/roles`, { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + }); + + if (jsonResponse.status !== 200 && jsonResponse.status !== 204) { + return jsonResponse; + } + + return jsonResponse.json(); + } + + async getPolicies() { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch(`${backendUrl}/api/permission/policies`, { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + }); + if (jsonResponse.status !== 200 && jsonResponse.status !== 204) { + return jsonResponse; + } + return jsonResponse.json(); + } + + async getAssociatedPolicies(entityReference: string) { + const { kind, namespace, name } = parseEntityRef(entityReference); + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch( + `${backendUrl}/api/permission/policies/${kind}/${namespace}/${name}`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + }, + ); + if (jsonResponse.status !== 200 && jsonResponse.status !== 204) { + return jsonResponse; + } + return jsonResponse.json(); + } + + async deleteRole(role: string) { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const { kind, namespace, name } = parseEntityRef(role); + const jsonResponse = await fetch( + `${backendUrl}/api/permission/roles/${kind}/${namespace}/${name}`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'application/json', + }, + method: 'DELETE', + }, + ); + return jsonResponse; + } + + async getRole(role: string) { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const { kind, namespace, name } = parseEntityRef(role); + const jsonResponse = await fetch( + `${backendUrl}/api/permission/roles/${kind}/${namespace}/${name}`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'application/json', + }, + }, + ); + if (jsonResponse.status !== 200 && jsonResponse.status !== 204) { + return jsonResponse; + } + return jsonResponse.json(); + } + + async getMembers() { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch( + `${backendUrl}/api/catalog/entities?filter=kind=user&filter=kind=group`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'application/json', + }, + }, + ); + if (jsonResponse.status !== 200 && jsonResponse.status !== 204) { + return jsonResponse; + } + return jsonResponse.json(); + } + + async listPermissions() { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch( + `${backendUrl}/api/permission/plugins/policies`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'application/json', + }, + }, + ); + if (jsonResponse.status !== 200 && jsonResponse.status !== 204) { + return jsonResponse; + } + return jsonResponse.json(); + } + + async createRole(role: Role) { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch(`${backendUrl}/api/permission/roles`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + body: JSON.stringify(role), + }); + if (jsonResponse.status !== 200 && jsonResponse.status !== 201) { + return jsonResponse.json(); + } + return jsonResponse; + } + + async updateRole(oldRole: Role, newRole: Role) { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const { kind, namespace, name } = parseEntityRef(oldRole.name); + const body = { + oldRole, + newRole, + }; + const jsonResponse = await fetch( + `${backendUrl}/api/permission/roles/${kind}/${namespace}/${name}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + body: JSON.stringify(body), + }, + ); + if ( + jsonResponse.status !== 200 && + jsonResponse.status !== 201 && + jsonResponse.status !== 204 + ) { + return jsonResponse.json(); + } + return jsonResponse; + } + + async updatePolicies( + entityReference: string, + oldPolicies: RoleBasedPolicy[], + newPolicies: RoleBasedPolicy[], + ) { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const { kind, namespace, name } = parseEntityRef(entityReference); + const body = { + oldPolicy: oldPolicies, + newPolicy: newPolicies, + }; + const jsonResponse = await fetch( + `${backendUrl}/api/permission/policies/${kind}/${namespace}/${name}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + body: JSON.stringify(body), + }, + ); + if (jsonResponse.status !== 200 && jsonResponse.status !== 201) { + return jsonResponse.json(); + } + return jsonResponse; + } + + async deletePolicies(entityReference: string, policies: RoleBasedPolicy[]) { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const { kind, namespace, name } = parseEntityRef(entityReference); + const jsonResponse = await fetch( + `${backendUrl}/api/permission/policies/${kind}/${namespace}/${name}`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(policies), + method: 'DELETE', + }, + ); + + if ( + jsonResponse.status !== 200 && + jsonResponse.status !== 201 && + jsonResponse.status !== 204 + ) { + return jsonResponse.json(); + } + return jsonResponse; + } + + async createPolicies(policies: RoleBasedPolicy[]) { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch(`${backendUrl}/api/permission/policies`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + body: JSON.stringify(policies), + }); + if (jsonResponse.status !== 200 && jsonResponse.status !== 201) { + return jsonResponse.json(); + } + return jsonResponse; + } + + async getPluginsConditionRules() { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch( + `${backendUrl}/api/permission/plugins/condition-rules`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'application/json', + }, + }, + ); + if (jsonResponse.status !== 200) { + return jsonResponse; + } + return jsonResponse.json(); + } + + async createConditionalPermission( + conditionalPermission: RoleBasedConditions, + ) { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch( + `${backendUrl}/api/permission/roles/conditions`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + body: JSON.stringify(conditionalPermission), + }, + ); + if (jsonResponse.status !== 200 && jsonResponse.status !== 201) { + return jsonResponse.json(); + } + return jsonResponse; + } + + async getRoleConditions(roleRef: string) { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch( + `${backendUrl}/api/permission/roles/conditions?roleEntityRef=${roleRef}`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'application/json', + }, + }, + ); + if (jsonResponse.status !== 200) { + return jsonResponse; + } + return jsonResponse.json(); + } + + async updateConditionalPolicies( + conditionId: number, + data: RoleBasedConditions, + ) { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch( + `${backendUrl}/api/permission/roles/conditions/${conditionId}}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + body: JSON.stringify(data), + }, + ); + if (jsonResponse.status !== 200 && jsonResponse.status !== 201) { + return jsonResponse.json(); + } + return jsonResponse; + } + + async deleteConditionalPolicies(conditionId: number) { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch( + `${backendUrl}/api/permission/roles/conditions/${conditionId}`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + method: 'DELETE', + }, + ); + + if ( + jsonResponse.status !== 200 && + jsonResponse.status !== 201 && + jsonResponse.status !== 204 + ) { + return jsonResponse.json(); + } + return jsonResponse; + } +} diff --git a/workspaces/rbac/plugins/rbac/src/components/Administration.test.tsx b/workspaces/rbac/plugins/rbac/src/components/Administration.test.tsx new file mode 100644 index 0000000000..6850c0276b --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/Administration.test.tsx @@ -0,0 +1,126 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { SidebarItem } from '@backstage/core-components'; +import { ApiRef, configApiRef } from '@backstage/core-plugin-api'; + +import { render, screen } from '@testing-library/react'; + +import { rbacApiRef } from '../api/RBACBackendClient'; +import { Administration } from './Administration'; + +let useAsyncMockResult: { loading: boolean; value?: { status: string } } = { + loading: false, + value: { status: 'Authorized' }, +}; + +jest.mock('react-use', () => ({ + ...jest.requireActual('react-use'), + useAsync: jest.fn().mockImplementation((fn: any, _deps: any) => { + fn(); + return useAsyncMockResult; + }), +})); + +const mockGetUserAuthorization = jest.fn(); + +const configMock = { + getOptionalBoolean: jest.fn(() => true), +}; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest.fn((apiRef: ApiRef) => { + if (apiRef === rbacApiRef) { + return { + getUserAuthorization: mockGetUserAuthorization, + }; + } + if (apiRef === configApiRef) { + return configMock; + } + return undefined; + }), +})); + +jest.mock('@backstage/core-components', () => ({ + SidebarItem: jest + .fn() + .mockImplementation(() =>
RBAC
), +})); + +const mockedSidebarItem = SidebarItem as jest.MockedFunction< + typeof SidebarItem +>; + +const mockUseApi = jest.fn(() => ({ + getUserAuthorization: mockGetUserAuthorization, +})); + +const mockRbacApiRef = jest.fn(); + +describe('RBAC component', () => { + beforeEach(() => { + mockGetUserAuthorization.mockClear(); + mockUseApi.mockClear(); + mockRbacApiRef.mockClear(); + mockedSidebarItem.mockClear(); + }); + + it('renders Administration sidebar item if user is authorized', async () => { + render(); + expect(mockedSidebarItem).toHaveBeenCalled(); + expect(screen.queryByText('RBAC')).toBeInTheDocument(); + expect(mockGetUserAuthorization).toHaveBeenCalledTimes(1); + }); + + it('does not render Administration sidebar item if user is not authorized', async () => { + useAsyncMockResult = { + loading: false, + value: { status: 'Unauthorized' }, + }; + + render(); + expect(mockedSidebarItem).not.toHaveBeenCalled(); + expect(mockGetUserAuthorization).toHaveBeenCalledTimes(1); + expect(screen.queryByText('RBAC')).toBeNull(); + }); + + it('does not render Administration sidebar item if user loading state is true', async () => { + useAsyncMockResult = { + loading: true, + value: undefined, + }; + + render(); + expect(mockedSidebarItem).not.toHaveBeenCalled(); + expect(screen.queryByText('RBAC')).toBeNull(); + }); + + it('does not render Administration sidebar item if plugin is disabled in the configuration', async () => { + useAsyncMockResult = { + loading: false, + value: { status: 'Authorized' }, + }; + configMock.getOptionalBoolean.mockReturnValueOnce(false); + + render(); + expect(mockedSidebarItem).not.toHaveBeenCalled(); + expect(mockGetUserAuthorization).toHaveBeenCalledTimes(1); + expect(screen.queryByText('RBAC')).toBeNull(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/Administration.tsx b/workspaces/rbac/plugins/rbac/src/components/Administration.tsx new file mode 100644 index 0000000000..d36df82e3a --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/Administration.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useAsync } from 'react-use'; + +import { SidebarItem } from '@backstage/core-components'; +import { + configApiRef, + IconComponent, + useApi, +} from '@backstage/core-plugin-api'; + +import { default as RbacIcon } from '@mui/icons-material/VpnKeyOutlined'; + +import { rbacApiRef } from '../api/RBACBackendClient'; + +export const Administration = () => { + const rbacApi = useApi(rbacApiRef); + const { loading: isUserLoading, value: result } = useAsync( + async () => await rbacApi.getUserAuthorization(), + [], + ); + + const config = useApi(configApiRef); + const isRBACPluginEnabled = config.getOptionalBoolean('permission.enabled'); + + if (!isUserLoading && isRBACPluginEnabled) { + return result?.status === 'Authorized' ? ( + + ) : null; + } + return null; +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/AddNestedConditionButton.tsx b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/AddNestedConditionButton.tsx new file mode 100644 index 0000000000..565a593df4 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/AddNestedConditionButton.tsx @@ -0,0 +1,53 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { Box, Typography } from '@material-ui/core'; +import Tooltip from '@material-ui/core/Tooltip'; +import HelpOutlineIcon from '@material-ui/icons/HelpOutline'; + +export const AddNestedConditionButton = () => { + const tooltipTitle = () => ( +
+ + Nested conditions are 1 layer rules within a main condition. It + lets you allow appropriate access by using detailed permissions based on + various conditions. You can add multiple nested conditions. + + + For example, you can allow access to all entity types in the main + condition and use a nested condition to limit the access to entities + owned by the user. + +
+ ); + return ( + + + Add Nested Condition + + + + + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ComplexConditionRow.tsx b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ComplexConditionRow.tsx new file mode 100644 index 0000000000..56b194296b --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ComplexConditionRow.tsx @@ -0,0 +1,272 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { PermissionCondition } from '@backstage/plugin-permission-common'; + +import { IconButton } from '@material-ui/core'; +import RemoveIcon from '@mui/icons-material/Remove'; + +import { + getNestedRuleErrors, + getRowKey, + getRowStyle, + getSimpleRuleErrors, + isSimpleRule, +} from '../../utils/conditional-access-utils'; +import { ConditionsFormRowFields } from './ConditionsFormRowFields'; +import { criterias } from './const'; +import { + AccessConditionsErrors, + ComplexErrors, + Condition, + ConditionsData, + NestedCriteriaErrors, + NotConditionType, + RulesData, +} from './types'; + +type ComplexConditionRowProps = { + conditionRow: ConditionsData; + nestedConditionRow: Condition[]; + criteria: keyof ConditionsData; + onRuleChange: (newCondition: ConditionsData) => void; + updateRules: (updatedNestedConditionRow: Condition[] | Condition) => void; + setErrors: React.Dispatch< + React.SetStateAction + >; + setRemoveAllClicked: React.Dispatch>; + conditionRulesData?: RulesData; + notConditionType?: NotConditionType; + classes: any; + currentCondition: Condition; + ruleIndex: number; + activeCriteria?: 'allOf' | 'anyOf'; + isNestedCondition?: boolean; + nestedConditionIndex?: number; + activeNestedCriteria?: 'allOf' | 'anyOf'; +}; + +export const ComplexConditionRow = ({ + conditionRow, + nestedConditionRow, + criteria, + onRuleChange, + updateRules, + setErrors, + setRemoveAllClicked, + conditionRulesData, + notConditionType, + classes, + currentCondition, + ruleIndex, + activeCriteria, + isNestedCondition = false, + nestedConditionIndex, + activeNestedCriteria, +}: ComplexConditionRowProps) => { + const handleRemoveSimpleConditionRule = ( + index: number, + ruleList: PermissionCondition[], + ) => { + if (!activeCriteria) { + return; + } + const updatedSimpleRules = ruleList.filter( + (_r, rindex) => index !== rindex, + ); + const nestedConditions = + (conditionRow[criteria] as PermissionCondition[])?.filter( + (con: PermissionCondition) => + criterias.allOf in con || + criterias.anyOf in con || + criterias.not in con, + ) || []; + + onRuleChange({ + [activeCriteria as keyof ConditionsData]: [ + ...updatedSimpleRules, + ...nestedConditions, + ], + }); + + setErrors(prevErrors => { + const updatedErrors = { ...prevErrors }; + + if (updatedErrors[activeCriteria]) { + const criteriaErrors = updatedErrors[activeCriteria] as ComplexErrors[]; + const simpleRuleErrors = getSimpleRuleErrors(criteriaErrors); + + if (Array.isArray(simpleRuleErrors) && simpleRuleErrors.length > 0) { + const updatedCriteriaErrors = [ + ...simpleRuleErrors.filter((_, rindex) => rindex !== index), + ...getNestedRuleErrors(criteriaErrors), + ]; + + updatedErrors[activeCriteria] = + updatedCriteriaErrors.length > 0 ? updatedCriteriaErrors : []; + } else { + delete updatedErrors[activeCriteria]; + } + } + + return updatedErrors; + }); + }; + + const handleRemoveNestedConditionRule = (nestedConditionCriteria: string) => { + const updatedNestedConditionRow: Condition[] = []; + + nestedConditionRow.forEach((c, index) => { + if (index === nestedConditionIndex) { + const updatedRules = ( + (c[ + nestedConditionCriteria as keyof Condition + ] as PermissionCondition[]) || [] + ).filter((_r, rindex) => rindex !== ruleIndex); + updatedNestedConditionRow.push({ + [nestedConditionCriteria as keyof Condition]: updatedRules, + }); + } else { + updatedNestedConditionRow.push(c); + } + }); + + updateRules( + criteria === criterias.not + ? updatedNestedConditionRow[0] + : updatedNestedConditionRow, + ); + + setErrors(prevErrors => { + const updatedErrors = { ...prevErrors }; + + if (updatedErrors[criteria] !== undefined) { + const criteriaErrors = updatedErrors[criteria] as ComplexErrors[]; + + if ( + criteria === criterias.not && + notConditionType === 'nested-condition' + ) { + ( + (updatedErrors[criteria] as NestedCriteriaErrors)[ + nestedConditionCriteria + ] as string[] + ).splice(ruleIndex, 1); + return updatedErrors; + } + + const nestedConditionErrors = getNestedRuleErrors(criteriaErrors); + + if ( + Array.isArray(nestedConditionErrors) && + nestedConditionIndex !== undefined + ) { + const nestedErrors = nestedConditionErrors[nestedConditionIndex]; + if (nestedErrors[nestedConditionCriteria]) { + const updatedNestedErrors = ( + nestedErrors[nestedConditionCriteria] as string[] + ).filter((_error, index) => index !== ruleIndex); + + if (updatedNestedErrors.length > 0) { + nestedErrors[nestedConditionCriteria] = updatedNestedErrors; + } else { + delete nestedErrors[nestedConditionCriteria]; + } + + nestedConditionErrors[nestedConditionIndex] = nestedErrors; + } + + updatedErrors[criteria] = [ + ...getSimpleRuleErrors(criteriaErrors), + ...nestedConditionErrors, + ]; + } + } + + return updatedErrors; + }); + }; + + const ruleList = isNestedCondition + ? (currentCondition[ + activeCriteria as keyof Condition + ] as PermissionCondition[]) + : ((conditionRow[activeCriteria as keyof Condition] as Condition[]).filter( + r => isSimpleRule(r), + ) as PermissionCondition[]); + + const disabled = + !isNestedCondition && + (conditionRow[criteria as keyof Condition] as Condition[]).length === 1 && + nestedConditionRow.length === 0 && + ruleIndex === 0; + const nestedDisabled = + isNestedCondition && + ( + nestedConditionRow[nestedConditionIndex ?? 0][ + activeNestedCriteria as keyof Condition + ] as Condition[] + ).length === 1 && + ruleIndex === 0; + + return ( + (currentCondition as PermissionCondition).resourceType && ( +
+ + handleRemoveNestedConditionRule(activeNestedCriteria) + : () => { + handleRemoveSimpleConditionRule(ruleIndex, ruleList); + } + } + > + + +
+ ) + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ComplexConditionRowButtons.tsx b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ComplexConditionRowButtons.tsx new file mode 100644 index 0000000000..976bce1ed1 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ComplexConditionRowButtons.tsx @@ -0,0 +1,95 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import AddIcon from '@mui/icons-material/Add'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; + +import { getDefaultRule } from '../../utils/conditional-access-utils'; +import { AddNestedConditionButton } from './AddNestedConditionButton'; +import { criterias } from './const'; +import { Condition, ConditionsData } from './types'; + +type ComplexConditionRowButtonsProps = { + conditionRow: ConditionsData; + onRuleChange: (newCondition: ConditionsData) => void; + criteria: string; + classes: any; + selPluginResourceType: string; + updateErrors: (_index: number) => void; + isNestedConditionRule: (condition: Condition) => boolean; + handleAddNestedCondition: (criteria: string) => void; +}; + +export const ComplexConditionRowButtons = ({ + conditionRow, + onRuleChange, + criteria, + classes, + selPluginResourceType, + updateErrors, + isNestedConditionRule, + handleAddNestedCondition, +}: ComplexConditionRowButtonsProps) => { + const findFirstNestedConditionIndex = (rules: Condition[]): number => { + return rules.findIndex(e => isNestedConditionRule(e)) || 0; + }; + const handleAddRule = () => { + const updatedRules = [ + ...(conditionRow.allOf ?? []), + ...(conditionRow.anyOf ?? []), + ]; + + const firstNestedConditionIndex = + findFirstNestedConditionIndex(updatedRules); + if (firstNestedConditionIndex !== -1) { + updatedRules.splice( + firstNestedConditionIndex, + 0, + getDefaultRule(selPluginResourceType), + ); + } else { + updatedRules.push(getDefaultRule(selPluginResourceType)); + } + + onRuleChange({ [criteria]: [...updatedRules] }); + updateErrors(firstNestedConditionIndex); + }; + + return ( + (criteria === criterias.allOf || criteria === criterias.anyOf) && ( + + + + + ) + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionRule.tsx b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionRule.tsx new file mode 100644 index 0000000000..9fde51154d --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionRule.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { + getDefaultRule, + ruleOptionDisabled, +} from '../../utils/conditional-access-utils'; +import { ConditionsFormRowFields } from './ConditionsFormRowFields'; +import { criterias } from './const'; +import { AccessConditionsErrors, ConditionsData, RulesData } from './types'; + +type ConditionRuleProps = { + conditionRow: ConditionsData; + selPluginResourceType: string; + onRuleChange: (newCondition: ConditionsData) => void; + criteria: string; + conditionRulesData?: RulesData; + setErrors: React.Dispatch< + React.SetStateAction + >; + setRemoveAllClicked: React.Dispatch>; +}; + +export const ConditionRule = ({ + conditionRow, + selPluginResourceType, + onRuleChange, + criteria, + conditionRulesData, + setErrors, + setRemoveAllClicked, +}: ConditionRuleProps) => { + return ( + criteria === criterias.condition && ( + + ruleOptionDisabled( + ruleOption, + conditionRow.condition ? [conditionRow.condition] : undefined, + ) + } + setRemoveAllClicked={setRemoveAllClicked} + /> + ) + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionalAccessSidebar.tsx b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionalAccessSidebar.tsx new file mode 100644 index 0000000000..8e2f201b87 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionalAccessSidebar.tsx @@ -0,0 +1,127 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { makeStyles } from '@material-ui/core'; +import Drawer from '@material-ui/core/Drawer'; +import CloseIcon from '@mui/icons-material/Close'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; + +import { ConditionsForm } from './ConditionsForm'; +import { ConditionsData, RulesData } from './types'; + +const useDrawerStyles = makeStyles(() => ({ + paper: { + ['@media (max-width: 960px)']: { + width: '-webkit-fill-available', + }, + width: '50vw', + height: '100vh', + gap: '3%', + display: '-webkit-inline-box', + }, +})); + +const useDrawerContentStyles = makeStyles(theme => ({ + sidebar: { + display: 'flex', + flexFlow: 'column', + justifyContent: 'space-between', + backgroundColor: `${theme.palette.background.default} !important`, + }, + header: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'baseline', + padding: theme.spacing(2.5), + fontFamily: theme.typography.fontFamily, + }, + headerSubtitle: { + fontWeight: 400, + fontFamily: theme.typography.fontFamily, + paddingTop: theme.spacing(1), + }, +})); + +type ConditionalAccessSidebarProps = { + open: boolean; + onClose: () => void; + onSave: (conditions?: ConditionsData) => void; + selPluginResourceType: string; + conditionRulesData?: RulesData; + conditionsFormVal?: ConditionsData; +}; + +export const ConditionalAccessSidebar = ({ + open, + onClose, + onSave, + selPluginResourceType, + conditionRulesData, + conditionsFormVal, +}: ConditionalAccessSidebarProps) => { + const classes = useDrawerStyles(); + const contentClasses = useDrawerContentStyles(); + return ( + + + + + + Configure access for the + {' '} + {selPluginResourceType} + + By default, the selected resource type will be visible to the + chosen users in step two. If you want to restrict or grant + permission to specific plugin resource type rule, select it and + add the required parameters. + + + + + + + + + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionsForm.test.tsx b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionsForm.test.tsx new file mode 100644 index 0000000000..1688de9bf2 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionsForm.test.tsx @@ -0,0 +1,135 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { fireEvent, render, screen } from '@testing-library/react'; + +import { mockTransformedConditionRules } from '../../__fixtures__/mockTransformedConditionRules'; +import { ConditionsForm } from './ConditionsForm'; + +jest.mock('@material-ui/core', () => ({ + ...jest.requireActual('@material-ui/core'), + makeStyles: jest.fn().mockReturnValue(() => ({})), +})); + +describe('ConditionsForm', () => { + const selPluginResourceType = 'catalog-entity'; + const onSaveMock = jest.fn(); + const onCloseMock = jest.fn(); + + const renderComponent = (props = {}) => + render( + , + ); + + beforeEach(() => { + onSaveMock.mockClear(); + onCloseMock.mockClear(); + }); + + it('renders without crashing', () => { + const { getByTestId } = renderComponent(); + expect(getByTestId('conditions-row')).toBeInTheDocument(); + expect(getByTestId('save-conditions')).toBeInTheDocument(); + expect(getByTestId('cancel-conditions')).toBeInTheDocument(); + expect(getByTestId('remove-conditions')).toBeInTheDocument(); + }); + + it('calls onClose when Cancel button is clicked', () => { + const { getByTestId } = renderComponent(); + fireEvent.click(getByTestId('cancel-conditions')); + expect(onCloseMock).toHaveBeenCalled(); + }); + + it('resets conditions data when Remove all button is clicked', () => { + const { getByTestId } = renderComponent({ + conditionsFormVal: { + condition: { + rule: 'HAS_LABEL', + resourceType: selPluginResourceType, + params: {}, + }, + }, + }); + + fireEvent.click(getByTestId('remove-conditions')); + fireEvent.click(getByTestId('save-conditions')); + + expect(onSaveMock).toHaveBeenCalledWith(undefined); + }); + + it('disables Save button if conditions are unchanged', () => { + const { getByTestId } = renderComponent(); + expect(getByTestId('save-conditions')).toBeDisabled(); + }); + + it('disables Save button if no rule is selected', () => { + const { getByTestId } = renderComponent(); + + fireEvent.change( + screen.getByRole('textbox', { + name: /rule/i, + }), + { target: { value: '' } }, + ); + + expect(getByTestId('save-conditions')).toBeDisabled(); + }); + + it('shows Multiple levels of nested conditions warning', () => { + const { getByTestId } = renderComponent({ + conditionsFormVal: { + anyOf: [ + { + rule: 'HAS_ANOTTATION', + resourceType: selPluginResourceType, + params: {}, + }, + { + allOf: [ + { + rule: 'HAS_ANOTTATION', + resourceType: selPluginResourceType, + params: {}, + }, + { + not: { + rule: 'HAS_LABEL', + resourceType: selPluginResourceType, + params: { + label: 'temp', + }, + }, + }, + ], + }, + ], + }, + }); + + expect( + getByTestId('multi-level-nested-conditions-warning'), + ).toBeInTheDocument(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionsForm.tsx b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionsForm.tsx new file mode 100644 index 0000000000..acdeca21ca --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionsForm.tsx @@ -0,0 +1,291 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { PermissionCondition } from '@backstage/plugin-permission-common'; + +import { Box, Button, makeStyles } from '@material-ui/core'; +import { Alert, AlertTitle } from '@material-ui/lab'; +import WarningIcon from '@mui/icons-material/Warning'; + +import { + hasAllOfOrAnyOfErrors, + hasNestedNotErrors, + hasSimpleConditionOrNotErrors, + initializeErrors, + isSimpleRule, + resetErrors, +} from '../../utils/conditional-access-utils'; +import { ConditionsFormRow } from './ConditionsFormRow'; +import { criterias } from './const'; +import { + AccessConditionsErrors, + Condition, + ConditionsData, + RulesData, +} from './types'; + +const useStyles = makeStyles(theme => ({ + form: { + padding: theme.spacing(2.5), + paddingTop: 0, + flexGrow: 1, + overflow: 'auto', + }, + addConditionButton: { + color: theme.palette.primary.light, + }, + footer: { + display: 'flex', + flexDirection: 'row', + gap: '15px', + alignItems: 'baseline', + borderTop: `2px solid ${theme.palette.border}`, + padding: theme.spacing(2.5), + '& button': { + textTransform: 'none', + }, + }, +})); + +type ConditionFormProps = { + conditionRulesData?: RulesData; + conditionsFormVal?: ConditionsData; + selPluginResourceType: string; + onClose: () => void; + onSave: (conditions?: ConditionsData) => void; +}; + +export const ConditionsForm = ({ + conditionRulesData, + selPluginResourceType, + conditionsFormVal, + onClose, + onSave, +}: ConditionFormProps) => { + const classes = useStyles(); + const [conditions, setConditions] = React.useState( + conditionsFormVal ?? { + condition: { + rule: '', + resourceType: selPluginResourceType, + params: {}, + }, + }, + ); + const [criteria, setCriteria] = React.useState( + (Object.keys(conditions)[0] as keyof ConditionsData) ?? criterias.condition, + ); + const [errors, setErrors] = React.useState< + AccessConditionsErrors | undefined + >(initializeErrors(criteria, conditions)); + + const [removeAllClicked, setRemoveAllClicked] = + React.useState(false); + + const flattenConditions = ( + conditionData: Condition[], + ): PermissionCondition[] => { + const flatConditions: PermissionCondition[] = []; + + const processCondition = (condition: Condition) => { + if ('rule' in condition) { + flatConditions.push(condition); + } else { + if (condition.allOf) { + condition.allOf.forEach(processCondition); + } + if (condition.anyOf) { + condition.anyOf.forEach(processCondition); + } + if (condition.not) { + if ('rule' in condition.not) { + flatConditions.push(condition.not); + } else { + processCondition(condition.not); + } + } + } + }; + conditionData.forEach(processCondition); + return flatConditions; + }; + + const isNoRuleSelected = () => { + switch (criteria) { + case criterias.condition: + return !conditions.condition?.rule; + case criterias.not: { + const flatConditions = flattenConditions([ + conditions.not as PermissionCondition, + ]); + return flatConditions.some(c => !c.rule); + } + case criterias.allOf: { + const flatConditions = flattenConditions(conditions.allOf || []); + return flatConditions.some(c => !c.rule); + } + case criterias.anyOf: { + const flatConditions = flattenConditions(conditions.anyOf || []); + return flatConditions.some(c => !c.rule); + } + default: + return true; + } + }; + + const hasAnyErrors = (): boolean => { + if (!errors) return false; + + if ( + criteria === criterias.condition || + (criteria === criterias.not && + isSimpleRule(conditions[criteria] as Condition)) + ) { + return hasSimpleConditionOrNotErrors(errors, criteria); + } + + if ( + criteria === criterias.not && + !isSimpleRule(conditions[criteria] as Condition) + ) { + return hasNestedNotErrors(errors, conditions, criteria); + } + + if (criteria === criterias.allOf || criteria === criterias.anyOf) { + return hasAllOfOrAnyOfErrors(errors, criteria); + } + + return false; + }; + + const isSaveDisabled = () => { + if (removeAllClicked) return false; + + return ( + hasAnyErrors() || + isNoRuleSelected() || + Object.is(conditionsFormVal, conditions) + ); + }; + + const hasMultiLevelNestedConditions = (): boolean => { + if (!Array.isArray(conditions[criteria])) { + return false; + } + + return (conditions[criteria] as Condition[]) + .filter(condition => !('rule' in condition)) + .some((firstLevelNestedCondition: Condition) => { + const nestedConditionCriteria = Object.keys( + firstLevelNestedCondition, + )[0]; + if ( + Array.isArray( + firstLevelNestedCondition[ + nestedConditionCriteria as keyof Condition + ], + ) + ) { + return ( + firstLevelNestedCondition[ + nestedConditionCriteria as keyof Condition + ] as Condition[] + ).some((con: Condition) => !('rule' in con)); + } + + return !Object.keys( + firstLevelNestedCondition[ + nestedConditionCriteria as keyof Condition + ] as Condition[], + ).includes('rule'); + }); + }; + + return ( + <> + + setConditions(newCondition)} + setCriteria={setCriteria} + setErrors={setErrors} + setRemoveAllClicked={setRemoveAllClicked} + /> + {hasMultiLevelNestedConditions() && ( + } + style={{ margin: '1.5rem 0 1rem 0' }} + severity="warning" + data-testid="multi-level-nested-conditions-warning" + > + + Multiple levels of nested conditions are not supported + + Only one level is displayed. Please use the CLI to view all nested + conditions. + + )} + + + + + + + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRow.tsx b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRow.tsx new file mode 100644 index 0000000000..b07fa03178 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRow.tsx @@ -0,0 +1,618 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { PermissionCondition } from '@backstage/plugin-permission-common'; + +import { + FormControlLabel, + IconButton, + Radio, + RadioGroup, + useTheme, +} from '@material-ui/core'; +import AddIcon from '@mui/icons-material/Add'; +import RemoveIcon from '@mui/icons-material/Remove'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; + +import { + extractNestedConditions, + getDefaultRule, + getNestedConditionSimpleRulesCount, + getSimpleRulesCount, + isNestedConditionRule, + isSimpleRule, + makeConditionsFormRowStyles, + nestedConditionButtons, + resetErrors, + ruleOptionDisabled, +} from '../../utils/conditional-access-utils'; +import { AddNestedConditionButton } from './AddNestedConditionButton'; +import { ComplexConditionRow } from './ComplexConditionRow'; +import { ComplexConditionRowButtons } from './ComplexConditionRowButtons'; +import { ConditionRule } from './ConditionRule'; +import { ConditionsFormRowFields } from './ConditionsFormRowFields'; +import { conditionButtons, criterias } from './const'; +import { CriteriaToggleButton } from './CriteriaToggleButton'; +import { + ComplexErrors, + Condition, + ConditionFormRowProps, + ConditionsData, + NestedCriteriaErrors, + NotConditionType, +} from './types'; + +export const ConditionsFormRow = ({ + conditionRulesData, + conditionRow, + selPluginResourceType, + criteria, + onRuleChange, + setCriteria, + setErrors, + setRemoveAllClicked, +}: ConditionFormRowProps) => { + const classes = makeConditionsFormRowStyles(); + const theme = useTheme(); + const [nestedConditionRow, setNestedConditionRow] = React.useState< + Condition[] + >([]); + const [notConditionType, setNotConditionType] = + React.useState(NotConditionType.SimpleCondition); + + React.useEffect(() => { + const nestedConditions: Condition[] = []; + const criteriaTypes = [criterias.allOf, criterias.anyOf, criterias.not]; + switch (criteria) { + case criterias.allOf: + extractNestedConditions( + conditionRow.allOf || [], + criteriaTypes, + nestedConditions, + ); + break; + case criterias.anyOf: + extractNestedConditions( + conditionRow.anyOf || [], + criteriaTypes, + nestedConditions, + ); + break; + case criterias.not: + if ( + conditionRow.not && + criteriaTypes.includes( + Object.keys(conditionRow.not)[0] as keyof ConditionsData, + ) + ) { + nestedConditions.push(conditionRow.not); + setNotConditionType(NotConditionType.NestedCondition); + } + break; + default: + break; + } + + setNestedConditionRow(nestedConditions); + }, [conditionRow, criteria]); + + const handleCriteriaChange = (val: keyof ConditionsData) => { + setCriteria(val); + setErrors(resetErrors(val)); + + const defaultRule = getDefaultRule(selPluginResourceType); + + const ruleMap = { + [criterias.condition]: { condition: defaultRule }, + [criterias.allOf]: { allOf: [defaultRule] }, + [criterias.anyOf]: { anyOf: [defaultRule] }, + [criterias.not]: { not: defaultRule }, + }; + + if (val === criterias.not) { + setNotConditionType(NotConditionType.SimpleCondition); + } + + const ruleChange = ruleMap[val]; + if (ruleChange) { + onRuleChange(ruleChange); + } + }; + + const updateRules = (updatedNestedConditionRow: Condition[] | Condition) => { + const existingSimpleCondition = + criteria !== criterias.not + ? (conditionRow[criteria as keyof Condition] as Condition[])?.filter( + con => isSimpleRule(con), + ) || [] + : []; + + const newCondition: Condition[] = Array.isArray(updatedNestedConditionRow) + ? [...existingSimpleCondition, ...updatedNestedConditionRow] + : [...existingSimpleCondition, updatedNestedConditionRow]; + + if (criteria === criterias.anyOf || criteria === criterias.allOf) { + onRuleChange({ + [criteria]: newCondition, + }); + } else if ( + criteria === criterias.not && + !Array.isArray(updatedNestedConditionRow) + ) { + onRuleChange({ + not: updatedNestedConditionRow, + }); + } + }; + + const handleNestedConditionCriteriaChange = ( + val: string, + nestedConditionIndex: number, + ) => { + const defaultRule = getDefaultRule(selPluginResourceType); + + const nestedConditionMap = { + [criterias.not]: { [val]: defaultRule }, + [criterias.allOf]: { [val]: [defaultRule] }, + [criterias.anyOf]: { [val]: [defaultRule] }, + [criterias.condition]: { [val]: [defaultRule] }, + }; + + const newCondition = nestedConditionMap[val] || { [val]: [defaultRule] }; + + setErrors(prevErrors => { + const updatedErrors = { ...prevErrors }; + + if (updatedErrors[criteria] !== undefined) { + if (criteria === criterias.not) { + (updatedErrors[criteria] as ComplexErrors) = + val !== criterias.not ? { [val]: [''] } : { [val]: '' }; + return updatedErrors; + } + const criteriaErrors = updatedErrors[criteria]; + const simpleRuleErrors = (criteriaErrors as ComplexErrors[]).filter( + (err: ComplexErrors) => typeof err === 'string', + ); + const nestedConditionErrors = ( + criteriaErrors as ComplexErrors[] + ).filter((err: ComplexErrors) => typeof err !== 'string'); + nestedConditionErrors[nestedConditionIndex] = + val !== criterias.not ? { [val]: [''] } : { [val]: '' }; + updatedErrors[criteria] = [ + ...simpleRuleErrors, + ...nestedConditionErrors, + ]; + } + + return updatedErrors; + }); + + if (criteria === criterias.not) { + updateRules(newCondition); + } else { + const updatedNestedConditionRow = nestedConditionRow.map((c, index) => { + if (index === nestedConditionIndex) { + return newCondition; + } + return c; + }); + updateRules(updatedNestedConditionRow); + } + }; + + const handleAddNestedCondition = (currentCriteria: string) => { + const newNestedCondition = { + [criterias.allOf]: [getDefaultRule(selPluginResourceType)], + }; + const updatedNestedConditionRow = [ + ...nestedConditionRow, + newNestedCondition, + ]; + updateRules( + currentCriteria === criterias.not + ? newNestedCondition + : updatedNestedConditionRow, + ); + + setErrors(prevErrors => { + const updatedErrors = { ...prevErrors }; + if (updatedErrors[currentCriteria]) { + const criteriaErrors = updatedErrors[ + currentCriteria + ] as ComplexErrors[]; + if (Array.isArray(criteriaErrors)) { + criteriaErrors.push({ [criterias.allOf]: [''] }); + } else { + (updatedErrors[currentCriteria] as ComplexErrors) = { + [criterias.allOf]: [''], + }; + } + } + return updatedErrors; + }); + }; + + const handleNotConditionTypeChange = (val: NotConditionType) => { + setNotConditionType(val); + setErrors(resetErrors(criteria, val)); + if (val === 'nested-condition') { + handleAddNestedCondition(criterias.not); + } else { + onRuleChange({ + not: getDefaultRule(selPluginResourceType), + }); + } + }; + + const handleAddRuleInNestedCondition = ( + nestedConditionCriteria: string, + nestedConditionIndex: number, + ) => { + const updatedNestedConditionRow: Condition[] = []; + + nestedConditionRow.forEach((c, index) => { + if (index === nestedConditionIndex) { + updatedNestedConditionRow.push({ + [nestedConditionCriteria as keyof Condition]: [ + ...((c[ + nestedConditionCriteria as keyof Condition + ] as PermissionCondition[]) || []), + getDefaultRule(selPluginResourceType), + ], + }); + } else { + updatedNestedConditionRow.push(c); + } + }); + updateRules( + criteria === criterias.not + ? updatedNestedConditionRow[0] + : updatedNestedConditionRow, + ); + + setErrors(prevErrors => { + const updatedErrors = { ...prevErrors }; + if (updatedErrors[criteria] !== undefined) { + const criteriaErrors = updatedErrors[criteria]; + if ( + criteria === criterias.not && + notConditionType === 'nested-condition' + ) { + ( + (criteriaErrors as NestedCriteriaErrors)[ + nestedConditionCriteria + ] as string[] + ).push(''); + return updatedErrors; + } + const simpleRuleErrors = (criteriaErrors as ComplexErrors[]).filter( + (err: ComplexErrors) => typeof err === 'string', + ); + const nestedConditionErrors = ( + criteriaErrors as ComplexErrors[] + ).filter((err: ComplexErrors) => typeof err !== 'string'); + + ( + (( + nestedConditionErrors[nestedConditionIndex] as NestedCriteriaErrors + )[nestedConditionCriteria] as string[]) || [] + ).push(''); + updatedErrors[criteria] = [ + ...simpleRuleErrors, + ...nestedConditionErrors, + ]; + } + return updatedErrors; + }); + }; + + const handleRemoveNestedCondition = (nestedConditionIndex: number) => { + const updatedNestedConditionRow = nestedConditionRow.filter( + (_, index) => index !== nestedConditionIndex, + ); + + updateRules(updatedNestedConditionRow); + setErrors(prevErrors => { + const updatedErrors = { ...prevErrors }; + + if (updatedErrors[criteria] !== undefined) { + const criteriaErrors = updatedErrors[criteria] as ComplexErrors[]; + const simpleRuleErrors = criteriaErrors.filter( + (err: ComplexErrors) => typeof err === 'string', + ); + const nestedConditionErrors = criteriaErrors.filter( + (err: ComplexErrors) => typeof err !== 'string', + ); + nestedConditionErrors.splice(nestedConditionIndex, 1); + + updatedErrors[criteria] = [ + ...simpleRuleErrors, + ...nestedConditionErrors, + ]; + } + + return updatedErrors; + }); + }; + + const updateErrors = (_index: number) => { + setErrors(prevErrors => { + const updatedErrors = { ...prevErrors }; + + if (!Array.isArray(updatedErrors[criteria])) { + updatedErrors[criteria] = []; + } + + const firstNestedConditionErrorIndex = + (updatedErrors[criteria] as ComplexErrors[]).findIndex( + e => typeof e !== 'string', + ) || 0; + + (updatedErrors[criteria] as ComplexErrors[]).splice( + firstNestedConditionErrorIndex, + 0, + '', + ); + + return updatedErrors; + }); + }; + + const renderNestedConditionRow = ( + nc: Condition, + nestedConditionIndex: number, + ) => { + const selectedNestedConditionCriteria = Object.keys(nc)[0]; + const simpleRulesCount = getSimpleRulesCount(conditionRow, criteria); + const nestedConditionsCount = nestedConditionRow.length; + const nestedConditionSimpleRulesCount = getNestedConditionSimpleRulesCount( + nc, + selectedNestedConditionCriteria, + ); + return ( + nestedConditionSimpleRulesCount > 0 && ( + +
+ + handleNestedConditionCriteriaChange( + newNestedCriteria, + nestedConditionIndex, + ) + } + className={classes.nestedConditioncriteriaButtonGroup} + > + {nestedConditionButtons.map(({ val, label }) => ( + + ))} + + {criteria !== criterias.not && ( + + handleRemoveNestedCondition(nestedConditionIndex) + } + > + + + )} +
+ + {selectedNestedConditionCriteria !== criterias.not && + ( + nc[ + selectedNestedConditionCriteria as keyof Condition + ] as PermissionCondition[] + ).map((c, ncrIndex) => ( + + ))} + {selectedNestedConditionCriteria === criterias.not && + ((nc as ConditionsData).not as PermissionCondition) + .resourceType && ( + + ruleOptionDisabled( + ruleOption, + (nc as ConditionsData).not + ? [(nc as ConditionsData).not as PermissionCondition] + : undefined, + ) + } + setRemoveAllClicked={setRemoveAllClicked} + nestedConditionRow={nestedConditionRow} + nestedConditionCriteria={selectedNestedConditionCriteria} + nestedConditionIndex={nestedConditionIndex} + updateRules={updateRules} + /> + )} + {selectedNestedConditionCriteria !== criterias.not && ( + + )} + +
+ ) + ); + }; + + return ( + + handleCriteriaChange(newCriteria)} + className={classes.criteriaButtonGroup} + > + {conditionButtons.map(({ val, label }) => ( + + ))} + + + {criteria !== criterias.condition && ( + + {criteria !== criterias.not && + (conditionRow[criteria] as PermissionCondition[])?.map( + (c, srIndex) => ( + + ), + )} + {criteria === criterias.not && ( + + handleNotConditionTypeChange(value as NotConditionType) + } + > + } + label="Add rule" + className={classes.radioLabel} + /> + {notConditionType === NotConditionType.SimpleCondition && ( + + ruleOptionDisabled( + ruleOption, + conditionRow.not + ? [conditionRow.not as PermissionCondition] + : undefined, + ) + } + setRemoveAllClicked={setRemoveAllClicked} + /> + )} + } + label={} + className={classes.radioLabel} + /> + + )} + + {nestedConditionRow?.length > 0 && + nestedConditionRow.map((nc, nestedConditionIndex) => + renderNestedConditionRow(nc, nestedConditionIndex), + )} + + )} + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRowFields.tsx b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRowFields.tsx new file mode 100644 index 0000000000..9c8a2b1955 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/ConditionsFormRowFields.tsx @@ -0,0 +1,345 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { PermissionCondition } from '@backstage/plugin-permission-common'; + +import { Box, TextField } from '@material-ui/core'; +import { Autocomplete } from '@material-ui/lab'; +import Form from '@rjsf/mui'; +import { + RegistryFieldsType, + RJSFSchema, + RJSFValidationError, + UiSchema, +} from '@rjsf/utils'; +import validator from '@rjsf/validator-ajv8'; + +import { + getNestedRuleErrors, + getSimpleRuleErrors, + isSimpleRule, + makeConditionsFormRowFieldsStyles, + setErrorMessage, +} from '../../utils/conditional-access-utils'; +import { criterias } from './const'; +import { CustomArrayField } from './CustomArrayField'; +import { RulesDropdownOption } from './RulesDropdownOption'; +import { + AccessConditionsErrors, + ComplexErrors, + Condition, + ConditionsData, + NestedCriteriaErrors, + RulesData, +} from './types'; + +type ConditionFormRowFieldsProps = { + oldCondition: Condition; + index?: number; + criteria: string; + onRuleChange: (newCondition: ConditionsData) => void; + conditionRow: ConditionsData | Condition; + conditionRulesData?: RulesData; + setErrors: React.Dispatch< + React.SetStateAction + >; + optionDisabled?: (ruleOption: string) => boolean; + setRemoveAllClicked: React.Dispatch>; + nestedConditionRow?: Condition[]; + nestedConditionCriteria?: string; + nestedConditionIndex?: number; + nestedConditionRuleIndex?: number; + updateRules?: (newCondition: Condition[] | Condition) => void; +}; + +export const ConditionsFormRowFields = ({ + oldCondition, + index, + criteria, + onRuleChange, + conditionRow, + conditionRulesData, + setErrors, + optionDisabled, + setRemoveAllClicked, + nestedConditionRow, + nestedConditionCriteria, + nestedConditionIndex, + nestedConditionRuleIndex, + updateRules, +}: ConditionFormRowFieldsProps) => { + const classes = makeConditionsFormRowFieldsStyles({ + isNotSimpleCondition: + criteria === criterias.not && !nestedConditionCriteria, + }); + const rules = conditionRulesData?.rules ?? []; + const paramsSchema = + conditionRulesData?.[(oldCondition as PermissionCondition).rule]?.schema; + + const schema: RJSFSchema = paramsSchema; + + const uiSchema: UiSchema = { + 'ui:submitButtonOptions': { + norender: true, + }, + 'ui:classNames': `${classes.params}`, + 'ui:field': 'array', + }; + + const customFields: RegistryFieldsType = { ArrayField: CustomArrayField }; + + const handleConditionChange = (newCondition: PermissionCondition) => { + setRemoveAllClicked(false); + switch (criteria) { + case criterias.condition: { + onRuleChange({ condition: newCondition }); + break; + } + case criterias.allOf: { + const updatedCriteria = (conditionRow as ConditionsData).allOf ?? []; + updatedCriteria[index ?? 0] = newCondition; + onRuleChange({ allOf: updatedCriteria }); + break; + } + case criterias.anyOf: { + const updatedCriteria = (conditionRow as ConditionsData).anyOf ?? []; + updatedCriteria[index ?? 0] = newCondition; + onRuleChange({ anyOf: updatedCriteria }); + break; + } + case criterias.not: { + onRuleChange({ not: newCondition }); + break; + } + default: + } + }; + + const handleNestedConditionChange = (newCondition: PermissionCondition) => { + if ( + !nestedConditionRow || + !nestedConditionCriteria || + nestedConditionIndex === undefined || + !updateRules + ) { + return; + } + const updatedNestedConditionRow: Condition[] = nestedConditionRow.map( + (c, i) => { + if (i === nestedConditionIndex) { + if (nestedConditionCriteria === criterias.not) { + return { + [nestedConditionCriteria]: newCondition, + }; + } + const updatedNestedConditionRules = ( + (c[ + nestedConditionCriteria as keyof Condition + ] as PermissionCondition[]) || [] + ).map((rule, rindex) => { + return rindex === nestedConditionRuleIndex ? newCondition : rule; + }); + + return { + [nestedConditionCriteria]: updatedNestedConditionRules, + }; + } + return c; + }, + ); + + updateRules( + criteria === criterias.not + ? updatedNestedConditionRow[0] + : updatedNestedConditionRow, + ); + }; + + const handleTransformErrors = (errors: RJSFValidationError[]) => { + // criteria: condition or not simple-condition + if ( + criteria === criterias.condition || + (criteria === criterias.not && + isSimpleRule(conditionRow[criteria as keyof Condition])) + ) { + setErrors(prevErrors => { + const updatedErrors = { ...prevErrors }; + updatedErrors[criteria] = setErrorMessage(errors); + + return updatedErrors; + }); + } + + // criteria: not nested-condition + if ( + criteria === criterias.not && + nestedConditionCriteria && + !isSimpleRule(conditionRow[criteria as keyof Condition]) + ) { + setErrors(prevErrors => { + const updatedErrors = { ...prevErrors }; + const nestedErrors = (updatedErrors[criteria] as ComplexErrors)[ + nestedConditionCriteria as keyof Condition + ] as NestedCriteriaErrors; + + // nestedCriteria: allOf or anyOf + if ( + Array.isArray(nestedErrors) && + nestedConditionRuleIndex !== undefined + ) { + nestedErrors[nestedConditionRuleIndex] = setErrorMessage(errors); + } else { + // nestedCriteria: not + updatedErrors[criteria] = { + [nestedConditionCriteria]: setErrorMessage(errors), + }; + } + + return updatedErrors; + }); + } + + // criteria: allOf or anyOf + if (criteria === criterias.allOf || criteria === criterias.anyOf) { + setErrors(prevErrors => { + const updatedErrors = { ...prevErrors }; + const simpleRuleErrors = getSimpleRuleErrors( + updatedErrors[ + criteria as keyof AccessConditionsErrors + ] as ComplexErrors[], + ); + if ( + Array.isArray(simpleRuleErrors) && + simpleRuleErrors.length > 0 && + index !== undefined + ) { + simpleRuleErrors[index] = setErrorMessage(errors); + } + + const nestedRuleErrors = getNestedRuleErrors( + updatedErrors[ + criteria as keyof AccessConditionsErrors + ] as ComplexErrors[], + ); + + // nestedCriteria: allOf or anyOf + if ( + nestedConditionCriteria && + nestedConditionIndex !== undefined && + nestedConditionRuleIndex !== undefined + ) { + const nestedConditionRuleList = + nestedRuleErrors[nestedConditionIndex][nestedConditionCriteria]; + + if (Array.isArray(nestedConditionRuleList)) { + nestedConditionRuleList[nestedConditionRuleIndex] = + setErrorMessage(errors); + } + } + + // nestedCriteria: not + if ( + Array.isArray(nestedRuleErrors) && + nestedRuleErrors.length > 0 && + nestedConditionCriteria === criterias.not && + nestedConditionIndex !== undefined + ) { + nestedRuleErrors[nestedConditionIndex][nestedConditionCriteria] = + setErrorMessage(errors); + } + + updatedErrors[criteria] = [...simpleRuleErrors, ...nestedRuleErrors]; + return updatedErrors; + }); + } + + return errors; + }; + + const onConditionChange = (newCondition: PermissionCondition) => { + if (nestedConditionRow) { + handleNestedConditionChange(newCondition); + } else { + handleConditionChange(newCondition); + } + }; + + return ( + + + optionDisabled ? optionDisabled(option) : false + } + onChange={(_event, ruleVal?: string | null) => + onConditionChange({ + ...oldCondition, + rule: ruleVal ?? '', + params: {}, + } as PermissionCondition) + } + renderOption={option => ( + + )} + renderInput={(params: any) => ( + + )} + /> + + {schema ? ( +
+ onConditionChange({ + ...oldCondition, + params: data.formData || {}, + } as PermissionCondition) + } + transformErrors={handleTransformErrors} + showErrorList={false} + liveValidate + /> + ) : ( + + )} + + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/CriteriaToggleButton.tsx b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/CriteriaToggleButton.tsx new file mode 100644 index 0000000000..74d43dec7e --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/CriteriaToggleButton.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { CSSProperties } from 'react'; + +import { Theme } from '@material-ui/core'; +import ToggleButton from '@mui/material/ToggleButton'; + +type CriteriaToggleButtonProps = { + val: string; + label: string; + selectedCriteria: string; + theme: Theme; +}; + +export const CriteriaToggleButton = ({ + val, + label, + selectedCriteria, + theme, +}: CriteriaToggleButtonProps) => { + const isSelected = val === selectedCriteria; + const buttonStyle: CSSProperties = { + color: isSelected ? theme.palette.infoText : theme.palette.textSubtle, + backgroundColor: isSelected ? theme.palette.infoBackground : '', + border: `1px solid ${theme.palette.border}`, + width: '100%', + textTransform: 'none', + padding: theme.spacing(1), + }; + + return ( + + {label} + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/CustomArrayField.tsx b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/CustomArrayField.tsx new file mode 100644 index 0000000000..f5db906c7b --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/CustomArrayField.tsx @@ -0,0 +1,69 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { makeStyles, TextField, Typography } from '@material-ui/core'; +import { getDefaultRegistry } from '@rjsf/core'; +import { FieldProps } from '@rjsf/utils'; +import { getInnerSchemaForArrayItem } from '@rjsf/utils/lib/schema/getDefaultFormState'; + +const useStyles = makeStyles(theme => ({ + arrayFieldDescription: { + marginTop: '5px', + fontWeight: 500, + color: `${theme.palette.grey[500]} !important`, + }, +})); + +export const CustomArrayField = (props: FieldProps) => { + const { name, required, schema: sch, formData, onChange } = props; + const classes = useStyles(); + const [fieldVal, setFieldVal] = React.useState( + formData?.toString() ?? '', + ); + + const arrayItemsType = getInnerSchemaForArrayItem(sch).type; + + const DefaultArrayField = getDefaultRegistry().fields.ArrayField; + + return arrayItemsType === 'string' ? ( + <> + { + const value = e.target.value; + setFieldVal(value); + onChange(value ? value.split(',').map(val => val.trim()) : []); + }} + required={required} + placeholder="string, string" + /> + + + {sch.description ?? ''} + + + + ) : ( + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/RulesDropdownOption.tsx b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/RulesDropdownOption.tsx new file mode 100644 index 0000000000..16c0243dbf --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/RulesDropdownOption.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { makeStyles, Typography } from '@material-ui/core'; +import Box from '@mui/material/Box'; + +import { RulesData } from './types'; + +const useStyles = makeStyles(theme => ({ + optionLabel: { + color: theme.palette.text.primary, + }, + optionDescription: { + color: theme.palette.text.secondary, + fontSize: '14px', + }, +})); + +type RulesDropdownOptionProps = { + label: string; + rulesData?: RulesData; +}; + +export const RulesDropdownOption = ({ + label, + rulesData, +}: RulesDropdownOptionProps) => { + const classes = useStyles(); + const description = rulesData?.[label]?.description ?? ''; + return ( + + {label} + + {description} + + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/const.ts b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/const.ts new file mode 100644 index 0000000000..f19db49f24 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/const.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConditionsData } from './types'; + +export const criterias = { + condition: 'condition' as keyof ConditionsData, + anyOf: 'anyOf' as keyof ConditionsData, + allOf: 'allOf' as keyof ConditionsData, + not: 'not' as keyof ConditionsData, +}; + +export const criteriasLabels = { + [criterias.condition]: 'Condition', + [criterias.allOf]: 'AllOf', + [criterias.anyOf]: 'AnyOf', + [criterias.not]: 'Not', +}; + +export const conditionButtons = [ + { val: criterias.condition, label: criteriasLabels[criterias.condition] }, + { val: criterias.allOf, label: criteriasLabels[criterias.allOf] }, + { val: criterias.anyOf, label: criteriasLabels[criterias.anyOf] }, + { val: criterias.not, label: criteriasLabels[criterias.not] }, +]; diff --git a/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/types.ts b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/types.ts new file mode 100644 index 0000000000..e61a524a3f --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ConditionalAccess/types.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PermissionCondition } from '@backstage/plugin-permission-common'; + +export type RulesData = { + rules: string[]; + [rule: string]: { + [key: string]: any; + }; +}; + +export type ResourceTypeRuleData = { + [resourceType: string]: RulesData; +}; + +export type ConditionRulesData = { + [plugin: string]: ResourceTypeRuleData; +}; + +export type ConditionRules = { + data?: ConditionRulesData; + error?: Error; +}; + +export type ConditionsData = { + allOf?: Condition[]; + anyOf?: Condition[]; + not?: Condition; + condition?: PermissionCondition; +}; + +export type Condition = PermissionCondition | ConditionsData; + +export type ComplexErrors = string | NestedCriteriaErrors; + +export type NestedCriteriaErrors = { + [nestedCriteria: string]: string[] | string; +}; + +export type AccessConditionsErrors = { + [criteria: string]: ComplexErrors[] | NestedCriteriaErrors | string; +}; + +export type ConditionFormRowProps = { + conditionRulesData?: RulesData; + conditionRow: ConditionsData; + onRuleChange: (newCondition: ConditionsData) => void; + selPluginResourceType: string; + criteria: keyof ConditionsData; + setCriteria: React.Dispatch>; + setErrors: React.Dispatch< + React.SetStateAction + >; + setRemoveAllClicked: React.Dispatch>; +}; + +export enum NotConditionType { + SimpleCondition = 'simple-condition', + NestedCondition = 'nested-condition', +} diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddMembersForm.test.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddMembersForm.test.tsx new file mode 100644 index 0000000000..49266748b0 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddMembersForm.test.tsx @@ -0,0 +1,255 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { + fireEvent, + render, + screen, + waitFor, + within, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import '@testing-library/jest-dom'; + +import { MemberEntity } from '../../types'; +import { AddMembersForm } from './AddMembersForm'; + +const membersData: { + members: MemberEntity[]; + loading: boolean; + error: Error; +} = { + members: [ + { + metadata: { name: '', etag: '' }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + spec: {}, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + spec: {}, + metadata: { name: 'test user1', etag: 'test user1' }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + spec: {}, + metadata: { name: 'test user2', etag: 'test user2' }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + spec: {}, + metadata: { name: 'test user3', etag: 'test user3' }, + }, + ], + loading: false, + error: { name: '', message: '' }, +}; +const selectedMembers = [ + { + id: 'test user1', + label: 'test user1', + etag: 'test user1', + type: 'User', + members: 1, + ref: 'user:default/test user1', + }, +]; + +describe('AddMembersForm', () => { + const mockSetFieldValue = jest.fn(); + + it('displays error message when membersData.error is provided', async () => { + const mockError = new Error('Failed to fetch'); + render( + , + ); + + expect( + screen.getByText(`Error fetching user and groups: ${mockError.message}`), + ).toBeInTheDocument(); + }); + + it('updates search field on input change', async () => { + render( + , + ); + + const input = screen.getByPlaceholderText( + 'Search by user name or group name', + ); + await userEvent.type(input, 'John Doe'); + + expect(input).toHaveValue('John Doe'); + }); + + it('calls setFieldValue with new selected member when an option is selected', async () => { + const { getByLabelText, findByText } = render( + , + ); + + fireEvent.mouseDown(getByLabelText(/Users and groups/)); + const memberOption = await findByText('test user1'); + const listboxElement = memberOption.closest('ul'); + + if (!listboxElement) { + throw new Error('Unable to find the listbox element.'); + } + + const listbox = within(listboxElement); + fireEvent.click(listbox.getByText(/test user1/)); + + await waitFor(() => { + expect(mockSetFieldValue).toHaveBeenCalledWith( + 'selectedMembers', + expect.arrayContaining([ + expect.objectContaining({ + label: 'test user1', + }), + ]), + ); + }); + }); + + it('filters members as the user types in the search input', async () => { + const user = userEvent.setup(); + const { getByPlaceholderText } = render( + , + ); + + await user.type( + getByPlaceholderText('Search by user name or group name'), + 'er1', + ); + + const elements = screen.getAllByTestId('test user1'); + const combinedText = elements.map(el => el.textContent).join(''); + expect(combinedText).toBe('test user1'); + }); + + it('updates the selected member and calls setFieldValue on selection', async () => { + const user = userEvent.setup(); + + const { getByPlaceholderText, findByText } = render( + , + ); + + await user.click(getByPlaceholderText('Search by user name or group name')); + + const memberOption = await findByText('test user2'); + const listboxElement = memberOption.closest('ul'); + + if (!listboxElement) { + throw new Error('Unable to find the listbox element.'); + } + + const listbox = within(listboxElement); + await user.click(listbox.getByText(/test user2/)); + + await waitFor(() => { + expect(mockSetFieldValue).toHaveBeenCalledWith( + 'selectedMembers', + expect.arrayContaining([ + expect.objectContaining({ + label: 'test user2', + }), + ]), + ); + }); + }); + + it('displays an error message if selectedMembersError is provided', () => { + const selectedMembersError = 'Error selecting members'; + + render( + , + ); + + expect(screen.getByText(selectedMembersError)).toBeInTheDocument(); + }); + + it('is able to clear the search input after selection', async () => { + const user = userEvent.setup(); + const { getByPlaceholderText, findByText } = render( + , + ); + + await user.click(getByPlaceholderText('Search by user name or group name')); + + const memberOption = await findByText('test user1'); + const listboxElement = memberOption.closest('ul'); + + if (!listboxElement) { + throw new Error('Unable to find the listbox element.'); + } + + const listbox = within(listboxElement); + + // user selected one option + await user.click(listbox.getByText(/test user1/)); + // user cleared the search input + await user.clear(getByPlaceholderText('Search by user name or group name')); + // user unfocused the search input + await user.click(document.body); + + // check if the selected member is cleared in search input + expect( + getByPlaceholderText('Search by user name or group name'), + ).toHaveValue(''); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddMembersForm.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddMembersForm.tsx new file mode 100644 index 0000000000..d4cf88ca8f --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddMembersForm.tsx @@ -0,0 +1,164 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { stringifyEntityRef } from '@backstage/catalog-model'; + +import { LinearProgress, TextField } from '@material-ui/core'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import { FormikErrors } from 'formik'; + +import { MemberEntity } from '../../types'; +import { + getChildGroupsCount, + getMembersCount, + getParentGroupsCount, +} from '../../utils/create-role-utils'; +import { MembersDropdownOption } from './MembersDropdownOption'; +import { RoleFormValues, SelectedMember } from './types'; + +type AddMembersFormProps = { + selectedMembers: SelectedMember[]; + selectedMembersError?: string; + membersData: { members: MemberEntity[]; loading: boolean; error: Error }; + setFieldValue: ( + field: string, + value: any, + shouldValidate?: boolean, + ) => Promise> | Promise; +}; + +export const AddMembersForm = ({ + selectedMembers, + selectedMembersError, + setFieldValue, + membersData, +}: AddMembersFormProps) => { + const [search, setSearch] = React.useState(''); + const [selectedMember, setSelectedMember] = React.useState({ + label: '', + etag: '', + type: '', + ref: '', + } as SelectedMember); + + const getDescription = (member: MemberEntity) => { + const memberCount = getMembersCount(member); + const parentCount = getParentGroupsCount(member); + const childCount = getChildGroupsCount(member); + + return member.kind === 'Group' + ? `${memberCount} members, ${parentCount} parent group, ${childCount} child groups` + : undefined; + }; + + const membersOptions: SelectedMember[] = React.useMemo(() => { + return membersData.members + ? membersData.members.map((member: MemberEntity, index: number) => { + const tag = + member.metadata.etag ?? + `${member.metadata.name}-${member.kind}-${index}`; + return { + id: tag, + label: member.spec?.profile?.displayName ?? member.metadata.name, + description: getDescription(member), + etag: tag, + type: member.kind, + namespace: member.metadata.namespace, + members: getMembersCount(member), + ref: stringifyEntityRef(member), + }; + }) + : ([] as SelectedMember[]); + }, [membersData.members]); + + const filteredMembers = React.useMemo(() => { + if (search) { + return membersOptions + .filter(m => + m.label + .toLocaleLowerCase('en-US') + .includes(search.toLocaleLowerCase('en-US')), + ) + .slice(0, 99); + } + + return membersOptions.slice(0, 99); + }, [membersOptions, search]); + + return ( + <> + + Search and select users and groups to be added. Selected users and + groups will appear in the members table. + +
+ option.label ?? ''} + getOptionSelected={(option: SelectedMember, value: SelectedMember) => + value.etag + ? option.etag === value.etag + : selectedMember.etag === value.etag + } + loading={membersData.loading} + loadingText={} + disableClearable + value={selectedMember} + onChange={(_e, value: SelectedMember) => { + setSelectedMember(value); + if (value) { + setSearch(value.label); + setFieldValue('selectedMembers', [...selectedMembers, value]); + } + }} + inputValue={search} + onInputChange={(_e, newSearch: string, reason) => + reason === 'input' && setSearch(newSearch) + } + getOptionDisabled={(option: SelectedMember) => + !!selectedMembers.find( + (sm: SelectedMember) => sm.etag === option.etag, + ) + } + renderOption={(option: SelectedMember, state) => ( + + )} + noOptionsText="No users and groups found." + clearOnEscape + renderInput={params => ( + + )} + /> +
+ {membersData.error?.message && ( + + {`Error fetching user and groups: ${membersData.error.message}`} + + )} + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddedMembersTable.test.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddedMembersTable.test.tsx new file mode 100644 index 0000000000..7b0b97c888 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddedMembersTable.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { Table } from '@backstage/core-components'; + +import { render, screen } from '@testing-library/react'; + +import { AddedMembersTable } from './AddedMembersTable'; +import { SelectedMember } from './types'; + +const setFieldValueMock = jest.fn(); + +const selectedMembers: SelectedMember[] = [ + { + id: 'test-1', + label: 'User 1', + etag: 'etag-1', + type: 'User', + ref: 'user:default/user-1', + }, + { + id: 'test-2', + label: 'Group 1', + etag: 'etag-2', + type: 'Group', + ref: 'group:default/test-2', + }, +]; + +jest.mock('@backstage/core-components'); +const mockedTable = Table as jest.MockedFunction; +mockedTable.mockImplementation( + jest.requireActual('@backstage/core-components').Table, +); + +describe('AddedMembersTable component', () => { + it('renders with empty content when no selected members', () => { + render( + , + ); + + expect(screen.queryByText('Users and groups')).toBeInTheDocument(); + expect( + screen.queryByText('No records. Selected users and groups appear here.'), + ).toBeInTheDocument(); + }); + + it('renders with selected members and correct title', () => { + render( + , + ); + + expect(mockedTable).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Users and groups (1 user, 1 group)', + data: selectedMembers, + }), + expect.anything(), + ); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddedMembersTable.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddedMembersTable.tsx new file mode 100644 index 0000000000..fbe94e1e16 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddedMembersTable.tsx @@ -0,0 +1,65 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { Table } from '@backstage/core-components'; + +import { makeStyles } from '@material-ui/core'; +import { FormikErrors } from 'formik'; + +import { getMembers } from '../../utils/rbac-utils'; +import { selectedMembersColumns } from './AddedMembersTableColumn'; +import { RoleFormValues, SelectedMember } from './types'; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); + +type AddedMembersTableProps = { + selectedMembers: SelectedMember[]; + setFieldValue: ( + field: string, + value: any, + shouldValidate?: boolean, + ) => Promise> | Promise; +}; + +export const AddedMembersTable = ({ + selectedMembers, + setFieldValue, +}: AddedMembersTableProps) => { + const classes = useStyles(); + return ( + 0 + ? `Users and groups (${getMembers(selectedMembers)})` + : 'Users and groups' + } + data={selectedMembers} + columns={selectedMembersColumns(selectedMembers, setFieldValue)} + emptyContent={ +
+ No records. Selected users and groups appear here. +
+ } + /> + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddedMembersTableColumn.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddedMembersTableColumn.tsx new file mode 100644 index 0000000000..1ceec99ca9 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/AddedMembersTableColumn.tsx @@ -0,0 +1,109 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { parseEntityRef } from '@backstage/catalog-model'; +import { Link, TableColumn } from '@backstage/core-components'; + +import { Box, IconButton } from '@material-ui/core'; +import Delete from '@mui/icons-material/Delete'; +import { FormikErrors } from 'formik'; + +import { RoleFormValues, SelectedMember } from './types'; + +export const reviewStepMemebersTableColumns = () => [ + { + title: 'Name', + field: 'label', + type: 'string', + }, + { + title: 'Type', + field: 'type', + type: 'string', + }, + { + title: 'Members', + field: 'members', + type: 'numeric', + align: 'left', + render: (mem: number) => { + if (mem || mem === 0) return mem; + return '-'; + }, + }, +]; + +export const selectedMembersColumns = ( + selectedMembers: SelectedMember[], + setFieldValue: ( + field: string, + value: any, + shouldValidate?: boolean, + ) => Promise> | Promise, +): TableColumn[] => { + const onRemove = (etag: string) => { + const updatedMembers = selectedMembers.filter( + (mem: SelectedMember) => mem.etag !== etag, + ); + setFieldValue('selectedMembers', updatedMembers); + }; + + return [ + { + title: 'Name', + field: 'label', + type: 'string', + render: props => { + const { kind, namespace, name } = parseEntityRef(props.ref); + return ( + + {props.label} + + ); + }, + }, + { + title: 'Type', + field: 'type', + type: 'string', + }, + { + title: 'Members', + field: 'members', + type: 'numeric', + align: 'left', + emptyValue: '-', + }, + { + title: 'Actions', + sorting: false, + render: (mem: SelectedMember) => { + return ( + + onRemove(mem.etag)} + aria-label="Remove" + title="Remove member" + > + + + + ); + }, + }, + ]; +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/CreateRolePage.test.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/CreateRolePage.test.tsx new file mode 100644 index 0000000000..1b380c9180 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/CreateRolePage.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useAsync } from 'react-use'; + +import { Content, Header, Page } from '@backstage/core-components'; + +import { render, screen } from '@testing-library/react'; + +import { mockMembers } from '../../__fixtures__/mockMembers'; +import { CreateRolePage } from './CreateRolePage'; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest.fn(), +})); + +jest.mock('react-use', () => ({ + useAsync: jest.fn(), +})); + +jest.mock('./RoleForm', () => ({ + RoleForm: () =>
RoleForm
, +})); + +jest.mock('@backstage/core-components', () => ({ + Page: jest.fn().mockImplementation(({ children }) => ( +
+ Create Role Page + {children} +
+ )), + Header: jest.fn().mockImplementation(({ children }) =>
{children}
), + Content: jest + .fn() + .mockImplementation(({ children }) =>
{children}
), +})); +const mockedPage = Page as jest.MockedFunction; +const mockedHeader = Header as jest.MockedFunction; +const mockedContent = Content as jest.MockedFunction; + +const useAsyncMock = useAsync as jest.MockedFunction; + +beforeEach(() => { + jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest.fn().mockReturnValue({ + getMembers: jest.fn().mockReturnValue(mockMembers), + }), + })); +}); + +describe('CreateRolePage', () => { + it('renders the RoleForm component', async () => { + useAsyncMock.mockReturnValueOnce({ + loading: false, + value: mockMembers, + }); + + render(); + expect(mockedPage).toHaveBeenCalled(); + expect(mockedHeader).toHaveBeenCalled(); + expect(mockedContent).toHaveBeenCalled(); + expect(screen.queryByText('Create Role Page')).toBeInTheDocument(); + expect(screen.queryByText('RoleForm')).toBeInTheDocument(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/CreateRolePage.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/CreateRolePage.tsx new file mode 100644 index 0000000000..bbb410593b --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/CreateRolePage.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useAsync } from 'react-use'; + +import { + Content, + ErrorPage, + Header, + Page, + Progress, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; + +import { rbacApiRef } from '../../api/RBACBackendClient'; +import { MemberEntity } from '../../types'; +import { initialPermissionPolicyRowValue } from './const'; +import { RoleForm } from './RoleForm'; +import { RoleFormValues } from './types'; + +export const CreateRolePage = () => { + const rbacApi = useApi(rbacApiRef); + const { + loading: membersLoading, + value: members, + error: membersError, + } = useAsync(async () => { + return await rbacApi.getMembers(); + }); + + const canReadUsersAndGroups = + !membersLoading && + !membersError && + Array.isArray(members) && + members.length > 0; + + const initialValues: RoleFormValues = { + name: '', + namespace: 'default', + kind: 'role', + description: '', + selectedMembers: [], + permissionPoliciesRows: [initialPermissionPolicyRowValue], + }; + + if (membersLoading) { + return ; + } + + return canReadUsersAndGroups ? ( + +
+ + + + + ) : ( + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/EditRolePage.test.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/EditRolePage.test.tsx new file mode 100644 index 0000000000..11dfdbcf69 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/EditRolePage.test.tsx @@ -0,0 +1,161 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { render, screen } from '@testing-library/react'; + +import { usePermissionPolicies } from '../../hooks/usePermissionPolicies'; +import { useSelectedMembers } from '../../hooks/useSelectedMembers'; +import { PermissionsData } from '../../types'; +import { EditRolePage } from './EditRolePage'; + +const usePermissionPoliciesMockData: PermissionsData[] = [ + { + permission: 'policy-entity', + plugin: 'permission', + policyString: ['Read', ', Create', ', Delete'], + policies: [ + { + policy: 'read', + effect: 'allow', + }, + { + policy: 'create', + effect: 'allow', + }, + { + policy: 'delete', + effect: 'allow', + }, + ], + }, +]; + +jest.mock('react-router-dom', () => ({ + useParams: jest.fn(), +})); +jest.mock('@backstage/core-components', () => ({ + useQueryParamState: jest.fn(), +})); +jest.mock('../../hooks/useSelectedMembers', () => ({ + useSelectedMembers: jest.fn(), +})); +jest.mock('../../hooks/usePermissionPolicies', () => ({ + usePermissionPolicies: jest.fn(), +})); + +jest.mock('./RoleForm', () => ({ + RoleForm: () =>
RoleForm
, +})); + +jest.mock('@backstage/core-components', () => ({ + useQueryParamState: () => ['roleName', jest.fn()], + Progress: () =>
MockedProgressComponent
, + ErrorPage: () =>
MockedErrorPageComponent
, + Page: jest.fn().mockImplementation(({ children }) => ( +
+ Edit Role Page + {children} +
+ )), + Header: jest.fn().mockImplementation(({ title, type, children }) => ( +
+ {`${title} ${type}`} + {children} +
+ )), + Content: jest + .fn() + .mockImplementation(({ children }) =>
{children}
), +})); + +const useParamsMock = useParams as jest.MockedFunction; +const useSelectedMembersMock = useSelectedMembers as jest.MockedFunction< + typeof useSelectedMembers +>; +const usePermissionPoliciesMock = usePermissionPolicies as jest.MockedFunction< + typeof usePermissionPolicies +>; + +beforeEach(() => { + useParamsMock.mockReturnValue({ + roleName: 'testRole', + roleNamespace: 'testNamespace', + roleKind: 'testKind', + }); + + usePermissionPoliciesMock.mockReturnValue({ + loading: false, + data: usePermissionPoliciesMockData, + retry: { + policiesRetry: jest.fn(), + permissionPoliciesRetry: jest.fn(), + conditionalPoliciesRetry: jest.fn(), + }, + error: new Error(''), + }); + + usePermissionPoliciesMock.mockClear(); +}); + +describe('EditRolePage', () => { + it('renders without crashing', () => { + useSelectedMembersMock.mockReturnValue({ + selectedMembers: [], + members: [], + role: { + name: 'testRole', + memberReferences: [], + }, + loading: false, + roleError: { name: '', message: '' }, + membersError: new Error(''), + canReadUsersAndGroups: true, + }); + render(); + expect(screen.getByText('Edit Role Page')).toBeInTheDocument(); + expect(screen.queryByText('RoleForm')).toBeInTheDocument(); + }); + + it('shows progress indicator when loading', () => { + useSelectedMembersMock.mockReturnValueOnce({ + loading: true, + members: [], + selectedMembers: [], + role: undefined, + membersError: { name: '', message: '' }, + roleError: { name: '', message: '' }, + canReadUsersAndGroups: true, + }); + render(); + expect(screen.queryByText('MockedProgressComponent')).toBeInTheDocument(); + }); + + it('shows error page when there is a role error', () => { + useSelectedMembersMock.mockReturnValueOnce({ + roleError: { name: 'Error', message: 'Error Message' }, + members: [], + selectedMembers: [], + role: undefined, + membersError: { name: 'Error', message: 'Error Message' }, + loading: false, + canReadUsersAndGroups: true, + }); + render(); + expect(screen.queryByText('MockedErrorPageComponent')).toBeInTheDocument(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/EditRolePage.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/EditRolePage.tsx new file mode 100644 index 0000000000..f0f1e4e1a8 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/EditRolePage.tsx @@ -0,0 +1,97 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { + Content, + ErrorPage, + Header, + Page, + Progress, + useQueryParamState, +} from '@backstage/core-components'; + +import { usePermissionPolicies } from '../../hooks/usePermissionPolicies'; +import { useSelectedMembers } from '../../hooks/useSelectedMembers'; +import { RoleForm } from './RoleForm'; +import { RoleFormValues } from './types'; + +export const EditRolePage = () => { + const { roleName, roleNamespace, roleKind } = useParams(); + const [queryParamState] = useQueryParamState('activeStep'); + const { + selectedMembers, + members, + role, + loading: loadingMembers, + roleError, + membersError, + canReadUsersAndGroups, + } = useSelectedMembers( + roleName ? `${roleKind}:${roleNamespace}/${roleName}` : '', + ); + + const { data, loading: loadingPolicies } = usePermissionPolicies( + `${roleKind}:${roleNamespace}/${roleName}`, + ); + + const initialValues: RoleFormValues = { + name: roleName || '', + namespace: roleNamespace || 'default', + kind: roleKind || 'role', + description: role?.metadata?.description ?? '', + selectedMembers, + permissionPoliciesRows: data, + }; + + if (loadingMembers || loadingPolicies) { + return ; + } + if (roleError.name) { + return ( + + ); + } + if (!canReadUsersAndGroups) { + return ; + } + + return ( + +
+ + + + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/MembersDropdownOption.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/MembersDropdownOption.tsx new file mode 100644 index 0000000000..7d00f692e2 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/MembersDropdownOption.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { Box, makeStyles } from '@material-ui/core'; +import { AutocompleteRenderOptionState } from '@material-ui/lab/Autocomplete'; +import Typography from '@mui/material/Typography'; +import match from 'autosuggest-highlight/match'; +import parse from 'autosuggest-highlight/parse'; + +import { SelectedMember } from './types'; + +type MembersDropdownOptionProps = { + option: SelectedMember; + state: AutocompleteRenderOptionState; +}; + +const useStyles = makeStyles(theme => ({ + optionLabel: { + color: theme.palette.text.primary, + }, + optionDescription: { + color: theme.palette.text.secondary, + }, +})); + +export const MembersDropdownOption = ({ + option, + state, +}: MembersDropdownOptionProps) => { + const classes = useStyles(); + const { inputValue } = state; + const { label, etag } = option; + const matches = match(label, inputValue, { insideWords: true }); + const parts = parse(label, matches); + + return ( + + {parts.map(part => ( + + {part.text} + + ))} +
+ + {option.description} + {' '} +
+ ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/PermissionPoliciesForm.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/PermissionPoliciesForm.tsx new file mode 100644 index 0000000000..5b106aa24e --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/PermissionPoliciesForm.tsx @@ -0,0 +1,241 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useAsync } from 'react-use'; + +import { Progress } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; + +import { makeStyles } from '@material-ui/core'; +import Button from '@material-ui/core/Button'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import AddIcon from '@mui/icons-material/Add'; +import { FormikErrors } from 'formik'; + +import { rbacApiRef } from '../../api/RBACBackendClient'; +import { useConditionRules } from '../../hooks/useConditionRules'; +import { PermissionsData } from '../../types'; +import { getPluginsPermissionPoliciesData } from '../../utils/create-role-utils'; +import { ConditionsData } from '../ConditionalAccess/types'; +import { initialPermissionPolicyRowValue } from './const'; +import { PermissionPoliciesFormRow } from './PermissionPoliciesFormRow'; +import { RoleFormValues } from './types'; + +const useStyles = makeStyles(theme => ({ + permissionPoliciesForm: { + padding: '20px', + border: `2px solid ${theme.palette.border}`, + borderRadius: '5px', + }, + addButton: { + color: theme.palette.primary.light, + }, +})); + +type PermissionPoliciesFormProps = { + permissionPoliciesRows: PermissionsData[]; + permissionPoliciesRowsError: FormikErrors[]; + setFieldValue: ( + field: string, + value: any, + shouldValidate?: boolean, + ) => Promise> | Promise; + setFieldError: (field: string, value: string | undefined) => void; + handleBlur: React.FocusEventHandler; +}; + +export const PermissionPoliciesForm = ({ + permissionPoliciesRows, + permissionPoliciesRowsError, + setFieldValue, + setFieldError, + handleBlur, +}: PermissionPoliciesFormProps) => { + const classes = useStyles(); + const rbacApi = useApi(rbacApiRef); + const conditionRules = useConditionRules(); + + const { + value: permissionPolicies, + loading: permissionPoliciesLoading, + error: permissionPoliciesErr, + } = useAsync(async () => { + return await rbacApi.listPermissions(); + }); + + const permissionPoliciesData = + !permissionPoliciesLoading && Array.isArray(permissionPolicies) + ? getPluginsPermissionPoliciesData(permissionPolicies) + : undefined; + + const onChangePlugin = (plugin: string, index: number) => { + setFieldValue(`permissionPoliciesRows[${index}].plugin`, plugin, true); + setFieldValue(`permissionPoliciesRows[${index}].permission`, '', false); + setFieldValue(`permissionPoliciesRows[${index}].isResourced`, false, false); + setFieldValue( + `permissionPoliciesRows[${index}].conditions`, + undefined, + false, + ); + setFieldValue( + `permissionPoliciesRows[${index}].policies`, + initialPermissionPolicyRowValue.policies, + false, + ); + }; + + const onChangePermission = ( + permission: string, + index: number, + isResourced: boolean, + policies?: string[], + ) => { + setFieldValue( + `permissionPoliciesRows[${index}].permission`, + permission, + true, + ); + setFieldValue( + `permissionPoliciesRows[${index}].isResourced`, + isResourced, + false, + ); + setFieldValue( + `permissionPoliciesRows[${index}].conditions`, + undefined, + false, + ); + setFieldValue( + `permissionPoliciesRows[${index}].policies`, + policies + ? policies.map(p => ({ policy: p, effect: 'allow' })) + : initialPermissionPolicyRowValue.policies, + false, + ); + }; + + const onChangePolicy = ( + isChecked: boolean, + policyIndex: number, + index: number, + ) => { + setFieldValue( + `permissionPoliciesRows[${index}].policies[${policyIndex}].effect`, + isChecked ? 'allow' : 'deny', + true, + ); + }; + + const onAddConditions = (index: number, conditions?: ConditionsData) => { + setFieldValue(`permissionPoliciesRows[${index}].conditions`, conditions); + if (!conditions) + setFieldValue(`permissionPoliciesRows[${index}].id`, undefined); + }; + + const onRowRemove = (index: number) => { + const finalPps = permissionPoliciesRows.filter( + (_pp, ppIndex) => index !== ppIndex, + ); + setFieldError(`permissionPoliciesRows[${index}]`, undefined); + setFieldValue('permissionPoliciesRows', finalPps, false); + }; + + const onRowAdd = () => + setFieldValue( + 'permissionPoliciesRows', + [...permissionPoliciesRows, initialPermissionPolicyRowValue], + false, + ); + + return ( +
+ + Permission policies can be selected for each plugin. You can add + multiple permission policies using +Add option. + +
+ {permissionPoliciesLoading ? ( + + ) : ( +
+ {permissionPoliciesRows.map((pp, index) => ( + onChangePlugin(plugin, index)} + onChangePermission={( + permission: string, + isResourced: boolean, + policies?: string[], + ) => onChangePermission(permission, index, isResourced, policies)} + onChangePolicy={(isChecked: boolean, policyIndex: number) => + onChangePolicy(isChecked, policyIndex, index) + } + onAddConditions={(conditions?: ConditionsData) => + onAddConditions(index, conditions) + } + onRemove={() => onRowRemove(index)} + handleBlur={handleBlur} + getPermissionDisabled={(permission: string) => { + const pluginPermissionPolicies = permissionPoliciesRows.filter( + ppr => ppr.plugin === pp.plugin, + ); + const previouslySelectedPermission = + !!pluginPermissionPolicies.find( + ppp => ppp.permission === permission, + ); + return ( + previouslySelectedPermission && + !permissionPoliciesData?.pluginsPermissions[pp.plugin] + ?.policies[permission ?? '']?.isResourced + ); + }} + /> + ))} + +
+ )} + {!permissionPoliciesLoading && + (permissionPoliciesErr?.message || + !Array.isArray(permissionPolicies)) && ( + <> +
+ + {`Error fetching the permission policies: ${ + permissionPoliciesErr?.message || + (permissionPolicies as Response)?.statusText + }`} + + + )} +
+ ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/PermissionPoliciesFormRow.test.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/PermissionPoliciesFormRow.test.tsx new file mode 100644 index 0000000000..e789490d50 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/PermissionPoliciesFormRow.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { fireEvent, render, screen, within } from '@testing-library/react'; + +import '@testing-library/jest-dom'; + +import { mockTransformedConditionRules } from '../../__fixtures__/mockTransformedConditionRules'; +import { PermissionPoliciesFormRow } from './PermissionPoliciesFormRow'; + +jest.mock('@material-ui/core', () => ({ + ...jest.requireActual('@material-ui/core'), + makeStyles: jest.fn().mockReturnValue(() => ({})), +})); + +describe('PermissionPoliciesFormRow', () => { + const mockProps = { + permissionPoliciesRowData: { + plugin: '', + permission: '', + policies: [], + isResourced: false, + }, + permissionPoliciesData: { + plugins: ['', 'Plugin1', 'Plugin2'], + pluginsPermissions: { + Plugin1: { + permissions: ['Permission1', 'Permission2'], + policies: {}, + }, + Plugin2: { + permissions: ['Permission1', 'Permission2'], + policies: {}, + }, + }, + }, + permissionPoliciesRowError: { plugin: '', permission: '' }, + rowCount: 2, + rowName: 'testRow', + conditionRules: { data: mockTransformedConditionRules }, + onRemove: jest.fn(), + onChangePlugin: jest.fn(), + onChangePermission: jest.fn(), + onChangePolicy: jest.fn(), + handleBlur: jest.fn(), + getPermissionDisabled: jest.fn().mockReturnValue(false), + onAddConditions: jest.fn(), + }; + + it('renders without crashing', () => { + render(); + expect( + screen.getByRole('textbox', { + name: /plugin/i, + }), + ).toBeInTheDocument(); + expect( + screen.getByRole('textbox', { + name: /resource type/i, + }), + ).toBeInTheDocument(); + }); + + it('calls onRemove when remove button is clicked', () => { + const { getByTitle } = render(); + const removeButton = getByTitle('Remove'); + fireEvent.click(removeButton); + expect(mockProps.onRemove).toHaveBeenCalled(); + }); + + it('calls onChangePlugin when a plugin is selected', async () => { + const { getByLabelText, findByText } = render( + , + ); + + fireEvent.mouseDown(getByLabelText(/Plugin/)); + const pluginOption = await findByText('Plugin1'); + const listboxElement = pluginOption.closest('ul'); + + if (!listboxElement) { + throw new Error('Unable to find the listbox element.'); + } + + const listbox = within(listboxElement); + fireEvent.click(listbox.getByText(/Plugin1/)); + + expect(mockProps.onChangePlugin).toHaveBeenCalledWith('Plugin1'); + }); + + it('opens sidebar on clicking conditional access button', async () => { + const newMockProps = { + ...mockProps, + permissionPoliciesRowData: { + plugin: 'catalog', + permission: 'catalog-entity', + isResourced: true, + policies: [], + }, + }; + + const { queryByTestId } = render( + , + ); + + expect(queryByTestId('rules-sidebar')).not.toBeInTheDocument(); + fireEvent.click(screen.getByLabelText('configure-access')); + expect(queryByTestId('rules-sidebar')).toBeInTheDocument(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/PermissionPoliciesFormRow.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/PermissionPoliciesFormRow.tsx new file mode 100644 index 0000000000..871245df86 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/PermissionPoliciesFormRow.tsx @@ -0,0 +1,251 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { FormLabel, makeStyles } from '@material-ui/core'; +import IconButton from '@material-ui/core/IconButton'; +import TextField from '@material-ui/core/TextField'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import ChecklistRtlIcon from '@mui/icons-material/ChecklistRtl'; +import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; +import RemoveIcon from '@mui/icons-material/Remove'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import { FormikErrors } from 'formik'; + +import { PermissionsData } from '../../types'; +import { getRulesNumber } from '../../utils/create-role-utils'; +import { ConditionalAccessSidebar } from '../ConditionalAccess/ConditionalAccessSidebar'; +import { ConditionRules, ConditionsData } from '../ConditionalAccess/types'; +import { PoliciesCheckboxGroup } from './PoliciesCheckboxGroup'; +import { PluginsPermissionPoliciesData } from './types'; + +const useStyles = makeStyles(theme => ({ + removeButton: { + color: theme.palette.grey[500], + flexGrow: 0, + alignSelf: 'center', + }, + conditionalAccessButton: { + fontSize: theme.typography.fontSize, + }, +})); + +type PermissionPoliciesFormRowProps = { + permissionPoliciesRowData: PermissionsData; + permissionPoliciesData?: PluginsPermissionPoliciesData; + permissionPoliciesRowError: FormikErrors; + rowCount: number; + rowName: string; + conditionRules: ConditionRules; + onRemove: () => void; + onChangePlugin: (plugin: string) => void; + onChangePermission: ( + permission: string, + isResourced: boolean, + policies?: string[], + ) => void; + onChangePolicy: (isChecked: boolean, policyIndex: number) => void; + handleBlur: React.FocusEventHandler; + getPermissionDisabled: (permission: string) => boolean; + onAddConditions: (conditions?: ConditionsData) => void; +}; + +export const PermissionPoliciesFormRow = ({ + permissionPoliciesRowData, + permissionPoliciesData, + permissionPoliciesRowError, + rowCount, + rowName, + conditionRules, + onRemove, + onChangePermission, + onChangePolicy, + onChangePlugin, + handleBlur, + getPermissionDisabled, + onAddConditions, +}: PermissionPoliciesFormRowProps) => { + const classes = useStyles(); + const { plugin: pluginError, permission: permissionError } = + permissionPoliciesRowError; + const { data: conditionRulesData, error: conditionRulesError } = + conditionRules; + const totalRules = getRulesNumber(permissionPoliciesRowData.conditions); + + const [sidebarOpen, setSidebarOpen] = React.useState(false); + + const tooltipTitle = () => ( +
+ + Define access conditions for the selected resource type using Rules. + Rules vary by resource type.{' '} + Users have access to the resource type content by default unless + configured otherwise. + +
+ ); + + const getTotalRules = (): string => { + let accessMessage = 'Configure access'; + + if (totalRules > 0) { + accessMessage += ` (${totalRules} ${totalRules > 1 ? 'rules' : 'rule'})`; + } + return accessMessage; + }; + + return ( +
+
+ + What can users/groups access? + +
+ { + onChangePlugin(value ?? ''); + }} + renderInput={(params: any) => ( + + )} + /> + + onChangePermission( + value ?? '', + permissionPoliciesData?.pluginsPermissions?.[ + permissionPoliciesRowData.plugin + ]?.policies[value ?? '']?.isResourced ?? false, + value + ? permissionPoliciesData?.pluginsPermissions?.[ + permissionPoliciesRowData.plugin + ]?.policies?.[value].policies + : undefined, + ) + } + getOptionDisabled={getPermissionDisabled} + getOptionLabel={option => option || ''} + renderInput={(params: any) => ( + + )} + /> +
+ {permissionPoliciesRowData.isResourced && + !!conditionRulesData?.[`${permissionPoliciesRowData.plugin}`]?.[ + `${permissionPoliciesRowData.permission}` + ]?.rules.length && ( + + )} +
+ onRemove()} + disabled={rowCount === 1} + data-testid={`${rowName}-remove`} + > + + +
+
+ + { + setSidebarOpen(false); + }} + onSave={(conditions?: ConditionsData) => { + onAddConditions(conditions); + setSidebarOpen(false); + }} + conditionsFormVal={permissionPoliciesRowData.conditions} + selPluginResourceType={permissionPoliciesRowData.permission} + conditionRulesData={ + conditionRulesData?.[`${permissionPoliciesRowData.plugin}`]?.[ + `${permissionPoliciesRowData.permission}` + ] + } + /> +
+ ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/PoliciesCheckboxGroup.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/PoliciesCheckboxGroup.tsx new file mode 100644 index 0000000000..4548a94aee --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/PoliciesCheckboxGroup.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import Checkbox from '@material-ui/core/Checkbox'; +import FormControl from '@material-ui/core/FormControl'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import FormGroup from '@material-ui/core/FormGroup'; +import FormLabel from '@material-ui/core/FormLabel'; + +import { PermissionsData } from '../../types'; +import { RowPolicy } from './types'; + +export const PoliciesCheckboxGroup = ({ + permissionPoliciesRowData, + rowName, + onChangePolicy, +}: { + permissionPoliciesRowData: PermissionsData; + rowName: string; + + onChangePolicy: (isChecked: boolean, policyIndex: number) => void; +}) => { + return ( + + + What actions they can do? + + + {permissionPoliciesRowData.policies.map( + (p: RowPolicy, index: number, self) => { + const labelCheckedArray = self.filter( + val => val.effect === 'allow', + ); + const labelCheckedCount = labelCheckedArray.length; + return ( + onChangePolicy(e.target.checked, index)} + color="primary" + /> + } + /> + ); + }, + )} + + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/ReviewStep.test.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/ReviewStep.test.tsx new file mode 100644 index 0000000000..db8f7c1437 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/ReviewStep.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { render, screen } from '@testing-library/react'; + +import { ReviewStep } from './ReviewStep'; + +import '@testing-library/jest-dom'; + +describe('ReviewStep', () => { + const mockValues = { + name: 'Test Role', + kind: 'user', + namespace: 'testns', + description: 'Test Description', + selectedMembers: [ + { label: 'User 1', etag: 'etag1', type: 'User', ref: 'User One' }, + ], + permissionPoliciesRows: [ + { + plugin: 'policy1', + permission: 'permission1', + policies: [{ policy: 'policy1', effect: 'allow' }], + }, + ], + }; + + it('renders "Review and create" for new roles', () => { + render(); + expect(screen.getByText('Review and create')).toBeInTheDocument(); + }); + + it('renders "Review and save" for editing existing roles', () => { + render(); + expect(screen.getByText('Review and save')).toBeInTheDocument(); + }); + + it('passes the correct metadata to StructuredMetadataTable', () => { + render(); + + expect(screen.getByText('Users and groups (1 user)')).toBeInTheDocument(); + expect(screen.getByText('Permission policies (1)')).toBeInTheDocument(); + expect(screen.getByText(mockValues.name)).toBeInTheDocument(); + expect(screen.getByText(mockValues.description)).toBeInTheDocument(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/ReviewStep.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/ReviewStep.tsx new file mode 100644 index 0000000000..1f0406b8fc --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/ReviewStep.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { StructuredMetadataTable } from '@backstage/core-components'; + +import Typography from '@mui/material/Typography'; + +import { getPermissionsNumber } from '../../utils/create-role-utils'; +import { getMembers } from '../../utils/rbac-utils'; +import { reviewStepMemebersTableColumns } from './AddedMembersTableColumn'; +import { ReviewStepTable } from './ReviewStepTable'; +import { selectedPermissionPoliciesColumn } from './SelectedPermissionPoliciesColumn'; +import { RoleFormValues } from './types'; + +const tableMetadata = (values: RoleFormValues) => { + const membersKey = + values.selectedMembers.length > 0 + ? `Users and groups (${getMembers(values.selectedMembers)})` + : 'Users and groups'; + const permissionPoliciesKey = `Permission policies (${getPermissionsNumber( + values, + )})`; + return { + 'Name and description of role': ( + <> + {values.name} +
+ {values.description} + + ), + [membersKey]: ( + + ), + [permissionPoliciesKey]: ( + + ), + }; +}; + +export const ReviewStep = ({ + values, + isEditing, +}: { + values: RoleFormValues; + isEditing: boolean; +}) => { + return ( +
+ + {isEditing ? 'Review and save' : 'Review and create'} + + key }} + /> +
+ ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/ReviewStepTable.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/ReviewStepTable.tsx new file mode 100644 index 0000000000..a665f75fe9 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/ReviewStepTable.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +export const ReviewStepTable = ({ + columns, + rows, + tableWrapperWidth, +}: { + columns: any[]; + rows: any[]; + tableWrapperWidth: number; +}) => { + return ( +
+
+ + + {columns.map(col => ( + + ))} + + + + {rows.map((row, rowIndex) => ( + + + {columns.map(rowCol => ( + + ))} + + + + ))} + +
+ {col.title} +
+ {rowCol.render + ? rowCol.render(row[rowCol.field]) + : row[rowCol.field] || (rowCol.emptyValue ?? '')} +
+ + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/RoleDetailsForm.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/RoleDetailsForm.tsx new file mode 100644 index 0000000000..e7dde978f0 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/RoleDetailsForm.tsx @@ -0,0 +1,74 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { TextField } from '@material-ui/core'; + +type RoleDetailsFormProps = { + name: string; + description?: string; + nameError?: string; + handleBlur: React.FocusEventHandler; + handleChange: React.ChangeEventHandler< + HTMLTextAreaElement | HTMLInputElement + >; +}; + +export const RoleDetailsForm = ({ + name, + description, + nameError, + handleBlur, + handleChange, +}: RoleDetailsFormProps) => { + return ( +
+ + +
+ ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/RoleForm.test.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/RoleForm.test.tsx new file mode 100644 index 0000000000..fa669a5d3c --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/RoleForm.test.tsx @@ -0,0 +1,411 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { errorApiRef } from '@backstage/core-plugin-api'; +import { translationApiRef } from '@backstage/core-plugin-api/alpha'; +import { MockErrorApi, TestApiProvider } from '@backstage/test-utils'; +import { MockTranslationApi } from '@backstage/test-utils/alpha'; + +import { render, screen } from '@testing-library/react'; +import { useFormik } from 'formik'; + +import { RoleForm } from './RoleForm'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + Link: React.forwardRef< + HTMLAnchorElement, + { to: string; children?: React.ReactNode } + >((props, ref) => ( + + {props.children} + + )), + useNavigate: jest.fn(), +})); + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest.fn(), +})); + +jest.mock('formik', () => ({ + ...jest.requireActual('formik'), + useFormik: jest.fn(), +})); + +const useFormikMock = useFormik as jest.Mock; + +describe('Create RoleForm', () => { + it('renders create role form correctly', async () => { + useFormikMock.mockReturnValue({ + errors: {}, + values: {}, + // mocked useFormik to return formik status with submitError + status: { submitError: '' }, + }); + render( + + + , + ); + + expect( + screen.getByText(/enter name and description of role/i), + ).toBeInTheDocument(); + expect(screen.getByTestId(/role-name/i)).toBeInTheDocument(); + expect(screen.getByTestId(/role-description/i)).toBeInTheDocument(); + expect(screen.getByText(/add users and groups/i)).toBeInTheDocument(); + }); + + it('shows error if there is any error in formik status', async () => { + useFormikMock.mockReturnValue({ + errors: {}, + values: {}, + // mocked useFormik to return formik status with submitError + status: { submitError: 'Unable to create role. Unexpected error' }, + }); + render( + + + , + ); + + expect( + screen.getByText(/Unable to create role. unexpected error/i), + ).toBeInTheDocument(); + }); +}); + +describe('Edit RoleForm', () => { + it('renders edit role form correctly', async () => { + useFormikMock.mockReturnValue({ + errors: {}, + values: {}, + // mocked useFormik to return formik status with submitError + status: { submitError: 'Unexpected error' }, + }); + render( + + + , + ); + + expect( + screen.getByText(/edit name and description of role/i), + ).toBeInTheDocument(); + expect(screen.getByTestId(/role-name/i)).toBeInTheDocument(); + expect(screen.getByTestId(/role-description/i)).toBeInTheDocument(); + expect(screen.getByText(/edit users and groups/i)).toBeInTheDocument(); + }); + + it('renders edit role form correctly with edit users and groups stepper active', async () => { + useFormikMock.mockReturnValue({ + errors: {}, + values: { + selectedMembers: [ + { + ref: 'user:default/janelle.dawe', + label: 'Janelle Dawe', + etag: 'b027e001c70faf091869106d4e9023f7bddb9502', + type: 'User', + namespace: 'default', + }, + ], + }, + // mocked useFormik to return formik status with submitError + status: { submitError: 'Unexpected error' }, + }); + render( + + + , + ); + + expect(screen.getByText(/edit users and groups/i)).toBeInTheDocument(); + expect(screen.getByText(/janelle dawe/i)).toBeInTheDocument(); + }); + + it('shows error if there is any error in formik status', async () => { + useFormikMock.mockReturnValue({ + errors: {}, + values: { + selectedMembers: [ + { + ref: 'user:default/janelle.dawe', + label: 'Janelle Dawe', + etag: 'b027e001c70faf091869106d4e9023f7bddb9502', + type: 'User', + namespace: 'default', + }, + ], + }, + // mocked useFormik to return formik status with submitError + status: { submitError: 'Unable to edit the role. Unexpected error' }, + }); + render( + + + , + ); + + expect( + screen.getByText(/unable to edit the role. unexpected error/i), + ).toBeInTheDocument(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/RoleForm.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/RoleForm.tsx new file mode 100644 index 0000000000..c32ec33d66 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/RoleForm.tsx @@ -0,0 +1,377 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { parseEntityRef } from '@backstage/catalog-model'; +import { SimpleStepper, SimpleStepperStep } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; + +import { + Box, + Button, + Card, + CardContent, + CardHeader, + Divider, + Paper, +} from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import { FormikErrors, FormikHelpers, useFormik } from 'formik'; + +import { rbacApiRef } from '../../api/RBACBackendClient'; +import { MemberEntity, PermissionsData, RoleError } from '../../types'; +import { + getConditionalPermissionPoliciesData, + getNewConditionalPolicies, + getPermissionPoliciesData, + getRemovedConditionalPoliciesIds, + getRoleData, + getUpdatedConditionalPolicies, + validationSchema, +} from '../../utils/create-role-utils'; +import { isSamePermissionPolicy, onlyInLeft } from '../../utils/rbac-utils'; +import { + createConditions, + createPermissions, + modifyConditions, + removeConditions, + removePermissions, +} from '../../utils/role-form-utils'; +import { AddedMembersTable } from './AddedMembersTable'; +import { AddMembersForm } from './AddMembersForm'; +import { PermissionPoliciesForm } from './PermissionPoliciesForm'; +import { ReviewStep } from './ReviewStep'; +import { RoleDetailsForm } from './RoleDetailsForm'; +import { RoleFormValues } from './types'; + +type RoleFormProps = { + membersData: { members: MemberEntity[]; loading: boolean; error: Error }; + titles: { + formTitle: string; + nameAndDescriptionTitle: string; + usersAndGroupsTitle: string; + permissionPoliciesTitle: string; + }; + submitLabel?: string; + roleName?: string; + step?: number; + initialValues: RoleFormValues; +}; + +export const RoleForm = ({ + roleName, + step, + titles, + membersData, + submitLabel, + initialValues, +}: RoleFormProps) => { + const [activeStep, setActiveStep] = React.useState(step || 0); + const navigate = useNavigate(); + const rbacApi = useApi(rbacApiRef); + + const navigateTo = (rName?: string, action?: string) => { + const currentRoleName = rName || roleName; + const stateProp = + currentRoleName && action + ? { + state: { + toastMessage: `Role ${currentRoleName} ${action} successfully`, + }, + } + : undefined; + if (step && currentRoleName) { + const { kind, namespace, name } = parseEntityRef(currentRoleName); + navigate(`../roles/${kind}/${namespace}/${name}`, stateProp); + } else { + navigate('..', stateProp); + } + }; + + const updateRole = async ( + name: string, + values: RoleFormValues, + formikHelpers: FormikHelpers, + ) => { + try { + const newData = getRoleData(values); + const newName = newData.name; + const newPermissionsData = getPermissionPoliciesData(values); + const newConditions = getNewConditionalPolicies(values); + const deleteConditions = getRemovedConditionalPoliciesIds( + values, + initialValues, + ); + const updateConditions = getUpdatedConditionalPolicies( + values, + initialValues, + ); + + const oldData = getRoleData(initialValues); + const res = await rbacApi.updateRole(oldData, newData); + if ((res as RoleError).error) { + throw new Error( + `${'Unable to edit the role. '}${(res as RoleError).error.message}`, + ); + } else { + const oldPermissionsData = getPermissionPoliciesData(initialValues); + const newPermissions = onlyInLeft( + newPermissionsData, + oldPermissionsData, + isSamePermissionPolicy, + ); + const deletePermissions = onlyInLeft( + oldPermissionsData, + newPermissionsData, + isSamePermissionPolicy, + ); + + await removePermissions(name, deletePermissions, rbacApi); + await createPermissions(newPermissions, rbacApi); + + await removeConditions(deleteConditions, rbacApi); + await modifyConditions(updateConditions, rbacApi); + await createConditions(newConditions, rbacApi); + + navigateTo(newName, 'updated'); + } + } catch (e) { + formikHelpers.setStatus({ submitError: e }); + } + }; + + const newRole = async ( + values: RoleFormValues, + formikHelpers: FormikHelpers, + ) => { + try { + const newData = getRoleData(values); + const newPermissionsData = getPermissionPoliciesData(values); + const newConditionalPermissionPoliciesData = + getConditionalPermissionPoliciesData(values); + + const res = await rbacApi.createRole(newData); + if ((res as RoleError).error) { + throw new Error( + `${'Unable to create role. '}${(res as RoleError).error.message}`, + ); + } + + await createPermissions( + newPermissionsData, + rbacApi, + 'Role was created successfully but unable to add permission policies to the role.', + ); + + await createConditions( + newConditionalPermissionPoliciesData, + rbacApi, + 'Role created successfully but unable to add conditions to the role.', + ); + + navigateTo(newData.name, 'created'); + } catch (e) { + formikHelpers.setStatus({ submitError: e }); + } + }; + + const formik = useFormik({ + enableReinitialize: true, + initialValues, + validationSchema: validationSchema, + onSubmit: async ( + values: RoleFormValues, + formikHelpers: FormikHelpers, + ) => { + if (roleName) { + updateRole(roleName, values, formikHelpers); + } else { + newRole(values, formikHelpers); + } + }, + }); + + const validateStepField = (fieldName: string) => { + switch (fieldName) { + case 'name': { + formik.validateField(fieldName); + return formik.errors.name; + } + case 'selectedMembers': { + formik.validateField(fieldName); + return formik.errors.selectedMembers; + } + case 'permissionPoliciesRows': { + formik.values.permissionPoliciesRows.forEach((_pp, index) => { + formik.validateField(`permissionPoliciesRows[${index}].plugin`); + formik.validateField(`permissionPoliciesRows[${index}].permission`); + }); + return formik.errors.permissionPoliciesRows; + } + default: + return undefined; + } + }; + + const handleNext = (fieldName?: string) => { + const error = fieldName && validateStepField(fieldName); + if (!fieldName || !error) { + formik.setErrors({}); + const stepNum = Math.min(activeStep + 1, 3); + setActiveStep(stepNum); + } + }; + + const canNextPermissionPoliciesStep = () => { + return ( + formik.values.permissionPoliciesRows.filter(pp => !!pp.plugin).length === + formik.values.permissionPoliciesRows.length && + (!formik.errors.permissionPoliciesRows || + ( + formik.errors.permissionPoliciesRows as unknown as FormikErrors< + PermissionsData[] + >[] + )?.filter(err => !!err)?.length === 0) + ); + }; + + const handleBack = () => setActiveStep(Math.max(activeStep - 1, 0)); + const handleCancel = () => { + navigateTo(); + }; + + const handleReset = (e: React.MouseEvent) => { + setActiveStep(0); + formik.handleReset(e); + }; + + return ( + + + + + + !!formik.values.name && !formik.errors.name, + onNext: () => handleNext('name'), + }} + > + + + + formik.values.selectedMembers?.length > 0 && + !formik.errors.selectedMembers, + onNext: () => handleNext('selectedMembers'), + showBack: true, + backText: 'Back', + onBack: handleBack, + }} + > + + +
+ +
+
+ canNextPermissionPoliciesStep(), + onNext: () => handleNext('permissionPoliciesRows'), + showBack: true, + backText: 'Back', + onBack: handleBack, + }} + > + [] + } + setFieldValue={formik.setFieldValue} + setFieldError={formik.setFieldError} + handleBlur={formik.handleBlur} + /> + + + + +
+ + + +
+
+
+ {formik.status?.submitError && ( + + {`${formik.status.submitError}`} + + )} + +
+
+ ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/SelectedPermissionPoliciesColumn.tsx b/workspaces/rbac/plugins/rbac/src/components/CreateRole/SelectedPermissionPoliciesColumn.tsx new file mode 100644 index 0000000000..045d2e5dcc --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/SelectedPermissionPoliciesColumn.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { getRulesNumber } from '../../utils/create-role-utils'; +import { ConditionsData } from '../ConditionalAccess/types'; +import { RowPolicy } from './types'; + +export const selectedPermissionPoliciesColumn = () => [ + { + title: 'Plugin', + field: 'plugin', + }, + { + title: 'Permission', + field: 'permission', + }, + { + title: 'Policies', + field: 'policies', + render: (policies: RowPolicy[]) => { + const policyStr = policies.reduce((acc: string, p) => { + if (p.effect === 'allow') return acc.concat(`${p.policy}, `); + return acc; + }, ''); + return policyStr.slice(0, policyStr.length - 2); + }, + }, + { + title: 'Conditional', + field: 'conditions', + render: (conditions: ConditionsData) => { + const totalRules = getRulesNumber(conditions); + return totalRules + ? `${totalRules} ${totalRules > 1 ? 'rules' : 'rule'}` + : '-'; + }, + }, +]; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/const.ts b/workspaces/rbac/plugins/rbac/src/components/CreateRole/const.ts new file mode 100644 index 0000000000..d31ac71980 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/const.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PermissionsData } from '../../types'; + +export const initialPermissionPolicyRowValue: PermissionsData = { + plugin: '', + permission: '', + policies: [ + { policy: 'Create', effect: 'deny' }, + { policy: 'Read', effect: 'deny' }, + { policy: 'Update', effect: 'deny' }, + { policy: 'Delete', effect: 'deny' }, + ], + isResourced: false, +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/CreateRole/types.ts b/workspaces/rbac/plugins/rbac/src/components/CreateRole/types.ts new file mode 100644 index 0000000000..ecafbd8c07 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/CreateRole/types.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PermissionsData } from '../../types'; + +export type SelectedMember = { + id?: string; + label: string; + etag: string; + namespace?: string; + type: string; + members?: number; + description?: string; + ref: string; +}; + +export type RowPolicy = { + policy: string; + effect: string; +}; + +export type RoleFormValues = { + name: string; + namespace: string; + kind: string; + description?: string; + selectedMembers: SelectedMember[]; + permissionPoliciesRows: PermissionsData[]; +}; + +export type PermissionPolicies = { + [permission: string]: { policies: string[]; isResourced: boolean }; +}; + +export type PluginsPermissions = { + [plugin: string]: { + permissions: string[]; + policies: PermissionPolicies; + }; +}; + +export type PluginsPermissionPoliciesData = { + plugins: string[]; + pluginsPermissions: PluginsPermissions; +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/DownloadUserStatistics.tsx b/workspaces/rbac/plugins/rbac/src/components/DownloadUserStatistics.tsx new file mode 100644 index 0000000000..ca07dd407f --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/DownloadUserStatistics.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { makeStyles } from '@material-ui/core'; + +import { licensedUsersApiRef } from '../api/LicensedUsersClient'; + +const useStyles = makeStyles(theme => ({ + linkStyle: { + color: theme.palette.link, + textDecoration: 'underline', + }, +})); + +function DownloadCSVLink() { + const classes = useStyles(); + const licensedUsersClient = useApi(licensedUsersApiRef); + const handleDownload = async ( + event: React.MouseEvent, + ) => { + event.preventDefault(); // Prevent the default link behavior + + try { + const response = await licensedUsersClient.downloadStatistics(); + + if (response.ok) { + // Get the CSV data as a string + const csvData = await response.text(); + + // Create a Blob from the CSV data + const blob = new Blob([csvData], { type: 'text/csv' }); + const url = window.URL.createObjectURL(blob); + + // Create a temporary link to trigger the download + const a = document.createElement('a'); + a.href = url; + a.download = 'licensed-users.csv'; + document.body.appendChild(a); + a.click(); + + // Clean up the temporary link and object URL + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + } else { + throw new Error( + `Failed to download the csv file with list licensed users ${response.statusText}`, + ); + } + } catch (error) { + throw new Error(`Error during the download: ${error}`); + } + }; + + return ( + + Download User List + + ); +} + +export default DownloadCSVLink; diff --git a/workspaces/rbac/plugins/rbac/src/components/EditRole.test.tsx b/workspaces/rbac/plugins/rbac/src/components/EditRole.test.tsx new file mode 100644 index 0000000000..f768705af7 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/EditRole.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; + +import { render, screen } from '@testing-library/react'; + +import '@testing-library/jest-dom'; + +import EditRole from './EditRole'; + +jest.mock('@backstage/catalog-model', () => ({ + ...jest.requireActual('@backstage/catalog-model'), + parseEntityRef: jest.fn().mockReturnValue({ + name: 'roleName', + namespace: 'default', + kind: 'Role', + }), +})); + +describe('EditRole', () => { + it('renders the button as disabled when disable is true', () => { + render( + + + , + ); + + expect(screen.getByRole('button', { name: 'Update' })).toHaveAttribute( + 'aria-disabled', + 'true', + ); + }); + + it('renders the button with correct tooltip and enabled state', () => { + const tooltipText = 'Edit Role Tooltip'; + render( + + + , + ); + + expect(screen.getByTestId('edit-role-btn')).toHaveAttribute( + 'title', + tooltipText, + ); + expect(screen.getByRole('button', { name: 'Update' })).toHaveAttribute( + 'aria-disabled', + 'false', + ); + }); + + it('sets the correct link path when "to" prop is provided', () => { + const toPath = '/custom/path'; + render( + + + , + ); + + expect(screen.getByRole('button')).toHaveAttribute('href', toPath); + }); + + it('sets the correct default link path based on roleName', () => { + render( + + + , + ); + + expect(screen.getByRole('button')).toHaveAttribute( + 'href', + expect.stringContaining('/role/Role/default/roleName'), + ); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/EditRole.tsx b/workspaces/rbac/plugins/rbac/src/components/EditRole.tsx new file mode 100644 index 0000000000..417e85d10b --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/EditRole.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { parseEntityRef } from '@backstage/catalog-model'; +import { Link } from '@backstage/core-components'; + +import { IconButton, Tooltip, Typography } from '@material-ui/core'; +import EditIcon from '@material-ui/icons/Edit'; + +type EditRoleProps = { + roleName: string; + disable: boolean; + tooltip?: string; + dataTestId: string; + to?: string; +}; + +const EditRole = ({ + roleName, + tooltip, + disable, + dataTestId, + to, +}: EditRoleProps) => { + const { name, namespace, kind } = parseEntityRef(roleName); + return ( + + + + + + + + ); +}; + +export default EditRole; diff --git a/workspaces/rbac/plugins/rbac/src/components/RbacPage.test.tsx b/workspaces/rbac/plugins/rbac/src/components/RbacPage.test.tsx new file mode 100644 index 0000000000..8b76c1805c --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RbacPage.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { + RequirePermission, + usePermission, +} from '@backstage/plugin-permission-react'; +import { renderInTestApp } from '@backstage/test-utils'; + +import { screen } from '@testing-library/react'; + +import { useCheckIfLicensePluginEnabled } from '../hooks/useCheckIfLicensePluginEnabled'; +import { useRoles } from '../hooks/useRoles'; +import { RbacPage } from './RbacPage'; + +jest.mock('@backstage/plugin-permission-react', () => ({ + usePermission: jest.fn(), + RequirePermission: jest.fn(), +})); + +jest.mock('../hooks/useRoles', () => ({ + useRoles: jest.fn(), +})); + +jest.mock('../hooks/useCheckIfLicensePluginEnabled', () => ({ + useCheckIfLicensePluginEnabled: jest.fn(), +})); + +const mockUsePermission = usePermission as jest.MockedFunction< + typeof usePermission +>; + +const mockUseRoles = useRoles as jest.MockedFunction; + +const mockUseCheckIfLicensePluginEnabled = + useCheckIfLicensePluginEnabled as jest.MockedFunction< + typeof useCheckIfLicensePluginEnabled + >; + +const RequirePermissionMock = RequirePermission as jest.MockedFunction< + typeof RequirePermission +>; + +describe('RbacPage', () => { + it('should render if authorized', async () => { + RequirePermissionMock.mockImplementation(props => <>{props.children}); + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockUseRoles.mockReturnValue({ + loading: true, + data: [], + error: { + rolesError: '', + policiesError: '', + roleConditionError: '', + }, + retry: { roleRetry: jest.fn(), policiesRetry: jest.fn() }, + createRoleAllowed: false, + createRoleLoading: false, + }); + mockUseCheckIfLicensePluginEnabled.mockReturnValue({ + loading: false, + isEnabled: false, + licenseCheckError: { + message: '', + name: '', + }, + }); + await renderInTestApp(); + expect(screen.getByText('RBAC')).toBeInTheDocument(); + }); + + it('should not render if not authorized', async () => { + RequirePermissionMock.mockImplementation(_props => <>Not Found); + mockUsePermission.mockReturnValue({ loading: false, allowed: false }); + + await renderInTestApp(); + expect(screen.getByText('Not Found')).toBeInTheDocument(); + }); + + it('should not render if loading', async () => { + RequirePermissionMock.mockImplementation(_props => null); + mockUsePermission.mockReturnValue({ loading: false, allowed: false }); + + const { queryByText } = await renderInTestApp(); + expect(queryByText('Not Found')).not.toBeInTheDocument(); + expect(queryByText('RBAC')).not.toBeInTheDocument(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/RbacPage.tsx b/workspaces/rbac/plugins/rbac/src/components/RbacPage.tsx new file mode 100644 index 0000000000..6a00f914a3 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RbacPage.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { Content, Header, Page } from '@backstage/core-components'; +import { RequirePermission } from '@backstage/plugin-permission-react'; + +import { DeleteDialogContextProvider } from '@janus-idp/shared-react'; + +import { policyEntityReadPermission } from '@backstage-community/plugin-rbac-common'; + +import { RolesList } from './RolesList/RolesList'; + +export const RbacPage = ({ useHeader = true }: { useHeader?: boolean }) => ( + + + {useHeader &&
} + + + + + + + +); diff --git a/workspaces/rbac/plugins/rbac/src/components/RoleOverview/AboutCard.test.tsx b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/AboutCard.test.tsx new file mode 100644 index 0000000000..450c56449c --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/AboutCard.test.tsx @@ -0,0 +1,104 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { renderInTestApp } from '@backstage/test-utils'; + +import { Role } from '@backstage-community/plugin-rbac-common'; + +import { useRole } from '../../hooks/useRole'; +import { AboutCard } from './AboutCard'; + +jest.mock('../../hooks/useRole', () => ({ + useRole: jest.fn(), +})); + +const mockRole: Role = { + name: 'role:default/rbac-admin', + memberReferences: ['user:default/tom', 'group:default/performance-dev-team'], + metadata: { + source: 'rest', + description: 'performance dev team', + lastModified: '2024-04-04T14:25:53.000Z', + modifiedBy: 'user:default/tim', + }, +}; + +const mockRoleWithoutDescription: Role = { + name: 'role:default/rbac-admin', + memberReferences: ['user:default/tom', 'group:default/performance-dev-team'], + metadata: { + source: 'rest', + description: undefined, + }, +}; + +const mockUseRole = useRole as jest.MockedFunction; + +describe('AboutCard', () => { + it('should show role metadata information', async () => { + mockUseRole.mockReturnValue({ + loading: false, + role: mockRole, + roleError: { + name: '', + message: '', + }, + }); + const { queryByText } = await renderInTestApp( + , + ); + expect(queryByText('About')).not.toBeNull(); + expect(queryByText('performance dev team')).not.toBeNull(); + expect(queryByText('user:default/tim')).not.toBeNull(); + expect(queryByText('4 Apr 2024, 14:25')).not.toBeNull(); + }); + + it('should display stub, when role metadata is absent', async () => { + mockUseRole.mockReturnValue({ + loading: false, + role: mockRoleWithoutDescription, + roleError: { + name: '', + message: '', + }, + }); + const { queryByText, queryAllByText } = await renderInTestApp( + , + ); + expect(queryByText('About')).not.toBeNull(); + expect(queryByText('No description')).not.toBeNull(); + expect(queryAllByText('No information').length).toEqual(2); + }); + + it('should show an error if api call fails', async () => { + mockUseRole.mockReturnValue({ + loading: false, + role: mockRole, + roleError: { + name: 'Role not found', + message: 'Role not found', + }, + }); + const { queryByText } = await renderInTestApp( + , + ); + expect( + queryByText('Error: Something went wrong while fetching role'), + ).not.toBeNull(); + expect(queryByText('Role not found')).not.toBeNull(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/RoleOverview/AboutCard.tsx b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/AboutCard.tsx new file mode 100644 index 0000000000..6f0bfacf5f --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/AboutCard.tsx @@ -0,0 +1,128 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { + MarkdownContent, + Progress, + WarningPanel, +} from '@backstage/core-components'; +import { AboutField } from '@backstage/plugin-catalog'; + +import { + Card, + CardContent, + CardHeader, + Grid, + makeStyles, +} from '@material-ui/core'; + +import { useRole } from '../../hooks/useRole'; + +const useStyles = makeStyles({ + gridItemCard: { + display: 'flex', + flexDirection: 'column', + height: 'calc(100% - 10px)', // for pages without content header + marginBottom: '10px', + }, + fullHeightCard: { + display: 'flex', + flexDirection: 'column', + height: '100%', + }, + gridItemCardContent: { + flex: 1, + }, + fullHeightCardContent: { + flex: 1, + }, + text: { + wordBreak: 'break-word', + }, +}); + +type AboutCardProps = { + roleName: string; +}; + +export const AboutCard = ({ roleName }: AboutCardProps) => { + const classes = useStyles(); + const cardClass = classes.gridItemCard; + const cardContentClass = classes.gridItemCardContent; + + const { role, roleError, loading } = useRole(roleName); + if (loading) { + return ; + } + + let lastModified = role?.metadata?.lastModified; + if (lastModified) { + const date = new Date(lastModified); + const time = date.toLocaleString('en-US', { + hour: '2-digit' as const, + minute: '2-digit' as const, + hour12: false, + timeZone: 'UTC', + }); + lastModified = `${date.getUTCDate()} ${date.toLocaleString('default', { + month: 'short', + })} ${date.getUTCFullYear()}, ${time}`; + } else { + lastModified = 'No information'; + } + + return ( + + + + {roleError.name ? ( +
+ +
+ ) : ( + + + + + + + + + + + + )} +
+
+ ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/RoleOverview/MembersCard.test.tsx b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/MembersCard.test.tsx new file mode 100644 index 0000000000..8b26f1ff45 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/MembersCard.test.tsx @@ -0,0 +1,179 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { usePermission } from '@backstage/plugin-permission-react'; +import { renderInTestApp } from '@backstage/test-utils'; + +import { MembersData } from '../../types'; +import { MembersCard } from './MembersCard'; + +jest.mock('../../hooks/useMembers', () => ({ + useMembers: jest.fn(), +})); + +jest.mock('@backstage/plugin-permission-react', () => ({ + usePermission: jest.fn(), +})); + +const useMembersMockData: MembersData[] = [ + { + name: 'Amelia Park', + type: 'User', + ref: { + namespace: 'default', + kind: 'user', + name: 'amelia.park', + }, + members: 0, + }, + { + name: 'Calum Leavy', + type: 'User', + ref: { + namespace: 'default', + kind: 'user', + name: 'calum.leavy', + }, + members: 0, + }, + { + name: 'Team B', + type: 'Group', + ref: { + namespace: 'default', + kind: 'group', + name: 'team-b', + }, + members: 5, + }, + { + name: 'Team C', + type: 'Group', + ref: { + namespace: 'default', + kind: 'group', + name: 'team-c', + }, + members: 5, + }, +]; + +const mockUsePermission = usePermission as jest.MockedFunction< + typeof usePermission +>; + +describe('MembersCard', () => { + it('should show list of Users and groups associated with the role when the data is loaded', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + const membersInfo = { + loading: false, + data: useMembersMockData, + error: undefined, + retry: { roleRetry: jest.fn(), membersRetry: jest.fn() }, + canReadUsersAndGroups: true, + }; + const { queryByText } = await renderInTestApp( + , + ); + expect(queryByText('Users and groups (2 users, 2 groups)')).not.toBeNull(); + expect(queryByText('Calum Leavy')).not.toBeNull(); + expect(queryByText('Amelia Park')).not.toBeNull(); + expect(queryByText('Team B')).not.toBeNull(); + }); + + it('should show empty table when there are no users and groups', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + const membersInfo = { + loading: false, + data: [], + error: undefined, + retry: { roleRetry: jest.fn(), membersRetry: jest.fn() }, + canReadUsersAndGroups: true, + }; + const { queryByText } = await renderInTestApp( + , + ); + expect(queryByText('Users and groups')).not.toBeNull(); + expect(queryByText('No records found')).not.toBeNull(); + }); + + it('should show an error if api call fails', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + const membersInfo = { + loading: false, + data: [], + error: { message: 'xyz' }, + retry: { roleRetry: jest.fn(), membersRetry: jest.fn() }, + canReadUsersAndGroups: false, + }; + const { queryByText } = await renderInTestApp( + , + ); + expect( + queryByText( + 'Error: Something went wrong while fetching the users and groups', + ), + ).not.toBeNull(); + + expect(queryByText('No records found')).not.toBeNull(); + }); + + it('should show edit icon when the user is authorized to update roles', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + const membersInfo = { + loading: false, + data: useMembersMockData, + error: undefined, + retry: { roleRetry: jest.fn(), membersRetry: jest.fn() }, + canReadUsersAndGroups: true, + }; + const { getByTestId } = await renderInTestApp( + , + ); + expect(getByTestId('update-members')).not.toBeNull(); + }); + + it('should disable edit icon when the user is not authorized to update roles', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: false }); + const membersInfo = { + loading: false, + data: useMembersMockData, + error: undefined, + retry: { roleRetry: jest.fn(), membersRetry: jest.fn() }, + canReadUsersAndGroups: false, + }; + const { queryByTestId } = await renderInTestApp( + , + ); + expect(queryByTestId('disable-update-members')).not.toBeNull(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/RoleOverview/MembersCard.tsx b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/MembersCard.tsx new file mode 100644 index 0000000000..0e0192c8c1 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/MembersCard.tsx @@ -0,0 +1,131 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { parseEntityRef } from '@backstage/catalog-model'; +import { Table, WarningPanel } from '@backstage/core-components'; +import { usePermission } from '@backstage/plugin-permission-react'; + +import { Card, CardContent, makeStyles } from '@material-ui/core'; +import CachedIcon from '@material-ui/icons/Cached'; + +import { policyEntityUpdatePermission } from '@backstage-community/plugin-rbac-common'; + +import { MembersInfo } from '../../hooks/useMembers'; +import { MembersData } from '../../types'; +import { getMembers } from '../../utils/rbac-utils'; +import EditRole from '../EditRole'; +import { columns } from './MembersListColumns'; + +type MembersCardProps = { + roleName: string; + membersInfo: MembersInfo; +}; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); + +const getRefreshIcon = () => ; +const getEditIcon = (isAllowed: boolean, roleName: string) => { + const { kind, name, namespace } = parseEntityRef(roleName); + + return ( + + ); +}; + +export const MembersCard = ({ roleName, membersInfo }: MembersCardProps) => { + const { data, loading, retry, error, canReadUsersAndGroups } = membersInfo; + const [members, setMembers] = React.useState(); + const policyEntityPermissionResult = usePermission({ + permission: policyEntityUpdatePermission, + resourceRef: policyEntityUpdatePermission.resourceType, + }); + + const classes = useStyles(); + const actions = [ + { + icon: getRefreshIcon, + tooltip: 'Refresh', + isFreeAction: true, + onClick: () => { + retry.roleRetry(); + retry.membersRetry(); + }, + }, + { + icon: () => + getEditIcon( + policyEntityPermissionResult.allowed && canReadUsersAndGroups, + roleName, + ), + tooltip: + policyEntityPermissionResult.allowed && canReadUsersAndGroups + ? 'Edit' + : 'Unauthorized to edit', + isFreeAction: true, + onClick: () => {}, + }, + ]; + + const onSearchResultsChange = (searchResults: MembersData[]) => { + setMembers(searchResults); + }; + + return ( + + + {!loading && error && ( +
+ +
+ )} + onSearchResultsChange(summary.data)} + options={{ padding: 'default', search: true, paging: true }} + data={data ?? []} + isLoading={loading} + columns={columns} + emptyContent={ +
+ No records found +
+ } + /> + + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/RoleOverview/MembersListColumns.tsx b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/MembersListColumns.tsx new file mode 100644 index 0000000000..795a60b3b3 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/MembersListColumns.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { Link, TableColumn } from '@backstage/core-components'; + +import { MembersData } from '../../types'; + +export const columns: TableColumn[] = [ + { + title: 'Name', + field: 'name', + type: 'string', + render: props => { + return ( + + {props.name} + + ); + }, + }, + { + title: 'Type', + field: 'type', + type: 'string', + }, + { + title: 'Members', + field: 'members', + type: 'numeric', + align: 'left', + render: (props: MembersData) => { + return props.type === 'User' ? '-' : props.members; + }, + customSort: (a, b) => { + if (a.members === 0) { + return -1; + } + if (b.members === 0) { + return 1; + } + if (a.members === b.members) { + return 0; + } + return a.members < b.members ? -1 : 1; + }, + }, +]; diff --git a/workspaces/rbac/plugins/rbac/src/components/RoleOverview/PermissionsCard.test.tsx b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/PermissionsCard.test.tsx new file mode 100644 index 0000000000..d55e6b90f4 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/PermissionsCard.test.tsx @@ -0,0 +1,201 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { usePermission } from '@backstage/plugin-permission-react'; +import { renderInTestApp } from '@backstage/test-utils'; + +import { mockFormInitialValues } from '../../__fixtures__/mockFormValues'; +import { usePermissionPolicies } from '../../hooks/usePermissionPolicies'; +import { PermissionsData } from '../../types'; +import { PermissionsCard } from './PermissionsCard'; + +jest.mock('../../hooks/usePermissionPolicies', () => ({ + usePermissionPolicies: jest.fn(), +})); + +jest.mock('@backstage/plugin-permission-react', () => ({ + usePermission: jest.fn(), +})); + +const usePermissionPoliciesMockData: PermissionsData[] = [ + { + permission: 'policy-entity', + plugin: 'permission', + policyString: ['Read', ', Create', ', Delete'], + policies: [ + { + policy: 'read', + effect: 'allow', + }, + { + policy: 'create', + effect: 'allow', + }, + { + policy: 'delete', + effect: 'allow', + }, + ], + }, +]; + +const mockPermissionPolicies = usePermissionPolicies as jest.MockedFunction< + typeof usePermissionPolicies +>; +const mockUsePermission = usePermission as jest.MockedFunction< + typeof usePermission +>; + +describe('PermissionsCard', () => { + it('should show list of Permission Policies when the data is loaded', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockPermissionPolicies.mockReturnValue({ + loading: false, + data: usePermissionPoliciesMockData, + retry: { + policiesRetry: jest.fn(), + permissionPoliciesRetry: jest.fn(), + conditionalPoliciesRetry: jest.fn(), + }, + error: new Error(''), + }); + const { queryByText } = await renderInTestApp( + , + ); + expect(queryByText('Permission Policies (3)')).not.toBeNull(); + expect(queryByText('Read, Create, Delete')).not.toBeNull(); + }); + + it('should show empty table when there are no permission policies', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockPermissionPolicies.mockReturnValue({ + loading: false, + data: [], + retry: { + policiesRetry: jest.fn(), + permissionPoliciesRetry: jest.fn(), + conditionalPoliciesRetry: jest.fn(), + }, + error: new Error(''), + }); + const { queryByText } = await renderInTestApp( + , + ); + expect(queryByText('Permission Policies')).not.toBeNull(); + expect(queryByText('No records found')).not.toBeNull(); + }); + it('should show an error if api call fails', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockPermissionPolicies.mockReturnValue({ + loading: false, + data: [], + retry: { + policiesRetry: jest.fn(), + permissionPoliciesRetry: jest.fn(), + conditionalPoliciesRetry: jest.fn(), + }, + error: { message: '404', name: 'Not Found' }, + }); + const { queryByText } = await renderInTestApp( + , + ); + expect( + queryByText( + 'Error: Something went wrong while fetching the permission policies', + ), + ).not.toBeNull(); + + expect(queryByText('No records found')).not.toBeNull(); + }); + it('should show edit icon when the user is authorized to update roles', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockPermissionPolicies.mockReturnValue({ + loading: false, + data: [], + error: new Error(''), + retry: { + policiesRetry: jest.fn(), + permissionPoliciesRetry: jest.fn(), + conditionalPoliciesRetry: jest.fn(), + }, + }); + const { getByTestId } = await renderInTestApp( + , + ); + expect(getByTestId('update-policies')).not.toBeNull(); + }); + + it('should disable edit icon when the user is not authorized to update roles', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: false }); + mockPermissionPolicies.mockReturnValue({ + loading: false, + data: [], + error: new Error(''), + retry: { + policiesRetry: jest.fn(), + permissionPoliciesRetry: jest.fn(), + conditionalPoliciesRetry: jest.fn(), + }, + }); + const { queryByTestId } = await renderInTestApp( + , + ); + expect(queryByTestId('disable-update-policies')).not.toBeNull(); + }); + + it('should show conditions rules count for Conditional permission policies when the data is loaded', async () => { + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockPermissionPolicies.mockReturnValue({ + loading: false, + data: [ + ...usePermissionPoliciesMockData, + ...mockFormInitialValues.permissionPoliciesRows, + ], + retry: { + policiesRetry: jest.fn(), + permissionPoliciesRetry: jest.fn(), + conditionalPoliciesRetry: jest.fn(), + }, + error: new Error(''), + }); + const { queryByText } = await renderInTestApp( + , + ); + expect(queryByText('Permission Policies (4)')).not.toBeNull(); + expect(queryByText('Read, Create, Delete', { exact: true })).not.toBeNull(); + expect(queryByText('Read', { exact: true })).not.toBeNull(); + expect(queryByText('1 rule')).not.toBeNull(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/RoleOverview/PermissionsCard.tsx b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/PermissionsCard.tsx new file mode 100644 index 0000000000..4f796a8e7f --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/PermissionsCard.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { parseEntityRef } from '@backstage/catalog-model'; +import { Table, WarningPanel } from '@backstage/core-components'; +import { usePermission } from '@backstage/plugin-permission-react'; + +import { Card, CardContent, makeStyles } from '@material-ui/core'; +import CachedIcon from '@material-ui/icons/Cached'; + +import { policyEntityUpdatePermission } from '@backstage-community/plugin-rbac-common'; + +import { usePermissionPolicies } from '../../hooks/usePermissionPolicies'; +import { PermissionsData } from '../../types'; +import EditRole from '../EditRole'; +import { columns } from './PermissionsListColumns'; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); + +type PermissionsCardProps = { + entityReference: string; + canReadUsersAndGroups: boolean; +}; + +const getRefreshIcon = () => ; +const getEditIcon = (isAllowed: boolean, roleName: string) => { + const { kind, name, namespace } = parseEntityRef(roleName); + + return ( + + ); +}; + +export const PermissionsCard = ({ + entityReference, + canReadUsersAndGroups, +}: PermissionsCardProps) => { + const { data, loading, retry, error } = + usePermissionPolicies(entityReference); + const [permissions, setPermissions] = React.useState(); + const permissionResult = usePermission({ + permission: policyEntityUpdatePermission, + resourceRef: policyEntityUpdatePermission.resourceType, + }); + const classes = useStyles(); + + const onSearchResultsChange = (searchResults: PermissionsData[]) => { + setPermissions(searchResults); + }; + + let numberOfPolicies = 0; + (permissions || data)?.forEach(p => { + if (p.conditions) { + numberOfPolicies++; + return; + } + numberOfPolicies = + numberOfPolicies + + p.policies.filter(pol => pol.effect === 'allow').length; + }); + const actions = [ + { + icon: getRefreshIcon, + tooltip: 'Refresh', + isFreeAction: true, + onClick: () => { + retry.permissionPoliciesRetry(); + retry.policiesRetry(); + retry.conditionalPoliciesRetry(); + }, + }, + { + icon: () => + getEditIcon( + permissionResult.allowed && canReadUsersAndGroups, + entityReference, + ), + tooltip: + permissionResult.allowed && canReadUsersAndGroups + ? 'Edit' + : 'Unauthorized to edit', + isFreeAction: true, + onClick: () => {}, + }, + ]; + + return ( + + + {error?.name && error.name !== 404 && ( +
+ +
+ )} +
0 + ? `Permission Policies (${numberOfPolicies})` + : 'Permission Policies' + } + actions={actions} + renderSummaryRow={summary => onSearchResultsChange(summary.data)} + options={{ padding: 'default', search: true, paging: true }} + data={data} + columns={columns} + isLoading={loading} + emptyContent={ +
+ No records found +
+ } + /> + + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/RoleOverview/PermissionsListColumns.tsx b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/PermissionsListColumns.tsx new file mode 100644 index 0000000000..fcdd5b1413 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/PermissionsListColumns.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TableColumn } from '@backstage/core-components'; + +import { PermissionsData } from '../../types'; +import { getRulesNumber } from '../../utils/create-role-utils'; + +export const columns: TableColumn[] = [ + { + title: 'Plugin', + field: 'plugin', + type: 'string', + }, + { + title: 'Permission', + field: 'permission', + type: 'string', + }, + { + title: 'Policies', + field: 'policyString', + type: 'string', + customSort: (a, b) => { + if (a.policies.length === 0) { + return -1; + } + if (b.policies.length === 0) { + return 1; + } + if (a.policies.length === b.policies.length) { + return 0; + } + return a.policies.length < b.policies.length ? -1 : 1; + }, + }, + { + title: 'Conditional', + field: 'conditions', + type: 'string', + render: (permissionsData: PermissionsData) => { + const totalRules = getRulesNumber(permissionsData.conditions); + return totalRules + ? `${totalRules} ${totalRules > 1 ? 'rules' : 'rule'}` + : '-'; + }, + }, +]; diff --git a/workspaces/rbac/plugins/rbac/src/components/RoleOverview/RoleOverviewPage.tsx b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/RoleOverviewPage.tsx new file mode 100644 index 0000000000..c09ba22c8a --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RoleOverview/RoleOverviewPage.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { Header, Page, TabbedLayout } from '@backstage/core-components'; + +import { Grid } from '@material-ui/core'; + +import { useLocationToast } from '../../hooks/useLocationToast'; +import { useMembers } from '../../hooks/useMembers'; +import { SnackbarAlert } from '../SnackbarAlert'; +import { useToast } from '../ToastContext'; +import { AboutCard } from './AboutCard'; +import { MembersCard } from './MembersCard'; +import { PermissionsCard } from './PermissionsCard'; + +export const RoleOverviewPage = () => { + const { roleName, roleNamespace, roleKind } = useParams(); + const { toastMessage, setToastMessage } = useToast(); + const membersInfo = useMembers(`${roleKind}:${roleNamespace}/${roleName}`); + + useLocationToast(setToastMessage); + + const onAlertClose = () => { + setToastMessage(''); + }; + + return ( + <> + + +
+ + + + + + + + + + + + + + + + + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/RolesList/DeleteRole.test.tsx b/workspaces/rbac/plugins/rbac/src/components/RolesList/DeleteRole.test.tsx new file mode 100644 index 0000000000..8e249aa5a0 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RolesList/DeleteRole.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import * as DeleteDialogContext from '@janus-idp/shared-react'; +import { fireEvent, render, screen } from '@testing-library/react'; + +import DeleteRole from './DeleteRole'; + +jest.mock('@janus-idp/shared-react', () => ({ + useDeleteDialog: jest.fn().mockReturnValue({ + deleteComponent: '', + setDeleteComponent: jest.fn(), + openDialog: false, + setOpenDialog: jest.fn(), + }), +})); +describe('DeleteRole', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders the button with the correct tooltip', () => { + render( + , + ); + + expect(screen.getByTestId('delete-admin-role')).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveAttribute( + 'title', + 'Delete Admin Role', + ); + }); + + it('calls openDialog with the roleName when clicked', () => { + render( + , + ); + + fireEvent.click(screen.getByRole('button')); + + const { setDeleteComponent, setOpenDialog } = + DeleteDialogContext.useDeleteDialog(); + expect(setDeleteComponent).toHaveBeenCalledWith({ roleName: 'Admin' }); + expect(setOpenDialog).toHaveBeenCalledWith(true); + }); + + it('disables the button when disable prop is true', () => { + render( + , + ); + + expect(screen.getByRole('button')).toBeDisabled(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/RolesList/DeleteRole.tsx b/workspaces/rbac/plugins/rbac/src/components/RolesList/DeleteRole.tsx new file mode 100644 index 0000000000..d6912ad0f0 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RolesList/DeleteRole.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { useDeleteDialog } from '@janus-idp/shared-react'; +import { IconButton, Tooltip } from '@material-ui/core'; +import Delete from '@mui/icons-material/Delete'; +import Typography from '@mui/material/Typography'; + +type DeleteRoleProps = { + roleName: string; + disable: boolean; + tooltip?: string; + dataTestId: string; +}; + +const DeleteRole = ({ + roleName, + tooltip, + disable, + dataTestId, +}: DeleteRoleProps) => { + const { setDeleteComponent, setOpenDialog } = useDeleteDialog(); + + const openDialog = (name: string) => { + setDeleteComponent({ roleName: name }); + setOpenDialog(true); + }; + + return ( + + + openDialog(roleName)} + aria-label="Delete" + disabled={disable} + title={tooltip || 'Delete Role'} + > + + + + + ); +}; +export default DeleteRole; diff --git a/workspaces/rbac/plugins/rbac/src/components/RolesList/DeleteRoleDialog.test.tsx b/workspaces/rbac/plugins/rbac/src/components/RolesList/DeleteRoleDialog.test.tsx new file mode 100644 index 0000000000..7a0b39beba --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RolesList/DeleteRoleDialog.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import DeleteRoleDialog from './DeleteRoleDialog'; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest.fn(), +})); + +jest.mock('../ToastContext', () => ({ + useToast: () => ({ setToastMessage: jest.fn() }), +})); + +describe('DeleteRoleDialog', () => { + it('renders delete role dialog correctly with Delete button disabled when open', () => { + render( + , + ); + expect(screen.queryByText(/Delete this role?/i)).toBeInTheDocument(); + const deleteButton = screen.getByRole('button', { name: /Delete/i }); + expect(deleteButton).toBeDisabled(); + }); + + it('does not render when not open', () => { + const { queryByText } = render( + , + ); + expect(queryByText(/Delete this role?/i)).not.toBeInTheDocument(); + }); + + it('enables the delete button when the role name is correctly entered', async () => { + const user = userEvent.setup(); + render( + , + ); + + const input = screen + .getAllByRole('textbox') + .find(element => element.getAttribute('name') === 'delete-role'); + + if (!input) { + throw new Error('Input not found'); + } + user.type(input, 'Test Role'); + + const deleteButton = screen.getByRole('button', { name: /Delete/i }); + await waitFor(() => { + expect(deleteButton).not.toBeDisabled(); + }); + }); + + it('shows an error when the deletion fails', async () => { + const mockDeleteRole = jest.fn().mockResolvedValue({ status: 400 }); + const useApiMock = useApi as jest.Mock; + useApiMock.mockReturnValue({ + getAssociatedPolicies: jest.fn(), + getRoleConditions: jest.fn(), + deleteRole: mockDeleteRole, + }); + + const user = userEvent.setup(); + render( + , + ); + + const input = screen + .getAllByRole('textbox') + .find(element => element.getAttribute('name') === 'delete-role'); + + if (!input) { + throw new Error('Input not found'); + } + await user.type(input, 'Test Role'); + + const deleteButton = screen.getByRole('button', { name: /Delete/i }); + await user.click(deleteButton); + await waitFor(() => { + expect( + screen.queryByText(/Unable to delete the role/i), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/RolesList/DeleteRoleDialog.tsx b/workspaces/rbac/plugins/rbac/src/components/RolesList/DeleteRoleDialog.tsx new file mode 100644 index 0000000000..b30d44794e --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RolesList/DeleteRoleDialog.tsx @@ -0,0 +1,223 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { + Box, + Button, + createStyles, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + makeStyles, + TextField, + Theme, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import ErrorIcon from '@material-ui/icons/Error'; +import { Alert } from '@material-ui/lab'; +import Typography from '@mui/material/Typography'; + +import { RoleBasedPolicy } from '@backstage-community/plugin-rbac-common'; + +import { rbacApiRef } from '../../api/RBACBackendClient'; +import { getMembers } from '../../utils/rbac-utils'; +import { + removeConditions, + removePermissions, +} from '../../utils/role-form-utils'; +import { useToast } from '../ToastContext'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + titleContainer: { + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + }, + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, + }), +); + +type DeleteRoleDialogProps = { + open: boolean; + closeDialog: () => void; + roleName: string; + propOptions: { + memberRefs: string[]; + permissions: number; + }; +}; + +const DeleteRoleDialog = ({ + open, + closeDialog, + roleName, + propOptions, +}: DeleteRoleDialogProps) => { + const classes = useStyles(); + const { setToastMessage } = useToast(); + const [deleteRoleValue, setDeleteRoleValue] = React.useState(); + const [disableDelete, setDisableDelete] = React.useState(false); + const [error, setError] = React.useState(''); + + const rbacApi = useApi(rbacApiRef); + + const deleteRole = async () => { + try { + const policies = await rbacApi.getAssociatedPolicies(roleName); + const conditionalPolicies = await rbacApi.getRoleConditions(roleName); + + if (Array.isArray(policies)) { + const allowedPolicies = policies.filter( + (policy: RoleBasedPolicy) => policy.effect !== 'deny', + ); + await removePermissions(roleName, allowedPolicies, rbacApi); + } + + if (Array.isArray(conditionalPolicies)) { + const conditionalPoliciesIds = conditionalPolicies.map(cp => cp.id); + await removeConditions(conditionalPoliciesIds, rbacApi); + } + + const response = await rbacApi.deleteRole(roleName); + if (response.status === 200 || response.status === 204) { + setToastMessage(`Role ${roleName} deleted successfully`); + closeDialog(); + } else { + setError(`Unable to delete the role. ${response.statusText}`); + } + } catch (err) { + setError(err instanceof Error ? err.message : `${err}`); + } + }; + + const onTextInput = (value: string) => { + setDeleteRoleValue(value); + if (value === '') { + setDisableDelete(true); + } else if (value === roleName) { + setDisableDelete(false); + } else { + setDisableDelete(true); + } + }; + + return ( + + + + + {' '} + Delete this role? + + + + + + + + + Are you sure you want to delete the role{' '} + + {roleName} + {' '} + ? +
+
+ Deleting this role is irreversible and will remove its functionality + from the system. Proceed with caution. +
+
+ The{' '} + {`${getMembers( + propOptions.memberRefs, + ).toLocaleLowerCase('en-US')}`}{' '} + associated with this role will lose access to all the{' '} + {`${propOptions.permissions} permission policies`}{' '} + specified in this role. +
+ onTextInput(value)} + onBlur={({ target: { value } }) => onTextInput(value)} + /> +
+ {error && ( + + {error} + + )} + + + + +
+ ); +}; + +export default DeleteRoleDialog; diff --git a/workspaces/rbac/plugins/rbac/src/components/RolesList/RolesList.test.tsx b/workspaces/rbac/plugins/rbac/src/components/RolesList/RolesList.test.tsx new file mode 100644 index 0000000000..1d049b9ee5 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RolesList/RolesList.test.tsx @@ -0,0 +1,368 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { + RequirePermission, + usePermission, +} from '@backstage/plugin-permission-react'; +import { renderInTestApp } from '@backstage/test-utils'; + +import { useCheckIfLicensePluginEnabled } from '../../hooks/useCheckIfLicensePluginEnabled'; +import { useRoles } from '../../hooks/useRoles'; +import { RolesData } from '../../types'; +import { RolesList } from './RolesList'; + +jest.mock('@backstage/plugin-permission-react', () => ({ + usePermission: jest.fn(), + RequirePermission: jest.fn(), +})); + +jest.mock('../../hooks/useRoles', () => ({ + useRoles: jest.fn(), +})); + +jest.mock('../../hooks/useCheckIfLicensePluginEnabled', () => ({ + useCheckIfLicensePluginEnabled: jest.fn(), +})); + +const useRolesMockData: RolesData[] = [ + { + name: 'role:default/guests', + description: '-', + members: ['user:default/xyz'], + permissions: 2, + modifiedBy: '-', + lastModified: '-', + actionsPermissionResults: { + delete: { allowed: true, loading: false }, + edit: { allowed: true, loading: false }, + }, + accessiblePlugins: ['catalog'], + }, + { + name: 'role:default/rbac_admin', + description: '-', + members: ['user:default/xyz', 'group:default/hkhkh'], + permissions: 4, + modifiedBy: '-', + lastModified: '-', + actionsPermissionResults: { + delete: { allowed: true, loading: false }, + edit: { allowed: true, loading: false }, + }, + accessiblePlugins: ['catalog', 'permission', 'scaffolder'], + }, +]; + +const mockUsePermission = usePermission as jest.MockedFunction< + typeof usePermission +>; + +const mockUseRoles = useRoles as jest.MockedFunction; +const mockUseCheckIfLicensePluginEnabled = + useCheckIfLicensePluginEnabled as jest.MockedFunction< + typeof useCheckIfLicensePluginEnabled + >; + +const RequirePermissionMock = RequirePermission as jest.MockedFunction< + typeof RequirePermission +>; + +describe('RolesList', () => { + it('should show list of roles when the roles are loaded', async () => { + RequirePermissionMock.mockImplementation(props => <>{props.children}); + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockUseRoles.mockReturnValue({ + loading: false, + data: useRolesMockData, + error: { + rolesError: '', + policiesError: '', + roleConditionError: '', + }, + retry: { roleRetry: jest.fn(), policiesRetry: jest.fn() }, + createRoleAllowed: false, + createRoleLoading: false, + }); + mockUseCheckIfLicensePluginEnabled.mockReturnValue({ + loading: false, + isEnabled: false, + licenseCheckError: { + message: '', + name: '', + }, + }); + const { queryByText } = await renderInTestApp(); + expect(queryByText('All roles (2)')).not.toBeNull(); + expect(queryByText('role:default/guests')).not.toBeNull(); + expect(queryByText('role:default/rbac_admin')).not.toBeNull(); + expect(queryByText('1 user, 1 group')).not.toBeNull(); + }); + + it('should show empty table when there are no roles', async () => { + RequirePermissionMock.mockImplementation(props => <>{props.children}); + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockUseRoles.mockReturnValue({ + loading: false, + data: [], + error: { + rolesError: '', + policiesError: '', + roleConditionError: '', + }, + retry: { roleRetry: jest.fn(), policiesRetry: jest.fn() }, + createRoleAllowed: false, + createRoleLoading: false, + }); + const { getByTestId, queryByText } = await renderInTestApp(); + expect(getByTestId('roles-table-empty')).not.toBeNull(); + expect(queryByText('Something went wrong')).not.toBeInTheDocument(); + }); + + it('should show delete icon if user is authorized to delete roles', async () => { + RequirePermissionMock.mockImplementation(props => <>{props.children}); + mockUsePermission + .mockReturnValueOnce({ loading: false, allowed: true }) + .mockReturnValue({ loading: false, allowed: true }); + mockUseRoles.mockReturnValue({ + loading: false, + data: useRolesMockData, + error: { + rolesError: '', + policiesError: '', + roleConditionError: '', + }, + retry: { roleRetry: jest.fn(), policiesRetry: jest.fn() }, + createRoleAllowed: false, + createRoleLoading: false, + }); + const { getByTestId, getByText } = await renderInTestApp(); + expect(getByTestId('delete-role-role:default/guests')).not.toBeNull(); + expect(getByText('Actions')).not.toBeNull(); + }); + + it('should show disabled delete icon if user is not authorized to delete roles', async () => { + RequirePermissionMock.mockImplementation(props => <>{props.children}); + mockUsePermission + .mockReturnValueOnce({ loading: false, allowed: true }) + .mockReturnValue({ loading: false, allowed: false }); + mockUseRoles.mockReturnValue({ + loading: false, + data: [ + { + ...useRolesMockData[0], + actionsPermissionResults: { + delete: { allowed: false, loading: false }, + edit: { allowed: true, loading: false }, + }, + }, + { + ...useRolesMockData[1], + actionsPermissionResults: { + delete: { allowed: false, loading: true }, + edit: { allowed: true, loading: false }, + }, + }, + ], + error: { + rolesError: '', + policiesError: '', + roleConditionError: '', + }, + retry: { roleRetry: jest.fn(), policiesRetry: jest.fn() }, + createRoleAllowed: false, + createRoleLoading: false, + }); + const { getByTestId } = await renderInTestApp(); + expect( + getByTestId('disable-delete-role-role:default/guests'), + ).not.toBeNull(); + expect(getByTestId('update-role-role:default/guests')).not.toBeNull(); + }); + + it('should show disabled edit icon if user is not authorized to update roles', async () => { + RequirePermissionMock.mockImplementation(props => <>{props.children}); + mockUsePermission + .mockReturnValueOnce({ loading: false, allowed: true }) + .mockReturnValue({ loading: false, allowed: false }); + mockUseRoles.mockReturnValue({ + loading: false, + data: [ + { + ...useRolesMockData[0], + actionsPermissionResults: { + delete: { allowed: true, loading: false }, + edit: { allowed: false, loading: false }, + }, + }, + { + ...useRolesMockData[1], + actionsPermissionResults: { + delete: { allowed: true, loading: true }, + edit: { allowed: false, loading: false }, + }, + }, + ], + error: { + rolesError: '', + policiesError: '', + roleConditionError: '', + }, + retry: { roleRetry: jest.fn(), policiesRetry: jest.fn() }, + createRoleAllowed: true, + createRoleLoading: false, + }); + const { getByTestId } = await renderInTestApp(); + expect( + getByTestId('disable-update-role-role:default/guests'), + ).not.toBeNull(); + expect(getByTestId('delete-role-role:default/rbac_admin')).not.toBeNull(); + }); + + it('should disable create button if user is not authorized to create roles', async () => { + RequirePermissionMock.mockImplementation(props => <>{props.children}); + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockUseRoles.mockReturnValue({ + loading: false, + data: useRolesMockData, + error: { + rolesError: '', + policiesError: '', + roleConditionError: '', + }, + retry: { roleRetry: jest.fn(), policiesRetry: jest.fn() }, + createRoleAllowed: false, + createRoleLoading: false, + }); + const { getByTestId } = await renderInTestApp(); + + expect(getByTestId('create-role').getAttribute('aria-disabled')).toEqual( + 'true', + ); + }); + + it('should enable create button if user is authorized to create roles', async () => { + RequirePermissionMock.mockImplementation(props => <>{props.children}); + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockUseRoles.mockReturnValue({ + loading: false, + data: useRolesMockData, + error: { + rolesError: '', + policiesError: '', + roleConditionError: '', + }, + retry: { roleRetry: jest.fn(), policiesRetry: jest.fn() }, + createRoleAllowed: true, + createRoleLoading: false, + }); + const { getByTestId } = await renderInTestApp(); + + expect(getByTestId('create-role').getAttribute('aria-disabled')).toEqual( + 'false', + ); + }); + + it('should show warning alert if user is not authorized to create roles', async () => { + RequirePermissionMock.mockImplementation(props => <>{props.children}); + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockUseRoles.mockReturnValue({ + loading: false, + data: useRolesMockData, + error: { + rolesError: '', + policiesError: '', + roleConditionError: '', + }, + retry: { roleRetry: jest.fn(), policiesRetry: jest.fn() }, + createRoleAllowed: false, + createRoleLoading: false, + }); + const { getByTestId } = await renderInTestApp(); + + expect(getByTestId('create-role-warning')).not.toBeNull(); + }); + + it('should show error message when there is an error fetching the roles', async () => { + RequirePermissionMock.mockImplementation(props => <>{props.children}); + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockUseRoles.mockReturnValue({ + loading: true, + data: [], + error: { + rolesError: 'Something went wrong', + policiesError: '', + roleConditionError: '', + }, + retry: { roleRetry: jest.fn(), policiesRetry: jest.fn() }, + createRoleAllowed: false, + createRoleLoading: false, + }); + + const { queryByText } = await renderInTestApp(); + expect(queryByText('Something went wrong')).toBeInTheDocument(); + }); + + it('should show error message when there is an error fetching the role conditions', async () => { + RequirePermissionMock.mockImplementation(props => <>{props.children}); + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockUseRoles.mockReturnValue({ + loading: true, + data: [], + error: { + rolesError: '', + policiesError: '', + roleConditionError: + 'Error fetching role conditions for role role:default/xyz, please try again later.', + }, + retry: { roleRetry: jest.fn(), policiesRetry: jest.fn() }, + createRoleAllowed: false, + createRoleLoading: false, + }); + + const { queryByText } = await renderInTestApp(); + expect( + queryByText( + 'Error fetching role conditions for role role:default/xyz, please try again later.', + ), + ).toBeInTheDocument(); + }); + + it('should show accessible plugins for each role', async () => { + RequirePermissionMock.mockImplementation(props => <>{props.children}); + mockUsePermission.mockReturnValue({ loading: false, allowed: true }); + mockUseRoles.mockReturnValue({ + loading: false, + data: useRolesMockData, + error: { + rolesError: '', + policiesError: '', + roleConditionError: '', + }, + retry: { roleRetry: jest.fn(), policiesRetry: jest.fn() }, + createRoleAllowed: false, + createRoleLoading: false, + }); + const { queryByText } = await renderInTestApp(); + expect(queryByText('role:default/guests')).not.toBeNull(); + expect(queryByText('Catalog', { exact: true })).not.toBeNull(); + expect(queryByText('role:default/rbac_admin')).not.toBeNull(); + expect( + queryByText('Catalog, Permission + 1', { exact: true }), + ).not.toBeNull(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/RolesList/RolesList.tsx b/workspaces/rbac/plugins/rbac/src/components/RolesList/RolesList.tsx new file mode 100644 index 0000000000..6d2db3be0f --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RolesList/RolesList.tsx @@ -0,0 +1,144 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { Progress, Table, WarningPanel } from '@backstage/core-components'; + +import { useDeleteDialog } from '@janus-idp/shared-react'; +import { makeStyles } from '@material-ui/core'; + +import { useCheckIfLicensePluginEnabled } from '../../hooks/useCheckIfLicensePluginEnabled'; +import { useLocationToast } from '../../hooks/useLocationToast'; +import { useRoles } from '../../hooks/useRoles'; +import { RolesData } from '../../types'; +import DownloadCSVLink from '../DownloadUserStatistics'; +import { SnackbarAlert } from '../SnackbarAlert'; +import { useToast } from '../ToastContext'; +import DeleteRoleDialog from './DeleteRoleDialog'; +import { columns } from './RolesListColumns'; +import { RolesListToolbar } from './RolesListToolbar'; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); + +export const RolesList = () => { + const { toastMessage, setToastMessage } = useToast(); + const { openDialog, setOpenDialog, deleteComponent } = useDeleteDialog(); + useLocationToast(setToastMessage); + const [roles, setRoles] = React.useState(); + const classes = useStyles(); + const { loading, data, retry, createRoleAllowed, createRoleLoading, error } = + useRoles(); + + const closeDialog = () => { + setOpenDialog(false); + retry.roleRetry(); + retry.policiesRetry(); + }; + + const onAlertClose = () => { + setToastMessage(''); + }; + const onSearchResultsChange = (searchResults: RolesData[]) => { + setRoles(searchResults.length); + }; + + const getErrorWarning = () => { + const errorTitleBase = 'Something went wrong while fetching the'; + const errorWarningArr = [ + { message: error?.rolesError, title: `${errorTitleBase} roles` }, + { + message: error?.policiesError, + title: `${errorTitleBase} permission policies`, + }, + { + message: error?.roleConditionError, + title: `${errorTitleBase} role conditions`, + }, + ]; + + return ( + errorWarningArr.find(({ message }) => message) || { + message: '', + title: '', + } + ); + }; + + const errorWarning = getErrorWarning(); + + const isLicensePluginEnabled = useCheckIfLicensePluginEnabled(); + if (isLicensePluginEnabled.loading) { + return ; + } + + return ( + <> + + + {errorWarning.message && ( +
+ +
+ )} +
onSearchResultsChange(summary.data)} + emptyContent={ +
+ No records found +
+ } + /> + {isLicensePluginEnabled.isEnabled && } + {openDialog && ( + d.name === deleteComponent.roleName)?.members || + [], + permissions: + data.find(d => d.name === deleteComponent.roleName) + ?.permissions || 0, + }} + /> + )} + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/RolesList/RolesListColumns.tsx b/workspaces/rbac/plugins/rbac/src/components/RolesList/RolesListColumns.tsx new file mode 100644 index 0000000000..813c28189f --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RolesList/RolesListColumns.tsx @@ -0,0 +1,120 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { parseEntityRef } from '@backstage/catalog-model'; +import { Link, TableColumn } from '@backstage/core-components'; + +import { Tooltip, Typography } from '@material-ui/core'; + +import { RolesData } from '../../types'; +import { getMembers } from '../../utils/rbac-utils'; +import EditRole from '../EditRole'; +import DeleteRole from './DeleteRole'; + +export const columns: TableColumn[] = [ + { + title: 'Name', + field: 'name', + type: 'string', + render: (props: RolesData) => { + const { kind, namespace, name } = parseEntityRef(props.name); + return ( + {props.name} + ); + }, + }, + { + title: 'Users and groups', + field: 'members', + type: 'string', + align: 'left', + render: props => getMembers(props.members), + customSort: (a, b) => { + if (a.members.length === 0) { + return -1; + } + if (b.members.length === 0) { + return 1; + } + if (a.members.length === b.members.length) { + return 0; + } + return a.members.length < b.members.length ? -1 : 1; + }, + }, + { + title: 'Accessible plugins', + field: 'accessiblePlugins', + type: 'string', + align: 'left', + render: (props: RolesData) => { + const pls = props.accessiblePlugins.map( + p => p[0].toLocaleUpperCase('en-US') + p.slice(1), + ); + const plsTooltip = pls.join(', '); + const plsOverflowCount = pls.length > 2 ? `+ ${pls.length - 2}` : ''; + + return pls.length > 0 ? ( + + + {pls.length === 1 + ? `${pls[0]}` + : `${pls[0]}, ${pls[1]} ${plsOverflowCount}`} + + + ) : ( + '-' + ); + }, + }, + { + title: 'Actions', + sorting: false, + render: (props: RolesData) => ( + <> + + + + ), + }, +]; diff --git a/workspaces/rbac/plugins/rbac/src/components/RolesList/RolesListToolbar.tsx b/workspaces/rbac/plugins/rbac/src/components/RolesList/RolesListToolbar.tsx new file mode 100644 index 0000000000..4817fd430b --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/RolesList/RolesListToolbar.tsx @@ -0,0 +1,92 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { LinkButton } from '@backstage/core-components'; + +import { makeStyles } from '@material-ui/core'; +import Alert from '@material-ui/lab/Alert'; +import AlertTitle from '@material-ui/lab/AlertTitle'; +import Typography from '@mui/material/Typography'; + +const useStyles = makeStyles(theme => ({ + toolbar: { + display: 'flex', + justifyContent: 'end', + marginBottom: '24px', + }, + rbacPreReqLink: { + color: theme.palette.link, + }, + alertTitle: { + fontWeight: 'bold', + }, +})); + +export const RolesListToolbar = ({ + createRoleAllowed, + createRoleLoading, +}: { + createRoleAllowed: boolean; + createRoleLoading: boolean; +}) => { + const classes = useStyles(); + return ( +
+ {!createRoleLoading && !createRoleAllowed && ( + + + Unable to create role. + + To enable create/edit role button, make sure required users/groups are + available in catalog as a role cannot be created without users/groups + and also the role associated with your user should have the permission + policies mentioned{' '} + + here + + . +
+ + + Note + + : Even after ingesting users/groups in catalog and applying above + permissions if the create/edit button is still disabled then please + contact your administrator as you might be conditionally restricted + from accessing the create/edit button. + +
+ )} +
+ + + Create + + +
+ ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/Router.test.tsx b/workspaces/rbac/plugins/rbac/src/components/Router.test.tsx new file mode 100644 index 0000000000..3679984edb --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/Router.test.tsx @@ -0,0 +1,175 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +import { RequirePermission } from '@backstage/plugin-permission-react'; + +import { render, screen } from '@testing-library/react'; + +import { Router } from './Router'; + +const configMock = { + getOptionalBoolean: jest.fn(() => true), +}; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest.fn(() => configMock), +})); + +jest.mock('./RbacPage', () => ({ + RbacPage: () =>
RBAC
, +})); + +jest.mock('./RoleOverview/RoleOverviewPage', () => ({ + RoleOverviewPage: () =>
Role
, +})); + +jest.mock('./CreateRole/CreateRolePage', () => ({ + CreateRolePage: () =>
CreateRole
, +})); + +jest.mock('./CreateRole/EditRolePage', () => ({ + EditRolePage: () =>
EditRole
, +})); + +jest.mock('@backstage/core-components', () => ({ + ErrorPage: jest.fn().mockImplementation(() =>
Mocked ErrorPage
), +})); + +jest.mock('@backstage/plugin-permission-react', () => ({ + RequirePermission: jest + .fn() + .mockImplementation(({ permission, resourceRef, children }) => ( +
+ {`${permission} ${resourceRef}`} + {children} +
+ )), +})); + +const mockedPrequirePermission = RequirePermission as jest.MockedFunction< + typeof RequirePermission +>; + +describe('Router component', () => { + it('renders RbacPage when path is "/"', () => { + render( + + + , + ); + expect(screen.queryByText('RBAC')).toBeInTheDocument(); + }); + + it(`should not render RbacPage when path is "/", when plugin is disabled`, () => { + configMock.getOptionalBoolean.mockReturnValueOnce(false); + render( + + + , + ); + expect(screen.queryByText('RBAC')).not.toBeInTheDocument(); + }); + + it('should render ErrorPage when rbac-backend plugin is disabled', () => { + configMock.getOptionalBoolean.mockReturnValueOnce(false); + render( + + + , + ); + + expect(screen.queryByText('Mocked ErrorPage')).toBeInTheDocument(); + }); + + it('renders RoleOverviewPage when path matches roleRouteRef', () => { + render( + + + , + ); + + expect(screen.queryByText('Role')).toBeInTheDocument(); + }); + + it('should not render RoleOverviewPage when path matches roleRouteRef, when plugin is disabled', () => { + configMock.getOptionalBoolean.mockReturnValueOnce(false); + render( + + + , + ); + + expect(screen.queryByText('Role')).not.toBeInTheDocument(); + }); + + it('renders CreateRolePage with the right permissions when path matches createRoleRouteRef', () => { + render( + + + , + ); + expect(mockedPrequirePermission).toHaveBeenCalledWith( + expect.objectContaining({ + permission: expect.objectContaining({ name: 'policy.entity.create' }), + resourceRef: expect.stringContaining('policy-entity'), + }), + expect.anything(), + ); + expect(screen.queryByText('CreateRole')).toBeInTheDocument(); + }); + + it('should not render CreateRolePage with the right permissions when path matches createRoleRouteRef, when plugin is disabled', () => { + configMock.getOptionalBoolean.mockReturnValueOnce(false); + render( + + + , + ); + + expect(screen.queryByText('CreateRole')).not.toBeInTheDocument(); + }); + + it('renders EditRolePage with the right permissions when path matches editRoleRouteRef', () => { + render( + + + , + ); + + expect(mockedPrequirePermission).toHaveBeenCalledWith( + expect.objectContaining({ + permission: expect.objectContaining({ name: 'policy.entity.update' }), + resourceRef: expect.stringContaining('policy-entity'), + }), + expect.anything(), + ); + expect(screen.queryByText('EditRole')).toBeInTheDocument(); + }); + + it('should not render EditRolePage with the right permissions when path matches editRoleRouteRef, when plugin is disabled', () => { + configMock.getOptionalBoolean.mockReturnValueOnce(false); + render( + + + , + ); + + expect(screen.queryByText('EditRole')).not.toBeInTheDocument(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/Router.tsx b/workspaces/rbac/plugins/rbac/src/components/Router.tsx new file mode 100644 index 0000000000..78d0025ad4 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/Router.tsx @@ -0,0 +1,83 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Route, Routes } from 'react-router-dom'; + +import { ErrorPage } from '@backstage/core-components'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { RequirePermission } from '@backstage/plugin-permission-react'; + +import { + policyEntityCreatePermission, + policyEntityUpdatePermission, +} from '@backstage-community/plugin-rbac-common'; + +import { createRoleRouteRef, editRoleRouteRef, roleRouteRef } from '../routes'; +import { CreateRolePage } from './CreateRole/CreateRolePage'; +import { EditRolePage } from './CreateRole/EditRolePage'; +import { RbacPage } from './RbacPage'; +import { RoleOverviewPage } from './RoleOverview/RoleOverviewPage'; +import { ToastContextProvider } from './ToastContext'; + +/** + * + * @public + */ +export const Router = ({ useHeader = true }: { useHeader?: boolean }) => { + const config = useApi(configApiRef); + const isRBACPluginEnabled = config.getOptionalBoolean('permission.enabled'); + + if (!isRBACPluginEnabled) { + return ( + + ); + } + + return ( + + + } /> + } /> + + + + } + /> + + + + } + /> + + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/SnackbarAlert.test.tsx b/workspaces/rbac/plugins/rbac/src/components/SnackbarAlert.test.tsx new file mode 100644 index 0000000000..3e961ce515 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/SnackbarAlert.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { fireEvent, render, screen } from '@testing-library/react'; + +import '@testing-library/jest-dom'; + +import { SnackbarAlert } from './SnackbarAlert'; + +describe('SnackbarAlert', () => { + it('displays the Snackbar with the message when toastMessage is provided', () => { + const toastMessage = 'Test message'; + render( + {}} />, + ); + + expect(screen.getByText(toastMessage)).toBeInTheDocument(); + expect(screen.getByRole('alert')).toHaveTextContent(toastMessage); + }); + + it('does not display the Snackbar when toastMessage is an empty string', () => { + render( {}} />); + + expect(screen.queryByText('Test message')).not.toBeInTheDocument(); + }); + + it('calls onAlertClose when the Snackbar is closed', () => { + const onAlertClose = jest.fn(); + render( + , + ); + + fireEvent.click(screen.getByLabelText(/close/i)); + + expect(onAlertClose).toHaveBeenCalled(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/components/SnackbarAlert.tsx b/workspaces/rbac/plugins/rbac/src/components/SnackbarAlert.tsx new file mode 100644 index 0000000000..fb5bd04520 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/SnackbarAlert.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import Snackbar from '@material-ui/core/Snackbar'; +import Alert from '@material-ui/lab/Alert'; + +export const SnackbarAlert = ({ + toastMessage, + onAlertClose, +}: { + toastMessage: string; + onAlertClose: () => void; +}) => { + return ( + + + {toastMessage} + + + ); +}; diff --git a/workspaces/rbac/plugins/rbac/src/components/ToastContext.tsx b/workspaces/rbac/plugins/rbac/src/components/ToastContext.tsx new file mode 100644 index 0000000000..c336214e6b --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/ToastContext.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { createContext, useContext } from 'react'; + +type ToastContextType = { + toastMessage: string; + setToastMessage: (message: string) => void; +}; + +export const ToastContext = createContext({ + toastMessage: '', + setToastMessage: () => {}, +}); + +export const ToastContextProvider = (props: any) => { + const [toastMessage, setToastMessage] = React.useState(''); + const toastContextProviderValue = React.useMemo( + () => ({ setToastMessage, toastMessage }), + [setToastMessage, toastMessage], + ); + return ( + + {props.children} + + ); +}; +export const useToast = () => useContext(ToastContext); diff --git a/workspaces/rbac/plugins/rbac/src/components/index.ts b/workspaces/rbac/plugins/rbac/src/components/index.ts new file mode 100644 index 0000000000..6bd1e457e4 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/components/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { RbacPage } from './RbacPage'; +export { Administration } from './Administration'; +export { RoleOverviewPage } from './RoleOverview/RoleOverviewPage'; +export { Router } from './Router'; diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useCheckIfLicensePluginEnabled.ts b/workspaces/rbac/plugins/rbac/src/hooks/useCheckIfLicensePluginEnabled.ts new file mode 100644 index 0000000000..337999d2f7 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useCheckIfLicensePluginEnabled.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useAsync } from 'react-use'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { licensedUsersApiRef } from '../api/LicensedUsersClient'; + +export const useCheckIfLicensePluginEnabled = (): { + loading: boolean; + isEnabled: boolean | undefined; + licenseCheckError: Error; +} => { + const licensedUsersClient = useApi(licensedUsersApiRef); + const { + value: isEnabled, + loading, + error: licenseCheckError, + } = useAsync(async () => await licensedUsersClient.isLicensePluginEnabled()); + + return { + loading, + isEnabled, + licenseCheckError: licenseCheckError as Error, + }; +}; diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useConditionRules.test.ts b/workspaces/rbac/plugins/rbac/src/hooks/useConditionRules.test.ts new file mode 100644 index 0000000000..52da269a6e --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useConditionRules.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderHook, waitFor } from '@testing-library/react'; + +import { mockConditionRules } from '../__fixtures__/mockConditionRules'; +import { mockTransformedConditionRules } from '../__fixtures__/mockTransformedConditionRules'; +import { useConditionRules } from './useConditionRules'; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest.fn().mockReturnValue({ + getPluginsConditionRules: jest.fn().mockReturnValue([ + { + pluginId: 'catalog', + rules: [mockConditionRules[0].rules[0], mockConditionRules[0].rules[1]], + }, + { + pluginId: 'scaffolder', + rules: [mockConditionRules[1].rules[0]], + }, + ]), + }), +})); + +describe('useConditionRules', () => { + it('should return condition-rules', async () => { + const { result } = renderHook(() => useConditionRules()); + + await waitFor(() => { + expect(result.current.data).toEqual(mockTransformedConditionRules); + expect(result.current.error).toBeUndefined(); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useConditionRules.ts b/workspaces/rbac/plugins/rbac/src/hooks/useConditionRules.ts new file mode 100644 index 0000000000..6695bfafd6 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useConditionRules.ts @@ -0,0 +1,102 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useAsync } from 'react-use'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { rbacApiRef } from '../api/RBACBackendClient'; +import { + ConditionRules, + ConditionRulesData, + ResourceTypeRuleData, + RulesData, +} from '../components/ConditionalAccess/types'; +import { ConditionRule, PluginConditionRules } from '../types'; +import { uniqBy } from '../utils/create-role-utils'; + +const getPluginsResourceTypes = ( + conditionRules: PluginConditionRules[], +): { [plugin: string]: string[] } => { + return conditionRules.reduce((acc, pluginRules) => { + return { + ...acc, + [`${pluginRules.pluginId}`]: uniqBy( + pluginRules.rules.map(rule => rule.resourceType), + val => val, + ), + }; + }, {}); +}; + +const getRuleData = (pluginRules: PluginConditionRules, resType: string) => { + return pluginRules.rules.reduce( + (ruleAcc: RulesData, rule: ConditionRule) => { + return rule.resourceType === resType + ? { + ...ruleAcc, + [`${rule.name}`]: { + schema: rule.paramsSchema, + description: rule.description, + }, + rules: [...ruleAcc.rules, rule.name], + } + : ruleAcc; + }, + { rules: [] }, + ); +}; + +const getConditionRulesData = (conditionRules: PluginConditionRules[]) => { + const pluginsResourceTypes = getPluginsResourceTypes(conditionRules); + + return conditionRules.reduce((acc: ConditionRulesData, pluginRules) => { + return { + ...acc, + [`${pluginRules.pluginId}`]: pluginsResourceTypes[ + pluginRules.pluginId + ].reduce((resAcc: ResourceTypeRuleData, resType: string) => { + return { + ...resAcc, + [`${resType}`]: getRuleData(pluginRules, resType), + }; + }, {}), + }; + }, {}); +}; + +export const useConditionRules = (): ConditionRules => { + const rbacApi = useApi(rbacApiRef); + + const { + value: conditionRules, + loading: conditionRulesLoading, + error: conditionRulesErr, + } = useAsync(async () => { + return await rbacApi.getPluginsConditionRules(); + }); + + const isConditionRulesAvailable = + !conditionRulesLoading && Array.isArray(conditionRules); + + const conditionRulesData = isConditionRulesAvailable + ? getConditionRulesData(conditionRules) + : undefined; + + return { + data: conditionRulesData, + error: conditionRulesErr, + }; +}; diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useLocationToast.test.ts b/workspaces/rbac/plugins/rbac/src/hooks/useLocationToast.test.ts new file mode 100644 index 0000000000..3c9c0f74a2 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useLocationToast.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useLocation } from 'react-router-dom'; + +import { renderHook } from '@testing-library/react-hooks'; + +import { useLocationToast } from './useLocationToast'; + +jest.mock('react-router-dom', () => ({ + useLocation: jest.fn(), +})); + +describe('useLocationToast', () => { + it('sets toast message based on location state', () => { + const mockSetToastMessage = jest.fn(); + + (useLocation as jest.Mock).mockReturnValue({ + state: { toastMessage: 'Success Message' }, + }); + + renderHook(() => useLocationToast(mockSetToastMessage)); + + expect(mockSetToastMessage).toHaveBeenCalledWith('Success Message'); + }); + + it('cleans up by setting toast message to an empty string', () => { + const mockSetToastMessage = jest.fn(); + + (useLocation as jest.Mock).mockReturnValue({ + state: { toastMessage: 'Success Message' }, + }); + + const { unmount } = renderHook(() => useLocationToast(mockSetToastMessage)); + unmount(); + + expect(mockSetToastMessage).toHaveBeenCalledWith(''); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useLocationToast.ts b/workspaces/rbac/plugins/rbac/src/hooks/useLocationToast.ts new file mode 100644 index 0000000000..be4d67dfd3 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useLocationToast.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +export const useLocationToast = ( + setToastMessage: (message: string) => void, +) => { + const location = useLocation(); + + useEffect(() => { + if (location?.state?.toastMessage) { + setToastMessage(location.state.toastMessage); + } + return () => setToastMessage(''); + }, [location, setToastMessage]); +}; diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useMembers.test.ts b/workspaces/rbac/plugins/rbac/src/hooks/useMembers.test.ts new file mode 100644 index 0000000000..2c5c86e7b4 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useMembers.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderHook, waitFor } from '@testing-library/react'; + +import { mockMembers } from '../__fixtures__/mockMembers'; +import { useMembers } from './useMembers'; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest.fn().mockReturnValue({ + getRole: jest.fn().mockReturnValue([ + { + memberReferences: [ + 'group:default/admins', + 'user:default/amelia.park', + 'user:default/calum.leavy', + 'group:default/team-b', + 'group:default/team-c', + ], + name: 'role:default/rbac_admin', + }, + ]), + getMembers: jest.fn().mockReturnValue(mockMembers), + }), +})); + +describe('useMembers', () => { + it('should return members', async () => { + const { result } = renderHook(() => useMembers('role:default/rbac_admin')); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.data).toHaveLength(5); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useMembers.ts b/workspaces/rbac/plugins/rbac/src/hooks/useMembers.ts new file mode 100644 index 0000000000..e031fc41fe --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useMembers.ts @@ -0,0 +1,143 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useAsyncRetry, useInterval } from 'react-use'; + +import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core-plugin-api'; + +import { rbacApiRef } from '../api/RBACBackendClient'; +import { MemberEntity, MembersData } from '../types'; +import { getMembersFromGroup } from '../utils/rbac-utils'; + +export type MembersInfo = { + loading: boolean; + data: MembersData[]; + retry: { roleRetry: () => void; membersRetry: () => void }; + error?: { message: string }; + canReadUsersAndGroups: boolean; +}; + +const getErrorText = ( + role: any, + members: any, +): { message: string } | undefined => { + if (!Array.isArray(role) && (role as Response)?.statusText) { + return { + message: `Unable to fetch role: ${(role as Response).statusText}`, + }; + } else if (!Array.isArray(members) && (members as Response)?.statusText) { + return { + message: `Unable to fetch members: ${(members as Response).statusText}`, + }; + } + return undefined; +}; + +const getMemberData = ( + memberResource: MemberEntity | undefined, + ref: string, +) => { + if (memberResource) { + return { + name: + memberResource.spec.profile?.displayName ?? + memberResource.metadata.name, + type: memberResource.kind, + ref: { + namespace: memberResource.metadata.namespace as string, + kind: memberResource.kind.toLocaleLowerCase('en-US'), + name: memberResource.metadata.name, + }, + members: + memberResource.kind === 'Group' + ? getMembersFromGroup(memberResource) + : 0, + }; + } + const { kind, namespace, name } = parseEntityRef(ref); + return { + name, + type: kind === 'user' ? 'User' : ('Group' as 'User' | 'Group'), + ref: { + namespace, + kind, + name, + }, + members: 0, + }; +}; + +export const useMembers = ( + roleName: string, + pollInterval?: number, +): MembersInfo => { + const rbacApi = useApi(rbacApiRef); + let data: MembersData[] = []; + const { + value: role, + retry: roleRetry, + error: roleError, + } = useAsyncRetry(async () => { + return await rbacApi.getRole(roleName); + }); + + const { + value: members, + retry: membersRetry, + error: membersError, + } = useAsyncRetry(async () => { + return await rbacApi.getMembers(); + }); + + const canReadUsersAndGroups = + !membersError && Array.isArray(members) && members.length > 0; + + const loading = !roleError && !membersError && !role && !members; + + data = React.useMemo( + () => + Array.isArray(role) + ? role[0].memberReferences.reduce((acc: MembersData[], ref: string) => { + const memberResource: MemberEntity | undefined = Array.isArray( + members, + ) + ? members.find(member => stringifyEntityRef(member) === ref) + : undefined; + const memberData = getMemberData(memberResource, ref); + acc.push(memberData); + return acc; + }, []) + : [], + [role, members], + ); + + useInterval( + () => { + roleRetry(); + membersRetry(); + }, + loading ? null : pollInterval || 10000, + ); + + return { + loading, + data, + retry: { roleRetry, membersRetry }, + error: getErrorText(role, members) || roleError || membersError, + canReadUsersAndGroups, + }; +}; diff --git a/workspaces/rbac/plugins/rbac/src/hooks/usePermissionPolicies.test.ts b/workspaces/rbac/plugins/rbac/src/hooks/usePermissionPolicies.test.ts new file mode 100644 index 0000000000..80a578a502 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/usePermissionPolicies.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderHook, waitFor } from '@testing-library/react'; + +import { mockConditions } from '../__fixtures__/mockConditions'; +import { mockPermissionPolicies } from '../__fixtures__/mockPermissionPolicies'; +import { mockAssociatedPolicies } from '../__fixtures__/mockPolicies'; +import { usePermissionPolicies } from './usePermissionPolicies'; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest + .fn() + .mockReturnValueOnce({ + getAssociatedPolicies: jest.fn().mockReturnValue(mockAssociatedPolicies), + listPermissions: jest.fn().mockReturnValue(mockPermissionPolicies), + getRoleConditions: jest.fn().mockReturnValue(mockConditions), + }) + .mockReturnValueOnce({ + getAssociatedPolicies: jest.fn().mockReturnValue(mockAssociatedPolicies), + listPermissions: jest.fn().mockReturnValue([]), + getRoleConditions: jest.fn().mockReturnValue([]), + }) + .mockReturnValue({ + getAssociatedPolicies: jest + .fn() + .mockReturnValue({ status: '403', statusText: 'Unauthorized' }), + listPermissions: jest.fn().mockReturnValue(mockPermissionPolicies), + getRoleConditions: jest.fn().mockReturnValue([]), + }), +})); + +describe('usePermissionPolicies', () => { + it('should return simple and conditional permission policies', async () => { + const { result } = renderHook(() => + usePermissionPolicies('role:default/rbac_admin'), + ); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.data).toHaveLength(8); + }); + }); + + it('should return empty permission policies when there are no permissions', async () => { + const { result } = renderHook(() => + usePermissionPolicies('role:default/rbac_admin'), + ); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.data).toHaveLength(0); + }); + }); + + it('should return an error when the fetch api call returns an error', async () => { + const { result } = renderHook(() => + usePermissionPolicies('role:default/rbac_admin'), + ); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.error).toEqual({ + message: 'Error fetching policies. Unauthorized', + name: '403', + }); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/hooks/usePermissionPolicies.ts b/workspaces/rbac/plugins/rbac/src/hooks/usePermissionPolicies.ts new file mode 100644 index 0000000000..43127a65fb --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/usePermissionPolicies.ts @@ -0,0 +1,137 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useAsyncRetry, useInterval } from 'react-use'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { rbacApiRef } from '../api/RBACBackendClient'; +import { getPluginsPermissionPoliciesData } from '../utils/create-role-utils'; +import { + getConditionalPermissionsData, + getPermissionsData, +} from '../utils/rbac-utils'; + +const getErrorText = ( + policies: any, + permissionPolicies: any, + conditionalPolicies: any, +): { name: number; message: string } | undefined => { + if (!Array.isArray(policies) && (policies as Response)?.statusText) { + return { + name: (policies as Response).status, + message: `Error fetching policies. ${(policies as Response).statusText}`, + }; + } else if ( + !Array.isArray(permissionPolicies) && + (permissionPolicies as Response)?.statusText + ) { + return { + name: (permissionPolicies as Response).status, + message: `Error fetching the plugins. ${ + (permissionPolicies as Response).statusText + }`, + }; + } else if ( + !Array.isArray(conditionalPolicies) && + (conditionalPolicies as Response)?.statusText + ) { + return { + name: (conditionalPolicies as Response).status, + message: `Error fetching the conditional permission policies. ${ + (conditionalPolicies as Response).statusText + }`, + }; + } + return undefined; +}; + +export const usePermissionPolicies = ( + entityReference: string, + pollInterval?: number, +) => { + const rbacApi = useApi(rbacApiRef); + const { + value: policies, + retry: policiesRetry, + error: policiesError, + } = useAsyncRetry(async () => { + return await rbacApi.getAssociatedPolicies(entityReference); + }); + + const { + value: conditionalPolicies, + retry: conditionalPoliciesRetry, + error: conditionalPoliciesError, + } = useAsyncRetry(async () => { + return await rbacApi.getRoleConditions(entityReference); + }); + + const { + value: permissionPolicies, + error: permissionPoliciesError, + retry: permissionPoliciesRetry, + } = useAsyncRetry(async () => { + return await rbacApi.listPermissions(); + }); + + const loading = + !permissionPoliciesError && + !policiesError && + !conditionalPoliciesError && + (!permissionPolicies || !policies || !conditionalPolicies); + + const allPermissionPolicies = React.useMemo( + () => (Array.isArray(permissionPolicies) ? permissionPolicies : []), + [permissionPolicies], + ); + + const data = React.useMemo(() => { + return Array.isArray(policies) + ? getPermissionsData(policies, allPermissionPolicies) + : []; + }, [allPermissionPolicies, policies]); + + const conditionsData = React.useMemo(() => { + const cpp = Array.isArray(conditionalPolicies) ? conditionalPolicies : []; + const pluginsPermissionsPoliciesData = + allPermissionPolicies.length > 0 + ? getPluginsPermissionPoliciesData(allPermissionPolicies) + : undefined; + return pluginsPermissionsPoliciesData + ? getConditionalPermissionsData(cpp, pluginsPermissionsPoliciesData) + : []; + }, [allPermissionPolicies, conditionalPolicies]); + + useInterval( + () => { + policiesRetry(); + permissionPoliciesRetry(); + conditionalPoliciesRetry(); + }, + loading ? null : pollInterval || null, + ); + return { + loading, + data: [...conditionsData, ...data], + retry: { policiesRetry, permissionPoliciesRetry, conditionalPoliciesRetry }, + error: + policiesError || + permissionPoliciesError || + conditionalPoliciesError || + getErrorText(policies, permissionPolicies, conditionalPolicies), + }; +}; diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useRole.test.ts b/workspaces/rbac/plugins/rbac/src/hooks/useRole.test.ts new file mode 100644 index 0000000000..10f843d227 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useRole.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderHook, waitFor } from '@testing-library/react'; + +import { mockMembers } from '../__fixtures__/mockMembers'; +import { useRole } from './useRole'; + +const apiMock = { + getRole: jest.fn().mockImplementation(), + getMembers: jest.fn().mockImplementation(), +}; + +jest.mock('@backstage/core-plugin-api', () => { + const actualApi = jest.requireActual('@backstage/core-plugin-api'); + return { + ...actualApi, + useApi: jest.fn().mockImplementation(() => { + return apiMock; + }), + }; +}); + +describe('useRole', () => { + beforeEach(() => { + apiMock.getRole = jest.fn().mockImplementation(async () => { + return [ + { + memberReferences: [ + 'group:default/admins', + 'user:default/amelia.park', + 'user:default/calum.leavy', + 'group:default/team-b', + 'group:default/team-c', + ], + name: 'role:default/rbac_admin', + metadata: { + source: 'rest', + description: 'default rbac admin group', + }, + }, + ]; + }); + apiMock.getMembers = jest.fn().mockImplementation(async () => mockMembers); + }); + + describe('useRole', () => { + it('should throw an error on get role', async () => { + apiMock.getRole = jest.fn().mockImplementation(() => { + throw new Error('Some error message'); + }); + const { result } = renderHook(() => useRole('role:default/rbac_admin')); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.roleError.message).toEqual('Some error message'); + }); + }); + + it('should return role', async () => { + const { result } = renderHook(() => useRole('role:default/rbac_admin')); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.role).toEqual({ + memberReferences: [ + 'group:default/admins', + 'user:default/amelia.park', + 'user:default/calum.leavy', + 'group:default/team-b', + 'group:default/team-c', + ], + name: 'role:default/rbac_admin', + metadata: { + source: 'rest', + description: 'default rbac admin group', + }, + }); + }); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useRole.ts b/workspaces/rbac/plugins/rbac/src/hooks/useRole.ts new file mode 100644 index 0000000000..9ddee2228b --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useRole.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useAsync } from 'react-use'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { Role } from '@backstage-community/plugin-rbac-common'; + +import { rbacApiRef } from '../api/RBACBackendClient'; + +export const useRole = ( + roleEntityRef: string, +): { + loading: boolean; + role: Role | undefined; + roleError: Error; +} => { + const rbacApi = useApi(rbacApiRef); + const { + value: roles, + loading, + error: roleError, + } = useAsync(async () => await rbacApi.getRole(roleEntityRef)); + + return { + loading, + role: Array.isArray(roles) ? roles[0] : undefined, + roleError: (roleError as Error) || { + name: (roles as Response)?.status, + message: `Error fetching the role. ${(roles as Response)?.statusText}`, + }, + }; +}; diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useRoles.test.ts b/workspaces/rbac/plugins/rbac/src/hooks/useRoles.test.ts new file mode 100644 index 0000000000..21814d24bb --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useRoles.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderHook, waitFor } from '@testing-library/react'; + +import { mockPolicies } from '../__fixtures__/mockPolicies'; +import { useRoles } from './useRoles'; + +jest.mock('@backstage/core-plugin-api', () => ({ + ...jest.requireActual('@backstage/core-plugin-api'), + useApi: jest.fn().mockReturnValue({ + getRoles: jest.fn().mockReturnValue([ + { + memberReferences: ['user:default/guest'], + name: 'role:default/guests', + }, + { + memberReferences: ['user:default/debsmita1', 'group:default/admins'], + name: 'role:default/rbac_admin', + }, + ]), + getPolicies: jest + .fn() + .mockReturnValueOnce(mockPolicies) + .mockReturnValue([ + { + entityReference: 'role:default/guests', + permission: 'catalog-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'role:default/guests', + permission: 'catalog.entity.create', + policy: 'use', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'policy-entity', + policy: 'delete', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'catalog-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'role:default/rbac_admin', + permission: 'catalog.entity.create', + policy: 'use', + effect: 'allow', + }, + ]), + getRoleConditions: jest.fn().mockReturnValue([]), + }), +})); + +describe('useRoles', () => { + it('should return all roles irrespective of permission policies', async () => { + const { result } = renderHook(() => useRoles()); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.data).toHaveLength(2); + }); + }); + + it('should return roles', async () => { + const { result } = renderHook(() => useRoles()); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.data).toHaveLength(2); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useRoles.ts b/workspaces/rbac/plugins/rbac/src/hooks/useRoles.ts new file mode 100644 index 0000000000..748d0ecbdb --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useRoles.ts @@ -0,0 +1,265 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { useAsync, useAsyncRetry, useInterval } from 'react-use'; + +import { useApi } from '@backstage/core-plugin-api'; +import { usePermission } from '@backstage/plugin-permission-react'; + +import { + isResourcedPolicy, + PluginPermissionMetaData, + policyEntityCreatePermission, + policyEntityDeletePermission, + policyEntityUpdatePermission, + Role, + RoleBasedPolicy, +} from '@backstage-community/plugin-rbac-common'; + +import { rbacApiRef } from '../api/RBACBackendClient'; +import { RolesData } from '../types'; +import { getPermissions, getPermissionsArray } from '../utils/rbac-utils'; + +type RoleWithConditionalPoliciesCount = Role & { + conditionalPoliciesCount: number; + accessiblePlugins: string[]; +}; + +export const useRoles = ( + pollInterval?: number, +): { + loading: boolean; + data: RolesData[]; + createRoleLoading: boolean; + createRoleAllowed: boolean; + error: { + rolesError: string; + policiesError: string; + roleConditionError: string; + }; + retry: { roleRetry: () => void; policiesRetry: () => void }; +} => { + const rbacApi = useApi(rbacApiRef); + const [newRoles, setNewRoles] = React.useState< + RoleWithConditionalPoliciesCount[] + >([]); + const [roleConditionError, setRoleConditionError] = + React.useState(''); + const { + value: roles, + retry: roleRetry, + error: rolesError, + } = useAsyncRetry(async () => await rbacApi.getRoles()); + + const { + value: policies, + retry: policiesRetry, + error: policiesError, + } = useAsyncRetry(async () => await rbacApi.getPolicies(), []); + + const { + loading: membersLoading, + value: members, + error: membersError, + } = useAsync(async () => { + return await rbacApi.getMembers(); + }); + + const { + value: permissionPolicies, + loading: loadingPermissionPolicies, + error: permissionPoliciesError, + } = useAsync(async () => { + return await rbacApi.listPermissions(); + }); + + const canReadUsersAndGroups = + !membersLoading && + !membersError && + Array.isArray(members) && + members.length > 0; + + const deletePermissionResult = usePermission({ + permission: policyEntityDeletePermission, + resourceRef: policyEntityDeletePermission.resourceType, + }); + + const policyEntityCreatePermissionResult = usePermission({ + permission: policyEntityCreatePermission, + resourceRef: policyEntityCreatePermission.resourceType, + }); + + const createRoleLoading = + policyEntityCreatePermissionResult.loading || membersLoading; + + const createRoleAllowed = + policyEntityCreatePermissionResult.allowed && canReadUsersAndGroups; + + const editPermissionResult = usePermission({ + permission: policyEntityUpdatePermission, + resourceRef: policyEntityUpdatePermission.resourceType, + }); + + React.useEffect(() => { + const fetchAllPermissionPolicies = async () => { + if (!Array.isArray(roles)) return; + const failedFetchConditionRoles: string[] = []; + const conditionPromises = roles.map(async role => { + try { + const conditionalPolicies = await rbacApi.getRoleConditions( + role.name, + ); + + if ((conditionalPolicies as any as Response)?.statusText) { + failedFetchConditionRoles.push(role.name); + throw new Error( + (conditionalPolicies as any as Response).statusText, + ); + } + const accessiblePlugins = + Array.isArray(conditionalPolicies) && conditionalPolicies.length > 0 + ? conditionalPolicies.map(c => c.pluginId) + : []; + return { + ...role, + conditionalPoliciesCount: Array.isArray(conditionalPolicies) + ? conditionalPolicies.length + : 0, + accessiblePlugins, + }; + } catch (error) { + setRoleConditionError( + `Error fetching role conditions for ${ + failedFetchConditionRoles.length > 1 ? 'roles' : 'role' + } ${failedFetchConditionRoles.join(', ')}, please try again later.`, + ); + return { + ...role, + conditionalPoliciesCount: 0, + accessiblePlugins: [], + }; + } + }); + + const updatedRoles = await Promise.all(conditionPromises); + setNewRoles(updatedRoles); + }; + + fetchAllPermissionPolicies(); + }, [roles, rbacApi]); + + const data: RolesData[] = React.useMemo( + () => + Array.isArray(newRoles) && newRoles?.length > 0 + ? newRoles.reduce( + (acc: RolesData[], role: RoleWithConditionalPoliciesCount) => { + const permissions = getPermissions( + role.name, + policies as RoleBasedPolicy[], + ); + + let accPls = role.accessiblePlugins; + if ( + !loadingPermissionPolicies && + !permissionPoliciesError && + (permissionPolicies as PluginPermissionMetaData[])?.length > 0 + ) { + const pls = getPermissionsArray( + role.name, + policies as RoleBasedPolicy[], + ).map( + po => + (permissionPolicies as PluginPermissionMetaData[]).find( + pp => + pp.policies?.find(pol => + isResourcedPolicy(pol) + ? po.permission === pol.resourceType + : po.permission === pol.name, + ), + )?.pluginId, + ); + accPls = [...accPls, ...pls].filter(val => !!val) as string[]; + } + const accessiblePlugins = accPls + .filter((val, index, plugins) => plugins.indexOf(val) === index) + .sort(); + + return [ + ...acc, + { + id: role.name, + name: role.name, + description: role.metadata?.description ?? '-', + members: role.memberReferences, + permissions: role.conditionalPoliciesCount + permissions, + modifiedBy: '-', + lastModified: '-', + actionsPermissionResults: { + delete: deletePermissionResult, + edit: { + allowed: + editPermissionResult.allowed && canReadUsersAndGroups, + loading: editPermissionResult.loading, + }, + }, + accessiblePlugins, + }, + ]; + }, + [], + ) + : [], + [ + newRoles, + policies, + loadingPermissionPolicies, + permissionPoliciesError, + permissionPolicies, + deletePermissionResult, + editPermissionResult.allowed, + editPermissionResult.loading, + canReadUsersAndGroups, + ], + ); + const loading = !rolesError && !policiesError && !roles && !policies; + + useInterval( + () => { + roleRetry(); + policiesRetry(); + }, + loading ? null : pollInterval || 10000, + ); + + return { + loading, + data, + error: { + rolesError: (rolesError?.message || + (typeof roles === 'object' + ? (roles as any as Response)?.statusText + : '')) as string, + policiesError: (policiesError?.message || + (typeof policies === 'object' + ? (policies as any as Response)?.statusText + : '')) as string, + roleConditionError, + }, + createRoleLoading, + createRoleAllowed, + retry: { roleRetry, policiesRetry }, + }; +}; diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useSelectedMembers.test.ts b/workspaces/rbac/plugins/rbac/src/hooks/useSelectedMembers.test.ts new file mode 100644 index 0000000000..e666ef2f9d --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useSelectedMembers.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderHook, waitFor } from '@testing-library/react'; + +import { mockMembers } from '../__fixtures__/mockMembers'; +import { useSelectedMembers } from './useSelectedMembers'; + +const apiMock = { + getRole: jest.fn().mockImplementation(), + getMembers: jest.fn().mockImplementation(), +}; + +jest.mock('@backstage/core-plugin-api', () => { + const actualApi = jest.requireActual('@backstage/core-plugin-api'); + return { + ...actualApi, + useApi: jest.fn().mockImplementation(() => { + return apiMock; + }), + }; +}); + +describe('useSelectedMembers', () => { + beforeEach(() => { + apiMock.getRole = jest.fn().mockImplementation(async () => { + return [ + { + memberReferences: [ + 'group:default/admins', + 'user:default/amelia.park', + 'user:default/calum.leavy', + 'group:default/team-b', + 'group:default/team-c', + ], + name: 'role:default/rbac_admin', + metadata: { + source: 'rest', + description: 'default rbac admin group', + }, + }, + ]; + }); + apiMock.getMembers = jest.fn().mockImplementation(async () => mockMembers); + }); + + it('should throw an error on get role', async () => { + apiMock.getRole = jest.fn().mockImplementation(() => { + throw new Error('Some error message'); + }); + const { result } = renderHook(() => + useSelectedMembers('role:default/rbac_admin'), + ); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.roleError.message).toEqual('Some error message'); + }); + }); + + it('should throw an error on get members', async () => { + apiMock.getMembers = jest.fn().mockImplementation(() => { + throw new Error('Some error message'); + }); + const { result } = renderHook(() => + useSelectedMembers('role:default/rbac_admin'), + ); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.membersError.message).toEqual('Some error message'); + }); + }); + + it('should return selected members', async () => { + expect(true).toBeTruthy(); + + const { result } = renderHook(() => + useSelectedMembers('role:default/rbac_admin'), + ); + await waitFor(() => { + expect(result.current.loading).toBeFalsy(); + expect(result.current.selectedMembers).toHaveLength(5); + expect(result.current.role).toEqual({ + memberReferences: [ + 'group:default/admins', + 'user:default/amelia.park', + 'user:default/calum.leavy', + 'group:default/team-b', + 'group:default/team-c', + ], + name: 'role:default/rbac_admin', + metadata: { + source: 'rest', + description: 'default rbac admin group', + }, + }); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/hooks/useSelectedMembers.ts b/workspaces/rbac/plugins/rbac/src/hooks/useSelectedMembers.ts new file mode 100644 index 0000000000..fabbf5c3ff --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/hooks/useSelectedMembers.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useAsync } from 'react-use'; + +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core-plugin-api'; + +import { Role } from '@backstage-community/plugin-rbac-common'; + +import { rbacApiRef } from '../api/RBACBackendClient'; +import { SelectedMember } from '../components/CreateRole/types'; +import { MemberEntity } from '../types'; +import { getSelectedMember } from '../utils/rbac-utils'; +import { useRole } from './useRole'; + +export const useSelectedMembers = ( + roleName: string, +): { + members: MemberEntity[]; + selectedMembers: SelectedMember[]; + role: Role | undefined; + membersError: Error; + roleError: Error; + loading: boolean; + canReadUsersAndGroups: boolean; +} => { + const rbacApi = useApi(rbacApiRef); + const { role, loading: roleLoading, roleError } = useRole(roleName); + + const { + loading: membersLoading, + value: members, + error: membersError, + } = useAsync(async () => { + return await rbacApi.getMembers(); + }); + + const canReadUsersAndGroups = + !membersLoading && + !membersError && + Array.isArray(members) && + members.length > 0; + + const data: SelectedMember[] = role + ? (role as Role).memberReferences.reduce((acc: SelectedMember[], ref) => { + const memberResource = + (Array.isArray(members) && + members.find(member => stringifyEntityRef(member) === ref)) || + undefined; + acc.push(getSelectedMember(memberResource, ref)); + + return acc; + }, []) + : []; + + return { + selectedMembers: data, + members: Array.isArray(members) ? members : ([] as MemberEntity[]), + role, + membersError: (membersError as Error) || { + name: (members as Response)?.status, + message: (members as Response)?.statusText, + }, + roleError: roleError, + loading: roleLoading || membersLoading, + canReadUsersAndGroups, + }; +}; diff --git a/workspaces/rbac/plugins/rbac/src/index.ts b/workspaces/rbac/plugins/rbac/src/index.ts new file mode 100644 index 0000000000..40e4bc8b9e --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { rbacPlugin, RbacPage, Administration } from './plugin'; + +export { default as AdminPanelSettingsOutlinedIcon } from '@mui/icons-material/AdminPanelSettingsOutlined'; +export { default as RbacIcon } from '@mui/icons-material/VpnKeyOutlined'; diff --git a/workspaces/rbac/plugins/rbac/src/plugin.test.ts b/workspaces/rbac/plugins/rbac/src/plugin.test.ts new file mode 100644 index 0000000000..fea91480b1 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { rbacPlugin } from './plugin'; + +describe('rbac', () => { + it('should export plugin', () => { + expect(rbacPlugin).toBeDefined(); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/plugin.ts b/workspaces/rbac/plugins/rbac/src/plugin.ts new file mode 100644 index 0000000000..0243a49c45 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/plugin.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + configApiRef, + createApiFactory, + createComponentExtension, + createPlugin, + createRoutableExtension, + identityApiRef, +} from '@backstage/core-plugin-api'; + +import { + LicensedUsersAPIClient, + licensedUsersApiRef, +} from './api/LicensedUsersClient'; +import { rbacApiRef, RBACBackendClient } from './api/RBACBackendClient'; +import { createRoleRouteRef, roleRouteRef, rootRouteRef } from './routes'; + +export const rbacPlugin = createPlugin({ + id: 'rbac', + routes: { + root: rootRouteRef, + role: roleRouteRef, + createRole: createRoleRouteRef, + }, + apis: [ + createApiFactory({ + api: rbacApiRef, + deps: { + configApi: configApiRef, + identityApi: identityApiRef, + }, + factory: ({ configApi, identityApi }) => + new RBACBackendClient({ configApi, identityApi }), + }), + createApiFactory({ + api: licensedUsersApiRef, + deps: { + configApi: configApiRef, + identityApi: identityApiRef, + }, + factory: ({ configApi, identityApi }) => + new LicensedUsersAPIClient({ configApi, identityApi }), + }), + ], +}); + +export const RbacPage = rbacPlugin.provide( + createRoutableExtension({ + name: 'RbacPage', + component: () => import('./components').then(m => m.Router), + mountPoint: rootRouteRef, + }), +); + +export const Administration = rbacPlugin.provide( + createComponentExtension({ + name: 'Administration', + component: { + lazy: () => import('./components').then(m => m.Administration), + }, + }), +); diff --git a/workspaces/rbac/plugins/rbac/src/routes.ts b/workspaces/rbac/plugins/rbac/src/routes.ts new file mode 100644 index 0000000000..01903ef1f0 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/routes.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'rbac', +}); + +export const roleRouteRef = createSubRouteRef({ + id: 'rbac-role-overview', + parent: rootRouteRef, + path: '/roles/:roleKind/:roleNamespace/:roleName', +}); + +export const createRoleRouteRef = createSubRouteRef({ + id: 'rbac-create-role', + parent: rootRouteRef, + path: '/role/new', +}); + +export const editRoleRouteRef = createSubRouteRef({ + id: 'rbac-edit-role', + parent: rootRouteRef, + path: '/role/:roleKind/:roleNamespace/:roleName', +}); diff --git a/workspaces/rbac/plugins/rbac/src/setupTests.ts b/workspaces/rbac/plugins/rbac/src/setupTests.ts new file mode 100644 index 0000000000..658016ffdd --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; diff --git a/workspaces/rbac/plugins/rbac/src/types.ts b/workspaces/rbac/plugins/rbac/src/types.ts new file mode 100644 index 0000000000..34009b0928 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/types.ts @@ -0,0 +1,95 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { GroupEntity, UserEntity } from '@backstage/catalog-model'; + +import { RJSFSchema } from '@rjsf/utils'; + +import { + PermissionAction, + RoleConditionalPolicyDecision, +} from '@backstage-community/plugin-rbac-common'; + +import { ConditionsData } from './components/ConditionalAccess/types'; +import { RowPolicy } from './components/CreateRole/types'; + +export type RolesData = { + name: string; + description: string; + members: string[]; + permissions: number; + modifiedBy: string; + lastModified: string; + actionsPermissionResults: { + delete: { allowed: boolean; loading: boolean }; + edit: { allowed: boolean; loading: boolean }; + }; + accessiblePlugins: string[]; +}; + +export type MembersData = { + name: string; + type: 'User' | 'Group'; + members: number; + ref: { + name: string; + namespace: string; + kind: string; + }; +}; + +export type PermissionsDataSet = { + plugin: string; + permission: string; + policies: Set; + policyString?: Set; + isResourced?: boolean; +}; + +export type PermissionsData = { + id?: number; + plugin: string; + permission: string; + policies: RowPolicy[]; + policyString?: string[] | string; + isResourced?: boolean; + conditions?: ConditionsData; +}; + +export type MemberEntity = UserEntity | GroupEntity; + +export type RoleError = { error: { name: string; message: string } }; + +export type RoleBasedConditions = Omit< + RoleConditionalPolicyDecision, + 'id' +>; + +export type ConditionRule = { + name: string; + description?: string; + resourceType: string; + paramsSchema: RJSFSchema; +}; + +export type PluginConditionRules = { + pluginId: string; + rules: ConditionRule[]; +}; + +export type UpdatedConditionsData = { + id: number; + updateCondition: RoleBasedConditions; +}[]; diff --git a/workspaces/rbac/plugins/rbac/src/utils/conditional-access-utils.test.ts b/workspaces/rbac/plugins/rbac/src/utils/conditional-access-utils.test.ts new file mode 100644 index 0000000000..33300d8038 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/utils/conditional-access-utils.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PermissionCondition } from '@backstage/plugin-permission-common'; + +import { criterias } from '../components/ConditionalAccess/const'; +import { + Condition, + ConditionsData, +} from '../components/ConditionalAccess/types'; +import { + extractNestedConditions, + ruleOptionDisabled, +} from './conditional-access-utils'; + +describe('ruleOptionDisabled', () => { + it('should return false if conditions are not provided', () => { + expect(ruleOptionDisabled('someRule')).toBe(false); + }); + + it('should return false if the ruleOption is not found in conditions', () => { + const conditions: PermissionCondition[] = [ + { rule: 'rule1', resourceType: 'catalog-entity', params: {} }, + { rule: 'rule2', resourceType: 'catalog-entity', params: {} }, + ]; + expect(ruleOptionDisabled('someRule', conditions)).toBe(false); + }); + + it('should return true if the ruleOption is found in conditions', () => { + const conditions: PermissionCondition[] = [ + { rule: 'rule1', resourceType: 'catalog-entity', params: {} }, + { rule: 'someRule', resourceType: 'catalog-entity', params: {} }, + ]; + expect(ruleOptionDisabled('someRule', conditions)).toBe(true); + }); + + it('should handle an empty conditions array', () => { + const conditions: PermissionCondition[] = []; + expect(ruleOptionDisabled('someRule', conditions)).toBe(false); + }); + + it('should return false if conditions is undefined', () => { + expect(ruleOptionDisabled('someRule')).toBe(false); + }); +}); + +describe('extractNestedConditions', () => { + const criteriaTypes = [criterias.allOf, criterias.anyOf, criterias.not]; + it('should add conditions matching criteria types to nestedConditions', () => { + const conditions: Condition[] = [ + { rule: 'rule1', resourceType: 'catalog-entity', params: {} }, + { + anyOf: [{ rule: 'rule2', resourceType: 'catalog-entity', params: {} }], + }, + ]; + const nestedConditions: Condition[] = []; + extractNestedConditions(conditions, criteriaTypes, nestedConditions); + + expect(nestedConditions).toEqual([ + { + anyOf: [{ rule: 'rule2', resourceType: 'catalog-entity', params: {} }], + }, + ]); + }); + + it('should not add conditions if nested conditions exist', () => { + const conditions: Condition[] = [ + { condition: { rule: 'value1', resourceType: 'value2' } }, + ]; + const nestedConditions: Condition[] = []; + + extractNestedConditions(conditions, criteriaTypes, nestedConditions); + + expect(nestedConditions).toEqual([]); + }); + + it('should handle an empty conditions array', () => { + const conditions: Condition[] = []; + const nestedConditions: Condition[] = []; + + extractNestedConditions(conditions, criteriaTypes, nestedConditions); + + expect(nestedConditions).toEqual([]); + }); + + it('should not duplicate conditions if only contains simple rules', () => { + const conditions: Condition[] = [ + { rule: 'rule1', resourceType: 'catalog-entity', params: {} }, + { rule: 'rule2', resourceType: 'catalog-entity', params: {} }, + ]; + const nestedConditions: ConditionsData[] = []; + + extractNestedConditions(conditions, criteriaTypes, nestedConditions); + + expect(nestedConditions).toEqual([]); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/utils/conditional-access-utils.ts b/workspaces/rbac/plugins/rbac/src/utils/conditional-access-utils.ts new file mode 100644 index 0000000000..f212af1901 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/utils/conditional-access-utils.ts @@ -0,0 +1,403 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PermissionCondition } from '@backstage/plugin-permission-common'; + +import { makeStyles, Theme } from '@material-ui/core'; +import { RJSFValidationError } from '@rjsf/utils'; + +import { + conditionButtons, + criterias, +} from '../components/ConditionalAccess/const'; +import { + AccessConditionsErrors, + ComplexErrors, + Condition, + ConditionsData, + NestedCriteriaErrors, + NotConditionType, +} from '../components/ConditionalAccess/types'; + +export const ruleOptionDisabled = ( + ruleOption: string, + conditions?: PermissionCondition[], +) => { + return !!(conditions || []).find(con => con.rule === ruleOption); +}; + +export const nestedConditionButtons = conditionButtons.filter( + button => button.val !== 'condition', +); + +export const extractNestedConditions = ( + conditions: Condition[], + criteriaTypes: string[], + nestedConditions: Condition[], +) => { + conditions.forEach(c => { + criteriaTypes.forEach(ct => { + if (Object.keys(c).includes(ct)) { + nestedConditions.push(c); + } + }); + }); +}; + +export const getDefaultRule = (selPluginResourceType: string) => ({ + rule: '', + resourceType: selPluginResourceType, + params: {}, +}); + +export const makeConditionsFormRowStyles = makeStyles(theme => ({ + conditionRow: { + padding: '20px', + border: `1px solid ${theme.palette.border}`, + borderRadius: '4px', + backgroundColor: theme.palette.background.default, + '& input': { + color: `${theme.palette.textContrast}!important`, + '&:-internal-autofill-selected, &:-webkit-autofill, &:-webkit-autofill:hover, &:-webkit-autofill:focus, &:-webkit-autofill:active': + { + WebkitBoxShadow: `0 0 0px 1000px ${theme.palette.background.paper} inset`, + WebkitTextFillColor: `${theme.palette.textContrast}!important`, + caretColor: `${theme.palette.textContrast}!important`, + }, + }, + '& button': { + textTransform: 'none', + }, + }, + nestedConditionRow: { + padding: '20px', + marginLeft: theme.spacing(3), + border: `1px solid ${theme.palette.border}`, + borderRadius: '4px', + backgroundColor: theme.palette.background.default, + '& input': { + backgroundColor: `${theme.palette.background.paper}!important`, + }, + }, + criteriaButtonGroup: { + backgroundColor: theme.palette.background.paper, + width: '80%', + }, + criteriaButton: { + width: '100%', + padding: `${theme.spacing(1)}px !important`, + }, + nestedConditioncriteriaButtonGroup: { + backgroundColor: theme.palette.background.paper, + width: '60%', + height: '100%', + }, + addRuleButton: { + display: 'flex !important', + color: theme.palette.primary.light, + textTransform: 'none', + }, + addNestedConditionButton: { + display: 'flex !important', + color: theme.palette.primary.light, + textTransform: 'none', + }, + removeRuleButton: { + color: theme.palette.grey[500], + flexGrow: 0, + alignSelf: 'baseline', + marginTop: theme.spacing(3.3), + }, + removeNestedRuleButton: { + color: theme.palette.grey[500], + flexGrow: 0, + alignSelf: 'baseline', + }, + radioGroup: { + margin: theme.spacing(1), + }, + radioLabel: { + marginTop: theme.spacing(1), + }, +})); + +interface StyleProps { + isNotSimpleCondition: boolean; +} +export const makeConditionsFormRowFieldsStyles = makeStyles( + theme => ({ + bgPaper: { + backgroundColor: theme.palette.background.paper, + }, + params: { + '& div[class*="MuiInputBase-root"]': { + backgroundColor: theme.palette.background.paper, + }, + '& span': { + color: theme.palette.textSubtle, + }, + '& input': { + color: theme.palette.textContrast, + }, + '& fieldset.MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.grey[500], + }, + '& div.MuiOutlinedInput-root': { + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.primary.light, + }, + '&.Mui-error .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.status.error, + '&:hover': { + borderColor: theme.palette.status.error, + }, + }, + }, + '& label.MuiFormLabel-root.Mui-focused': { + color: theme.palette.primary.light, + }, + '& label.MuiFormLabel-root.Mui-error': { + color: theme.palette.status.error, + }, + '& div.MuiOutlinedInput-root:hover fieldset': { + borderColor: + theme.palette.type === 'dark' ? theme.palette.textContrast : 'unset', + }, + '& label': { + color: theme.palette.textSubtle, + }, + }, + inputFieldContainer: { + display: 'flex', + flexFlow: 'row', + gap: '10px', + flexGrow: 1, + margin: ({ isNotSimpleCondition }) => + isNotSimpleCondition ? '-1.5rem 0 0 1.85rem' : '0', + }, + }), +); + +export const getSimpleRulesCount = ( + conditionRow: ConditionsData, + criteria: string, +): number => { + if (criteria === criterias.not) { + return (conditionRow[criteria as keyof Condition] as PermissionCondition) + .resourceType + ? 1 + : 0; + } + if (criteria === criterias.condition) { + return 1; + } + return (conditionRow[criteria as keyof Condition] as Condition[]).filter( + (e: Condition) => 'rule' in e, + ).length; +}; + +export const initializeErrors = ( + criteria: keyof ConditionsData, + conditions: ConditionsData, +): AccessConditionsErrors => { + const errors: AccessConditionsErrors = {}; + const initialize = (cond: Condition | ConditionsData): ComplexErrors => { + if ('rule' in cond) { + return ''; + } + + const nestedErrors: NestedCriteriaErrors = {}; + if (cond.allOf) { + nestedErrors.allOf = (cond.allOf.map(initialize) as string[]) || []; + } + if (cond.anyOf) { + nestedErrors.anyOf = (cond.anyOf.map(initialize) as string[]) || []; + } + if (cond.not) { + nestedErrors.not = (initialize(cond.not) as string) || ''; + } + + return nestedErrors; + }; + + if (criteria === criterias.condition) { + errors.condition = ''; + } else if (criteria === criterias.not) { + const notCondition = conditions.not; + + let notConditionType; + if (notCondition === undefined) { + notConditionType = NotConditionType.SimpleCondition; + } else if ('rule' in notCondition) { + notConditionType = NotConditionType.SimpleCondition; + } else { + notConditionType = NotConditionType.NestedCondition; + } + + if (notConditionType === NotConditionType.SimpleCondition) { + errors.not = ''; + } else { + errors.not = initialize(conditions.not!); + } + } else if (criteria === criterias.allOf || criteria === criterias.anyOf) { + errors[criteria] = Array.isArray(conditions[criteria]) + ? (conditions[criteria] as Condition[]).map(initialize) + : ['']; + } + + return errors; +}; + +export const resetErrors = ( + criteria: string, + notConditionType = NotConditionType.SimpleCondition, +): AccessConditionsErrors => { + const errors: AccessConditionsErrors = {}; + + if ( + criteria === criterias.condition || + (criteria === criterias.not && + notConditionType === NotConditionType.SimpleCondition) + ) { + errors[criteria] = ''; + } + + if (criteria === criterias.allOf || criteria === criterias.anyOf) { + errors[criteria] = [''] as ComplexErrors[]; + } + + if ( + criteria === criterias.not && + notConditionType === NotConditionType.NestedCondition + ) { + (errors[criteria] as ComplexErrors) = { [criterias.allOf]: [''] }; + } + + return errors; +}; + +export const setErrorMessage = (errors: RJSFValidationError[]) => + errors[0] ? `Error in the ${errors[0].property} field.` : ''; + +export const getSimpleRuleErrors = (errors: ComplexErrors[]): string[] => + (errors.filter( + (err: ComplexErrors) => typeof err === 'string', + ) as string[]) || []; + +export const getNestedRuleErrors = ( + errors: ComplexErrors[], +): NestedCriteriaErrors[] => + (errors.filter( + (err: ComplexErrors) => typeof err !== 'string', + ) as NestedCriteriaErrors[]) || []; + +export const isNestedConditionRule = (r: Condition): boolean => { + return ( + criterias.allOf in (r as ConditionsData) || + criterias.anyOf in (r as ConditionsData) || + criterias.not in (r as ConditionsData) + ); +}; + +export const getNestedConditionSimpleRulesCount = ( + nc: Condition, + c: string, +): number => { + if (c === criterias.not) { + return (nc[c as keyof Condition] as PermissionCondition).resourceType + ? 1 + : 0; + } + + return (nc[c as keyof Condition] as Condition[]).filter( + r => 'resourceType' in r, + ).length; +}; + +export const getRowStyle = (c: Condition, isNestedCondition: boolean) => + isNestedCondition + ? { + display: + (c as PermissionCondition).rule !== undefined ? 'flex' : 'none', + } + : { display: 'flex', gap: '10px' }; + +export const getRowKey = (isNestedCondition: boolean, ruleIndex: number) => + isNestedCondition + ? `nestedCondition-rule-${ruleIndex}` + : `condition-rule-${ruleIndex}`; + +export const hasAllOfOrAnyOfErrors = ( + errors: AccessConditionsErrors, + criteria: string, +): boolean => { + if (!errors) return false; + + const criteriaErrors = errors[ + criteria as keyof AccessConditionsErrors + ] as ComplexErrors[]; + const simpleRuleErrors = criteriaErrors.filter( + e => typeof e === 'string', + ) as string[]; + const nestedRuleErrors = criteriaErrors.filter( + e => typeof e !== 'string', + ) as NestedCriteriaErrors[]; + + if (simpleRuleErrors.some(e => e.length > 0)) { + return true; + } + + return nestedRuleErrors.some(err => { + const nestedCriteria = Object.keys(err)[0] as keyof NestedCriteriaErrors; + const nestedErrors = err[nestedCriteria]; + + if (Array.isArray(nestedErrors)) { + return nestedErrors.some(e => e.length > 0); + } + return nestedErrors?.length > 0; + }); +}; + +export const hasSimpleConditionOrNotErrors = ( + errors: AccessConditionsErrors, + criteria: string, +): boolean => { + if (!errors) return false; + return ( + ((errors[criteria as keyof AccessConditionsErrors] as string) || '') + .length > 0 + ); +}; + +export const hasNestedNotErrors = ( + errors: AccessConditionsErrors, + conditions: ConditionsData, + criteria: keyof ConditionsData, +): boolean => { + if (!errors) return false; + const nestedCriteria = Object.keys(conditions[criteria]!)[0]; + const nestedErrors = ( + errors[ + criterias.not as keyof AccessConditionsErrors + ] as NestedCriteriaErrors + )[nestedCriteria]; + + if (Array.isArray(nestedErrors)) { + return nestedErrors.some(e => e.length > 0); + } + return nestedErrors?.length > 0; +}; + +export const isSimpleRule = (con: Condition): boolean => 'rule' in con; diff --git a/workspaces/rbac/plugins/rbac/src/utils/create-role-utils.test.ts b/workspaces/rbac/plugins/rbac/src/utils/create-role-utils.test.ts new file mode 100644 index 0000000000..e7f9dcae79 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/utils/create-role-utils.test.ts @@ -0,0 +1,704 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { PolicyDetails } from '@backstage-community/plugin-rbac-common'; + +import { + mockFormCurrentValues, + mockFormInitialValues, +} from '../__fixtures__/mockFormValues'; +import { mockMembers } from '../__fixtures__/mockMembers'; +import { mockPermissionPolicies } from '../__fixtures__/mockPermissionPolicies'; +import { ConditionsData } from '../components/ConditionalAccess/types'; +import { + getChildGroupsCount, + getConditionalPermissionPoliciesData, + getMembersCount, + getNewConditionalPolicies, + getParentGroupsCount, + getPermissionPolicies, + getPermissionPoliciesData, + getPluginsPermissionPoliciesData, + getRemovedConditionalPoliciesIds, + getRoleData, + getRulesNumber, + getUpdatedConditionalPolicies, +} from './create-role-utils'; + +describe('getRoleData', () => { + it('should return role data object', () => { + let values = { + name: 'testRole', + namespace: 'default', + kind: 'group', + selectedMembers: [ + { + type: 'User', + namespace: 'default', + label: 'user1', + etag: '1', + ref: 'user:default/user1', + }, + { + type: 'Group', + namespace: 'default', + label: 'group1', + etag: '2', + ref: 'group:default/group1', + }, + ], + permissionPoliciesRows: [ + { + plugin: '', + permission: '', + policies: [ + { policy: 'Create', effect: 'deny' }, + { policy: 'Read', effect: 'deny' }, + { policy: 'Update', effect: 'deny' }, + { policy: 'Delete', effect: 'deny' }, + ], + isResourced: false, + }, + ], + }; + + let result = getRoleData(values); + + expect(result).toEqual({ + memberReferences: ['user:default/user1', 'group:default/group1'], + metadata: { + description: undefined, + }, + name: 'group:default/testRole', + }); + + values = { + name: 'testRole', + namespace: 'default', + kind: 'user', + selectedMembers: [ + { + type: 'User', + namespace: 'default', + label: 'user1', + etag: '1', + ref: 'user:default/user1', + }, + { + type: 'Group', + namespace: 'default', + label: 'group1', + etag: '2', + ref: 'group:default/group1', + }, + ], + permissionPoliciesRows: [ + { + plugin: '', + permission: '', + policies: [ + { policy: 'Create', effect: 'deny' }, + { policy: 'Read', effect: 'deny' }, + { policy: 'Update', effect: 'deny' }, + { policy: 'Delete', effect: 'deny' }, + ], + isResourced: false, + }, + ], + }; + + result = getRoleData(values); + + expect(result).toEqual({ + memberReferences: ['user:default/user1', 'group:default/group1'], + metadata: { + description: undefined, + }, + name: 'user:default/testRole', + }); + }); +}); + +describe('getMembersCount', () => { + it('should return the number of members for a group', () => { + const group = mockMembers[0]; + + const result = getMembersCount(group); + + expect(result).toBe(2); + }); + + it('should return 0 if there are no members in the group', () => { + const group = mockMembers[1]; + + const result = getMembersCount(group); + + expect(result).toBe(0); + }); + + it('should return undefined for non-group entities', () => { + const user = mockMembers[2]; + + const result = getMembersCount(user); + + expect(result).toBeUndefined(); + }); +}); + +describe('getParentGroupsCount', () => { + it('should return the number of parent groups for a group', () => { + const group = mockMembers[0]; + + const result = getParentGroupsCount(group); + + expect(result).toBe(1); + }); + + it('should return undefined for non-group entities', () => { + const user = mockMembers[2]; + + const result = getParentGroupsCount(user); + + expect(result).toBeUndefined(); + }); +}); + +describe('getChildGroupsCount', () => { + it('should return the number of child groups for a group', () => { + const group = mockMembers[8]; + + const result = getChildGroupsCount(group); + + expect(result).toBe(2); + }); + + it('should return undefined for non-group entities', () => { + const user = mockMembers[2]; + + const result = getChildGroupsCount(user); + + expect(result).toBeUndefined(); + }); +}); + +describe('getPermissionPolicies', () => { + it('returns empty object for empty input', () => { + const result = getPermissionPolicies([]); + expect(result).toEqual({}); + }); + + it('correctly transforms policies into PermissionPolicies', () => { + const policies: PolicyDetails[] = [ + { + resourceType: 'catalog-entity', + name: 'catalog.entity.read', + policy: 'read', + }, + { + name: 'catalog.entity.create', + policy: 'create', + }, + { + resourceType: 'catalog-entity', + name: 'catalog.entity.delete', + policy: 'delete', + }, + { + resourceType: 'catalog-entity', + name: 'catalog.entity.update', + policy: 'update', + }, + ]; + const result = getPermissionPolicies(policies); + expect(result).toEqual({ + 'catalog-entity': { + policies: ['Read', 'Delete', 'Update'], + isResourced: true, + }, + 'catalog.entity.create': { policies: ['Create'], isResourced: false }, + }); + }); +}); + +describe('getPluginsPermissionPoliciesData', () => { + it('returns empty object for empty input', () => { + const result = getPluginsPermissionPoliciesData([]); + expect(result).toEqual({ plugins: [], pluginsPermissions: {} }); + }); + + it('correctly transforms pluginsPermissionPolicies', () => { + const result = getPluginsPermissionPoliciesData(mockPermissionPolicies); + expect(result).toEqual({ + plugins: ['catalog', 'scaffolder', 'permission'], + pluginsPermissions: { + catalog: { + permissions: [ + 'catalog-entity', + 'catalog.entity.create', + 'catalog.location.read', + 'catalog.location.create', + 'catalog.location.delete', + ], + policies: { + 'catalog-entity': { + policies: ['Read', 'Delete', 'Update'], + isResourced: true, + }, + 'catalog.entity.create': { + policies: ['Create'], + isResourced: false, + }, + 'catalog.location.read': { + policies: ['Read'], + isResourced: false, + }, + 'catalog.location.create': { + policies: ['Create'], + isResourced: false, + }, + 'catalog.location.delete': { + policies: ['Delete'], + isResourced: false, + }, + }, + }, + scaffolder: { + permissions: ['scaffolder-template', 'scaffolder-action'], + policies: { + 'scaffolder-template': { policies: ['Read'], isResourced: true }, + 'scaffolder-action': { policies: ['Use'], isResourced: true }, + }, + }, + permission: { + permissions: ['policy-entity'], + policies: { + 'policy-entity': { + policies: ['Read', 'Create', 'Delete', 'Update'], + isResourced: false, + }, + }, + }, + }, + }); + }); +}); + +describe('getPermissionPoliciesData', () => { + it('returns empty array for empty input', () => { + const result = getPermissionPoliciesData({ + kind: 'role', + name: 'testRole', + namespace: 'default', + selectedMembers: [], + permissionPoliciesRows: [], + }); + expect(result).toEqual([]); + }); + + it('correctly transforms permissionPoliciesRows into RoleBasedPolicy', () => { + const values = { + name: 'testRole', + namespace: 'default', + kind: 'role', + selectedMembers: [], + permissionPoliciesRows: [ + { + plugin: 'scaffolder', + permission: 'scaffolder-template', + policies: [ + { + policy: 'Read', + effect: 'allow', + }, + ], + }, + { + plugin: 'catalog', + permission: 'catalog-entity', + policies: [ + { + policy: 'Read', + effect: 'allow', + }, + { + policy: 'Delete', + effect: 'allow', + }, + { + policy: 'Update', + effect: 'allow', + }, + ], + }, + ], + }; + const result = getPermissionPoliciesData(values); + expect(result).toEqual([ + { + entityReference: 'role:default/testRole', + permission: 'scaffolder-template', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'role:default/testRole', + permission: 'catalog-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'role:default/testRole', + permission: 'catalog-entity', + policy: 'delete', + effect: 'allow', + }, + { + entityReference: 'role:default/testRole', + permission: 'catalog-entity', + policy: 'update', + effect: 'allow', + }, + ]); + }); +}); + +describe('getConditionalPermissionPoliciesData', () => { + it('should return conditional permission policies data correctly', () => { + const values = mockFormCurrentValues; + + const result = getConditionalPermissionPoliciesData(values); + + expect(result).toEqual([ + { + result: 'CONDITIONAL', + roleEntityRef: 'user:default/div', + pluginId: 'catalog', + resourceType: 'catalog-entity', + permissionMapping: ['read'], + conditions: { + rule: 'HAS_LABEL', + params: { + label: 'temp', + }, + resourceType: 'catalog-entity', + }, + }, + ]); + }); + + it('should return empty array if permissionPoliciesRows is empty', () => { + const values = { + kind: 'user', + name: 'div', + namespace: 'default', + selectedMembers: [], + permissionPoliciesRows: [], + }; + + const result = getConditionalPermissionPoliciesData(values); + + expect(result).toEqual([]); + }); +}); + +describe('getUpdatedConditionalPolicies', () => { + it('should return updated conditional policies correctly', () => { + const values = { + kind: 'user', + name: 'div', + namespace: 'default', + selectedMembers: [], + permissionPoliciesRows: [ + { + id: 1, + permission: 'catalog-entity', + policies: [{ policy: 'update', effect: 'allow' }], + isResourced: true, + plugin: 'catalog', + conditions: { + allOf: [ + { + rule: 'HAS_LABEL', + params: { + label: 'temp', + }, + resourceType: 'catalog-entity', + }, + { + rule: 'HAS_SPEC', + params: { + label: 'test', + }, + resourceType: 'catalog-entity', + }, + ], + }, + }, + ], + }; + + const initialValues = mockFormInitialValues; + + const result = getUpdatedConditionalPolicies(values, initialValues); + + expect(result).toEqual([ + { + id: 1, + updateCondition: { + result: 'CONDITIONAL', + roleEntityRef: 'user:default/div', + pluginId: 'catalog', + resourceType: 'catalog-entity', + permissionMapping: ['update'], + conditions: { + allOf: [ + { + rule: 'HAS_LABEL', + params: { + label: 'temp', + }, + resourceType: 'catalog-entity', + }, + { + rule: 'HAS_SPEC', + params: { + label: 'test', + }, + resourceType: 'catalog-entity', + }, + ], + }, + }, + }, + ]); + }); + + it('should return empty array if values.permissionPoliciesRows is empty', () => { + const values = { + kind: 'user', + name: 'div', + namespace: 'default', + selectedMembers: [], + permissionPoliciesRows: [], + }; + + const initialValues = mockFormInitialValues; + + const result = getUpdatedConditionalPolicies(values, initialValues); + + expect(result).toEqual([]); + }); +}); + +describe('getNewConditionalPolicies', () => { + it('should return new conditional policies correctly', () => { + const values = mockFormCurrentValues; + + const result = getNewConditionalPolicies(values); + + expect(result).toEqual([ + { + result: 'CONDITIONAL', + roleEntityRef: 'user:default/div', + pluginId: 'catalog', + resourceType: 'catalog-entity', + permissionMapping: ['read'], + conditions: { + rule: 'HAS_LABEL', + params: { + label: 'temp', + }, + resourceType: 'catalog-entity', + }, + }, + ]); + }); + + it('should return empty array if values.permissionPoliciesRows is empty', () => { + const values = { + kind: 'user', + name: 'div', + namespace: 'default', + selectedMembers: [], + permissionPoliciesRows: [], + }; + + const result = getNewConditionalPolicies(values); + + expect(result).toEqual([]); + }); +}); + +describe('getRemovedConditionalPoliciesIds', () => { + it('should return removed conditional policies IDs correctly', () => { + const values = { + kind: 'user', + name: 'div', + namespace: 'default', + selectedMembers: [], + permissionPoliciesRows: [], + }; + + const initialValues = mockFormInitialValues; + + const result = getRemovedConditionalPoliciesIds(values, initialValues); + + expect(result).toEqual([1]); + }); + + it('should return empty array if both values.permissionPoliciesRows and initialValues.permissionPoliciesRows are empty', () => { + const values = { + kind: 'user', + name: 'div', + namespace: 'default', + selectedMembers: [], + permissionPoliciesRows: [], + }; + + const initialValues = { + kind: 'user', + name: 'div', + namespace: 'default', + selectedMembers: [], + permissionPoliciesRows: [], + }; + + const result = getRemovedConditionalPoliciesIds(values, initialValues); + + expect(result).toEqual([]); + }); +}); + +describe('getRulesNumber', () => { + it('should return the correct number of rules', () => { + const conditions: ConditionsData = { + allOf: [ + { + rule: 'HAS_ANNOTATION', + resourceType: 'catalog-entity', + params: { + annotation: 'k', + }, + }, + { + allOf: [ + { + rule: 'HAS_LABEL', + resourceType: 'catalog-entity', + params: { + label: 'h', + }, + }, + ], + }, + { + anyOf: [ + { + rule: 'HAS_LABEL', + resourceType: 'catalog-entity', + params: { + label: 'h', + }, + }, + { + rule: 'HAS_ANNOTATION', + resourceType: 'catalog-entity', + params: { + annotation: 'k', + }, + }, + ], + }, + ], + }; + + const result = getRulesNumber(conditions); + expect(result).toBe(4); + }); + + it('should return 0 when there are no conditions', () => { + const result = getRulesNumber(); + expect(result).toBe(0); + }); + + it('should count rules correctly with nested anyOf and allOf', () => { + const conditions: ConditionsData = { + anyOf: [ + { + rule: 'HAS_LABEL', + resourceType: 'catalog-entity', + params: { + label: 'x', + }, + }, + { + allOf: [ + { + rule: 'HAS_ANNOTATION', + resourceType: 'catalog-entity', + params: { + annotation: 'y', + }, + }, + { + anyOf: [ + { + rule: 'HAS_LABEL', + resourceType: 'catalog-entity', + params: { + label: 'z', + }, + }, + ], + }, + ], + }, + ], + }; + + const result = getRulesNumber(conditions); + expect(result).toBe(3); + }); + + it('should count single condition correctly', () => { + const conditions: ConditionsData = { + condition: { + rule: 'HAS_LABEL', + resourceType: 'catalog-entity', + params: { + label: 'a', + }, + }, + }; + + const result = getRulesNumber(conditions); + expect(result).toBe(1); + }); + + it('should count rules correctly with not condition', () => { + const conditions: ConditionsData = { + not: { + rule: 'HAS_LABEL', + resourceType: 'catalog-entity', + params: { + label: 'b', + }, + }, + }; + + const result = getRulesNumber(conditions); + expect(result).toBe(1); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/utils/create-role-utils.ts b/workspaces/rbac/plugins/rbac/src/utils/create-role-utils.ts new file mode 100644 index 0000000000..1d83da6e7d --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/utils/create-role-utils.ts @@ -0,0 +1,342 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { getTitleCase } from '@janus-idp/shared-react'; +import * as yup from 'yup'; + +import { + isResourcedPolicy, + PluginPermissionMetaData, + PolicyDetails, + ResourcedPolicy, + Role, + RoleBasedPolicy, +} from '@backstage-community/plugin-rbac-common'; + +import { criterias } from '../components/ConditionalAccess/const'; +import { ConditionsData } from '../components/ConditionalAccess/types'; +import { + PermissionPolicies, + PluginsPermissionPoliciesData, + PluginsPermissions, + RoleFormValues, + SelectedMember, +} from '../components/CreateRole/types'; +import { + MemberEntity, + PermissionsData, + RoleBasedConditions, + UpdatedConditionsData, +} from '../types'; + +export const uniqBy = (arr: string[], iteratee: (arg: string) => any) => { + return arr.filter( + (x, i, self) => i === self.findIndex(y => iteratee(x) === iteratee(y)), + ); +}; + +export const getRoleData = (values: RoleFormValues): Role => { + return { + memberReferences: values.selectedMembers.map( + (mem: SelectedMember) => mem.ref, + ), + name: `${values.kind}:${values.namespace}/${values.name}`, + metadata: { + description: values.description, + }, + }; +}; + +export const validationSchema = yup.object({ + name: yup.string().required('Name is required'), + selectedMembers: yup.array().min(1, 'No member selected'), + permissionPoliciesRows: yup.array().of( + yup.object().shape({ + plugin: yup.string().required('Plugin is required'), + permission: yup.string().required('Permission is required'), + }), + ), +}); + +export const getMembersCount = (member: MemberEntity) => { + return member.kind === 'Group' + ? member.relations?.reduce((acc: any, relation: { type: string }) => { + let temp = acc; + if (relation.type === 'hasMember') { + temp++; + } + return temp; + }, 0) + : undefined; +}; + +export const getParentGroupsCount = (member: MemberEntity) => { + return member.kind === 'Group' + ? member.relations?.reduce((acc: any, relation: { type: string }) => { + let temp = acc; + if (relation.type === 'childOf') { + temp++; + } + return temp; + }, 0) + : undefined; +}; + +export const getChildGroupsCount = (member: MemberEntity) => { + return member.kind === 'Group' + ? member.relations?.reduce((acc: any, relation: { type: string }) => { + let temp = acc; + if (relation.type === 'parentOf') { + temp++; + } + return temp; + }, 0) + : undefined; +}; + +export const getPermissionPolicies = ( + policies: PolicyDetails[], +): PermissionPolicies => { + return policies.reduce( + (ppsAcc: PermissionPolicies, policy: PolicyDetails) => { + const permission = isResourcedPolicy(policy) + ? (policy as ResourcedPolicy).resourceType + : policy.name; + return { + ...ppsAcc, + [permission]: policies.reduce( + (policiesAcc: { policies: string[]; isResourced: boolean }, pol) => { + const perm = isResourcedPolicy(pol) + ? (pol as ResourcedPolicy).resourceType + : pol.name; + if (permission === perm) + return { + policies: uniqBy( + [...policiesAcc.policies, getTitleCase(pol.policy as string)], + val => val, + ), + isResourced: isResourcedPolicy(pol), + }; + return policiesAcc; + }, + { policies: [], isResourced: false }, + ), + }; + }, + {}, + ); +}; + +export const getPluginsPermissionPoliciesData = ( + pluginsPermissionPolicies: PluginPermissionMetaData[], +): PluginsPermissionPoliciesData => { + const plugins: string[] = pluginsPermissionPolicies.map( + pluginPp => pluginPp.pluginId, + ); + const pluginsPermissions = pluginsPermissionPolicies.reduce( + (acc: PluginsPermissions, pp, index) => { + const permissions = pp.policies.reduce((plcAcc: string[], plc) => { + const permission = isResourcedPolicy(plc) + ? (plc as ResourcedPolicy).resourceType + : plc.name; + return [...plcAcc, permission]; + }, []); + return { + ...acc, + [plugins[index]]: { + permissions: uniqBy(permissions ?? [], val => val), + policies: { + ...(pp.policies ? getPermissionPolicies(pp.policies) : {}), + }, + }, + }; + }, + {}, + ); + return { plugins, pluginsPermissions }; +}; + +export const getPermissionPoliciesData = ( + values: RoleFormValues, +): RoleBasedPolicy[] => { + const { kind, name, namespace, permissionPoliciesRows } = values; + + return permissionPoliciesRows.reduce( + (acc: RoleBasedPolicy[], permissionPolicyRow) => { + const { permission, policies, conditions } = permissionPolicyRow; + const permissionPoliciesData = policies.reduce( + (pAcc: RoleBasedPolicy[], policy) => { + if (policy.effect === 'allow' && !conditions) { + return [ + ...pAcc, + { + entityReference: `${kind}:${namespace}/${name}`, + permission: `${permission}`, + policy: policy.policy.toLocaleLowerCase('en-US'), + effect: 'allow', + }, + ]; + } + return pAcc; + }, + [], + ); + return [...acc, ...permissionPoliciesData]; + }, + [], + ); +}; + +export const getConditionalPermissionPoliciesData = ( + values: RoleFormValues, +) => { + const { kind, name, namespace, permissionPoliciesRows } = values; + + return permissionPoliciesRows.reduce( + (acc: RoleBasedConditions[], permissionPolicyRow: PermissionsData) => { + const { permission, policies, isResourced, plugin, conditions } = + permissionPolicyRow; + const permissionMapping = policies.reduce((pAcc: string[], policy) => { + if (policy.effect === 'allow') { + return [...pAcc, policy.policy.toLocaleLowerCase('en-US')]; + } + return pAcc; + }, []); + return isResourced && conditions + ? [ + ...acc, + { + result: 'CONDITIONAL', + roleEntityRef: `${kind}:${namespace}/${name}`, + pluginId: `${plugin}`, + resourceType: `${permission}`, + permissionMapping, + conditions: + Object.keys(conditions)[0] === criterias.condition + ? { ...conditions.condition } + : conditions, + } as RoleBasedConditions, + ] + : acc; + }, + [] as RoleBasedConditions[], + ); +}; + +export const getUpdatedConditionalPolicies = ( + values: RoleFormValues, + initialValues: RoleFormValues, +): UpdatedConditionsData => { + const initialConditionsWithId = initialValues.permissionPoliciesRows.filter( + ppr => ppr.id, + ); + + const conditionsWithId = values.permissionPoliciesRows.filter(ppr => ppr.id); + + return conditionsWithId.length > 0 + ? conditionsWithId.reduce( + ( + acc: { id: number; updateCondition: RoleBasedConditions }[], + condition: PermissionsData, + ) => { + const conditionExists = initialConditionsWithId.find( + c => c.id === condition.id, + ); + + if (conditionExists && condition.id) + return [ + ...acc, + { + id: condition.id, + updateCondition: getConditionalPermissionPoliciesData({ + ...values, + permissionPoliciesRows: [condition], + })[0], + }, + ]; + return acc; + }, + [], + ) + : []; +}; + +export const getNewConditionalPolicies = (values: RoleFormValues) => { + const newValues = { ...values }; + const newPermissionPolicies = values.permissionPoliciesRows.filter( + ppr => !ppr.id, + ); + newValues.permissionPoliciesRows = newPermissionPolicies; + return getConditionalPermissionPoliciesData(newValues); +}; + +export const getRemovedConditionalPoliciesIds = ( + values: RoleFormValues, + initialValues: RoleFormValues, +) => { + const initialConditionsIds = initialValues.permissionPoliciesRows + .map(ppr => ppr.id) + .filter(id => id); + + const newConditionsIds = values.permissionPoliciesRows + .map(ppr => ppr.id) + .filter(id => id); + + return initialConditionsIds.length > 0 + ? initialConditionsIds.reduce((acc: number[], oldId) => { + const conditionExists = newConditionsIds.includes(oldId); + if (conditionExists) return acc; + return oldId ? [...acc, oldId] : acc; + }, []) + : []; +}; + +export const getPermissionsNumber = (values: RoleFormValues) => { + return ( + getPermissionPoliciesData(values).length + + getConditionalPermissionPoliciesData(values).length + ); +}; + +export const getConditionsNumber = (values: RoleFormValues) => { + return getConditionalPermissionPoliciesData(values)?.length ?? 0; +}; + +export const getRulesNumber = (conditions?: ConditionsData) => { + if (!conditions) return 0; + let rulesNumber = 0; + + if (conditions.allOf) { + rulesNumber += conditions.allOf.reduce((acc, condition) => { + return acc + getRulesNumber(condition as ConditionsData); + }, 0); + } + + if (conditions.anyOf) { + rulesNumber += conditions.anyOf.reduce((acc, condition) => { + return acc + getRulesNumber(condition as ConditionsData); + }, 0); + } + + if (conditions.not) { + rulesNumber += getRulesNumber(conditions.not as ConditionsData); + } + + if (conditions.condition || Object.keys(conditions).includes('rule')) { + rulesNumber += 1; + } + + return rulesNumber; +}; diff --git a/workspaces/rbac/plugins/rbac/src/utils/rbac-utils.test.ts b/workspaces/rbac/plugins/rbac/src/utils/rbac-utils.test.ts new file mode 100644 index 0000000000..d739d2d9a5 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/utils/rbac-utils.test.ts @@ -0,0 +1,519 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { GroupEntity } from '@backstage/catalog-model'; +import { + AllOfCriteria, + AuthorizeResult, + PermissionCondition, +} from '@backstage/plugin-permission-common'; + +import { + PermissionAction, + RoleConditionalPolicyDecision, +} from '@backstage-community/plugin-rbac-common'; + +import { mockConditions } from '../__fixtures__/mockConditions'; +import { mockPermissionPolicies } from '../__fixtures__/mockPermissionPolicies'; +import { + getConditionalPermissionsData, + getConditionsData, + getConditionUpperCriteria, + getMembers, + getMembersFromGroup, + getPermissions, + getPermissionsData, + getPluginInfo, + getPoliciesData, +} from './rbac-utils'; + +const mockPolicies = [ + { + entityReference: 'role:default/guests', + permission: 'catalog-entity', + policy: 'read', + effect: 'deny', + }, + { + entityReference: 'role:default/guests', + permission: 'catalog.entity.create', + policy: 'use', + effect: 'deny', + }, + { + entityReference: 'user:default/xyz', + permission: 'policy-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'user:default/xyz', + permission: 'policy-entity', + policy: 'create', + effect: 'allow', + }, + { + entityReference: 'user:default/xyz', + permission: 'policy-entity', + policy: 'delete', + effect: 'allow', + }, + { + entityReference: 'user:default/xyz', + permission: 'catalog-entity', + policy: 'read', + effect: 'allow', + }, + { + entityReference: 'user:default/xyz', + permission: 'catalog.entity.create', + policy: 'use', + effect: 'allow', + }, +]; + +describe('rbac utils', () => { + it('should list associated allowed permissions for a role', () => { + expect(getPermissions('role:default/guests', mockPolicies)).toBe(0); + expect(getPermissions('user:default/xyz', mockPolicies)).toBe(5); + }); + + it('should return number of users and groups in member references', () => { + expect(getMembers(['user:default/xyz', 'group:default/admins'])).toBe( + '1 user, 1 group', + ); + + expect( + getMembers([ + 'user:default/xyz', + 'group:default/admins', + 'user:default/alice', + ]), + ).toBe('2 users, 1 group'); + + expect(getMembers(['user:default/xyz'])).toBe('1 user'); + + expect(getMembers(['group:default/xyz'])).toBe('1 group'); + + expect(getMembers([])).toBe('No members'); + }); + + it('should return number of members in a group', () => { + let resource: GroupEntity = { + metadata: { + namespace: 'default', + annotations: {}, + name: 'team-b', + description: 'Team B', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'team', + profile: {}, + parent: 'backstage', + children: [], + }, + relations: [ + { + type: 'childOf', + targetRef: 'group:default/backstage', + }, + { + type: 'hasMember', + targetRef: 'user:default/amelia.park', + }, + { + type: 'hasMember', + targetRef: 'user:default/colette.brock', + }, + { + type: 'hasMember', + targetRef: 'user:default/jenny.doe', + }, + { + type: 'hasMember', + targetRef: 'user:default/jonathon.page', + }, + { + type: 'hasMember', + targetRef: 'user:default/justine.barrow', + }, + ], + }; + expect(getMembersFromGroup(resource)).toBe(5); + + resource = { + metadata: { + namespace: 'default', + annotations: {}, + name: 'team-b', + description: 'Team B', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + spec: { + type: 'team', + profile: {}, + parent: 'backstage', + children: [], + }, + relations: [ + { + type: 'childOf', + targetRef: 'group:default/backstage', + }, + ], + }; + + expect(getMembersFromGroup(resource)).toBe(0); + }); + + it('should return plugin-id of the policy', () => { + expect( + getPluginInfo(mockPermissionPolicies, 'catalog-entity').pluginId, + ).toBe('catalog'); + expect( + getPluginInfo(mockPermissionPolicies, 'scaffolder-template').pluginId, + ).toBe('scaffolder'); + }); + + it('should return if the permission is resourced', () => { + expect( + getPluginInfo(mockPermissionPolicies, 'catalog-entity').isResourced, + ).toBe(true); + expect( + getPluginInfo(mockPermissionPolicies, 'scaffolder-template').isResourced, + ).toBe(true); + }); + + it('should return the permissions data', () => { + let data = getPermissionsData(mockPolicies, mockPermissionPolicies); + expect(data[0]).toEqual({ + permission: 'policy-entity', + plugin: 'permission', + policies: [ + { + effect: 'allow', + policy: 'Read', + }, + { + effect: 'allow', + policy: 'Create', + }, + { + effect: 'allow', + policy: 'Delete', + }, + { + effect: 'deny', + policy: 'Update', + }, + ], + policyString: ['Read', ', Create', ', Delete'], + isResourced: false, + }); + data = getPermissionsData(mockPolicies, []); + expect(data[0]).toEqual({ + permission: 'policy-entity', + plugin: '-', + policies: [ + { + effect: 'allow', + policy: 'Read', + }, + { + effect: 'allow', + policy: 'Create', + }, + { + effect: 'allow', + policy: 'Delete', + }, + ], + policyString: ['Read', ', Create', ', Delete'], + isResourced: false, + }); + }); +}); + +describe('getConditionUpperCriteria', () => { + it('should return the upper criteria', () => { + const conditions = mockConditions[1].conditions; + + const result = getConditionUpperCriteria(conditions); + + expect(result).toEqual('allOf'); + }); + + it('should return undefined if no upper criteria is found', () => { + const conditions = mockConditions[0].conditions; + + const result = getConditionUpperCriteria(conditions); + + expect(result).toBeUndefined(); + }); +}); + +describe('getConditionsData', () => { + it('should return conditions data correctly for simple condition', () => { + const conditions = mockConditions[0].conditions; + + const result = getConditionsData(conditions); + + expect(result).toEqual({ + condition: { + rule: 'HAS_ANNOTATION', + resourceType: 'catalog-entity', + params: { annotation: 'temp' }, + }, + }); + }); + + it('should return conditions data correctly for any upper criteria', () => { + const conditions = mockConditions[1].conditions; + + const result = getConditionsData(conditions); + + expect(result).toEqual({ + allOf: [ + { + rule: 'HAS_LABEL', + resourceType: 'catalog-entity', + params: { label: 'temp' }, + }, + { + rule: 'HAS_METADATA', + resourceType: 'catalog-entity', + params: { key: 'status' }, + }, + ], + }); + }); + + it('should return nested conditions if nested condition exists', () => { + const conditions = { + allOf: [mockConditions[1].conditions, mockConditions[0].conditions], + } as AllOfCriteria; + + const result = getConditionsData(conditions); + const expectedResult = { + allOf: [ + { + allOf: [ + { + params: { label: 'temp' }, + resourceType: 'catalog-entity', + rule: 'HAS_LABEL', + }, + { + params: { key: 'status' }, + resourceType: 'catalog-entity', + rule: 'HAS_METADATA', + }, + ], + }, + { + params: { annotation: 'temp' }, + resourceType: 'catalog-entity', + rule: 'HAS_ANNOTATION', + }, + ], + }; + + expect(result).toEqual(expectedResult); + }); +}); + +describe('getPoliciesData', () => { + it('should return policies data correctly', () => { + const allowedPermissions = ['read', 'update']; + const policies = ['read', 'update', 'delete']; + + const result = getPoliciesData(allowedPermissions, policies); + + expect(result).toEqual([ + { policy: 'read', effect: 'allow' }, + { policy: 'update', effect: 'allow' }, + { policy: 'delete', effect: 'deny' }, + ]); + }); + + it('should return empty array if no policies provided', () => { + const allowedPermissions = ['read', 'write']; + const policies: string[] = []; + + const result = getPoliciesData(allowedPermissions, policies); + + expect(result).toEqual([]); + }); + + it('should return all policies as deny if no allowed permissions provided', () => { + const allowedPermissions: string[] = []; + const policies = ['read', 'update', 'delete']; + + const result = getPoliciesData(allowedPermissions, policies); + + expect(result).toEqual([ + { policy: 'read', effect: 'deny' }, + { policy: 'update', effect: 'deny' }, + { policy: 'delete', effect: 'deny' }, + ]); + }); +}); + +describe('getConditionalPermissionsData', () => { + it('should return conditional permissions data correctly', () => { + const conditionalPermissions = [mockConditions[0]]; + const permissionPolicies = { + plugins: ['catalog'], + pluginsPermissions: { + ['catalog']: { + permissions: ['catalog-entity'], + policies: { + ['catalog-entity']: { + policies: ['read', 'update', 'delete'], + isResourced: true, + }, + }, + }, + }, + }; + + const result = getConditionalPermissionsData( + conditionalPermissions, + permissionPolicies, + ); + + expect(result).toEqual([ + { + plugin: 'catalog', + permission: 'catalog-entity', + isResourced: true, + policies: [ + { policy: 'read', effect: 'allow' }, + { policy: 'update', effect: 'deny' }, + { policy: 'delete', effect: 'deny' }, + ], + policyString: 'Read', + conditions: { + condition: { + rule: 'HAS_ANNOTATION', + resourceType: 'catalog-entity', + params: { annotation: 'temp' }, + }, + }, + id: 1, + }, + ]); + }); + + it('should return empty array if no conditional permissions provided', () => { + const conditionalPermissions: RoleConditionalPolicyDecision[] = + []; + const permissionPolicies = { + plugins: ['catalog'], + pluginsPermissions: { + ['catalog']: { + permissions: ['catalog-entity'], + policies: { + ['catalog-entity']: { + policies: ['read', 'update', 'delete'], + isResourced: true, + }, + }, + }, + }, + }; + + const result = getConditionalPermissionsData( + conditionalPermissions, + permissionPolicies, + ); + + expect(result).toEqual([]); + }); + + it('should return nested conditional permission with nested upper criteria', () => { + const conditionalPermissions = [ + { + id: 1, + pluginId: 'catalog', + result: AuthorizeResult.CONDITIONAL, + resourceType: 'catalog-entity', + permissionMapping: ['read'], + conditions: { + allOf: [mockConditions[1].conditions, mockConditions[0].conditions], + } as AllOfCriteria, + }, + ] as RoleConditionalPolicyDecision[]; + + const permissionPolicies = { + plugins: ['catalog'], + pluginsPermissions: { + ['catalog']: { + permissions: ['catalog-entity'], + policies: { + ['catalog-entity']: { + policies: ['read', 'update', 'delete'], + isResourced: true, + }, + }, + }, + }, + }; + const result = getConditionalPermissionsData( + conditionalPermissions, + permissionPolicies, + ); + + const expectedResultConditions = { + allOf: [ + { + allOf: [ + { + params: { + label: 'temp', + }, + resourceType: 'catalog-entity', + rule: 'HAS_LABEL', + }, + { + params: { + key: 'status', + }, + resourceType: 'catalog-entity', + rule: 'HAS_METADATA', + }, + ], + }, + { + params: { + annotation: 'temp', + }, + resourceType: 'catalog-entity', + rule: 'HAS_ANNOTATION', + }, + ], + }; + + expect(result[0].conditions?.allOf).toHaveLength(2); + + const allOfConditions = result[0].conditions?.allOf || []; + expect(allOfConditions[0]).toHaveProperty('allOf'); + expect(allOfConditions[1]).toHaveProperty('params'); + expect(result[0].conditions).toEqual(expectedResultConditions); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/utils/rbac-utils.ts b/workspaces/rbac/plugins/rbac/src/utils/rbac-utils.ts new file mode 100644 index 0000000000..139c0df5f9 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/utils/rbac-utils.ts @@ -0,0 +1,400 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + GroupEntity, + isUserEntity, + parseEntityRef, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { + AllOfCriteria, + AnyOfCriteria, + NotCriteria, + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; + +import { getTitleCase } from '@janus-idp/shared-react'; + +import { + isResourcedPolicy, + PermissionAction, + PluginPermissionMetaData, + PolicyDetails, + RoleBasedPolicy, + RoleConditionalPolicyDecision, +} from '@backstage-community/plugin-rbac-common'; + +import { criterias } from '../components/ConditionalAccess/const'; +import { ConditionsData } from '../components/ConditionalAccess/types'; +import { + PluginsPermissionPoliciesData, + RowPolicy, + SelectedMember, +} from '../components/CreateRole/types'; +import { + MemberEntity, + MembersData, + PermissionsData, + PermissionsDataSet, +} from '../types'; +import { getMembersCount } from './create-role-utils'; + +export const getPermissionsArray = ( + role: string, + policies: RoleBasedPolicy[], +): RoleBasedPolicy[] => { + if (!policies || policies?.length === 0 || !Array.isArray(policies)) { + return []; + } + return policies.filter( + (policy: RoleBasedPolicy) => + policy.entityReference === role && policy.effect !== 'deny', + ); +}; + +export const getPermissions = ( + role: string, + policies: RoleBasedPolicy[], +): number => { + return getPermissionsArray(role, policies).length; +}; + +export const getMembersString = (res: { + users: number; + groups: number; +}): string => { + let membersString = ''; + if (res.users > 0) { + membersString = `${res.users} ${res.users > 1 ? 'users' : 'user'}`; + } + if (res.groups > 0) { + membersString = membersString.concat( + membersString.length > 0 ? ', ' : '', + `${res.groups} ${res.groups > 1 ? 'groups' : 'group'}`, + ); + } + return membersString; +}; + +export const getMembers = ( + members: (string | MembersData | SelectedMember)[], +): string => { + if (!members || members.length === 0) { + return 'No members'; + } + + const res = members.reduce( + (acc, member) => { + if (typeof member === 'object') { + if (member.type === 'User' || member.type === 'user') { + acc.users++; + } else { + acc.groups++; + } + } else { + const entity = parseEntityRef(member) as any; + if (isUserEntity(entity)) { + acc.users++; + } else { + acc.groups++; + } + } + return acc; + }, + { users: 0, groups: 0 }, + ); + + return getMembersString(res); +}; + +export const getMembersFromGroup = (group: GroupEntity): number => { + const membersList = group.relations?.reduce((acc, relation) => { + let temp = acc; + if (relation.type === 'hasMember') { + temp++; + } + return temp; + }, 0); + return membersList ?? 0; +}; + +export const getPluginInfo = ( + permissions: PluginPermissionMetaData[], + permissionName?: string, +): { pluginId: string; isResourced: boolean } => + permissions.reduce( + ( + acc: { pluginId: string; isResourced: boolean }, + p: PluginPermissionMetaData, + ) => { + const policy = p.policies.find(pol => { + if (pol.name === permissionName) { + return true; + } + if (isResourcedPolicy(pol)) { + return pol.resourceType === permissionName; + } + return false; + }); + if (policy) { + return { + pluginId: p.pluginId || '-', + isResourced: isResourcedPolicy(policy) || false, + }; + } + return acc; + }, + { pluginId: '-', isResourced: false }, + ); + +const getPolicy = (str: string) => { + const arr = str.split('.'); + return arr[arr.length - 1]; +}; + +const getAllPolicies = ( + permission: string, + allowedPolicies: RowPolicy[], + policies: PolicyDetails[], +) => { + const deniedPolicies = policies?.reduce((acc, p) => { + const perm = isResourcedPolicy(p) ? p.resourceType : p.name; + if ( + permission === perm && + !allowedPolicies.find( + allowedPolicy => + allowedPolicy.policy.toLocaleLowerCase('en-US') === + p.policy?.toLocaleLowerCase('en-US'), + ) + ) { + acc.push({ + policy: getTitleCase(p.policy) || 'Use', + effect: 'deny', + }); + } + return acc; + }, [] as RowPolicy[]); + return [...(allowedPolicies || []), ...(deniedPolicies || [])]; +}; + +export const getPermissionsData = ( + policies: RoleBasedPolicy[], + permissionPolicies: PluginPermissionMetaData[], +): PermissionsData[] => { + const data = policies.reduce( + (acc: PermissionsDataSet[], policy: RoleBasedPolicy) => { + if (policy?.effect === 'allow') { + const policyStr = + policy?.policy ?? getPolicy(policy.permission as string); + const policyTitleCase = getTitleCase(policyStr); + const permission = acc.find( + plugin => + plugin.permission === policy.permission && + !plugin.policies.has({ + policy: policyTitleCase || 'Use', + effect: 'allow', + }), + ); + if (permission) { + permission.policyString?.add( + policyTitleCase ? `, ${policyTitleCase}` : ', Use', + ); + permission.policies.add({ + policy: policyTitleCase || 'Use', + effect: policy.effect, + }); + } else { + const policyString = new Set(); + const policiesSet = new Set<{ policy: string; effect: string }>(); + acc.push({ + permission: policy.permission ?? '-', + plugin: getPluginInfo(permissionPolicies, policy?.permission) + .pluginId, + policyString: policyString.add(policyTitleCase || 'Use'), + policies: policiesSet.add({ + policy: policyTitleCase || 'Use', + effect: policy.effect, + }), + isResourced: getPluginInfo(permissionPolicies, policy?.permission) + .isResourced, + }); + } + } + return acc; + }, + [], + ); + return data.map((p: PermissionsDataSet) => ({ + ...p, + ...(p.policyString ? { policyString: Array.from(p.policyString) } : {}), + policies: getAllPolicies( + p.permission, + Array.from(p.policies), + permissionPolicies.find(pp => pp.pluginId === p.plugin) + ?.policies as PolicyDetails[], + ), + })) as PermissionsData[]; +}; + +export const getConditionUpperCriteria = ( + conditions: PermissionCriteria | string, +): string | undefined => { + return Object.keys(conditions).find(key => + [criterias.allOf, criterias.anyOf, criterias.not].includes( + key as keyof ConditionsData, + ), + ); +}; + +export const getConditionsData = ( + conditions: PermissionCriteria, +): ConditionsData | undefined => { + const upperCriteria = + getConditionUpperCriteria(conditions) ?? criterias.condition; + + switch (upperCriteria) { + case criterias.allOf: { + const allOfConditions = (conditions as AllOfCriteria) + .allOf; + allOfConditions.map(aoc => { + if (getConditionUpperCriteria(aoc)) { + return getConditionsData(aoc); + } + return aoc; + }); + return { allOf: allOfConditions as PermissionCondition[] }; + } + case criterias.anyOf: { + const anyOfConditions = (conditions as AnyOfCriteria) + .anyOf; + anyOfConditions.map(aoc => { + if (getConditionUpperCriteria(aoc)) { + return getConditionsData(aoc); + } + return aoc; + }); + return { anyOf: anyOfConditions as PermissionCondition[] }; + } + case criterias.not: { + const notCondition = (conditions as NotCriteria).not; + const nestedCondition = getConditionUpperCriteria(notCondition) + ? getConditionsData(notCondition) + : notCondition; + return { not: nestedCondition as PermissionCondition }; + } + default: + return { condition: conditions as PermissionCondition }; + } +}; + +export const getPoliciesData = ( + allowedPermissions: string[], + policies: string[], +): RowPolicy[] => { + return policies.map(p => ({ + policy: p, + ...(allowedPermissions.includes(p.toLocaleLowerCase('en-US')) + ? { effect: 'allow' } + : { effect: 'deny' }), + })); +}; + +export const getConditionalPermissionsData = ( + conditionalPermissions: RoleConditionalPolicyDecision[], + permissionPolicies: PluginsPermissionPoliciesData, +): PermissionsData[] => { + return conditionalPermissions.reduce((acc: any, cp) => { + const conditions = getConditionsData(cp.conditions); + const allPolicies = + permissionPolicies.pluginsPermissions?.[cp.pluginId]?.policies?.[ + cp.resourceType + ]?.policies ?? []; + const allowedPermissions = cp.permissionMapping.map(action => + action.toLocaleLowerCase('en-US'), + ); + const policyString = allowedPermissions + .map(p => p[0].toLocaleUpperCase('en-US') + p.slice(1)) + .join(', '); + + return [ + ...acc, + ...(conditions + ? [ + { + plugin: cp.pluginId, + permission: cp.resourceType, + isResourced: true, + policies: getPoliciesData(allowedPermissions, allPolicies), + policyString, + conditions, + id: cp.id, + }, + ] + : []), + ]; + }, []); +}; + +export const getSelectedMember = ( + memberResource: MemberEntity | undefined, + ref: string, +): SelectedMember => { + if (memberResource) { + return { + id: memberResource.metadata.etag as string, + ref: stringifyEntityRef(memberResource), + label: + memberResource.spec.profile?.displayName ?? + memberResource.metadata.name, + etag: memberResource.metadata.etag as string, + type: memberResource.kind, + namespace: memberResource.metadata.namespace as string, + members: getMembersCount(memberResource), + }; + } else if (ref) { + const { kind, namespace, name } = parseEntityRef(ref); + return { + id: `${kind}-${namespace}-${name}`, + ref, + label: name, + etag: `${kind}-${namespace}-${name}`, + type: kind, + namespace: namespace, + members: kind === 'group' ? 0 : undefined, + }; + } + return {} as SelectedMember; +}; + +export const isSamePermissionPolicy = ( + a: RoleBasedPolicy, + b: RoleBasedPolicy, +) => + a.entityReference === b.entityReference && + a.permission === b.permission && + a.policy === b.policy && + a.effect === b.effect; + +export const onlyInLeft = ( + left: RoleBasedPolicy[], + right: RoleBasedPolicy[], + compareFunction: (a: RoleBasedPolicy, b: RoleBasedPolicy) => boolean, +) => + left.filter( + leftValue => + !right.some(rightValue => compareFunction(leftValue, rightValue)), + ); diff --git a/workspaces/rbac/plugins/rbac/src/utils/role-form-utils.test.ts b/workspaces/rbac/plugins/rbac/src/utils/role-form-utils.test.ts new file mode 100644 index 0000000000..eb368682b7 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/utils/role-form-utils.test.ts @@ -0,0 +1,221 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { IdentityApi } from '@backstage/core-plugin-api'; +import { MockConfigApi } from '@backstage/test-utils'; + +import { RoleBasedPolicy } from '@backstage-community/plugin-rbac-common'; + +import { mockNewConditions } from '../__fixtures__/mockConditions'; +import { + mockAssociatedPolicies, + mockPolicies, +} from '../__fixtures__/mockPolicies'; +import { RBACAPI, RBACBackendClient } from '../api/RBACBackendClient'; +import { RoleBasedConditions, UpdatedConditionsData } from '../types'; +import { + createConditions, + createPermissions, + modifyConditions, + removeConditions, + removePermissions, +} from './role-form-utils'; + +jest.mock('../api/RBACBackendClient'); + +describe('RBAC Permissions Functions', () => { + let mockRbacApi: RBACAPI; + + const bearerToken = 'test-token'; + + const identityApi = { + async getCredentials() { + return { token: bearerToken }; + }, + } as IdentityApi; + + const mockConfigApi = new MockConfigApi({ + permission: { + enabled: true, + }, + }); + + beforeEach(() => { + mockRbacApi = new RBACBackendClient({ + configApi: mockConfigApi, + identityApi, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('createPermissions', () => { + it('should call createPolicies with the correct parameters', async () => { + const newPermissions: RoleBasedPolicy[] = mockAssociatedPolicies; + mockRbacApi.createPolicies = jest + .fn() + .mockResolvedValue({ status: 200 } as Response); + + await createPermissions(newPermissions, mockRbacApi); + expect(mockRbacApi.createPolicies).toHaveBeenCalledWith(newPermissions); + }); + + it('should throw an error if createPolicies returns an error', async () => { + const newPermissions: RoleBasedPolicy[] = mockAssociatedPolicies; + const errorMsg = 'Mock error message'; + mockRbacApi.createPolicies = jest + .fn() + .mockResolvedValue({ error: { message: errorMsg, name: 'Not found' } }); + + await expect( + createPermissions(newPermissions, mockRbacApi), + ).rejects.toThrow( + `Unable to create the permission policies. ${errorMsg}`, + ); + }); + }); + + describe('removePermissions', () => { + it('should call deletePolicies with the correct parameters', async () => { + const name = 'role:default/rbac_admin'; + const deletePermissions: RoleBasedPolicy[] = mockPolicies; + mockRbacApi.deletePolicies = jest + .fn() + .mockResolvedValue({ status: 204 } as Response); + + await removePermissions(name, deletePermissions, mockRbacApi); + expect(mockRbacApi.deletePolicies).toHaveBeenCalledWith( + name, + deletePermissions, + ); + }); + + it('should throw an error if deletePolicies returns an error', async () => { + const name = 'role:default/rbac_admin'; + const deletePermissions: RoleBasedPolicy[] = mockPolicies; + const errorMsg = 'Mock error message'; + mockRbacApi.deletePolicies = jest + .fn() + .mockResolvedValue({ error: { message: errorMsg, name: 'Not found' } }); + + await expect( + removePermissions(name, deletePermissions, mockRbacApi), + ).rejects.toThrow( + `Unable to delete the permission policies. ${errorMsg}`, + ); + }); + }); + + describe('removeConditions', () => { + it('should call deleteConditionalPolicies for each condition', async () => { + const deleteConditions = [1, 2, 3]; + mockRbacApi.deleteConditionalPolicies = jest + .fn() + .mockResolvedValue({ status: 204 } as Response); + + await removeConditions(deleteConditions, mockRbacApi); + deleteConditions.forEach(cid => { + expect(mockRbacApi.deleteConditionalPolicies).toHaveBeenCalledWith(cid); + }); + }); + + it('should throw an error if any deleteConditionalPolicies call returns an error', async () => { + const deleteConditions = [1, 2, 3]; + const errorMsg = 'Mock error message'; + mockRbacApi.deleteConditionalPolicies = jest + .fn() + .mockResolvedValueOnce({ status: 204 } as Response) + .mockResolvedValueOnce({ status: 204 } as Response) + .mockResolvedValueOnce({ + error: { message: errorMsg, name: 'Not found' }, + }); + + await expect( + removeConditions(deleteConditions, mockRbacApi), + ).rejects.toThrow( + `Unable to remove conditions from the role. ${errorMsg}`, + ); + }); + }); + + describe('modifyConditions', () => { + it('should call updateConditionalPolicies for each condition', async () => { + const updateConditions: UpdatedConditionsData = [ + { + id: 1, + updateCondition: mockNewConditions[0], + }, + ]; + mockRbacApi.updateConditionalPolicies = jest + .fn() + .mockResolvedValue({ status: 200 } as Response); + + await modifyConditions(updateConditions, mockRbacApi); + updateConditions.forEach(({ id, updateCondition }) => { + expect(mockRbacApi.updateConditionalPolicies).toHaveBeenCalledWith( + id, + updateCondition, + ); + }); + }); + + it('should throw an error if any updateConditionalPolicies call returns an error', async () => { + const updateConditions: UpdatedConditionsData = [ + { + id: 2, + updateCondition: mockNewConditions[1], + }, + ]; + const errorMsg = 'Mock error message'; + mockRbacApi.updateConditionalPolicies = jest.fn().mockResolvedValue({ + error: { message: errorMsg, name: 'Not found' }, + }); + + await expect( + modifyConditions(updateConditions, mockRbacApi), + ).rejects.toThrow(`Unable to update conditions. ${errorMsg}`); + }); + }); + + describe('createConditions', () => { + it('should call createConditionalPermission for each condition', async () => { + const newConditions: RoleBasedConditions[] = mockNewConditions; + mockRbacApi.createConditionalPermission = jest + .fn() + .mockResolvedValue({ status: 200 } as Response); + + await createConditions(newConditions, mockRbacApi); + newConditions.forEach(cpp => { + expect(mockRbacApi.createConditionalPermission).toHaveBeenCalledWith( + cpp, + ); + }); + }); + + it('should throw an error if any createConditionalPermission call returns an error', async () => { + const newConditions: RoleBasedConditions[] = mockNewConditions; + const errorMsg = 'Mock error message'; + mockRbacApi.createConditionalPermission = jest.fn().mockResolvedValue({ + error: { message: errorMsg, name: 'Not found' }, + }); + + await expect( + createConditions(newConditions, mockRbacApi), + ).rejects.toThrow(`Unable to add conditions to the role. ${errorMsg}`); + }); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/src/utils/role-form-utils.ts b/workspaces/rbac/plugins/rbac/src/utils/role-form-utils.ts new file mode 100644 index 0000000000..b9ae3f3c44 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/src/utils/role-form-utils.ts @@ -0,0 +1,127 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { RoleBasedPolicy } from '@backstage-community/plugin-rbac-common'; + +import { RBACAPI } from '../api/RBACBackendClient'; +import { + RoleBasedConditions, + RoleError, + UpdatedConditionsData, +} from '../types'; + +export const createPermissions = async ( + newPermissions: RoleBasedPolicy[], + rbacApi: RBACAPI, + errorMsgPrefix?: string, +) => { + if (newPermissions.length > 0) { + const permissionsRes = await rbacApi.createPolicies(newPermissions); + if ((permissionsRes as unknown as RoleError).error) { + throw new Error( + `${errorMsgPrefix || 'Unable to create the permission policies.'} ${ + (permissionsRes as unknown as RoleError).error.message + }`, + ); + } + } +}; + +export const removePermissions = async ( + name: string, + deletePermissions: RoleBasedPolicy[], + rbacApi: RBACAPI, +) => { + if (deletePermissions.length > 0) { + const permissionsRes = await rbacApi.deletePolicies( + name, + deletePermissions, + ); + if ((permissionsRes as unknown as RoleError).error) { + throw new Error( + `Unable to delete the permission policies. ${ + (permissionsRes as unknown as RoleError).error.message + }`, + ); + } + } +}; + +export const removeConditions = async ( + deleteConditions: number[], + rbacApi: RBACAPI, +) => { + if (deleteConditions.length > 0) { + const promises = deleteConditions.map(cid => + rbacApi.deleteConditionalPolicies(cid), + ); + + const cppRes: (Response | RoleError)[] = await Promise.all(promises); + const cpErr = cppRes + .map(r => (r as unknown as RoleError).error?.message) + .filter(m => m); + + if (cpErr.length > 0) { + throw new Error( + `Unable to remove conditions from the role. ${cpErr.join('\n')}`, + ); + } + } +}; + +export const modifyConditions = async ( + updateConditions: UpdatedConditionsData, + rbacApi: RBACAPI, +) => { + if (updateConditions.length > 0) { + const promises = updateConditions.map(({ id, updateCondition }) => + rbacApi.updateConditionalPolicies(id, updateCondition), + ); + + const cppRes: (Response | RoleError)[] = await Promise.all(promises); + const cpErr = cppRes + .map(r => (r as unknown as RoleError).error?.message) + .filter(m => m); + + if (cpErr.length > 0) { + throw new Error(`Unable to update conditions. ${cpErr.join('\n')}`); + } + } +}; + +export const createConditions = async ( + newConditions: RoleBasedConditions[], + rbacApi: RBACAPI, + errorMsgPrefix?: string, +) => { + if (newConditions.length > 0) { + const promises = newConditions.map(cpp => + rbacApi.createConditionalPermission(cpp), + ); + + const cppRes: (Response | RoleError)[] = await Promise.all(promises); + const cpErr = cppRes + .map(r => (r as unknown as RoleError).error?.message) + .filter(m => m); + + if (cpErr.length > 0) { + throw new Error( + `${ + errorMsgPrefix || 'Unable to add conditions to the role.' + } ${cpErr.join('\n')}`, + ); + } + } +}; diff --git a/workspaces/rbac/plugins/rbac/tests/rbac.spec.ts b/workspaces/rbac/plugins/rbac/tests/rbac.spec.ts new file mode 100644 index 0000000000..6060852c52 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/tests/rbac.spec.ts @@ -0,0 +1,366 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, Page, test } from '@playwright/test'; + +import { + Common, + verifyCellsInTable, + verifyColumnHeading, + verifyText, +} from './rbacHelper'; + +test.describe('RBAC plugin', () => { + let page: Page; + let common: Common; + const RoleOverviewPO = { + updatePolicies: 'span[data-testid="update-policies"]', + updateMembers: 'span[data-testid="update-members"]', + }; + + const navigateToRole = async (roleName: string) => { + await expect( + page.getByRole('heading', { name: 'All roles (2)' }), + ).toBeVisible({ timeout: 20000 }); + await page + .locator(`a`) + .filter({ hasText: `role:default/${roleName}` }) + .click(); + await expect( + page.getByRole('heading', { name: `role:default/${roleName}` }), + ).toBeVisible({ timeout: 20000 }); + await page.getByRole('tab', { name: 'Overview' }).click(); + await page.locator(RoleOverviewPO.updatePolicies).click(); + await expect(page.getByRole('heading', { name: 'Edit Role' })).toBeVisible({ + timeout: 20000, + }); + }; + + const finishAndVerifyUpdate = async (button: string, message: string) => { + await common.clickButton('Next'); + await common.clickButton(button); + await verifyText(message, page); + if (button === 'Save') { + await page.locator(`a`).filter({ hasText: 'RBAC' }).click(); + } + }; + + test.beforeAll(async ({ browser }) => { + const context = await browser.newContext(); + page = await context.newPage(); + common = new Common(page); + await common.loginAsGuest(); + const navSelector = 'nav [aria-label="Administration"]'; + await page.locator(navSelector).click(); + await expect(page.getByRole('heading', { name: 'RBAC' })).toBeVisible({ + timeout: 20000, + }); + }); + + test.afterAll(async ({ browser }) => { + await browser.close(); + }); + + test('Should show 2 roles in the list, column headings and cells', async () => { + await expect( + page.getByRole('heading', { name: 'All roles (2)' }), + ).toBeVisible({ timeout: 20000 }); + + const columns = [ + 'Name', + 'Users and groups', + 'Accessible plugins', + 'Actions', + ]; + await verifyColumnHeading(columns, page); + + const roleName = new RegExp(/^(role|user|group):[a-zA-Z]+\/[\w@*.~-]+$/); + const usersAndGroups = new RegExp( + /^(1\s(user|group)|[2-9]\s(users|groups))(, (1\s(user|group)|[2-9]\s(users|groups)))?$/, + ); + const accessiblePlugins = /\d/; + const cellIdentifier = [roleName, usersAndGroups, accessiblePlugins]; + + await verifyCellsInTable(cellIdentifier, page); + }); + + test('View details of role', async () => { + const roleName = 'role:default/rbac_admin'; + await page.locator(`a`).filter({ hasText: roleName }).click(); + await expect(page.getByRole('heading', { name: roleName })).toBeVisible({ + timeout: 20000, + }); + + await expect(page.getByRole('tab', { name: 'Overview' })).toBeVisible({ + timeout: 20000, + }); + await expect(page.getByText('About')).toBeVisible(); + + // verify users and groups table + await expect( + page.getByRole('heading', { name: 'Users and groups (1 user, 1 group)' }), + ).toBeVisible({ timeout: 20000 }); + + await verifyColumnHeading(['Name', 'Type', 'Members'], page); + + const name = new RegExp(/^(\w+)$/); + const type = new RegExp(/^(User|Group)$/); + const members = /^(-|\d+)$/; + const userGroupCellIdentifier = [name, type, members]; + await verifyCellsInTable(userGroupCellIdentifier, page); + + // verify permission policy table + await expect( + page.getByRole('heading', { name: 'Permission policies (10)' }), + ).toBeVisible({ timeout: 20000 }); + await verifyColumnHeading(['Plugin', 'Permission', 'Policies'], page); + const policies = + /^(?:(Read|Create|Update|Delete)(?:, (?:Read|Create|Update|Delete))*|Use)$/; + await verifyCellsInTable([policies], page); + + await page.locator(`a`).filter({ hasText: 'RBAC' }).click(); + }); + + test('Edit an existing role', async () => { + const roleName = 'role:default/rbac_admin'; + await page.locator(`a`).filter({ hasText: roleName }).click(); + await expect(page.getByRole('heading', { name: roleName })).toBeVisible({ + timeout: 20000, + }); + await page.getByRole('tab', { name: 'Overview' }).click(); + + await page.locator(RoleOverviewPO.updateMembers).click(); + await expect(page.getByRole('heading', { name: 'Edit Role' })).toBeVisible({ + timeout: 20000, + }); + await page + .getByPlaceholder('Search by user name or group name') + .fill('Guest User'); + await page.getByText('Guest User').click(); + await expect( + page.getByRole('heading', { + name: 'Users and groups (2 users, 1 group)', + }), + ).toBeVisible({ + timeout: 20000, + }); + await common.clickButton('Next'); + await common.clickButton('Next'); + await common.clickButton('Save'); + await verifyText('Role role:default/rbac_admin updated successfully', page); + + // alert doesn't show up after Cancel button is clicked + await page.locator(RoleOverviewPO.updateMembers).click(); + await expect(page.getByRole('heading', { name: 'Edit Role' })).toBeVisible({ + timeout: 20000, + }); + await common.clickButton('Cancel'); + await expect(page.getByRole('alert')).toHaveCount(0); + + // edit/update policies + await page.locator(RoleOverviewPO.updatePolicies).click(); + await expect(page.getByRole('heading', { name: 'Edit Role' })).toBeVisible({ + timeout: 20000, + }); + + await page.getByTestId('AddIcon').click(); + await page.getByPlaceholder('Select a plugin').last().click(); + await page.getByText('scaffolder').click(); + await page.getByPlaceholder('Select a resource type').last().click(); + await page.getByText('scaffolder-action').click(); + + // update existing conditional policy + await page + .getByText('Configure access (1 rule)', { exact: true }) + .first() + .click(); + await page.getByPlaceholder('Select a rule').first().click(); + await page.getByText('HAS_METADATA').click(); + await page.getByLabel('key').fill('status'); + await page.getByTestId('save-conditions').click(); + + // remove existing conditional policy + await page.getByTestId('permissionPoliciesRows[1]-remove').first().click(); + expect( + page.getByText('Configure access (2 rules)', { exact: true }), + ).toBeHidden(); + + await common.clickButton('Next'); + await common.clickButton('Save'); + await verifyText('Role role:default/rbac_admin updated successfully', page); + + await page.locator(`a`).filter({ hasText: 'RBAC' }).click(); + }); + + test('Create role from rolelist page with simple/conditional permission policies', async () => { + await expect( + page.getByRole('heading', { name: 'All roles (2)' }), + ).toBeVisible({ timeout: 20000 }); + + // create-role + await page.getByTestId('create-role').click(); + await expect( + page.getByRole('heading', { name: 'Create role' }), + ).toBeVisible({ timeout: 20000 }); + + await page.fill('input[name="name"]', 'sample-role-1'); + await page.fill('textarea[name="description"]', 'Test Description data'); + await common.clickButton('Next'); + + await page + .getByPlaceholder('Search by user name or group name') + .fill('Guest Use'); + await page.getByText('Guest User').click(); + await expect( + page.getByRole('heading', { + name: 'Users and groups (1 user)', + }), + ).toBeVisible({ + timeout: 20000, + }); + await common.clickButton('Next'); + + await page.getByPlaceholder('Select a plugin').first().click(); + await page.getByRole('option', { name: 'permission' }).click(); + await page.getByPlaceholder('Select a resource type').first().click(); + await page.getByText('policy-entity').click(); + + await page.getByRole('button', { name: 'Add' }).click(); + + await page.getByPlaceholder('Select a plugin').last().click(); + await page.getByText('scaffolder').click(); + await page.getByPlaceholder('Select a resource type').last().click(); + await page.getByText('scaffolder-action').click(); + await page.getByText('Configure access').first().click(); + await page.getByPlaceholder('Select a rule').first().click(); + await page.getByText('HAS_ACTION_ID').click(); + await page.getByLabel('actionId').fill('temp'); + await page.getByTestId('save-conditions').click(); + await expect(page.getByText('Configure access (1 rule)')).toBeVisible({ + timeout: 20000, + }); + + await page.getByRole('button', { name: 'Add' }).click(); + + await page.getByPlaceholder('Select a plugin').last().click(); + await page.getByText('catalog').click(); + await page.getByPlaceholder('Select a resource type').last().click(); + await page.getByText('catalog-entity').click(); + await page.getByText('Configure access').last().click(); + await page.getByRole('button', { name: 'AllOf' }).click(); + await page.getByPlaceholder('Select a rule').first().click(); + await page.getByText('HAS_LABEL').click(); + await page.getByLabel('label').fill('temp'); + await page.getByRole('button', { name: 'Add rule' }).click(); + await page.getByPlaceholder('Select a rule').last().click(); + await page.getByText('HAS_SPEC').click(); + await page.getByLabel('key').fill('test'); + await page.getByTestId('save-conditions').click(); + await expect(page.getByText('Configure access (2 rules)')).toBeVisible({ + timeout: 20000, + }); + await finishAndVerifyUpdate( + 'Create', + 'Role role:default/sample-role-1 created successfully', + ); + }); + + test('Edit role to convert simple policy into conditional policy', async () => { + navigateToRole('guests'); + + // update simple policy to add conditions + await page.getByText('Configure access', { exact: true }).click(); + await page.getByPlaceholder('Select a rule').first().click(); + await page.getByText('HAS_METADATA').click(); + await page.getByLabel('key').fill('status'); + await page.getByTestId('save-conditions').click(); + + expect( + page.getByText('Configure access (1 rule)', { exact: true }), + ).toBeVisible(); + + finishAndVerifyUpdate( + 'Save', + 'Role role:default/guests updated successfully', + ); + }); + + test('Edit role to convert conditional policy into nested conditional policy', async () => { + await navigateToRole('guests'); + + await page.getByText('Configure access', { exact: true }).click(); + await page.getByText('AllOf', { exact: true }).click(); + await page.getByPlaceholder('Select a rule').first().click(); + await page.getByText('HAS_LABEL').click(); + await page.getByLabel('label').fill('dev'); + await page.getByText('Add nested condition').click(); + await page.getByPlaceholder('Select a rule').last().click(); + await page.getByText('HAS_METADATA').click(); + await page.getByLabel('key').fill('status'); + await page.getByTestId('save-conditions').click(); + + await expect( + page.getByText('Configure access (2 rules)', { exact: true }), + ).toBeVisible(); + + await finishAndVerifyUpdate( + 'Save', + 'Role role:default/guests updated successfully', + ); + }); + + test('Edit existing nested conditional policy', async () => { + await navigateToRole('rbac_admin'); + + await page.getByText('Configure access (9 rules)', { exact: true }).click(); + await expect(page.getByText('AllOf')).toHaveCount(2, { timeout: 20000 }); + await page.getByText('Add nested condition').click(); + await page.getByText('Not', { exact: true }).last().click(); + await page.getByPlaceholder('Select a rule').last().click(); + await page.getByText('HAS_LABEL').last().click(); + await page.getByLabel('label').last().fill('test'); + await page.getByTestId('save-conditions').click(); + + await expect( + page.getByText('Configure access (10 rules)', { exact: true }), + ).toBeVisible(); + + await finishAndVerifyUpdate( + 'Save', + 'Role role:default/rbac_admin updated successfully', + ); + }); + + test('Remove existing nested conditional policy', async () => { + await navigateToRole('rbac_admin'); + + await expect( + page.getByText('Configure access (9 rules)', { exact: true }), + ).toHaveCount(1); + await page.getByText('Configure access (9 rules)', { exact: true }).click(); + await expect(page.getByText('AllOf')).toHaveCount(2, { timeout: 20000 }); + await page.getByTestId('remove-nested-condition').last().click(); + await page.getByTestId('save-conditions').click(); + + await expect( + page.getByText('Configure access (2 rules)', { exact: true }), + ).toHaveCount(2); + + await finishAndVerifyUpdate( + 'Save', + 'Role role:default/rbac_admin updated successfully', + ); + }); +}); diff --git a/workspaces/rbac/plugins/rbac/tests/rbacHelper.ts b/workspaces/rbac/plugins/rbac/tests/rbacHelper.ts new file mode 100644 index 0000000000..979692b458 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/tests/rbacHelper.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, type Locator, type Page } from '@playwright/test'; + +export const verifyCellsInTable = async ( + cellIdentifier: (string | RegExp)[], + page: Page, +) => { + for (const text of cellIdentifier) { + const cellLocator = page + .locator('td[class*="MuiTableCell-root"]') + .filter({ hasText: text }); + const count = await cellLocator.count(); + + if (count === 0) { + throw new Error( + `Expected at least one cell with text matching ${text}, but none were found.`, + ); + } + + // Checks if all matching cells are visible. + for (let i = 0; i < count; i++) { + await expect(cellLocator.nth(i)).toBeVisible(); + } + } +}; + +export const verifyColumnHeading = async ( + columns: (string | RegExp)[], + page: Page, +) => { + const thead = page.locator('thead'); + for (const col of columns) { + await expect( + thead.getByRole('columnheader', { name: col, exact: true }), + ).toBeVisible(); + } +}; + +export const verifyText = async ( + text: string | RegExp, + page: Page, + exact: boolean = true, +) => { + const element = page.getByText(text, { exact: exact }).first(); + await element.scrollIntoViewIfNeeded(); + await expect(element).toBeVisible(); +}; + +export class Common { + page: Page; + + constructor(page: Page) { + this.page = page; + } + + async verifyHeading(heading: string) { + const headingLocator = this.page + .locator('h1, h2, h3, h4, h5, h6') + .filter({ hasText: heading }) + .first(); + await headingLocator.waitFor({ state: 'visible', timeout: 30000 }); + await expect(headingLocator).toBeVisible(); + } + + async clickButton( + label: string, + clickOpts?: Parameters[0], + getByTextOpts: Parameters[1] = { exact: true }, + ) { + const muiButtonLabel = 'span[class^="MuiButton-label"]'; + const selector = `${muiButtonLabel}:has-text("${label}")`; + const button = this.page + .locator(selector) + .getByText(label, getByTextOpts) + .first(); + await button.waitFor({ state: 'visible' }); + await button.click(clickOpts); + } + + async waitForSideBarVisible() { + await this.page.waitForSelector('nav a', { timeout: 120000 }); + } + + async loginAsGuest() { + await this.page.goto('/'); + // TODO - Remove it after https://issues.redhat.com/browse/RHIDP-2043. A Dynamic plugin for Guest Authentication Provider needs to be created + this.page.on('dialog', async dialog => { + await dialog.accept(); + }); + + await this.verifyHeading('Select a sign-in method'); + await this.clickButton('Enter'); + await this.waitForSideBarVisible(); + } +} diff --git a/workspaces/rbac/plugins/rbac/tsconfig.json b/workspaces/rbac/plugins/rbac/tsconfig.json new file mode 100644 index 0000000000..ae77164361 --- /dev/null +++ b/workspaces/rbac/plugins/rbac/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src", "dev", "migrations"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "../../dist-types/plugins/rbac", + "rootDir": "." + } +} diff --git a/workspaces/rbac/plugins/rbac/turbo.json b/workspaces/rbac/plugins/rbac/turbo.json new file mode 100644 index 0000000000..c9f4b07baf --- /dev/null +++ b/workspaces/rbac/plugins/rbac/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "tsc": { + "outputs": ["../../dist-types/plugins/rbac/**"] + } + } +} diff --git a/workspaces/rbac/tsconfig.json b/workspaces/rbac/tsconfig.json new file mode 100644 index 0000000000..aed5129a4b --- /dev/null +++ b/workspaces/rbac/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": [ + "packages/*/src", + "plugins/*/src", + "plugins/*/dev", + "plugins/*/migrations" + ], + "files": ["node_modules/@backstage/cli/asset-types/asset-types.d.ts"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "dist-types", + "rootDir": ".", + "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2022"], + "target": "ES2022", + "useUnknownInCatchVariables": false + } +} diff --git a/workspaces/rbac/yarn.lock b/workspaces/rbac/yarn.lock new file mode 100644 index 0000000000..85f839ec31 --- /dev/null +++ b/workspaces/rbac/yarn.lock @@ -0,0 +1,27438 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 6 + cacheKey: 8 + +"@adobe/css-tools@npm:^4.4.0": + version: 4.4.0 + resolution: "@adobe/css-tools@npm:4.4.0" + checksum: 1f08fb49bf17fc7f2d1a86d3e739f29ca80063d28168307f1b0a962ef37501c5667271f6771966578897f2e94e43c4770fd802728a6e6495b812da54112d506a + languageName: node + linkType: hard + +"@ampproject/remapping@npm:^2.2.0": + version: 2.3.0 + resolution: "@ampproject/remapping@npm:2.3.0" + dependencies: + "@jridgewell/gen-mapping": ^0.3.5 + "@jridgewell/trace-mapping": ^0.3.24 + checksum: d3ad7b89d973df059c4e8e6d7c972cbeb1bb2f18f002a3bd04ae0707da214cb06cc06929b65aa2313b9347463df2914772298bae8b1d7973f246bb3f2ab3e8f0 + languageName: node + linkType: hard + +"@apidevtools/json-schema-ref-parser@npm:9.0.6": + version: 9.0.6 + resolution: "@apidevtools/json-schema-ref-parser@npm:9.0.6" + dependencies: + "@jsdevtools/ono": ^7.1.3 + call-me-maybe: ^1.0.1 + js-yaml: ^3.13.1 + checksum: c7ff53623ab8a9dd221772a5757fa0b9e5167a5ac3a71c23596634bae6efc85d8efcdebbe17f73ee5c027ea5afc48c705e8a720f02c4909f9a357d8027040b7b + languageName: node + linkType: hard + +"@apidevtools/openapi-schemas@npm:^2.1.0": + version: 2.1.0 + resolution: "@apidevtools/openapi-schemas@npm:2.1.0" + checksum: 4a8f64935b9049ef21e41fa4b188f39f6bc3f5291cebd451701db1115451ccb246a739e46cc5ce9ecdec781671431db40db7851acdac84a990a45756e0f32de3 + languageName: node + linkType: hard + +"@apidevtools/swagger-methods@npm:^3.0.2": + version: 3.0.2 + resolution: "@apidevtools/swagger-methods@npm:3.0.2" + checksum: d06b1ac5c1956613c4c6be695612ef860cd4e962b93a509ca551735a328a856cae1e33399cac1dcbf8333ba22b231746f3586074769ef0e172cf549ec9e7eaae + languageName: node + linkType: hard + +"@apidevtools/swagger-parser@npm:^10.1.0": + version: 10.1.0 + resolution: "@apidevtools/swagger-parser@npm:10.1.0" + dependencies: + "@apidevtools/json-schema-ref-parser": 9.0.6 + "@apidevtools/openapi-schemas": ^2.1.0 + "@apidevtools/swagger-methods": ^3.0.2 + "@jsdevtools/ono": ^7.1.3 + ajv: ^8.6.3 + ajv-draft-04: ^1.0.0 + call-me-maybe: ^1.0.1 + peerDependencies: + openapi-types: ">=7" + checksum: c7c923755bd025ee2cae97e1cfd525538523ba74c341a0ac814c023ffe5e63fc2d997539a8ccf9a0fcec41a2d6337d40cc5735acb991ddcbb415853a241908d1 + languageName: node + linkType: hard + +"@apisyouwonthate/style-guide@npm:^1.4.0": + version: 1.5.0 + resolution: "@apisyouwonthate/style-guide@npm:1.5.0" + dependencies: + "@stoplight/spectral-formats": ^1.2.0 + "@stoplight/spectral-functions": ^1.6.1 + checksum: e19c7a758342e9e5abba27c3a589375cde997a6f2f6ec7fc599e0abe0de52481554e1676776ec93ba7141f4a2ad365ca99e7e007fbcf4bbe3c40fbc4f7ea53e2 + languageName: node + linkType: hard + +"@asyncapi/specs@npm:^4.1.0": + version: 4.3.1 + resolution: "@asyncapi/specs@npm:4.3.1" + dependencies: + "@types/json-schema": ^7.0.11 + checksum: 886f116550af884d1c0b73a35ec40ae18eb7169a9230658b7ddabf6e57bb1f148dedfbbf059e142354d6d8e2dd22839cc6990cae58f7f09d5c4d0d80c6c127a5 + languageName: node + linkType: hard + +"@aws-crypto/crc32@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/crc32@npm:5.2.0" + dependencies: + "@aws-crypto/util": ^5.2.0 + "@aws-sdk/types": ^3.222.0 + tslib: ^2.6.2 + checksum: 1ddf7ec3fccf106205ff2476d90ae1d6625eabd47752f689c761b71e41fe451962b7a1c9ed25fe54e17dd747a62fbf4de06030fe56fe625f95285f6f70b96c57 + languageName: node + linkType: hard + +"@aws-crypto/crc32c@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/crc32c@npm:5.2.0" + dependencies: + "@aws-crypto/util": ^5.2.0 + "@aws-sdk/types": ^3.222.0 + tslib: ^2.6.2 + checksum: 0b399de8607c59e1e46c05d2b24a16b56d507944fdac925c611f0ba7302f5555c098139806d7da1ebef1f89bf4e4b5d4dec74d4809ce0f18238b72072065effe + languageName: node + linkType: hard + +"@aws-crypto/sha1-browser@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha1-browser@npm:5.2.0" + dependencies: + "@aws-crypto/supports-web-crypto": ^5.2.0 + "@aws-crypto/util": ^5.2.0 + "@aws-sdk/types": ^3.222.0 + "@aws-sdk/util-locate-window": ^3.0.0 + "@smithy/util-utf8": ^2.0.0 + tslib: ^2.6.2 + checksum: 8b04af601d945c5ef0f5f733b55681edc95b81c02ce5067b57f1eb4ee718e45485cf9aeeb7a84da9131656d09e1c4bc78040ec759f557a46703422d8df098d59 + languageName: node + linkType: hard + +"@aws-crypto/sha256-browser@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha256-browser@npm:5.2.0" + dependencies: + "@aws-crypto/sha256-js": ^5.2.0 + "@aws-crypto/supports-web-crypto": ^5.2.0 + "@aws-crypto/util": ^5.2.0 + "@aws-sdk/types": ^3.222.0 + "@aws-sdk/util-locate-window": ^3.0.0 + "@smithy/util-utf8": ^2.0.0 + tslib: ^2.6.2 + checksum: 773f12f2026d82a6bb4a23a8f491894a6d32525bd9b8bfbc12896526cf11882a7607a671c478c45f9cd7d6ba1caaed48a62b67c6f725244bd83a1275108f46c7 + languageName: node + linkType: hard + +"@aws-crypto/sha256-js@npm:5.2.0, @aws-crypto/sha256-js@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha256-js@npm:5.2.0" + dependencies: + "@aws-crypto/util": ^5.2.0 + "@aws-sdk/types": ^3.222.0 + tslib: ^2.6.2 + checksum: 007fbe0436d714d0d0d282e2b61c90e45adcb9ad75eac9ac7ba03d32b56624afd09b2a9ceb4d659661cf17c51d74d1900ab6b00eacafc002da1101664955ca53 + languageName: node + linkType: hard + +"@aws-crypto/supports-web-crypto@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/supports-web-crypto@npm:5.2.0" + dependencies: + tslib: ^2.6.2 + checksum: 6ffc21de48b2b2c3e918193101d7e8fe949d47b37688892e1c39eaedaa938be80c0f404fe1c874c30cce16781026777a53bf47d5d90143ca91d0feb7c4a6f830 + languageName: node + linkType: hard + +"@aws-crypto/util@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/util@npm:5.2.0" + dependencies: + "@aws-sdk/types": ^3.222.0 + "@smithy/util-utf8": ^2.0.0 + tslib: ^2.6.2 + checksum: f0f81d9d2771c59946cfec48b86cb23d39f78a966c4a1f89d4753abdc3cb38de06f907d1e6450059b121d48ac65d612ab88bdb70014553a077fc3dabddfbf8d6 + languageName: node + linkType: hard + +"@aws-sdk/abort-controller@npm:^3.347.0": + version: 3.370.0 + resolution: "@aws-sdk/abort-controller@npm:3.370.0" + dependencies: + "@aws-sdk/types": 3.370.0 + tslib: ^2.5.0 + checksum: 0095e83186de9ce150826d5afc59ae02de0a05508595226edec187c96ff6b46687a4b3ba9a9051a25b85a6051c7d7aeba347e8a7a0632edbe116ee3c60376842 + languageName: node + linkType: hard + +"@aws-sdk/client-codecommit@npm:^3.350.0": + version: 3.675.0 + resolution: "@aws-sdk/client-codecommit@npm:3.675.0" + dependencies: + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/client-sso-oidc": 3.675.0 + "@aws-sdk/client-sts": 3.675.0 + "@aws-sdk/core": 3.667.0 + "@aws-sdk/credential-provider-node": 3.675.0 + "@aws-sdk/middleware-host-header": 3.667.0 + "@aws-sdk/middleware-logger": 3.667.0 + "@aws-sdk/middleware-recursion-detection": 3.667.0 + "@aws-sdk/middleware-user-agent": 3.669.0 + "@aws-sdk/region-config-resolver": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@aws-sdk/util-endpoints": 3.667.0 + "@aws-sdk/util-user-agent-browser": 3.675.0 + "@aws-sdk/util-user-agent-node": 3.669.0 + "@smithy/config-resolver": ^3.0.9 + "@smithy/core": ^2.4.8 + "@smithy/fetch-http-handler": ^3.2.9 + "@smithy/hash-node": ^3.0.7 + "@smithy/invalid-dependency": ^3.0.7 + "@smithy/middleware-content-length": ^3.0.9 + "@smithy/middleware-endpoint": ^3.1.4 + "@smithy/middleware-retry": ^3.0.23 + "@smithy/middleware-serde": ^3.0.7 + "@smithy/middleware-stack": ^3.0.7 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/node-http-handler": ^3.2.4 + "@smithy/protocol-http": ^4.1.4 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + "@smithy/url-parser": ^3.0.7 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.23 + "@smithy/util-defaults-mode-node": ^3.0.23 + "@smithy/util-endpoints": ^2.1.3 + "@smithy/util-middleware": ^3.0.7 + "@smithy/util-retry": ^3.0.7 + "@smithy/util-utf8": ^3.0.0 + "@types/uuid": ^9.0.1 + tslib: ^2.6.2 + uuid: ^9.0.1 + checksum: 6cbad10a8634b8091b7f3a0994bfcec8c716c70a213b84ae1e30fd8194a8fa7c018aafc9bccd29e5424ecc8181dfc955c27d1184ffe269faccb43d7385966288 + languageName: node + linkType: hard + +"@aws-sdk/client-cognito-identity@npm:3.675.0": + version: 3.675.0 + resolution: "@aws-sdk/client-cognito-identity@npm:3.675.0" + dependencies: + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/client-sso-oidc": 3.675.0 + "@aws-sdk/client-sts": 3.675.0 + "@aws-sdk/core": 3.667.0 + "@aws-sdk/credential-provider-node": 3.675.0 + "@aws-sdk/middleware-host-header": 3.667.0 + "@aws-sdk/middleware-logger": 3.667.0 + "@aws-sdk/middleware-recursion-detection": 3.667.0 + "@aws-sdk/middleware-user-agent": 3.669.0 + "@aws-sdk/region-config-resolver": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@aws-sdk/util-endpoints": 3.667.0 + "@aws-sdk/util-user-agent-browser": 3.675.0 + "@aws-sdk/util-user-agent-node": 3.669.0 + "@smithy/config-resolver": ^3.0.9 + "@smithy/core": ^2.4.8 + "@smithy/fetch-http-handler": ^3.2.9 + "@smithy/hash-node": ^3.0.7 + "@smithy/invalid-dependency": ^3.0.7 + "@smithy/middleware-content-length": ^3.0.9 + "@smithy/middleware-endpoint": ^3.1.4 + "@smithy/middleware-retry": ^3.0.23 + "@smithy/middleware-serde": ^3.0.7 + "@smithy/middleware-stack": ^3.0.7 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/node-http-handler": ^3.2.4 + "@smithy/protocol-http": ^4.1.4 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + "@smithy/url-parser": ^3.0.7 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.23 + "@smithy/util-defaults-mode-node": ^3.0.23 + "@smithy/util-endpoints": ^2.1.3 + "@smithy/util-middleware": ^3.0.7 + "@smithy/util-retry": ^3.0.7 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: f4957e60c0b64ff6305d31954c436e8fd144c26d924009f92e174364f892f05dacbd1586e293b70f9de66f33db2b230f66584d6b8c02e952446a333ad9f4e2dc + languageName: node + linkType: hard + +"@aws-sdk/client-s3@npm:^3.350.0": + version: 3.675.0 + resolution: "@aws-sdk/client-s3@npm:3.675.0" + dependencies: + "@aws-crypto/sha1-browser": 5.2.0 + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/client-sso-oidc": 3.675.0 + "@aws-sdk/client-sts": 3.675.0 + "@aws-sdk/core": 3.667.0 + "@aws-sdk/credential-provider-node": 3.675.0 + "@aws-sdk/middleware-bucket-endpoint": 3.667.0 + "@aws-sdk/middleware-expect-continue": 3.667.0 + "@aws-sdk/middleware-flexible-checksums": 3.669.0 + "@aws-sdk/middleware-host-header": 3.667.0 + "@aws-sdk/middleware-location-constraint": 3.667.0 + "@aws-sdk/middleware-logger": 3.667.0 + "@aws-sdk/middleware-recursion-detection": 3.667.0 + "@aws-sdk/middleware-sdk-s3": 3.674.0 + "@aws-sdk/middleware-ssec": 3.667.0 + "@aws-sdk/middleware-user-agent": 3.669.0 + "@aws-sdk/region-config-resolver": 3.667.0 + "@aws-sdk/signature-v4-multi-region": 3.674.0 + "@aws-sdk/types": 3.667.0 + "@aws-sdk/util-endpoints": 3.667.0 + "@aws-sdk/util-user-agent-browser": 3.675.0 + "@aws-sdk/util-user-agent-node": 3.669.0 + "@aws-sdk/xml-builder": 3.662.0 + "@smithy/config-resolver": ^3.0.9 + "@smithy/core": ^2.4.8 + "@smithy/eventstream-serde-browser": ^3.0.10 + "@smithy/eventstream-serde-config-resolver": ^3.0.7 + "@smithy/eventstream-serde-node": ^3.0.9 + "@smithy/fetch-http-handler": ^3.2.9 + "@smithy/hash-blob-browser": ^3.1.6 + "@smithy/hash-node": ^3.0.7 + "@smithy/hash-stream-node": ^3.1.6 + "@smithy/invalid-dependency": ^3.0.7 + "@smithy/md5-js": ^3.0.7 + "@smithy/middleware-content-length": ^3.0.9 + "@smithy/middleware-endpoint": ^3.1.4 + "@smithy/middleware-retry": ^3.0.23 + "@smithy/middleware-serde": ^3.0.7 + "@smithy/middleware-stack": ^3.0.7 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/node-http-handler": ^3.2.4 + "@smithy/protocol-http": ^4.1.4 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + "@smithy/url-parser": ^3.0.7 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.23 + "@smithy/util-defaults-mode-node": ^3.0.23 + "@smithy/util-endpoints": ^2.1.3 + "@smithy/util-middleware": ^3.0.7 + "@smithy/util-retry": ^3.0.7 + "@smithy/util-stream": ^3.1.9 + "@smithy/util-utf8": ^3.0.0 + "@smithy/util-waiter": ^3.1.6 + tslib: ^2.6.2 + checksum: a7a26976930246d3ab138aa01dd90410ae58f0b446d2294d2f4dc817f259f65f4ba8813aad0e86be3265aa758f287fdc7ffa17060001caa252b218e5080ab248 + languageName: node + linkType: hard + +"@aws-sdk/client-sso-oidc@npm:3.675.0": + version: 3.675.0 + resolution: "@aws-sdk/client-sso-oidc@npm:3.675.0" + dependencies: + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/core": 3.667.0 + "@aws-sdk/credential-provider-node": 3.675.0 + "@aws-sdk/middleware-host-header": 3.667.0 + "@aws-sdk/middleware-logger": 3.667.0 + "@aws-sdk/middleware-recursion-detection": 3.667.0 + "@aws-sdk/middleware-user-agent": 3.669.0 + "@aws-sdk/region-config-resolver": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@aws-sdk/util-endpoints": 3.667.0 + "@aws-sdk/util-user-agent-browser": 3.675.0 + "@aws-sdk/util-user-agent-node": 3.669.0 + "@smithy/config-resolver": ^3.0.9 + "@smithy/core": ^2.4.8 + "@smithy/fetch-http-handler": ^3.2.9 + "@smithy/hash-node": ^3.0.7 + "@smithy/invalid-dependency": ^3.0.7 + "@smithy/middleware-content-length": ^3.0.9 + "@smithy/middleware-endpoint": ^3.1.4 + "@smithy/middleware-retry": ^3.0.23 + "@smithy/middleware-serde": ^3.0.7 + "@smithy/middleware-stack": ^3.0.7 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/node-http-handler": ^3.2.4 + "@smithy/protocol-http": ^4.1.4 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + "@smithy/url-parser": ^3.0.7 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.23 + "@smithy/util-defaults-mode-node": ^3.0.23 + "@smithy/util-endpoints": ^2.1.3 + "@smithy/util-middleware": ^3.0.7 + "@smithy/util-retry": ^3.0.7 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + peerDependencies: + "@aws-sdk/client-sts": ^3.675.0 + checksum: 5bc6ec63b6881e02f00ea76141d063c28d8de2d93434fde987afff744ea8ef19c7bbf1bf8ecc2aa03a6780d7be43b0ea562f8565cfb37badb36bf183b060b3a5 + languageName: node + linkType: hard + +"@aws-sdk/client-sso@npm:3.675.0": + version: 3.675.0 + resolution: "@aws-sdk/client-sso@npm:3.675.0" + dependencies: + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/core": 3.667.0 + "@aws-sdk/middleware-host-header": 3.667.0 + "@aws-sdk/middleware-logger": 3.667.0 + "@aws-sdk/middleware-recursion-detection": 3.667.0 + "@aws-sdk/middleware-user-agent": 3.669.0 + "@aws-sdk/region-config-resolver": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@aws-sdk/util-endpoints": 3.667.0 + "@aws-sdk/util-user-agent-browser": 3.675.0 + "@aws-sdk/util-user-agent-node": 3.669.0 + "@smithy/config-resolver": ^3.0.9 + "@smithy/core": ^2.4.8 + "@smithy/fetch-http-handler": ^3.2.9 + "@smithy/hash-node": ^3.0.7 + "@smithy/invalid-dependency": ^3.0.7 + "@smithy/middleware-content-length": ^3.0.9 + "@smithy/middleware-endpoint": ^3.1.4 + "@smithy/middleware-retry": ^3.0.23 + "@smithy/middleware-serde": ^3.0.7 + "@smithy/middleware-stack": ^3.0.7 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/node-http-handler": ^3.2.4 + "@smithy/protocol-http": ^4.1.4 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + "@smithy/url-parser": ^3.0.7 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.23 + "@smithy/util-defaults-mode-node": ^3.0.23 + "@smithy/util-endpoints": ^2.1.3 + "@smithy/util-middleware": ^3.0.7 + "@smithy/util-retry": ^3.0.7 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: f478c7c997e99fb8ea381ff6580faa0bc6f4bbfbafd41ed3a4521b1dcd7c0ee78cc431373f3b9d4ae385f9e2c30b85e83831ed2de6a0dbec831dd5ad7a59aaf9 + languageName: node + linkType: hard + +"@aws-sdk/client-sts@npm:3.675.0, @aws-sdk/client-sts@npm:^3.350.0": + version: 3.675.0 + resolution: "@aws-sdk/client-sts@npm:3.675.0" + dependencies: + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/client-sso-oidc": 3.675.0 + "@aws-sdk/core": 3.667.0 + "@aws-sdk/credential-provider-node": 3.675.0 + "@aws-sdk/middleware-host-header": 3.667.0 + "@aws-sdk/middleware-logger": 3.667.0 + "@aws-sdk/middleware-recursion-detection": 3.667.0 + "@aws-sdk/middleware-user-agent": 3.669.0 + "@aws-sdk/region-config-resolver": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@aws-sdk/util-endpoints": 3.667.0 + "@aws-sdk/util-user-agent-browser": 3.675.0 + "@aws-sdk/util-user-agent-node": 3.669.0 + "@smithy/config-resolver": ^3.0.9 + "@smithy/core": ^2.4.8 + "@smithy/fetch-http-handler": ^3.2.9 + "@smithy/hash-node": ^3.0.7 + "@smithy/invalid-dependency": ^3.0.7 + "@smithy/middleware-content-length": ^3.0.9 + "@smithy/middleware-endpoint": ^3.1.4 + "@smithy/middleware-retry": ^3.0.23 + "@smithy/middleware-serde": ^3.0.7 + "@smithy/middleware-stack": ^3.0.7 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/node-http-handler": ^3.2.4 + "@smithy/protocol-http": ^4.1.4 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + "@smithy/url-parser": ^3.0.7 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.23 + "@smithy/util-defaults-mode-node": ^3.0.23 + "@smithy/util-endpoints": ^2.1.3 + "@smithy/util-middleware": ^3.0.7 + "@smithy/util-retry": ^3.0.7 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: 257cf92ccbe3dcfac57c8657c347de4d04f73612025f15dd748b7f65afbc9b41bf623dcf90cbaf6c24546eb025a213364d69db2d30082afeb13d198bb1094064 + languageName: node + linkType: hard + +"@aws-sdk/core@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/core@npm:3.667.0" + dependencies: + "@aws-sdk/types": 3.667.0 + "@smithy/core": ^2.4.8 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/property-provider": ^3.1.7 + "@smithy/protocol-http": ^4.1.4 + "@smithy/signature-v4": ^4.2.0 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + "@smithy/util-middleware": ^3.0.7 + fast-xml-parser: 4.4.1 + tslib: ^2.6.2 + checksum: da4d0e3971e88d2dc72214ea04f95f35e4e03f6323f3bc438e378cd479816d5ee3aa8fd224778639803d344691edd6fac2096b26bda337afdaba8abcee3bd40c + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-cognito-identity@npm:3.675.0": + version: 3.675.0 + resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.675.0" + dependencies: + "@aws-sdk/client-cognito-identity": 3.675.0 + "@aws-sdk/types": 3.667.0 + "@smithy/property-provider": ^3.1.7 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: b5fb07a8108556726df0f5237df103cb2595dcda00d5b4f11c58321dfacb177d1907d044996139142374729d458e25f29b157a3e597ea3ea81826b6ca17219cb + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-env@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.667.0" + dependencies: + "@aws-sdk/core": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@smithy/property-provider": ^3.1.7 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: d077c5370ba5a90e11e0722d4a7820c8075a610bd099c519b710a58fc1770ecd2ad3a401b00d4467016c215df293e08a25ff798f42deb7bab8a559eca2fff245 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-http@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/credential-provider-http@npm:3.667.0" + dependencies: + "@aws-sdk/core": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@smithy/fetch-http-handler": ^3.2.9 + "@smithy/node-http-handler": ^3.2.4 + "@smithy/property-provider": ^3.1.7 + "@smithy/protocol-http": ^4.1.4 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + "@smithy/util-stream": ^3.1.9 + tslib: ^2.6.2 + checksum: 9f300ca39a607e10c6b79f374de6e306dabe54ceab527b898bcd205b6b7225e01f03dc888c9b4258aadf4b6d786547fc599787a246eb797c3e80044c7ea9cf09 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-ini@npm:3.675.0": + version: 3.675.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.675.0" + dependencies: + "@aws-sdk/core": 3.667.0 + "@aws-sdk/credential-provider-env": 3.667.0 + "@aws-sdk/credential-provider-http": 3.667.0 + "@aws-sdk/credential-provider-process": 3.667.0 + "@aws-sdk/credential-provider-sso": 3.675.0 + "@aws-sdk/credential-provider-web-identity": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@smithy/credential-provider-imds": ^3.2.4 + "@smithy/property-provider": ^3.1.7 + "@smithy/shared-ini-file-loader": ^3.1.8 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + peerDependencies: + "@aws-sdk/client-sts": ^3.675.0 + checksum: cbe841276cd155181bde85a74d01f690f7aa444ef6b4c8ac824e718d3ce840c11d2e3355412a0bf6e04367402af1b2e56720f05cd5ddea6fcc644bb3bef93b6f + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-node@npm:3.675.0, @aws-sdk/credential-provider-node@npm:^3.350.0": + version: 3.675.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.675.0" + dependencies: + "@aws-sdk/credential-provider-env": 3.667.0 + "@aws-sdk/credential-provider-http": 3.667.0 + "@aws-sdk/credential-provider-ini": 3.675.0 + "@aws-sdk/credential-provider-process": 3.667.0 + "@aws-sdk/credential-provider-sso": 3.675.0 + "@aws-sdk/credential-provider-web-identity": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@smithy/credential-provider-imds": ^3.2.4 + "@smithy/property-provider": ^3.1.7 + "@smithy/shared-ini-file-loader": ^3.1.8 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 9f36e8e8643323dcb88d1412b18d45a0e040f195e88b02224d209068d964679cdfd4e1d1e0af8db00e3b0f4614fb498e545e615f5024c6aed06fabadee4fae36 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-process@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.667.0" + dependencies: + "@aws-sdk/core": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@smithy/property-provider": ^3.1.7 + "@smithy/shared-ini-file-loader": ^3.1.8 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: eab090d81ca09ca690e94f4a37a81c20479b3099c25f410684610de707d17ee8dc20750a3bdd0c7c5eaec1d843313acee3e2a260aed37c1379cf0d36b7044f36 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-sso@npm:3.675.0": + version: 3.675.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.675.0" + dependencies: + "@aws-sdk/client-sso": 3.675.0 + "@aws-sdk/core": 3.667.0 + "@aws-sdk/token-providers": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@smithy/property-provider": ^3.1.7 + "@smithy/shared-ini-file-loader": ^3.1.8 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: a72ee7aee3d34f08c8a4aa8ac8b4985768f98aa5bded4b4d962c8d2371cff47f66811113f9fde80de83459253c12c904b63d86076de184a33bbe4868eb9e21ae + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-web-identity@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.667.0" + dependencies: + "@aws-sdk/core": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@smithy/property-provider": ^3.1.7 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + peerDependencies: + "@aws-sdk/client-sts": ^3.667.0 + checksum: 02dff83f72a7d4a2ab4c763ae0c73a8b3d8c4cff89b10d8015d08db2699638babb0458280c60799bef69efce509c3c94ad6e69a041a0df25bc15bd05be1500dc + languageName: node + linkType: hard + +"@aws-sdk/credential-providers@npm:^3.350.0": + version: 3.675.0 + resolution: "@aws-sdk/credential-providers@npm:3.675.0" + dependencies: + "@aws-sdk/client-cognito-identity": 3.675.0 + "@aws-sdk/client-sso": 3.675.0 + "@aws-sdk/client-sts": 3.675.0 + "@aws-sdk/core": 3.667.0 + "@aws-sdk/credential-provider-cognito-identity": 3.675.0 + "@aws-sdk/credential-provider-env": 3.667.0 + "@aws-sdk/credential-provider-http": 3.667.0 + "@aws-sdk/credential-provider-ini": 3.675.0 + "@aws-sdk/credential-provider-node": 3.675.0 + "@aws-sdk/credential-provider-process": 3.667.0 + "@aws-sdk/credential-provider-sso": 3.675.0 + "@aws-sdk/credential-provider-web-identity": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@smithy/credential-provider-imds": ^3.2.4 + "@smithy/property-provider": ^3.1.7 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: e0eb8db74f1aed41b8b97280efbc306fbf454a2dcf3d03792a57948cbbfe77178539f27628a0e45cb2ae421290e0779109b22b81cbd7c5a625fca0b5a2b98db4 + languageName: node + linkType: hard + +"@aws-sdk/middleware-bucket-endpoint@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.667.0" + dependencies: + "@aws-sdk/types": 3.667.0 + "@aws-sdk/util-arn-parser": 3.568.0 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/protocol-http": ^4.1.4 + "@smithy/types": ^3.5.0 + "@smithy/util-config-provider": ^3.0.0 + tslib: ^2.6.2 + checksum: b67c9438fc574848691072e8f22003d600818489dd066183f5d39243eeae6863170b7c2cfbed0fba16864e2fc6a3198cd23a916faa00086014600c5832b9c954 + languageName: node + linkType: hard + +"@aws-sdk/middleware-expect-continue@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.667.0" + dependencies: + "@aws-sdk/types": 3.667.0 + "@smithy/protocol-http": ^4.1.4 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: c5da94a95ba837bfa3bddd17c9cf0be9e63b0f02fcbfd3394ecdc918f6949715847be8568e710aa5631a628b6b6f27d0ecaad23ff8917207f3ef6a948e62e68f + languageName: node + linkType: hard + +"@aws-sdk/middleware-flexible-checksums@npm:3.669.0": + version: 3.669.0 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.669.0" + dependencies: + "@aws-crypto/crc32": 5.2.0 + "@aws-crypto/crc32c": 5.2.0 + "@aws-sdk/core": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@smithy/is-array-buffer": ^3.0.0 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/protocol-http": ^4.1.4 + "@smithy/types": ^3.5.0 + "@smithy/util-middleware": ^3.0.7 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: 65fc21716ec7d1ff1f241e1fe90a1ae185a8e2421ebaa8b41118006b787df91335b1a6188bd4c7efa722ca427d71fae9439fbbe9d12fc9896ffba70a15540b7d + languageName: node + linkType: hard + +"@aws-sdk/middleware-host-header@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.667.0" + dependencies: + "@aws-sdk/types": 3.667.0 + "@smithy/protocol-http": ^4.1.4 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: e545c3f2182dc6bf812ef19219850e3b86dfc72e7a596b70c6444ccccf79d6939cc1ad00c0261aaacf4cdaf7e5e1f0a17b11a5d197da467a518444172876e671 + languageName: node + linkType: hard + +"@aws-sdk/middleware-location-constraint@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.667.0" + dependencies: + "@aws-sdk/types": 3.667.0 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: bd92e6cb7a6bf823d7ff8703b668498820634f47f503d6d854b3fc809bf7fca65cc6673b5c057c88ae45c64a2926cbe291f46ee9d8fcc6be96bbfe41409242bf + languageName: node + linkType: hard + +"@aws-sdk/middleware-logger@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/middleware-logger@npm:3.667.0" + dependencies: + "@aws-sdk/types": 3.667.0 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 47d7c9fa8f6d5cceeafe91fc35b63c9a18b57bdf289a2d44983409af61c3c52330ff6400c5d1208a250f98bda62d4c1eddb91f3f6c4f83846b10c3c35e317748 + languageName: node + linkType: hard + +"@aws-sdk/middleware-recursion-detection@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.667.0" + dependencies: + "@aws-sdk/types": 3.667.0 + "@smithy/protocol-http": ^4.1.4 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 80b4f28b76e2bba9058ebdf5d19d889f0b5b005ebae0ae7998097620b53502b7f5f8fdd5d17ae4a44a2357f83977e55e840f93255cfcb41a013de0c830553022 + languageName: node + linkType: hard + +"@aws-sdk/middleware-sdk-s3@npm:3.674.0": + version: 3.674.0 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.674.0" + dependencies: + "@aws-sdk/core": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@aws-sdk/util-arn-parser": 3.568.0 + "@smithy/core": ^2.4.8 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/protocol-http": ^4.1.4 + "@smithy/signature-v4": ^4.2.0 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + "@smithy/util-config-provider": ^3.0.0 + "@smithy/util-middleware": ^3.0.7 + "@smithy/util-stream": ^3.1.9 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: d2138edb382ce0f1e9a2e73766214f5b2620a7de921944c493b0c4864a50233857b42daccb3efa9187fcda098b9f910f8183233436004757dd3125fa29a58953 + languageName: node + linkType: hard + +"@aws-sdk/middleware-ssec@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/middleware-ssec@npm:3.667.0" + dependencies: + "@aws-sdk/types": 3.667.0 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 16210a33fb0fc718b46196f3d8cd4931e04b2e09660d4c41ff95a04dbbd09f267115d7e30d29efd1e87102e515e659260714df43b89c409cd53b624767750641 + languageName: node + linkType: hard + +"@aws-sdk/middleware-user-agent@npm:3.669.0": + version: 3.669.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.669.0" + dependencies: + "@aws-sdk/core": 3.667.0 + "@aws-sdk/types": 3.667.0 + "@aws-sdk/util-endpoints": 3.667.0 + "@smithy/core": ^2.4.8 + "@smithy/protocol-http": ^4.1.4 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: c1295321b7767c726428db155d417cdc3459532746bf11b18c5ea296c128137379570a7f83cd1da012b8d64fb0228b9e75f9b66fdd7249f945f020c901bcdb9d + languageName: node + linkType: hard + +"@aws-sdk/region-config-resolver@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.667.0" + dependencies: + "@aws-sdk/types": 3.667.0 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/types": ^3.5.0 + "@smithy/util-config-provider": ^3.0.0 + "@smithy/util-middleware": ^3.0.7 + tslib: ^2.6.2 + checksum: be102ae253d5c6ec934151a427e24187620a180ad7fadf17af14244e3e3dc7dd7a6c7c9c3597adc2994aa2fb4f39146749a4070b6cf96ac182f177161a0f48dc + languageName: node + linkType: hard + +"@aws-sdk/signature-v4-multi-region@npm:3.674.0": + version: 3.674.0 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.674.0" + dependencies: + "@aws-sdk/middleware-sdk-s3": 3.674.0 + "@aws-sdk/types": 3.667.0 + "@smithy/protocol-http": ^4.1.4 + "@smithy/signature-v4": ^4.2.0 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 02914e48fe1e6601d9fa1915526a57e0ea358bea83cfbea14cdadfbf490c724437da8c69bc9e3ec6a4df31f65a6d0530f743931a70c3b109db7a07f06c4c0120 + languageName: node + linkType: hard + +"@aws-sdk/token-providers@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/token-providers@npm:3.667.0" + dependencies: + "@aws-sdk/types": 3.667.0 + "@smithy/property-provider": ^3.1.7 + "@smithy/shared-ini-file-loader": ^3.1.8 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + peerDependencies: + "@aws-sdk/client-sso-oidc": ^3.667.0 + checksum: cd783b95f3bbe6b33eb75abbad2ea36cec91fc2788376711a2a866e3d1f17b21313004f3e75dadd270ca41ee59f3cc57198dbf51bf222d472ccda249c47ece3b + languageName: node + linkType: hard + +"@aws-sdk/types@npm:3.370.0": + version: 3.370.0 + resolution: "@aws-sdk/types@npm:3.370.0" + dependencies: + "@smithy/types": ^1.1.0 + tslib: ^2.5.0 + checksum: 105a5768f20075035c2250de69f782ea4219c9ed8cd426c9ab57605616c8b1d534764d3c5b29e9715eb68a0e3f99b27ed463c410a3d728abf3c4ad59347e9f4e + languageName: node + linkType: hard + +"@aws-sdk/types@npm:3.667.0, @aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.347.0": + version: 3.667.0 + resolution: "@aws-sdk/types@npm:3.667.0" + dependencies: + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 54471245c8e5ba7542e2e19044b350cde965aa7ef71fcbc931e4ae2436ff620b07234d777035db6b34e5deac74ac41efaa41b6b378044d4597e7463ecff22925 + languageName: node + linkType: hard + +"@aws-sdk/util-arn-parser@npm:3.568.0, @aws-sdk/util-arn-parser@npm:^3.310.0": + version: 3.568.0 + resolution: "@aws-sdk/util-arn-parser@npm:3.568.0" + dependencies: + tslib: ^2.6.2 + checksum: e3c45e5d524a772954d0a33614d397414185b9eb635423d01253cad1c1b9add625798ed9cf23343d156fae89c701f484bc062ab673f67e2e2edfe362fde6d170 + languageName: node + linkType: hard + +"@aws-sdk/util-endpoints@npm:3.667.0": + version: 3.667.0 + resolution: "@aws-sdk/util-endpoints@npm:3.667.0" + dependencies: + "@aws-sdk/types": 3.667.0 + "@smithy/types": ^3.5.0 + "@smithy/util-endpoints": ^2.1.3 + tslib: ^2.6.2 + checksum: c9e2baccba71c43f52570ff0c2e522126657fadd4dba184cd9edd3d4f17bfa527728e5c4f841cedaa527cab6ad4ecc4dcc87714748785fbdeb98b29b9f4cbc24 + languageName: node + linkType: hard + +"@aws-sdk/util-locate-window@npm:^3.0.0": + version: 3.568.0 + resolution: "@aws-sdk/util-locate-window@npm:3.568.0" + dependencies: + tslib: ^2.6.2 + checksum: 354db5187beee4203c7ec6583556ab14ecde9644c06aaa51fa2528131836d3fc73035a3b080c904e108c49defce20d5562893113b93d819b70497f47989bb578 + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-browser@npm:3.675.0": + version: 3.675.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.675.0" + dependencies: + "@aws-sdk/types": 3.667.0 + "@smithy/types": ^3.5.0 + bowser: ^2.11.0 + tslib: ^2.6.2 + checksum: 4aee69eb989edb674577c593d0b6eb493b68d11c9901d54500e1acc049d39312a259b057b6fba01a61fffc861c0548162085a8cd7d3b1477ac5ccb73d8d0c550 + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-node@npm:3.669.0": + version: 3.669.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.669.0" + dependencies: + "@aws-sdk/middleware-user-agent": 3.669.0 + "@aws-sdk/types": 3.667.0 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + peerDependencies: + aws-crt: ">=1.0.0" + peerDependenciesMeta: + aws-crt: + optional: true + checksum: 89af06914764efbccc2bdd07f8cc26c4949a20dfed30064d71f1e310cfa9b3bf40a94c2245723209a790bdacf71ced9d5c2742760205879417e220f00808add2 + languageName: node + linkType: hard + +"@aws-sdk/xml-builder@npm:3.662.0": + version: 3.662.0 + resolution: "@aws-sdk/xml-builder@npm:3.662.0" + dependencies: + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 4571dfe225133ccad480b20cf3f0887386e90579608b524e4e84447c1e995aea1df8d6ec0ce82e7d121ad3f0d9d94c6a85a2ce9db22440da46f3bcb3fb7351d7 + languageName: node + linkType: hard + +"@azure/abort-controller@npm:^2.0.0": + version: 2.1.2 + resolution: "@azure/abort-controller@npm:2.1.2" + dependencies: + tslib: ^2.6.2 + checksum: 22176c04ea01498311c6bbd336669f6e3faffad1cbb0c9ebc6ee9c1ff2cf958fd17ce73c7354b99d8bda9fcd311325ece7bee248875279174e3fc460e8b1a63d + languageName: node + linkType: hard + +"@azure/core-auth@npm:^1.4.0, @azure/core-auth@npm:^1.8.0, @azure/core-auth@npm:^1.9.0": + version: 1.9.0 + resolution: "@azure/core-auth@npm:1.9.0" + dependencies: + "@azure/abort-controller": ^2.0.0 + "@azure/core-util": ^1.11.0 + tslib: ^2.6.2 + checksum: 4050112188db093c5e01caca0175708c767054c0cea4202430ff43ee42a16430235752ccc0002caea1796c8f01b4f6369c878762bf4c1b2f61af1b7ac13182fc + languageName: node + linkType: hard + +"@azure/core-client@npm:^1.9.2": + version: 1.9.2 + resolution: "@azure/core-client@npm:1.9.2" + dependencies: + "@azure/abort-controller": ^2.0.0 + "@azure/core-auth": ^1.4.0 + "@azure/core-rest-pipeline": ^1.9.1 + "@azure/core-tracing": ^1.0.0 + "@azure/core-util": ^1.6.1 + "@azure/logger": ^1.0.0 + tslib: ^2.6.2 + checksum: 961b829dfda4f734a763e9480a2ea622a7031ba2da4126d0add6e351a9f73ddc5782bf2b766735d976b61da3857014e0a90223d1f85d1c68468747a7a56851c3 + languageName: node + linkType: hard + +"@azure/core-rest-pipeline@npm:^1.17.0, @azure/core-rest-pipeline@npm:^1.9.1": + version: 1.17.0 + resolution: "@azure/core-rest-pipeline@npm:1.17.0" + dependencies: + "@azure/abort-controller": ^2.0.0 + "@azure/core-auth": ^1.8.0 + "@azure/core-tracing": ^1.0.1 + "@azure/core-util": ^1.9.0 + "@azure/logger": ^1.0.0 + http-proxy-agent: ^7.0.0 + https-proxy-agent: ^7.0.0 + tslib: ^2.6.2 + checksum: 8a79cbaaae295964bb8d18cb44873e705ebe3f9217fe74d83415b7266e46c3d6297c799d6e5e49516b165d273e0b794bf0ed14bb6aa875d09d4a90c3a559b6df + languageName: node + linkType: hard + +"@azure/core-tracing@npm:^1.0.0, @azure/core-tracing@npm:^1.0.1": + version: 1.2.0 + resolution: "@azure/core-tracing@npm:1.2.0" + dependencies: + tslib: ^2.6.2 + checksum: 202ebf411a3076bd2c48b7a4c1b63335f53be6dd97f7d53500e3191b7ed0fdad25de219f422e777fde824031fd5c67087654de0304a5c0cd67c38cdcab96117c + languageName: node + linkType: hard + +"@azure/core-util@npm:^1.11.0, @azure/core-util@npm:^1.6.1, @azure/core-util@npm:^1.9.0": + version: 1.11.0 + resolution: "@azure/core-util@npm:1.11.0" + dependencies: + "@azure/abort-controller": ^2.0.0 + tslib: ^2.6.2 + checksum: 91e3ec329d9eddaa66be5efb1785dad68dcb48dd779fca36e39db041673230510158ff5ca9ccef9f19c3e4d8e9af29f66a367cfc31a7b94d2541f80ef94ec797 + languageName: node + linkType: hard + +"@azure/identity@npm:^4.0.0": + version: 4.5.0 + resolution: "@azure/identity@npm:4.5.0" + dependencies: + "@azure/abort-controller": ^2.0.0 + "@azure/core-auth": ^1.9.0 + "@azure/core-client": ^1.9.2 + "@azure/core-rest-pipeline": ^1.17.0 + "@azure/core-tracing": ^1.0.0 + "@azure/core-util": ^1.11.0 + "@azure/logger": ^1.0.0 + "@azure/msal-browser": ^3.26.1 + "@azure/msal-node": ^2.15.0 + events: ^3.0.0 + jws: ^4.0.0 + open: ^8.0.0 + stoppable: ^1.1.0 + tslib: ^2.2.0 + checksum: 07d15898f194a220376d8d9c0ee891c93c6da188e44e76810fb781bf3bb7424498a6c1fa5b92c5a4d31f62b7398953f8a5bcf0f0ed57ed72239ce1c4f594b355 + languageName: node + linkType: hard + +"@azure/logger@npm:^1.0.0": + version: 1.1.4 + resolution: "@azure/logger@npm:1.1.4" + dependencies: + tslib: ^2.6.2 + checksum: d4bfd83f31afc465689e02ac2d8eb0a1c6573cc47ea3fa18778c5d7d096ee7a4fdc130f00e9d162ec8ed192aeb9a54d5c3ab15bd7a12bbe039d5f594ba0f797b + languageName: node + linkType: hard + +"@azure/msal-browser@npm:^3.26.1": + version: 3.26.1 + resolution: "@azure/msal-browser@npm:3.26.1" + dependencies: + "@azure/msal-common": 14.15.0 + checksum: 70ebea1abc4bc6b0e5a250f865cffd24a1aeb615a35e7b572dad11369d486a7aeb4af60048c5f6a5bc3627fad65dbdc8c118f16086cb3f9cc03931699b08f4f7 + languageName: node + linkType: hard + +"@azure/msal-common@npm:14.15.0": + version: 14.15.0 + resolution: "@azure/msal-common@npm:14.15.0" + checksum: 072e4ca58856997df2e82935c818801a69a85df16d7dccdfed02c1b8f8a772751594efe1b918433c760202348a99aa6ec9d99cc0f018ab2f1659186ad2a8b88e + languageName: node + linkType: hard + +"@azure/msal-node@npm:^2.15.0": + version: 2.15.0 + resolution: "@azure/msal-node@npm:2.15.0" + dependencies: + "@azure/msal-common": 14.15.0 + jsonwebtoken: ^9.0.0 + uuid: ^8.3.0 + checksum: 10dd1c273e2465d519d28ee04d1c9e2e4ecfa2cab664b38677502c626139f86a16f7d78c645e6727d550a84dfa7773ebea1fb2cc7454870f3c6507d601e6ef2f + languageName: node + linkType: hard + +"@babel/code-frame@npm:7.0.0": + version: 7.0.0 + resolution: "@babel/code-frame@npm:7.0.0" + dependencies: + "@babel/highlight": ^7.0.0 + checksum: 0483e67fea3ee5930c163c7dc729a2a5250afab49d0b52e187dfdb7b6382e256fa269e3b3f7af0d55cce27f145c79112934a9d2b8854dd3953c8337a61c0c619 + languageName: node + linkType: hard + +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0, @babel/code-frame@npm:^7.8.3": + version: 7.26.2 + resolution: "@babel/code-frame@npm:7.26.2" + dependencies: + "@babel/helper-validator-identifier": ^7.25.9 + js-tokens: ^4.0.0 + picocolors: ^1.0.0 + checksum: db13f5c42d54b76c1480916485e6900748bbcb0014a8aca87f50a091f70ff4e0d0a6db63cade75eb41fcc3d2b6ba0a7f89e343def4f96f00269b41b8ab8dd7b8 + languageName: node + linkType: hard + +"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.25.8, @babel/compat-data@npm:^7.25.9": + version: 7.26.2 + resolution: "@babel/compat-data@npm:7.26.2" + checksum: d52fae9b0dc59b409d6005ae6b172e89329f46d68136130065ebe923a156fc633e0f1c8600b3e319b9e0f99fd948f64991a5419e2e9431d00d9d235d5f7a7618 + languageName: node + linkType: hard + +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.19.6, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.0": + version: 7.26.0 + resolution: "@babel/core@npm:7.26.0" + dependencies: + "@ampproject/remapping": ^2.2.0 + "@babel/code-frame": ^7.26.0 + "@babel/generator": ^7.26.0 + "@babel/helper-compilation-targets": ^7.25.9 + "@babel/helper-module-transforms": ^7.26.0 + "@babel/helpers": ^7.26.0 + "@babel/parser": ^7.26.0 + "@babel/template": ^7.25.9 + "@babel/traverse": ^7.25.9 + "@babel/types": ^7.26.0 + convert-source-map: ^2.0.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.3 + semver: ^6.3.1 + checksum: b296084cfd818bed8079526af93b5dfa0ba70282532d2132caf71d4060ab190ba26d3184832a45accd82c3c54016985a4109ab9118674347a7e5e9bc464894e6 + languageName: node + linkType: hard + +"@babel/generator@npm:^7.23.6, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.26.0, @babel/generator@npm:^7.7.2": + version: 7.26.2 + resolution: "@babel/generator@npm:7.26.2" + dependencies: + "@babel/parser": ^7.26.2 + "@babel/types": ^7.26.0 + "@jridgewell/gen-mapping": ^0.3.5 + "@jridgewell/trace-mapping": ^0.3.25 + jsesc: ^3.0.2 + checksum: 6ff850b7d6082619f8c2f518d993cf7254cfbaa20b026282cbef5c9b2197686d076a432b18e36c4d1a42721c016df4f77a8f62c67600775d9683621d534b91b4 + languageName: node + linkType: hard + +"@babel/helper-annotate-as-pure@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-annotate-as-pure@npm:7.25.7" + dependencies: + "@babel/types": ^7.25.7 + checksum: 4b3680b31244ee740828cd7537d5e5323dd9858c245a02f5636d54e45956f42d77bbe9e1dd743e6763eb47c25967a8b12823002cc47809f5f7d8bc24eefe0304 + languageName: node + linkType: hard + +"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.25.7" + dependencies: + "@babel/traverse": ^7.25.7 + "@babel/types": ^7.25.7 + checksum: 91e9c620daa3bf61904530c0204b0eec140cab716757e82c43564839f6beaeb83c10fd075c238b27e4745fd51a5c2d93ee836d7012036ef83dbb074162cb093c + languageName: node + linkType: hard + +"@babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.25.7, @babel/helper-compilation-targets@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-compilation-targets@npm:7.25.9" + dependencies: + "@babel/compat-data": ^7.25.9 + "@babel/helper-validator-option": ^7.25.9 + browserslist: ^4.24.0 + lru-cache: ^5.1.1 + semver: ^6.3.1 + checksum: 3af536e2db358b38f968abdf7d512d425d1018fef2f485d6f131a57a7bcaed32c606b4e148bb230e1508fa42b5b2ac281855a68eb78270f54698c48a83201b9b + languageName: node + linkType: hard + +"@babel/helper-create-class-features-plugin@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-create-class-features-plugin@npm:7.25.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.25.7 + "@babel/helper-member-expression-to-functions": ^7.25.7 + "@babel/helper-optimise-call-expression": ^7.25.7 + "@babel/helper-replace-supers": ^7.25.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.25.7 + "@babel/traverse": ^7.25.7 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 6b04760b405cff47b82c7e121fc3fe335bc470806bff49467675581f1cfe285a68ed3d6b00001ad47e28aa4b224f095e03eb7a184dc35e3c651e8f83e0cc6f43 + languageName: node + linkType: hard + +"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.25.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.25.7 + regexpu-core: ^6.1.1 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 378a882dda9387ca74347e55016cee616b28ceb30fee931d6904740cd7d3826cba0541f198721933d0f623cd3120aa0836d53704ebf2dcd858954c62e247eb15 + languageName: node + linkType: hard + +"@babel/helper-define-polyfill-provider@npm:^0.6.2": + version: 0.6.2 + resolution: "@babel/helper-define-polyfill-provider@npm:0.6.2" + dependencies: + "@babel/helper-compilation-targets": ^7.22.6 + "@babel/helper-plugin-utils": ^7.22.5 + debug: ^4.1.1 + lodash.debounce: ^4.0.8 + resolve: ^1.14.2 + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 2bba965ea9a4887ddf9c11d51d740ab473bd7597b787d042c325f6a45912dfe908c2d6bb1d837bf82f7e9fa51e6ad5150563c58131d2bb85515e63d971414a9c + languageName: node + linkType: hard + +"@babel/helper-member-expression-to-functions@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-member-expression-to-functions@npm:7.25.7" + dependencies: + "@babel/traverse": ^7.25.7 + "@babel/types": ^7.25.7 + checksum: 12141c17b92a36a00f878abccbee1dfdd848fa4995d502b623190076f10696241949b30e51485187cee1c1527dbf4610a59d8fd80d2e31aac1131e474b5bfed6 + languageName: node + linkType: hard + +"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.25.7, @babel/helper-module-imports@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-module-imports@npm:7.25.9" + dependencies: + "@babel/traverse": ^7.25.9 + "@babel/types": ^7.25.9 + checksum: 1b411ce4ca825422ef7065dffae7d8acef52023e51ad096351e3e2c05837e9bf9fca2af9ca7f28dc26d596a588863d0fedd40711a88e350b736c619a80e704e6 + languageName: node + linkType: hard + +"@babel/helper-module-transforms@npm:^7.25.7, @babel/helper-module-transforms@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/helper-module-transforms@npm:7.26.0" + dependencies: + "@babel/helper-module-imports": ^7.25.9 + "@babel/helper-validator-identifier": ^7.25.9 + "@babel/traverse": ^7.25.9 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 942eee3adf2b387443c247a2c190c17c4fd45ba92a23087abab4c804f40541790d51ad5277e4b5b1ed8d5ba5b62de73857446b7742f835c18ebd350384e63917 + languageName: node + linkType: hard + +"@babel/helper-optimise-call-expression@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-optimise-call-expression@npm:7.25.7" + dependencies: + "@babel/types": ^7.25.7 + checksum: 5555d2d3f11f424e38ad8383efccc7ebad4f38fddd2782de46c5fcbf77a5e1e0bc5b8cdbee3bd59ab38f353690568ffe08c7830f39b0aff23f5179d345799f06 + languageName: node + linkType: hard + +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.25.7, @babel/helper-plugin-utils@npm:^7.8.0": + version: 7.25.7 + resolution: "@babel/helper-plugin-utils@npm:7.25.7" + checksum: eef4450361e597f11247d252e69207324dfe0431df9b8bcecc8bef1204358e93fa7776a659c3c4f439e9ee71cd967aeca6c4d6034ebc17a7ae48143bbb580f2f + languageName: node + linkType: hard + +"@babel/helper-remap-async-to-generator@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-remap-async-to-generator@npm:7.25.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.25.7 + "@babel/helper-wrap-function": ^7.25.7 + "@babel/traverse": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: f68b4a56d894a556948d8ea052cd7c01426f309ea48395d1914a1332f0d6e8579874fbe7e4c165713dd43ac049c7e79ebb1f9fbb48397d9c803209dd1ff41758 + languageName: node + linkType: hard + +"@babel/helper-replace-supers@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-replace-supers@npm:7.25.7" + dependencies: + "@babel/helper-member-expression-to-functions": ^7.25.7 + "@babel/helper-optimise-call-expression": ^7.25.7 + "@babel/traverse": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: bbfb4de148b1ce24d0f953b1e7cd31a8f8e8e881f3cd908d1848c0f453c87b4a1529c0b9c5a9e8b70de734a6993b3bb2f3594af16f46f5324a9461aaa04976c4 + languageName: node + linkType: hard + +"@babel/helper-simple-access@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-simple-access@npm:7.25.7" + dependencies: + "@babel/traverse": ^7.25.7 + "@babel/types": ^7.25.7 + checksum: 684d0b0330c42d62834355f127df3ed78f16e6f1f66213c72adb7b3b0bcd6283ea8792f5b172868b3ca6518c479b54e18adac564219519072dda9053cca210bd + languageName: node + linkType: hard + +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.25.7" + dependencies: + "@babel/traverse": ^7.25.7 + "@babel/types": ^7.25.7 + checksum: 2fbdcef036135ffd14ab50861e3560c455e532f9a470e7ed97141b6a7f17bfcc2977b29d16affd0634c6656de4fcc0e91f3bc62a50a4e5d6314cb6164c4d3a67 + languageName: node + linkType: hard + +"@babel/helper-string-parser@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-string-parser@npm:7.25.9" + checksum: 6435ee0849e101681c1849868278b5aee82686ba2c1e27280e5e8aca6233af6810d39f8e4e693d2f2a44a3728a6ccfd66f72d71826a94105b86b731697cdfa99 + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.25.7, @babel/helper-validator-identifier@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-identifier@npm:7.25.9" + checksum: 5b85918cb1a92a7f3f508ea02699e8d2422fe17ea8e82acd445006c0ef7520fbf48e3dbcdaf7b0a1d571fc3a2715a29719e5226636cb6042e15fe6ed2a590944 + languageName: node + linkType: hard + +"@babel/helper-validator-option@npm:^7.25.7, @babel/helper-validator-option@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-option@npm:7.25.9" + checksum: 9491b2755948ebbdd68f87da907283698e663b5af2d2b1b02a2765761974b1120d5d8d49e9175b167f16f72748ffceec8c9cf62acfbee73f4904507b246e2b3d + languageName: node + linkType: hard + +"@babel/helper-wrap-function@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-wrap-function@npm:7.25.7" + dependencies: + "@babel/template": ^7.25.7 + "@babel/traverse": ^7.25.7 + "@babel/types": ^7.25.7 + checksum: 3da877ae06b83eec4ddfa3b667e8a5efbaf04078788756daea4a3c027caa0f7f0ee7f3f559ea9be4e88dd4d895c68bebbd11630277bb20fc43d0c7794f094d2a + languageName: node + linkType: hard + +"@babel/helpers@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/helpers@npm:7.26.0" + dependencies: + "@babel/template": ^7.25.9 + "@babel/types": ^7.26.0 + checksum: d77fe8d45033d6007eadfa440355c1355eed57902d5a302f450827ad3d530343430a21210584d32eef2f216ae463d4591184c6fc60cf205bbf3a884561469200 + languageName: node + linkType: hard + +"@babel/highlight@npm:^7.0.0": + version: 7.25.7 + resolution: "@babel/highlight@npm:7.25.7" + dependencies: + "@babel/helper-validator-identifier": ^7.25.7 + chalk: ^2.4.2 + js-tokens: ^4.0.0 + picocolors: ^1.0.0 + checksum: b6aa45c5bf7ecc16b8204bbed90335706131ac6cacb0f1bfb1b862ada3741539c913b56c9d26beb56cece0c231ffab36f66aa36aac6b04b32669c314705203f2 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.0, @babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.0, @babel/parser@npm:^7.26.2": + version: 7.26.2 + resolution: "@babel/parser@npm:7.26.2" + dependencies: + "@babel/types": ^7.26.0 + bin: + parser: ./bin/babel-parser.js + checksum: c88b5ea0adf357ef909cdc2c31e284a154943edc59f63f6e8a4c20bf773a1b2f3d8c2205e59c09ca7cdad91e7466300114548876529277a80651b6436a48d5d9 + languageName: node + linkType: hard + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/traverse": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 38f7622dabe9eeaa2996efd5787a32d030d9cd175ce54d6b5673561452da79c9cd29126eee08756004638d0da640280a3fee93006f2eddb958f8744fb0ced86f + languageName: node + linkType: hard + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: bf37ec72d79ab7c1f12d201dd71b9e26f27082fffbbdf1a7104564b1f72cbb900f439cdca1ac25a9f600b8bc2b0ad1fa9a48361b6b8982d38f6ad861806af42c + languageName: node + linkType: hard + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 6a095db359733b588b6e9e01c3926d2a51db2a9c02c0bdf54a916831f4f59865ea3660955bd420776522b204f610bfb0226e2bf3cfd8f830292a46f6629b3b8b + languageName: node + linkType: hard + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.25.7 + "@babel/plugin-transform-optional-chaining": ^7.25.7 + peerDependencies: + "@babel/core": ^7.13.0 + checksum: 63135dd20398b2f957ab4d76cd6c8e2f83be2cb6b1cb1af9781f7bb2b90e06b495f3b9df14398801aefc270ff04cc7c64dab49fed8724bfc46ea0e115f98e693 + languageName: node + linkType: hard + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/traverse": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 8a60b36c4e645f2e7b606a9e36568cbf94a1e3a21bbd318ab29d3e8e4795eed524b620fc771ac0ab8ceef26c2b750f416c7c600c4bab2dff4fcad789c9fe620a + languageName: node + linkType: hard + +"@babel/plugin-proposal-private-property-in-object@npm:7.21.0-placeholder-for-preset-env.2": + version: 7.21.0-placeholder-for-preset-env.2 + resolution: "@babel/plugin-proposal-private-property-in-object@npm:7.21.0-placeholder-for-preset-env.2" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d97745d098b835d55033ff3a7fb2b895b9c5295b08a5759e4f20df325aa385a3e0bc9bd5ad8f2ec554a44d4e6525acfc257b8c5848a1345cb40f26a30e277e91 + languageName: node + linkType: hard + +"@babel/plugin-syntax-async-generators@npm:^7.8.4": + version: 7.8.4 + resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7ed1c1d9b9e5b64ef028ea5e755c0be2d4e5e4e3d6cf7df757b9a8c4cfa4193d268176d0f1f7fbecdda6fe722885c7fda681f480f3741d8a2d26854736f05367 + languageName: node + linkType: hard + +"@babel/plugin-syntax-bigint@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-bigint@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3a10849d83e47aec50f367a9e56a6b22d662ddce643334b087f9828f4c3dd73bdc5909aaeabe123fed78515767f9ca43498a0e621c438d1cd2802d7fae3c9648 + languageName: node + linkType: hard + +"@babel/plugin-syntax-class-properties@npm:^7.12.13": + version: 7.12.13 + resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" + dependencies: + "@babel/helper-plugin-utils": ^7.12.13 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 24f34b196d6342f28d4bad303612d7ff566ab0a013ce89e775d98d6f832969462e7235f3e7eaf17678a533d4be0ba45d3ae34ab4e5a9dcbda5d98d49e5efa2fc + languageName: node + linkType: hard + +"@babel/plugin-syntax-class-static-block@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-class-static-block@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": ^7.14.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3e80814b5b6d4fe17826093918680a351c2d34398a914ce6e55d8083d72a9bdde4fbaf6a2dcea0e23a03de26dc2917ae3efd603d27099e2b98380345703bf948 + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-assertions@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: b2f994bc7b6dffdcc3fb144cf29fb2516d1e9b5ca276b30f9ed4f9dc8e55abb5a57511a23877665e609659f6da12c89b9ad01e8408650dcb309f00502b790ced + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-attributes@npm:^7.24.7, @babel/plugin-syntax-import-attributes@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: fbef3dc25cc262eec8547a0ae751fb962f81c07cd6260a5ce7b52a4af1a157882648f9b6dd481ea16bf4a24166695dc1a6e5b53d42234bfccc0322dce2a86ca8 + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-meta@npm:^7.10.4": + version: 7.10.4 + resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 166ac1125d10b9c0c430e4156249a13858c0366d38844883d75d27389621ebe651115cb2ceb6dc011534d5055719fa1727b59f39e1ab3ca97820eef3dcab5b9b + languageName: node + linkType: hard + +"@babel/plugin-syntax-json-strings@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-json-strings@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bf5aea1f3188c9a507e16efe030efb996853ca3cadd6512c51db7233cc58f3ac89ff8c6bdfb01d30843b161cfe7d321e1bf28da82f7ab8d7e6bc5464666f354a + languageName: node + linkType: hard + +"@babel/plugin-syntax-jsx@npm:^7.25.7, @babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.25.7 + resolution: "@babel/plugin-syntax-jsx@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3584566707a1c92e48b3ad2423af73bc4497093fb17fb786977fc5aef6130ae7a2f7856a7848431bed1ac21b4a8d86d2ff4505325b700f76f9bd57b4e95a2297 + languageName: node + linkType: hard + +"@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4": + version: 7.10.4 + resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: aff33577037e34e515911255cdbb1fd39efee33658aa00b8a5fd3a4b903585112d037cce1cc9e4632f0487dc554486106b79ccd5ea63a2e00df4363f6d4ff886 + languageName: node + linkType: hard + +"@babel/plugin-syntax-nullish-coalescing-operator@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-nullish-coalescing-operator@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 87aca4918916020d1fedba54c0e232de408df2644a425d153be368313fdde40d96088feed6c4e5ab72aac89be5d07fef2ddf329a15109c5eb65df006bf2580d1 + languageName: node + linkType: hard + +"@babel/plugin-syntax-numeric-separator@npm:^7.10.4": + version: 7.10.4 + resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 01ec5547bd0497f76cc903ff4d6b02abc8c05f301c88d2622b6d834e33a5651aa7c7a3d80d8d57656a4588f7276eba357f6b7e006482f5b564b7a6488de493a1 + languageName: node + linkType: hard + +"@babel/plugin-syntax-object-rest-spread@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-object-rest-spread@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: fddcf581a57f77e80eb6b981b10658421bc321ba5f0a5b754118c6a92a5448f12a0c336f77b8abf734841e102e5126d69110a306eadb03ca3e1547cab31f5cbf + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-catch-binding@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-catch-binding@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 910d90e72bc90ea1ce698e89c1027fed8845212d5ab588e35ef91f13b93143845f94e2539d831dc8d8ededc14ec02f04f7bd6a8179edd43a326c784e7ed7f0b9 + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-chaining@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-chaining@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: eef94d53a1453361553c1f98b68d17782861a04a392840341bc91780838dd4e695209c783631cf0de14c635758beafb6a3a65399846ffa4386bff90639347f30 + languageName: node + linkType: hard + +"@babel/plugin-syntax-private-property-in-object@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-private-property-in-object@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": ^7.14.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: b317174783e6e96029b743ccff2a67d63d38756876e7e5d0ba53a322e38d9ca452c13354a57de1ad476b4c066dbae699e0ca157441da611117a47af88985ecda + languageName: node + linkType: hard + +"@babel/plugin-syntax-top-level-await@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": ^7.14.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bbd1a56b095be7820029b209677b194db9b1d26691fe999856462e66b25b281f031f3dfd91b1619e9dcf95bebe336211833b854d0fb8780d618e35667c2d0d7e + languageName: node + linkType: hard + +"@babel/plugin-syntax-typescript@npm:^7.25.7, @babel/plugin-syntax-typescript@npm:^7.7.2": + version: 7.25.7 + resolution: "@babel/plugin-syntax-typescript@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: b347da4c681d41c1780417939e9a0388c23cbe46ac9d2d6e5ef2119914bce11ea607963252a87e2c9f8e09eb5e0dac6b9741d79a7c7214c49b314d325d79ba8b + languageName: node + linkType: hard + +"@babel/plugin-syntax-unicode-sets-regex@npm:^7.18.6": + version: 7.18.6 + resolution: "@babel/plugin-syntax-unicode-sets-regex@npm:7.18.6" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.18.6 + "@babel/helper-plugin-utils": ^7.18.6 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: a651d700fe63ff0ddfd7186f4ebc24447ca734f114433139e3c027bc94a900d013cf1ef2e2db8430425ba542e39ae160c3b05f06b59fd4656273a3df97679e9c + languageName: node + linkType: hard + +"@babel/plugin-transform-arrow-functions@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-arrow-functions@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e3433df7f487393a207d9942db604493f07b1f59dd8995add55d97ffe6a8f566360fbc9bf54b820a76f05308e46fca524069087e5c975a22b978faa711d56bf6 + languageName: node + linkType: hard + +"@babel/plugin-transform-async-generator-functions@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.25.8" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-remap-async-to-generator": ^7.25.7 + "@babel/traverse": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e2bb32f0722b558bafc18c5cd2a0cf0da056923e79b0225c8a88115c2659d8ca684013f16c796f003e37358bbeb250e2ddca410d13b1797b219ea69a56d836d7 + languageName: node + linkType: hard + +"@babel/plugin-transform-async-to-generator@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.25.7" + dependencies: + "@babel/helper-module-imports": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-remap-async-to-generator": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 86fa335fb8990c6c6421dcf48f137a3df3ddbc892170797fcfcd63e1fe13d4398aec0ea1c19fb384b5750f4f7ff71fb7b48c2ec1d0e4ac44813c9319bb5d3bae + languageName: node + linkType: hard + +"@babel/plugin-transform-block-scoped-functions@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: eeb34b860a873abdb642b35702084b2c7a926e24cc1761f64a275076615119f9b6b42480448484743479998f637a103af0f1ff709187583eadf42cd70ffbc1dd + languageName: node + linkType: hard + +"@babel/plugin-transform-block-scoping@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-block-scoping@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 183b985bc155fa6e85da472ca31fb6839c5d0c7b7ab722540aa8f8cadaeaae6da939c7073be3008a05ed62abd0c95e35e27cde0d653f77e0b1a8ff59d57054af + languageName: node + linkType: hard + +"@babel/plugin-transform-class-properties@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-class-properties@npm:7.25.7" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 4d0ae6b775f58fd8bbccc93e2424af17b70f44c060a2386ef9eb765422acbe969969829dab96b762155db818fa0207a8a678a0e487e555965eda441c837bf866 + languageName: node + linkType: hard + +"@babel/plugin-transform-class-static-block@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-class-static-block@npm:7.25.8" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.12.0 + checksum: 2cc64441c98bc93e1596a030f1a43b068980060f38373b1c985d60e05041eacf9753ed5440cae1cfa03c1dae7ffccfb2ffc8d93b83d584e0f3e8600313a3e034 + languageName: node + linkType: hard + +"@babel/plugin-transform-classes@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-classes@npm:7.25.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.25.7 + "@babel/helper-compilation-targets": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-replace-supers": ^7.25.7 + "@babel/traverse": ^7.25.7 + globals: ^11.1.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 2793844dd4bccc6ec3233371f2bece0d22faa5ff29b90a0e122e873444637aa79dc87a2e7201d8d7f5e356a49a24efa7459bf5f49843246ba1e4bf8bb33bf2ec + languageName: node + linkType: hard + +"@babel/plugin-transform-computed-properties@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-computed-properties@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/template": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 9496e25e7846c61190747f2b8763cd8ed129f794d689acc7cd3406d0b60757d39c974cc67868d046b6b96c608f41e5c98b85075d6a4935550045db66ed177ee5 + languageName: node + linkType: hard + +"@babel/plugin-transform-destructuring@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-destructuring@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 8b4015ef0c9117515b107ef0cd138108f1b025b40393d1da364c5c8123674d6f01523e8786d5bd2fae6d95fa9ec67b6fe7b868d69e930ea9701f337a160e2133 + languageName: node + linkType: hard + +"@babel/plugin-transform-dotall-regex@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-dotall-regex@npm:7.25.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 62fc2650ed45d5c208650ae5b564d9fb414af65df789fda0ec210383524c471f5ec647a72de1abd314a9a30b02c1748f13e42fa0c0d3eb33de6948956040bc73 + languageName: node + linkType: hard + +"@babel/plugin-transform-duplicate-keys@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-duplicate-keys@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3e9e8c6a7b52fdd73949a66de84a3f9232654990644e2dd036debb6014e3a4d548ae44ee1e6697aaf8d927fb9ea8907b340831f9003a4168377c16441ff1ee47 + languageName: node + linkType: hard + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.25.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: b8c5d59bdf2ac88cc7a0efe737f7749e61a759a31943ed2147d9431454d2013c5fc900ce2b401a80c5e0b1a1cce7699c5bbabe1b6415fc3b037c557733522260 + languageName: node + linkType: hard + +"@babel/plugin-transform-dynamic-import@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-dynamic-import@npm:7.25.8" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 23ee7fb57ff4ed5a5df2bdf92eebf74af35b891c53fc6e724c907db4b8803a1a3f61916c40088e2bcfa5a7a9adc62fcbf1dade36b139dfce08efd10fb77036b5 + languageName: node + linkType: hard + +"@babel/plugin-transform-exponentiation-operator@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.25.7" + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6ad8fa4435ddac508e1c13ae692ca5ee78ec5a33e0485cbfa1866cc2e58efe98ffecc55be28baa2e85233b279ad28cecf2d310b6c36a4861bec789093c4f5737 + languageName: node + linkType: hard + +"@babel/plugin-transform-export-namespace-from@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.25.8" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 8bce1d8349b3383a8d2e9f65960873605e15608a9ebdbc81de270c42f9e623011666b1d997ebd142aca2d1bcb67275f594a9b4939729abe4ed4939b8d5358e3f + languageName: node + linkType: hard + +"@babel/plugin-transform-for-of@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-for-of@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 1f637257dea72b5b6f501ba15a56e51742772ad29297b135ddb14d10601da6ecaeda8bf1acaf258e71be6b66cbd9f08dacadf3cd1b6559d1098b6fef1d1a5410 + languageName: node + linkType: hard + +"@babel/plugin-transform-function-name@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-function-name@npm:7.25.7" + dependencies: + "@babel/helper-compilation-targets": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/traverse": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 5153243f856f966c04239b1b54ab36bc78bd1f8d9e8aca538d8f8d5d1794876a439045907c3217c69c411a72487e2a07b24b87399a9346fa7ac87154e5fd756c + languageName: node + linkType: hard + +"@babel/plugin-transform-json-strings@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-json-strings@npm:7.25.8" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 375f3b7c52805daf8fc6df341ffa00e41bf2ae96bcb433c2ae1e3239d1b0163a5264090a94f3b84c0a14c4052a26a786130e4f1b140546e8b91e26d6363e35aa + languageName: node + linkType: hard + +"@babel/plugin-transform-literals@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-literals@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: da0cec184628e156e79437bd22fad09e2656f4a5583c83b64e0e9b399180bc8703948556237530bd3edc2d41dbea61f13c523cd4c7f0e8f5a1f1d19ed5725cf0 + languageName: node + linkType: hard + +"@babel/plugin-transform-logical-assignment-operators@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.25.8" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6a3a3916352942b739163dea84521938592b346db40ddbaa26cd26b8633c5510a9c1547ff83c83cea4cd79325f8f59bf2ad9b5bea0f6e43b4ce418543fd1db20 + languageName: node + linkType: hard + +"@babel/plugin-transform-member-expression-literals@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-member-expression-literals@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 56b6d64187dca90a4ac9f1aa39474715d232e8afe6f14524c265df03d25513911a9110b0238b03ce7d380d2a15d08dbc580fc2372fa61a78a5f713d65abaf05e + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-amd@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-modules-amd@npm:7.25.7" + dependencies: + "@babel/helper-module-transforms": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: fe2415ec5297637c96f886e69d4d107b37b467b1877fd423ff2cd60877a2a081cb57aad3bc4f0770f5b70b9a80c3874243dc0f7a0a4c9521423aa40a8865d27c + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-commonjs@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.25.7" + dependencies: + "@babel/helper-module-transforms": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-simple-access": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 440ba085e0c66a8f65a760f669f699623c759c8e13c57aed6df505e1ded1df7d5f050c07a4ff3273c4a327301058f5dcfeea6743cbd260bd4fed5f4e7006c5d7 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-systemjs@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.25.7" + dependencies: + "@babel/helper-module-transforms": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-validator-identifier": ^7.25.7 + "@babel/traverse": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a546ee32c8997f7686883297413988dd461f4ed068ae4b999b95acb471148243affb1fad52f1511c175eba7affc8ad5a76059825e15b7d135c1ad231cffc62f1 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-umd@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-modules-umd@npm:7.25.7" + dependencies: + "@babel/helper-module-transforms": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 881e4795ebde02ef84402ec0dc05be8b36aa659766c8fb0a54ebb5b0343752a660d43f81272a1a5181ee2c4008feddb1216172903e0254d4d310728c8d8df29b + languageName: node + linkType: hard + +"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.25.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 7f7e0f372171d8da5c5098b3459b2f855f4b10789ae60b77a66f45af91f63f170bb567d1544603f8b25ce4536aa3c00e13b1a8f034f3b984c45b1cd21851fb35 + languageName: node + linkType: hard + +"@babel/plugin-transform-new-target@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-new-target@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ce3cfe70aaf6c9947c87247c9f1baab8c0a2b70b96cc8ae524cc797641138470316e34640dcb36eb659939ed0e31a5af8038edd09c700ab178b3f2194082a030 + languageName: node + linkType: hard + +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.25.8" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 9941b638a4dce9e1bde3bd26d426fc0250c811f7fdfa76f6d1310e37f30b051e829e5027441c75ca4e0559dddbb0db9ac231a972d848e75abd1b4b57ec0b7b08 + languageName: node + linkType: hard + +"@babel/plugin-transform-numeric-separator@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.25.8" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: c6e710a2690e149e6b53259d079a11b2f2dc8df120711453dfccf31332c1195eded45354008f2549a99e321bad46e753c19c1fd3eb8c0350f2a542e8fe3b3997 + languageName: node + linkType: hard + +"@babel/plugin-transform-object-rest-spread@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.25.8" + dependencies: + "@babel/helper-compilation-targets": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/plugin-transform-parameters": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 592c838b279fb5054493ce1f424c7d6e2b2d35c0d45736d1555f4dfdcd42a0744c27b3702e1e37a67c06a80791dee70970439353103016f8218c46f4fccc3db3 + languageName: node + linkType: hard + +"@babel/plugin-transform-object-super@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-object-super@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-replace-supers": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 74f83a1e9a2313bd06888a786ebfa71cfa2fba383861d1b5db168e1eb67ed06b1e76cf8d4d352b441281d5582f2d8009ff80bf37e8ef074e44686637d5ceb3cf + languageName: node + linkType: hard + +"@babel/plugin-transform-optional-catch-binding@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.25.8" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 060e42934b8fb8fc7b3e85604af9f03cb79b246760d71756bbba6dfe59c7a6c373779f642cb918c64f42cdd434bae340e8a07cfba61665d94d83a47462b27570 + languageName: node + linkType: hard + +"@babel/plugin-transform-optional-chaining@npm:^7.25.7, @babel/plugin-transform-optional-chaining@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.25.8" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 234cf8487aa6e61d1d73073f780686490f81eaa1792f9e8da3d0db6bd979b9aa29804b34f9ade80ee5e9c77e65e95d7dc8650d1a34e90511be43341065a75dfc + languageName: node + linkType: hard + +"@babel/plugin-transform-parameters@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-parameters@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: cd139c3852153bb8bbfdcd07865e0ba6d177dabd75e4fc65dd4859956072fca235855a7d03672544f4337bda15924685c2c09f77e704fb85ee069c6acf7a0033 + languageName: node + linkType: hard + +"@babel/plugin-transform-private-methods@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-private-methods@npm:7.25.7" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: c952adc58bfb00ef8c68deb03d2aa12b2d12ba9cd02bcc93b47d9f28f0fa16c08534e5099b916703b1d2f4dc037e5838e7f66b0dce650e7af8c1f41ca69af2c9 + languageName: node + linkType: hard + +"@babel/plugin-transform-private-property-in-object@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.25.8" + dependencies: + "@babel/helper-annotate-as-pure": ^7.25.7 + "@babel/helper-create-class-features-plugin": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ecb2519bfbd0a469879348f74c0b7dd45955c7d0987d7d4e4ac8bddab482f971c1f3305808160a71e06c8d17b7783158258668d7ff5696c6d841e5de52b7b6a4 + languageName: node + linkType: hard + +"@babel/plugin-transform-property-literals@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-property-literals@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 4a2b04efea116350de22c57f2247b0e626d638fcd755788563fd1748904dd0aba1048909b667d149ec8e8d4dde3afb1ba36604db04eb66a623c29694d139fd01 + languageName: node + linkType: hard + +"@babel/plugin-transform-react-constant-elements@npm:^7.18.12": + version: 7.25.7 + resolution: "@babel/plugin-transform-react-constant-elements@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 93f27c1eccf66785f35442d5b5ed8d1c34c087878f9a9d017195f781eab3c30c5b9454ae7332dd18e69c7770525960516be2c8b54c696471c7c8752a7bba691f + languageName: node + linkType: hard + +"@babel/plugin-transform-react-display-name@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-react-display-name@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 099c1d6866f8af9cf0fc3b93e8c705f30d20079de6e9661185f648acded42dea50a4926161856f5c62e62f8ae195f71b31d74e2c98cc1a7f917cebcaca01fc86 + languageName: node + linkType: hard + +"@babel/plugin-transform-react-jsx-development@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-react-jsx-development@npm:7.25.7" + dependencies: + "@babel/plugin-transform-react-jsx": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: b047db378579debe4f3f0089825d57f7ded33b5b1684f73b4ab19768e71c06c5545aaef5e4f824b70da2611c9b0126c345f6515aaa5061df1d164362d9f54fca + languageName: node + linkType: hard + +"@babel/plugin-transform-react-jsx@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-react-jsx@npm:7.25.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.25.7 + "@babel/helper-module-imports": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/plugin-syntax-jsx": ^7.25.7 + "@babel/types": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d87dd44fca94d95d41ca833639e9d74f94555a5fe2c428c44e2cda1c40485f4345beceb5d209b1892b7a91ad271d67496833e5eb1646021130888d5cb6d6df67 + languageName: node + linkType: hard + +"@babel/plugin-transform-react-pure-annotations@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.25.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7d4af70f5dede21f7fd4124373ea535ed35a2ad472a0d746a23a476b17c686c546de605ee4bc8d50c4e50516e9396034bc1ff99e15649a420abfad227fae5c12 + languageName: node + linkType: hard + +"@babel/plugin-transform-regenerator@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-regenerator@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + regenerator-transform: ^0.15.2 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e64e60334cd5efe5d57c94366fe3675ce480439a432169691d5e58dd786ed85658291c25b14087b48c51e58dcdc4112ef9d87c59d32d9d358f19a9bff9e359f6 + languageName: node + linkType: hard + +"@babel/plugin-transform-reserved-words@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-reserved-words@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e84d94e451970f8c080fc234d9eaa063e12717288be1da1947914fc9c25b74e3b30c5e678c31fa0102d5c0fb31b56f4fdb4871e352a60b3b5465323575996290 + languageName: node + linkType: hard + +"@babel/plugin-transform-shorthand-properties@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-shorthand-properties@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 62f4fbd1aeec76a0bc41c89fad30ee0687b2070720a3f21322e769d889a12bd58f76c73901b3dff6e6892fb514411839482a2792b99f26a73b0dd8f57cb6b3d8 + languageName: node + linkType: hard + +"@babel/plugin-transform-spread@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-spread@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e1c61d71fc4712205e8a0bc2317f7d94485ace36ae77fbd5babf773dc3173b3b33de9e8d5107796df1a064afba62841bf652b367d5f22e314810f8ed3adb92d5 + languageName: node + linkType: hard + +"@babel/plugin-transform-sticky-regex@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-sticky-regex@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ea1f3d9bf99bfb81c6f67e115d56c1bc9ffe9ea82d1489d591a59965cbda2f4a3a5e6eca7d1ed04b6cc41f44f9edf4f58ac6e04a3be00d9ad4da695d2818c052 + languageName: node + linkType: hard + +"@babel/plugin-transform-template-literals@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-template-literals@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f1776fb4181ca41a35adb8a427748999b6c24cbb25778b78f716179e9c8bc28b03ef88da8062914e6327ef277844b4bbdac9dc0c6d6076855fc36af593661275 + languageName: node + linkType: hard + +"@babel/plugin-transform-typeof-symbol@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-typeof-symbol@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 20936bfbc7d5bea54e958643860dffa5fd8aca43b898c379d925d8c2b8c4c3fa309e2f8a29392e377314cb2856e0441dbb2e7ffd1a88d257af3b1958dc34b516 + languageName: node + linkType: hard + +"@babel/plugin-transform-typescript@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-typescript@npm:7.25.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.25.7 + "@babel/helper-create-class-features-plugin": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.25.7 + "@babel/plugin-syntax-typescript": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d3b419a05e032385a6666c0612e23f18d54c60e6ec7613fec377424f1b338e4cc1229a2a6b9df0b18bb2b15e8d25024cdabd160c3b86e66f4e13d021695f1b82 + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-escapes@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-unicode-escapes@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 70c10e757fa431380b2262d1a22fe6c84c8a9c53aa6627e35ef411ce47b763aa64436f77d58e4c49c9f931ba4bda91b404017f4f3a7864ed5fe71fabc6494188 + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-property-regex@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.25.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 87bcfca6e6fb787c207d57b6fe065fe28e16d817231069e25da9ee8b75f35d52b3e7ab5afb7ba65b2f72ea5697863fb4eebdb2797dbf32c7e8412bfdb6d57ca3 + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-regex@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-unicode-regex@npm:7.25.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ba7247dbd6e368f7f6367679021e44a6ad012e0673018a5f9bb69893bfbc5a61690275bd086de8e5c39533d6c31448e765b8c30d2bc5aae92e0bed69b6b63d98 + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-sets-regex@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.25.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 7d4b4fdd991ba8acfe164f73bc7fba3e81891c8b8b5ccaf2812ed18324225fbdac8643e09c1aa271cec436d9a336788709a1a997a63985c78a3bbebcc18d1ffe + languageName: node + linkType: hard + +"@babel/preset-env@npm:^7.19.4": + version: 7.25.8 + resolution: "@babel/preset-env@npm:7.25.8" + dependencies: + "@babel/compat-data": ^7.25.8 + "@babel/helper-compilation-targets": ^7.25.7 + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-validator-option": ^7.25.7 + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ^7.25.7 + "@babel/plugin-bugfix-safari-class-field-initializer-scope": ^7.25.7 + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.25.7 + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.25.7 + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ^7.25.7 + "@babel/plugin-proposal-private-property-in-object": 7.21.0-placeholder-for-preset-env.2 + "@babel/plugin-syntax-import-assertions": ^7.25.7 + "@babel/plugin-syntax-import-attributes": ^7.25.7 + "@babel/plugin-syntax-unicode-sets-regex": ^7.18.6 + "@babel/plugin-transform-arrow-functions": ^7.25.7 + "@babel/plugin-transform-async-generator-functions": ^7.25.8 + "@babel/plugin-transform-async-to-generator": ^7.25.7 + "@babel/plugin-transform-block-scoped-functions": ^7.25.7 + "@babel/plugin-transform-block-scoping": ^7.25.7 + "@babel/plugin-transform-class-properties": ^7.25.7 + "@babel/plugin-transform-class-static-block": ^7.25.8 + "@babel/plugin-transform-classes": ^7.25.7 + "@babel/plugin-transform-computed-properties": ^7.25.7 + "@babel/plugin-transform-destructuring": ^7.25.7 + "@babel/plugin-transform-dotall-regex": ^7.25.7 + "@babel/plugin-transform-duplicate-keys": ^7.25.7 + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ^7.25.7 + "@babel/plugin-transform-dynamic-import": ^7.25.8 + "@babel/plugin-transform-exponentiation-operator": ^7.25.7 + "@babel/plugin-transform-export-namespace-from": ^7.25.8 + "@babel/plugin-transform-for-of": ^7.25.7 + "@babel/plugin-transform-function-name": ^7.25.7 + "@babel/plugin-transform-json-strings": ^7.25.8 + "@babel/plugin-transform-literals": ^7.25.7 + "@babel/plugin-transform-logical-assignment-operators": ^7.25.8 + "@babel/plugin-transform-member-expression-literals": ^7.25.7 + "@babel/plugin-transform-modules-amd": ^7.25.7 + "@babel/plugin-transform-modules-commonjs": ^7.25.7 + "@babel/plugin-transform-modules-systemjs": ^7.25.7 + "@babel/plugin-transform-modules-umd": ^7.25.7 + "@babel/plugin-transform-named-capturing-groups-regex": ^7.25.7 + "@babel/plugin-transform-new-target": ^7.25.7 + "@babel/plugin-transform-nullish-coalescing-operator": ^7.25.8 + "@babel/plugin-transform-numeric-separator": ^7.25.8 + "@babel/plugin-transform-object-rest-spread": ^7.25.8 + "@babel/plugin-transform-object-super": ^7.25.7 + "@babel/plugin-transform-optional-catch-binding": ^7.25.8 + "@babel/plugin-transform-optional-chaining": ^7.25.8 + "@babel/plugin-transform-parameters": ^7.25.7 + "@babel/plugin-transform-private-methods": ^7.25.7 + "@babel/plugin-transform-private-property-in-object": ^7.25.8 + "@babel/plugin-transform-property-literals": ^7.25.7 + "@babel/plugin-transform-regenerator": ^7.25.7 + "@babel/plugin-transform-reserved-words": ^7.25.7 + "@babel/plugin-transform-shorthand-properties": ^7.25.7 + "@babel/plugin-transform-spread": ^7.25.7 + "@babel/plugin-transform-sticky-regex": ^7.25.7 + "@babel/plugin-transform-template-literals": ^7.25.7 + "@babel/plugin-transform-typeof-symbol": ^7.25.7 + "@babel/plugin-transform-unicode-escapes": ^7.25.7 + "@babel/plugin-transform-unicode-property-regex": ^7.25.7 + "@babel/plugin-transform-unicode-regex": ^7.25.7 + "@babel/plugin-transform-unicode-sets-regex": ^7.25.7 + "@babel/preset-modules": 0.1.6-no-external-plugins + babel-plugin-polyfill-corejs2: ^0.4.10 + babel-plugin-polyfill-corejs3: ^0.10.6 + babel-plugin-polyfill-regenerator: ^0.6.1 + core-js-compat: ^3.38.1 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3aefaf13b483e620c1a0a81c2c643554e07a39a55cab2b775938b09ff01123ac7710e46e25b8340ec163f540092e0a39e016d4ac22ae9818384296bc4dbe99b1 + languageName: node + linkType: hard + +"@babel/preset-modules@npm:0.1.6-no-external-plugins": + version: 0.1.6-no-external-plugins + resolution: "@babel/preset-modules@npm:0.1.6-no-external-plugins" + dependencies: + "@babel/helper-plugin-utils": ^7.0.0 + "@babel/types": ^7.4.4 + esutils: ^2.0.2 + peerDependencies: + "@babel/core": ^7.0.0-0 || ^8.0.0-0 <8.0.0 + checksum: 4855e799bc50f2449fb5210f78ea9e8fd46cf4f242243f1e2ed838e2bd702e25e73e822e7f8447722a5f4baa5e67a8f7a0e403f3e7ce04540ff743a9c411c375 + languageName: node + linkType: hard + +"@babel/preset-react@npm:^7.18.6": + version: 7.25.7 + resolution: "@babel/preset-react@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-validator-option": ^7.25.7 + "@babel/plugin-transform-react-display-name": ^7.25.7 + "@babel/plugin-transform-react-jsx": ^7.25.7 + "@babel/plugin-transform-react-jsx-development": ^7.25.7 + "@babel/plugin-transform-react-pure-annotations": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: df6318345bc202fec0b38fd53f6d936975682d45eadf0e753376a39d7ac61e2dc9dd9e6fca768295378abb3fbd08510a5d9f586c9bd37e757e60c00b6ecf1a57 + languageName: node + linkType: hard + +"@babel/preset-typescript@npm:^7.18.6": + version: 7.25.7 + resolution: "@babel/preset-typescript@npm:7.25.7" + dependencies: + "@babel/helper-plugin-utils": ^7.25.7 + "@babel/helper-validator-option": ^7.25.7 + "@babel/plugin-syntax-jsx": ^7.25.7 + "@babel/plugin-transform-modules-commonjs": ^7.25.7 + "@babel/plugin-transform-typescript": ^7.25.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e482651092a8f73f13bdabc70d670381c1ccc7764f7f68abdc8ebb173c850e3e762d00ec1f562ef026eb616a5a339b140111d33f5a9c8e9c98130b68eb176f04 + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.25.7 + resolution: "@babel/runtime@npm:7.25.7" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 1d6133ed1cf1de1533cfe84a4a8f94525271a0d93f6af4f2cdae14884ec3c8a7148664ddf7fd2a14f82cc4485904a1761821a55875ad241c8b4034e95e7134b2 + languageName: node + linkType: hard + +"@babel/template@npm:^7.25.7, @babel/template@npm:^7.25.9, @babel/template@npm:^7.3.3": + version: 7.25.9 + resolution: "@babel/template@npm:7.25.9" + dependencies: + "@babel/code-frame": ^7.25.9 + "@babel/parser": ^7.25.9 + "@babel/types": ^7.25.9 + checksum: 103641fea19c7f4e82dc913aa6b6ac157112a96d7c724d513288f538b84bae04fb87b1f1e495ac1736367b1bc30e10f058b30208fb25f66038e1f1eb4e426472 + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.24.0, @babel/traverse@npm:^7.25.7, @babel/traverse@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/traverse@npm:7.25.9" + dependencies: + "@babel/code-frame": ^7.25.9 + "@babel/generator": ^7.25.9 + "@babel/parser": ^7.25.9 + "@babel/template": ^7.25.9 + "@babel/types": ^7.25.9 + debug: ^4.3.1 + globals: ^11.1.0 + checksum: 901d325662ff1dd9bc51de00862e01055fa6bc374f5297d7e3731f2f0e268bbb1d2141f53fa82860aa308ee44afdcf186a948f16c83153927925804b95a9594d + languageName: node + linkType: hard + +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.0, @babel/types@npm:^7.25.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.26.0 + resolution: "@babel/types@npm:7.26.0" + dependencies: + "@babel/helper-string-parser": ^7.25.9 + "@babel/helper-validator-identifier": ^7.25.9 + checksum: a3dd37dabac693018872da96edb8c1843a605c1bfacde6c3f504fba79b972426a6f24df70aa646356c0c1b19bdd2c722c623c684a996c002381071680602280d + languageName: node + linkType: hard + +"@backstage-community/plugin-rbac-backend@workspace:plugins/rbac-backend": + version: 0.0.0-use.local + resolution: "@backstage-community/plugin-rbac-backend@workspace:plugins/rbac-backend" + dependencies: + "@backstage-community/plugin-rbac-common": ^1.12.0 + "@backstage-community/plugin-rbac-node": ^1.8.0 + "@backstage/backend-defaults": ^0.5.2 + "@backstage/backend-plugin-api": ^1.0.1 + "@backstage/backend-test-utils": 1.0.2 + "@backstage/catalog-client": ^1.7.1 + "@backstage/catalog-model": ^1.7.0 + "@backstage/cli": 0.28.2 + "@backstage/config": 1.2.0 + "@backstage/core-plugin-api": 1.10.0 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-auth-node": ^0.5.3 + "@backstage/plugin-permission-backend": ^0.5.50 + "@backstage/plugin-permission-common": ^0.8.1 + "@backstage/plugin-permission-node": ^0.8.4 + "@backstage/types": 1.1.1 + "@dagrejs/graphlib": ^2.1.13 + "@janus-idp/backstage-plugin-audit-log-node": ^1.7.0 + "@spotify/prettier-config": ^15.0.0 + "@types/express": 4.17.21 + "@types/lodash": ^4.14.151 + "@types/node": 18.19.34 + "@types/supertest": 2.0.16 + casbin: ^5.27.1 + chokidar: ^3.6.0 + csv-parse: ^5.5.5 + express: ^4.18.2 + js-yaml: ^4.1.0 + knex: ^3.0.0 + knex-mock-client: 2.0.1 + lodash: ^4.17.21 + msw: 1.3.3 + prettier: 3.3.3 + qs: 6.11.2 + supertest: 6.3.4 + typeorm-adapter: ^1.6.1 + languageName: unknown + linkType: soft + +"@backstage-community/plugin-rbac-common@^1.12.0, @backstage-community/plugin-rbac-common@workspace:plugins/rbac-common": + version: 0.0.0-use.local + resolution: "@backstage-community/plugin-rbac-common@workspace:plugins/rbac-common" + dependencies: + "@backstage/cli": 0.28.2 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-permission-common": ^0.8.1 + "@spotify/prettier-config": ^15.0.0 + prettier: 3.3.3 + peerDependencies: + "@backstage/errors": ^1.2.4 + "@backstage/plugin-permission-common": ^0.8.1 + languageName: unknown + linkType: soft + +"@backstage-community/plugin-rbac-node@^1.8.0, @backstage-community/plugin-rbac-node@workspace:plugins/rbac-node": + version: 0.0.0-use.local + resolution: "@backstage-community/plugin-rbac-node@workspace:plugins/rbac-node" + dependencies: + "@backstage/backend-plugin-api": ^1.0.1 + "@backstage/cli": 0.28.2 + "@spotify/prettier-config": ^15.0.0 + prettier: 3.3.3 + languageName: unknown + linkType: soft + +"@backstage-community/plugin-rbac@workspace:plugins/rbac": + version: 0.0.0-use.local + resolution: "@backstage-community/plugin-rbac@workspace:plugins/rbac" + dependencies: + "@backstage-community/plugin-rbac-common": ^1.12.0 + "@backstage/catalog-model": ^1.7.0 + "@backstage/cli": 0.28.2 + "@backstage/core-app-api": 1.15.1 + "@backstage/core-components": ^0.15.1 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/dev-utils": 1.1.2 + "@backstage/plugin-catalog": ^1.24.0 + "@backstage/plugin-catalog-common": ^1.1.0 + "@backstage/plugin-permission-common": ^0.8.1 + "@backstage/plugin-permission-react": ^0.4.27 + "@backstage/test-utils": 1.7.0 + "@backstage/theme": ^0.6.0 + "@janus-idp/shared-react": ^2.13.0 + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.11.3 + "@material-ui/lab": ^4.0.0-alpha.45 + "@mui/icons-material": 5.16.4 + "@mui/material": ^5.14.18 + "@playwright/test": 1.45.3 + "@redhat-developer/red-hat-developer-hub-theme": 0.4.0 + "@rjsf/core": ^5.21.2 + "@rjsf/mui": ^5.21.2 + "@rjsf/utils": ^5.21.2 + "@rjsf/validator-ajv8": ^5.21.2 + "@spotify/prettier-config": ^15.0.0 + "@testing-library/dom": ^10.0.0 + "@testing-library/jest-dom": ^6.0.0 + "@testing-library/react": ^15.0.0 + "@testing-library/react-hooks": 8.0.1 + "@testing-library/user-event": 14.5.2 + "@types/autosuggest-highlight": 3.2.3 + "@types/node": 18.19.34 + "@types/react": ^18.2.58 + autosuggest-highlight: ^3.3.4 + canvas: ^2.11.2 + formik: ^2.4.5 + msw: 1.3.3 + prettier: 3.3.3 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: ^6.0.0 + react-use: ^17.4.0 + start-server-and-test: 2.0.8 + yup: ^1.3.2 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: ^6.0.0 + languageName: unknown + linkType: soft + +"@backstage/app-defaults@npm:^1.5.12": + version: 1.5.12 + resolution: "@backstage/app-defaults@npm:1.5.12" + dependencies: + "@backstage/core-app-api": ^1.15.1 + "@backstage/core-components": ^0.15.1 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/plugin-permission-react": ^0.4.27 + "@backstage/theme": ^0.6.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 8dcb107bc954ed423260d8d2bde4d23436da9772550623ca10f62928de0b8eea982730f593594376f2bf5257094d287cda649b1527548738011e3c08e29a41ca + languageName: node + linkType: hard + +"@backstage/backend-app-api@npm:^1.0.1": + version: 1.0.1 + resolution: "@backstage/backend-app-api@npm:1.0.1" + dependencies: + "@backstage/backend-common": ^0.25.0 + "@backstage/backend-plugin-api": ^1.0.1 + "@backstage/cli-common": ^0.1.14 + "@backstage/config": ^1.2.0 + "@backstage/config-loader": ^1.9.1 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-auth-node": ^0.5.3 + "@backstage/plugin-permission-node": ^0.8.4 + "@backstage/types": ^1.1.1 + "@manypkg/get-packages": ^1.1.3 + compression: ^1.7.4 + cookie: ^0.7.0 + cors: ^2.8.5 + express: ^4.17.1 + express-promise-router: ^4.1.0 + helmet: ^6.0.0 + jose: ^5.0.0 + knex: ^3.0.0 + lodash: ^4.17.21 + logform: ^2.3.2 + luxon: ^3.0.0 + minimatch: ^9.0.0 + minimist: ^1.2.5 + morgan: ^1.10.0 + node-fetch: ^2.7.0 + node-forge: ^1.3.1 + path-to-regexp: ^8.0.0 + selfsigned: ^2.0.0 + stoppable: ^1.1.0 + triple-beam: ^1.4.1 + uuid: ^9.0.0 + winston: ^3.2.1 + winston-transport: ^4.5.0 + checksum: 5696cfd35b9804be92568103932e8109dcbafe2bafd6eca4e04f1935f6128f6098528ecbb61d81fb78c959cd47f85e611203e624807f3b5956d2d3e7e7b5b554 + languageName: node + linkType: hard + +"@backstage/backend-common@npm:^0.25.0": + version: 0.25.0 + resolution: "@backstage/backend-common@npm:0.25.0" + dependencies: + "@aws-sdk/abort-controller": ^3.347.0 + "@aws-sdk/client-codecommit": ^3.350.0 + "@aws-sdk/client-s3": ^3.350.0 + "@aws-sdk/credential-providers": ^3.350.0 + "@aws-sdk/types": ^3.347.0 + "@backstage/backend-dev-utils": ^0.1.5 + "@backstage/backend-plugin-api": ^1.0.0 + "@backstage/cli-common": ^0.1.14 + "@backstage/config": ^1.2.0 + "@backstage/config-loader": ^1.9.1 + "@backstage/errors": ^1.2.4 + "@backstage/integration": ^1.15.0 + "@backstage/integration-aws-node": ^0.1.12 + "@backstage/plugin-auth-node": ^0.5.2 + "@backstage/types": ^1.1.1 + "@google-cloud/storage": ^7.0.0 + "@keyv/memcache": ^1.3.5 + "@keyv/redis": ^2.5.3 + "@kubernetes/client-node": 0.20.0 + "@manypkg/get-packages": ^1.1.3 + "@octokit/rest": ^19.0.3 + "@types/cors": ^2.8.6 + "@types/dockerode": ^3.3.0 + "@types/express": ^4.17.6 + "@types/luxon": ^3.0.0 + "@types/webpack-env": ^1.15.2 + archiver: ^7.0.0 + base64-stream: ^1.0.0 + compression: ^1.7.4 + concat-stream: ^2.0.0 + cors: ^2.8.5 + dockerode: ^4.0.0 + express: ^4.17.1 + express-promise-router: ^4.1.0 + fs-extra: ^11.2.0 + git-url-parse: ^14.0.0 + helmet: ^6.0.0 + isomorphic-git: ^1.23.0 + jose: ^5.0.0 + keyv: ^4.5.2 + knex: ^3.0.0 + lodash: ^4.17.21 + logform: ^2.3.2 + luxon: ^3.0.0 + minimatch: ^9.0.0 + minimist: ^1.2.5 + morgan: ^1.10.0 + mysql2: ^3.0.0 + node-fetch: ^2.7.0 + node-forge: ^1.3.1 + p-limit: ^3.1.0 + path-to-regexp: ^8.0.0 + pg: ^8.11.3 + pg-format: ^1.0.4 + raw-body: ^2.4.1 + selfsigned: ^2.0.0 + stoppable: ^1.1.0 + tar: ^6.1.12 + triple-beam: ^1.4.1 + uuid: ^9.0.0 + winston: ^3.2.1 + winston-transport: ^4.5.0 + yauzl: ^3.0.0 + yn: ^4.0.0 + peerDependencies: + pg-connection-string: ^2.3.0 + peerDependenciesMeta: + pg-connection-string: + optional: true + checksum: 34d2b92b5fd7f6d8f25975d121634079586d022665a51b676ba47053050e0f22556f80d59a75a10e0a8f1803a718448b7aed4bd1dcd6f1d348785c97cf5a8c9d + languageName: node + linkType: hard + +"@backstage/backend-defaults@npm:^0.5.2": + version: 0.5.2 + resolution: "@backstage/backend-defaults@npm:0.5.2" + dependencies: + "@aws-sdk/abort-controller": ^3.347.0 + "@aws-sdk/client-codecommit": ^3.350.0 + "@aws-sdk/client-s3": ^3.350.0 + "@aws-sdk/credential-providers": ^3.350.0 + "@aws-sdk/types": ^3.347.0 + "@backstage/backend-app-api": ^1.0.1 + "@backstage/backend-common": ^0.25.0 + "@backstage/backend-dev-utils": ^0.1.5 + "@backstage/backend-plugin-api": ^1.0.1 + "@backstage/cli-common": ^0.1.14 + "@backstage/cli-node": ^0.2.9 + "@backstage/config": ^1.2.0 + "@backstage/config-loader": ^1.9.1 + "@backstage/errors": ^1.2.4 + "@backstage/integration": ^1.15.1 + "@backstage/integration-aws-node": ^0.1.12 + "@backstage/plugin-auth-node": ^0.5.3 + "@backstage/plugin-events-node": ^0.4.2 + "@backstage/plugin-permission-node": ^0.8.4 + "@backstage/types": ^1.1.1 + "@google-cloud/storage": ^7.0.0 + "@keyv/memcache": ^1.3.5 + "@keyv/redis": ^2.5.3 + "@manypkg/get-packages": ^1.1.3 + "@octokit/rest": ^19.0.3 + "@opentelemetry/api": ^1.3.0 + "@types/cors": ^2.8.6 + "@types/express": ^4.17.6 + archiver: ^7.0.0 + base64-stream: ^1.0.0 + better-sqlite3: ^11.0.0 + compression: ^1.7.4 + concat-stream: ^2.0.0 + cookie: ^0.7.0 + cors: ^2.8.5 + cron: ^3.0.0 + express: ^4.17.1 + express-promise-router: ^4.1.0 + fs-extra: ^11.2.0 + git-url-parse: ^15.0.0 + helmet: ^6.0.0 + isomorphic-git: ^1.23.0 + jose: ^5.0.0 + keyv: ^4.5.2 + knex: ^3.0.0 + lodash: ^4.17.21 + logform: ^2.3.2 + luxon: ^3.0.0 + minimatch: ^9.0.0 + minimist: ^1.2.5 + morgan: ^1.10.0 + mysql2: ^3.0.0 + node-fetch: ^2.7.0 + node-forge: ^1.3.1 + p-limit: ^3.1.0 + path-to-regexp: ^8.0.0 + pg: ^8.11.3 + pg-connection-string: ^2.3.0 + pg-format: ^1.0.4 + raw-body: ^2.4.1 + selfsigned: ^2.0.0 + stoppable: ^1.1.0 + tar: ^6.1.12 + triple-beam: ^1.4.1 + uuid: ^9.0.0 + winston: ^3.2.1 + winston-transport: ^4.5.0 + yauzl: ^3.0.0 + yn: ^4.0.0 + zod: ^3.22.4 + checksum: 9d0f494dae99e9a3cee8dfcb0d15c72ddb2ca2069a1c545f58a8a521a95688c275656bd152585067f3264bc97f532dc0dcfeb267e15a24153ebfae4fb0d3abbe + languageName: node + linkType: hard + +"@backstage/backend-dev-utils@npm:^0.1.5": + version: 0.1.5 + resolution: "@backstage/backend-dev-utils@npm:0.1.5" + checksum: 7c7eced8cc6fe88b6b54d7b9f04953dbfd07846772368a0b269d4e75da30133b61e4fe29782c0dc0aa547234d75ff60a985f378f92911680a9172fa8f2820e5b + languageName: node + linkType: hard + +"@backstage/backend-plugin-api@npm:^1.0.0, @backstage/backend-plugin-api@npm:^1.0.1": + version: 1.0.1 + resolution: "@backstage/backend-plugin-api@npm:1.0.1" + dependencies: + "@backstage/cli-common": ^0.1.14 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-auth-node": ^0.5.3 + "@backstage/plugin-permission-common": ^0.8.1 + "@backstage/types": ^1.1.1 + "@types/express": ^4.17.6 + "@types/luxon": ^3.0.0 + express: ^4.17.1 + knex: ^3.0.0 + luxon: ^3.0.0 + checksum: a0d1dce15c1c90eec64e4f8d7d2eddd4ab43ebf4573db041b1ee835f27939e97eb162c20661fbafb3e8bc223c422512eea1a0327700164e51e328d620ca925c8 + languageName: node + linkType: hard + +"@backstage/backend-test-utils@npm:1.0.2": + version: 1.0.2 + resolution: "@backstage/backend-test-utils@npm:1.0.2" + dependencies: + "@backstage/backend-app-api": ^1.0.1 + "@backstage/backend-defaults": ^0.5.2 + "@backstage/backend-plugin-api": ^1.0.1 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-auth-node": ^0.5.3 + "@backstage/plugin-events-node": ^0.4.2 + "@backstage/types": ^1.1.1 + "@keyv/memcache": ^1.3.5 + "@keyv/redis": ^2.5.3 + "@types/express": ^4.17.6 + "@types/express-serve-static-core": ^4.17.5 + "@types/keyv": ^4.2.0 + "@types/qs": ^6.9.6 + better-sqlite3: ^11.0.0 + cookie: ^0.7.0 + express: ^4.17.1 + fs-extra: ^11.0.0 + keyv: ^4.5.2 + knex: ^3.0.0 + msw: ^1.0.0 + mysql2: ^3.0.0 + pg: ^8.11.3 + pg-connection-string: ^2.3.0 + testcontainers: ^10.0.0 + textextensions: ^5.16.0 + uuid: ^9.0.0 + yn: ^4.0.0 + peerDependencies: + "@types/jest": "*" + checksum: c45d663b7aec8b3b821f8a7cd37a4fdb6a70164eb31fd16f52b5a9261982829dc3f499f1094f0ae5ab9b4539608440f45ef0a7cc1cfc671df816bc48e585351f + languageName: node + linkType: hard + +"@backstage/catalog-client@npm:^1.7.1": + version: 1.7.1 + resolution: "@backstage/catalog-client@npm:1.7.1" + dependencies: + "@backstage/catalog-model": ^1.7.0 + "@backstage/errors": ^1.2.4 + cross-fetch: ^4.0.0 + uri-template: ^2.0.0 + checksum: 0a70a929e95b4e424b021010202e19b68ab5ad3a6b6613e5c01850f31f6067e33ebb8863119c197bc213e9d86793d5368d0ad100288802e72eb6e818f54e765f + languageName: node + linkType: hard + +"@backstage/catalog-model@npm:^1.7.0": + version: 1.7.0 + resolution: "@backstage/catalog-model@npm:1.7.0" + dependencies: + "@backstage/errors": ^1.2.4 + "@backstage/types": ^1.1.1 + ajv: ^8.10.0 + lodash: ^4.17.21 + checksum: 6ff537e9e6064d35fa4a173a1c96f94e904489494a67a136e2dd0a743f9e3f4fd8a1f7a661fe8495dfbb642aabcc8fbf1746a300ad496b6e4a5d02f4db00f914 + languageName: node + linkType: hard + +"@backstage/cli-common@npm:^0.1.14": + version: 0.1.14 + resolution: "@backstage/cli-common@npm:0.1.14" + checksum: 6c5031ae31f08b405e5e59105d98e43dc6d865f960e5d016067267ecabccd5a892ab65d59d5b9e31850dccddb9eb29e06bf360ab6be8f7949991561ddb163fcb + languageName: node + linkType: hard + +"@backstage/cli-node@npm:^0.2.9": + version: 0.2.9 + resolution: "@backstage/cli-node@npm:0.2.9" + dependencies: + "@backstage/cli-common": ^0.1.14 + "@backstage/errors": ^1.2.4 + "@backstage/types": ^1.1.1 + "@manypkg/get-packages": ^1.1.3 + "@yarnpkg/parsers": ^3.0.0 + fs-extra: ^11.2.0 + semver: ^7.5.3 + zod: ^3.22.4 + checksum: 39a3332cc0dd732a51726d803c322ec7423c6380e494b3ef91d5c71aabd60626bc355ede3fbb8a4da4714806c4663b6bef109bb6c7100160452c0e71620dac9b + languageName: node + linkType: hard + +"@backstage/cli@npm:0.28.2, @backstage/cli@npm:^0.28.0": + version: 0.28.2 + resolution: "@backstage/cli@npm:0.28.2" + dependencies: + "@backstage/catalog-model": ^1.7.0 + "@backstage/cli-common": ^0.1.14 + "@backstage/cli-node": ^0.2.9 + "@backstage/config": ^1.2.0 + "@backstage/config-loader": ^1.9.1 + "@backstage/errors": ^1.2.4 + "@backstage/eslint-plugin": ^0.1.10 + "@backstage/integration": ^1.15.1 + "@backstage/release-manifests": ^0.0.11 + "@backstage/types": ^1.1.1 + "@manypkg/get-packages": ^1.1.3 + "@module-federation/enhanced": ^0.6.0 + "@octokit/graphql": ^5.0.0 + "@octokit/graphql-schema": ^13.7.0 + "@octokit/oauth-app": ^4.2.0 + "@octokit/request": ^6.0.0 + "@pmmmwh/react-refresh-webpack-plugin": ^0.5.7 + "@rollup/plugin-commonjs": ^26.0.0 + "@rollup/plugin-json": ^6.0.0 + "@rollup/plugin-node-resolve": ^15.0.0 + "@rollup/plugin-yaml": ^4.0.0 + "@spotify/eslint-config-base": ^15.0.0 + "@spotify/eslint-config-react": ^15.0.0 + "@spotify/eslint-config-typescript": ^15.0.0 + "@sucrase/webpack-loader": ^2.0.0 + "@svgr/core": 6.5.x + "@svgr/plugin-jsx": 6.5.x + "@svgr/plugin-svgo": 6.5.x + "@svgr/rollup": 6.5.x + "@svgr/webpack": 6.5.x + "@swc/core": ^1.3.46 + "@swc/helpers": ^0.5.0 + "@swc/jest": ^0.2.22 + "@types/jest": ^29.5.11 + "@types/webpack-env": ^1.15.2 + "@typescript-eslint/eslint-plugin": ^6.12.0 + "@typescript-eslint/parser": ^6.7.2 + "@yarnpkg/lockfile": ^1.1.0 + "@yarnpkg/parsers": ^3.0.0 + bfj: ^8.0.0 + buffer: ^6.0.3 + chalk: ^4.0.0 + chokidar: ^3.3.1 + commander: ^12.0.0 + cross-fetch: ^4.0.0 + cross-spawn: ^7.0.3 + css-loader: ^6.5.1 + ctrlc-windows: ^2.1.0 + esbuild: ^0.24.0 + esbuild-loader: ^4.0.0 + eslint: ^8.6.0 + eslint-config-prettier: ^9.0.0 + eslint-formatter-friendly: ^7.0.0 + eslint-plugin-deprecation: ^2.0.0 + eslint-plugin-import: ^2.25.4 + eslint-plugin-jest: ^28.0.0 + eslint-plugin-jsx-a11y: ^6.5.1 + eslint-plugin-react: ^7.28.0 + eslint-plugin-react-hooks: ^4.3.0 + eslint-plugin-unused-imports: ^3.0.0 + eslint-webpack-plugin: ^4.0.0 + express: ^4.17.1 + fork-ts-checker-webpack-plugin: ^9.0.0 + fs-extra: ^11.2.0 + git-url-parse: ^15.0.0 + glob: ^7.1.7 + global-agent: ^3.0.0 + globby: ^11.1.0 + handlebars: ^4.7.3 + html-webpack-plugin: ^5.3.1 + inquirer: ^8.2.0 + jest: ^29.7.0 + jest-cli: ^29.7.0 + jest-css-modules: ^2.1.0 + jest-environment-jsdom: ^29.0.2 + jest-runtime: ^29.0.2 + json-schema: ^0.4.0 + lodash: ^4.17.21 + mini-css-extract-plugin: ^2.4.2 + minimatch: ^9.0.0 + node-fetch: ^2.7.0 + node-libs-browser: ^2.2.1 + npm-packlist: ^5.0.0 + ora: ^5.3.0 + p-limit: ^3.1.0 + p-queue: ^6.6.2 + pirates: ^4.0.6 + postcss: ^8.1.0 + process: ^0.11.10 + raw-loader: ^4.0.2 + react-dev-utils: ^12.0.0-next.60 + react-refresh: ^0.14.0 + recursive-readdir: ^2.2.2 + replace-in-file: ^7.1.0 + rollup: ^4.0.0 + rollup-plugin-dts: ^6.1.0 + rollup-plugin-esbuild: ^6.1.1 + rollup-plugin-postcss: ^4.0.0 + rollup-pluginutils: ^2.8.2 + run-script-webpack-plugin: ^0.2.0 + semver: ^7.5.3 + style-loader: ^3.3.1 + sucrase: ^3.20.2 + swc-loader: ^0.2.3 + tar: ^6.1.12 + terser-webpack-plugin: ^5.1.3 + ts-morph: ^23.0.0 + util: ^0.12.3 + webpack: ^5.94.0 + webpack-dev-server: ^5.0.0 + webpack-node-externals: ^3.0.0 + yaml: ^2.0.0 + yargs: ^16.2.0 + yml-loader: ^2.1.0 + yn: ^4.0.0 + zod: ^3.22.4 + peerDependencies: + "@modyfi/vite-plugin-yaml": ^1.1.0 + "@rspack/core": ^1.0.10 + "@rspack/dev-server": ^1.0.9 + "@rspack/plugin-react-refresh": ^1.0.0 + "@vitejs/plugin-react": ^4.0.4 + vite: ^4.4.9 + vite-plugin-html: ^3.2.0 + vite-plugin-node-polyfills: ^0.22.0 + peerDependenciesMeta: + "@modyfi/vite-plugin-yaml": + optional: true + "@rspack/core": + optional: true + "@rspack/dev-server": + optional: true + "@rspack/plugin-react-refresh": + optional: true + "@vitejs/plugin-react": + optional: true + vite: + optional: true + vite-plugin-html: + optional: true + vite-plugin-node-polyfills: + optional: true + bin: + backstage-cli: bin/backstage-cli + checksum: 32e75a897a7a7b14df6ce43b66fdd5c9fb11b54b88fd3655257055536c77d38bf2d0c5fefbd44348f6250493673396c566eab695007f89d870757956ef659159 + languageName: node + linkType: hard + +"@backstage/config-loader@npm:^1.9.1": + version: 1.9.1 + resolution: "@backstage/config-loader@npm:1.9.1" + dependencies: + "@backstage/cli-common": ^0.1.14 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/types": ^1.1.1 + "@types/json-schema": ^7.0.6 + ajv: ^8.10.0 + chokidar: ^3.5.2 + fs-extra: ^11.2.0 + json-schema: ^0.4.0 + json-schema-merge-allof: ^0.8.1 + json-schema-traverse: ^1.0.0 + lodash: ^4.17.21 + minimist: ^1.2.5 + node-fetch: ^2.7.0 + typescript-json-schema: ^0.65.0 + yaml: ^2.0.0 + checksum: e13ab3cab7a443aa94a5861bf9fe19208bd85a4087f495d6e51d007ff25fcf2c56c26c3682c476422cf407be97dfa6fbe5817595f1f5523a307eae1c23fcc489 + languageName: node + linkType: hard + +"@backstage/config@npm:1.2.0, @backstage/config@npm:^1.2.0": + version: 1.2.0 + resolution: "@backstage/config@npm:1.2.0" + dependencies: + "@backstage/errors": ^1.2.4 + "@backstage/types": ^1.1.1 + checksum: 7844f0f086f894eca110f5c68832cd7c0beca2dc0ce2139b10af1d2cde6faf25afb249d3f980375def338b0ad885ef9e98f0d5a1b475bfe54c51b2b6636f1fef + languageName: node + linkType: hard + +"@backstage/core-app-api@npm:1.15.1, @backstage/core-app-api@npm:^1.15.1": + version: 1.15.1 + resolution: "@backstage/core-app-api@npm:1.15.1" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.10 + "@types/prop-types": ^15.7.3 + history: ^5.0.0 + i18next: ^22.4.15 + lodash: ^4.17.21 + prop-types: ^15.7.2 + react-use: ^17.2.4 + zen-observable: ^0.10.0 + zod: ^3.22.4 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 0ace62b7fdef97bf808c584204da512e2d00517d5af270fe3fd14ce286bf24d297f3d42cfa84e383c5fc955d28250b0e5fc62823633da84ad7ef2caaff05609a + languageName: node + linkType: hard + +"@backstage/core-compat-api@npm:^0.3.1": + version: 0.3.1 + resolution: "@backstage/core-compat-api@npm:0.3.1" + dependencies: + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/frontend-plugin-api": ^0.9.0 + "@backstage/version-bridge": ^1.0.10 + lodash: ^4.17.21 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 84fa2896fd85d5c4f2c6c204b6330d92bf928d57a5afc4092c63e952a2417a1257ec914aa7fd22252ea285baeeb55d70611ed95a1748463dd428f8da5fc617aa + languageName: node + linkType: hard + +"@backstage/core-components@npm:^0.15.1": + version: 0.15.1 + resolution: "@backstage/core-components@npm:0.15.1" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/errors": ^1.2.4 + "@backstage/theme": ^0.6.0 + "@backstage/version-bridge": ^1.0.10 + "@date-io/core": ^1.3.13 + "@material-table/core": ^3.1.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.61 + "@react-hookz/web": ^24.0.0 + "@types/react-sparklines": ^1.7.0 + ansi-regex: ^6.0.1 + classnames: ^2.2.6 + d3-selection: ^3.0.0 + d3-shape: ^3.0.0 + d3-zoom: ^3.0.0 + dagre: ^0.8.5 + linkify-react: 4.1.3 + linkifyjs: 4.1.3 + lodash: ^4.17.21 + pluralize: ^8.0.0 + qs: ^6.9.4 + rc-progress: 3.5.1 + react-helmet: 6.1.0 + react-hook-form: ^7.12.2 + react-idle-timer: 5.7.2 + react-markdown: ^8.0.0 + react-sparklines: ^1.7.0 + react-syntax-highlighter: ^15.4.5 + react-use: ^17.3.2 + react-virtualized-auto-sizer: ^1.0.11 + react-window: ^1.8.6 + remark-gfm: ^3.0.1 + zen-observable: ^0.10.0 + zod: ^3.22.4 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 1eee1340919893194b34e9ab4237147910caecf1158c511481bbb80630bdd66d7dc0e156f0c1448e6b301689e32f7b98c0bb2f8127f2ffe85e596d9871903fcd + languageName: node + linkType: hard + +"@backstage/core-plugin-api@npm:1.10.0, @backstage/core-plugin-api@npm:^1.10.0": + version: 1.10.0 + resolution: "@backstage/core-plugin-api@npm:1.10.0" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.10 + history: ^5.0.0 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 50647e0a33946a981cdb9b03c7282e761ff95b4fc33f3dcbaa08536740e8be0d555e0d8995fb66484eb5518a10583113df31965d15e2786a388d16ae54a7fba6 + languageName: node + linkType: hard + +"@backstage/dev-utils@npm:1.1.2": + version: 1.1.2 + resolution: "@backstage/dev-utils@npm:1.1.2" + dependencies: + "@backstage/app-defaults": ^1.5.12 + "@backstage/catalog-model": ^1.7.0 + "@backstage/core-app-api": ^1.15.1 + "@backstage/core-components": ^0.15.1 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/integration-react": ^1.2.0 + "@backstage/plugin-catalog-react": ^1.14.0 + "@backstage/theme": ^0.6.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + react-use: ^17.2.4 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 6695acd5d4633892a3f55848e95b3ea2c969ad0f4601134aad1256a5366505f29fcfe1b0ee35c6086c1b16a92706fa585b6d91fdbff30bf0697d47815e6724b6 + languageName: node + linkType: hard + +"@backstage/e2e-test-utils@npm:^0.1.1": + version: 0.1.1 + resolution: "@backstage/e2e-test-utils@npm:0.1.1" + dependencies: + "@manypkg/get-packages": ^1.1.3 + fs-extra: ^11.0.0 + peerDependencies: + "@playwright/test": ^1.32.3 + peerDependenciesMeta: + "@playwright/test": + optional: true + checksum: 3f7751452edd9a60cdb49176cf010ab5d7760287ecbe7b7b7c8218ceccce4263f86b27e0906a3d71744a2eafb530d6c1e2bacb8bc049b22922ae6c5a0764ff6a + languageName: node + linkType: hard + +"@backstage/errors@npm:^1.2.4": + version: 1.2.4 + resolution: "@backstage/errors@npm:1.2.4" + dependencies: + "@backstage/types": ^1.1.1 + serialize-error: ^8.0.1 + checksum: ed988b2d3594a2fe989dd45fe197154e522194e30602552224e4a2bf6ed895c671e7f832d5c01b8e24881484698ccf3abaf2930dba5374bccfdaa283f4850fb9 + languageName: node + linkType: hard + +"@backstage/eslint-plugin@npm:^0.1.10": + version: 0.1.10 + resolution: "@backstage/eslint-plugin@npm:0.1.10" + dependencies: + "@manypkg/get-packages": ^1.1.3 + minimatch: ^9.0.0 + checksum: 1952a39e1ba5ef1c71cb48ba7908c4e545fc20eba962a2481d574f80b998f21d1ced7602b04415ba4b5ae1aa52c289c307b9d3f0950eeedecc895dcbb0c878a4 + languageName: node + linkType: hard + +"@backstage/frontend-app-api@npm:^0.10.0": + version: 0.10.0 + resolution: "@backstage/frontend-app-api@npm:0.10.0" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/core-app-api": ^1.15.1 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/errors": ^1.2.4 + "@backstage/frontend-defaults": ^0.1.1 + "@backstage/frontend-plugin-api": ^0.9.0 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.10 + lodash: ^4.17.21 + zod: ^3.22.4 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 76728d37614e56d2bf7ee5f6f2d73b57e1c8d8a2aba6d11563645ef1e25db1dfd55910143fda3586b5ef156f3753530b1690b5ac52b8c957e161ab8fae07eb1e + languageName: node + linkType: hard + +"@backstage/frontend-defaults@npm:^0.1.1": + version: 0.1.1 + resolution: "@backstage/frontend-defaults@npm:0.1.1" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/frontend-app-api": ^0.10.0 + "@backstage/frontend-plugin-api": ^0.9.0 + "@backstage/plugin-app": ^0.1.1 + "@react-hookz/web": ^24.0.0 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 7d74ba5e24726cc322ba2edb1b92c787b6c78ff8a4594ab584631f2cf9232e1de928afd23e49ae04d21501d034acbd99432e5394eeb88798ee520e9d2151b820 + languageName: node + linkType: hard + +"@backstage/frontend-plugin-api@npm:^0.9.0": + version: 0.9.0 + resolution: "@backstage/frontend-plugin-api@npm:0.9.0" + dependencies: + "@backstage/core-components": ^0.15.1 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.10 + "@material-ui/core": ^4.12.4 + lodash: ^4.17.21 + zod: ^3.22.4 + zod-to-json-schema: ^3.21.4 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 12902ce830631878ab1b5ecc22c9b62c4534e45c21b83ff88d8b806a2649ccbfc2fce789764052bc495a3162ed1fae466f6ed6b39aeb3b377da20202dad28fc3 + languageName: node + linkType: hard + +"@backstage/frontend-test-utils@npm:^0.2.1": + version: 0.2.1 + resolution: "@backstage/frontend-test-utils@npm:0.2.1" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/frontend-app-api": ^0.10.0 + "@backstage/frontend-plugin-api": ^0.9.0 + "@backstage/plugin-app": ^0.1.1 + "@backstage/test-utils": ^1.7.0 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.10 + zod: ^3.22.4 + peerDependencies: + "@testing-library/react": ^16.0.0 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: eedf49b53b600bbadf98aabc1c8b9082b976eaa3d9c151c9a9eda17ba0ed2e2c2054044d28db95980cf505c14ee8116220a95355a72e71ed22a22e319ef596cb + languageName: node + linkType: hard + +"@backstage/integration-aws-node@npm:^0.1.12": + version: 0.1.12 + resolution: "@backstage/integration-aws-node@npm:0.1.12" + dependencies: + "@aws-sdk/client-sts": ^3.350.0 + "@aws-sdk/credential-provider-node": ^3.350.0 + "@aws-sdk/credential-providers": ^3.350.0 + "@aws-sdk/types": ^3.347.0 + "@aws-sdk/util-arn-parser": ^3.310.0 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + checksum: 01c62b22bdb06eafa174c6f80a95f332df867cebed4554be328efd1f1338dedb86e6bdb7cfda2f2acb1a6a8a92891024da7c81b7ddbfb269b72c3725a54de576 + languageName: node + linkType: hard + +"@backstage/integration-react@npm:^1.2.0": + version: 1.2.0 + resolution: "@backstage/integration-react@npm:1.2.0" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/integration": ^1.15.1 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 8cfbb94c9d9ebad8ed0c7f05a4491cf70421a313271e47f2717f217b800b7cbe947adb68a4ce15b65a2f918d042d3d6e5e0c46485354e760012ae94080d26acb + languageName: node + linkType: hard + +"@backstage/integration@npm:^1.15.0, @backstage/integration@npm:^1.15.1": + version: 1.15.1 + resolution: "@backstage/integration@npm:1.15.1" + dependencies: + "@azure/identity": ^4.0.0 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@octokit/auth-app": ^4.0.0 + "@octokit/rest": ^19.0.3 + cross-fetch: ^4.0.0 + git-url-parse: ^15.0.0 + lodash: ^4.17.21 + luxon: ^3.0.0 + checksum: 078e366fc704fcc061f16ba461c1b11f90c07920af9fc5a6894abbe4232c184ee925ab6c1f2af7c02233d27d6722395e034909ef251edd7438484fa31e68833a + languageName: node + linkType: hard + +"@backstage/plugin-app@npm:^0.1.1": + version: 0.1.1 + resolution: "@backstage/plugin-app@npm:0.1.1" + dependencies: + "@backstage/core-components": ^0.15.1 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/frontend-plugin-api": ^0.9.0 + "@backstage/integration-react": ^1.2.0 + "@backstage/plugin-permission-react": ^0.4.27 + "@backstage/theme": ^0.6.0 + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.61 + react-use: ^17.2.4 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: bb75599f8feb8847249a4f5b08aac43be0e61495f16144e4571b3bf9e794c290e0203d73693ae99731c9a4d873cd7ac6ce03a45945b45937834bb1b9eaaa03db + languageName: node + linkType: hard + +"@backstage/plugin-auth-node@npm:^0.5.2, @backstage/plugin-auth-node@npm:^0.5.3": + version: 0.5.3 + resolution: "@backstage/plugin-auth-node@npm:0.5.3" + dependencies: + "@backstage/backend-common": ^0.25.0 + "@backstage/backend-plugin-api": ^1.0.1 + "@backstage/catalog-client": ^1.7.1 + "@backstage/catalog-model": ^1.7.0 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/types": ^1.1.1 + "@types/express": "*" + "@types/passport": ^1.0.3 + express: ^4.17.1 + jose: ^5.0.0 + lodash: ^4.17.21 + node-fetch: ^2.7.0 + passport: ^0.7.0 + winston: ^3.2.1 + zod: ^3.22.4 + zod-to-json-schema: ^3.21.4 + zod-validation-error: ^3.4.0 + checksum: 6a8fcac434b3653011aa634fab973b9bdc9daf141335948669bcbd8e2c5f97e0797feaaaae34e17e20334835cdb83fea6c7b50939f1a18f9c88e245406230c50 + languageName: node + linkType: hard + +"@backstage/plugin-catalog-common@npm:^1.1.0": + version: 1.1.0 + resolution: "@backstage/plugin-catalog-common@npm:1.1.0" + dependencies: + "@backstage/catalog-model": ^1.7.0 + "@backstage/plugin-permission-common": ^0.8.1 + "@backstage/plugin-search-common": ^1.2.14 + checksum: 291a589cfa6d6d06dbb01d6343c005f4dad1d837b3f2d56ce7d0f1cb89b90c92af4e1dd17931cdcd2b6666b11eba0f8726f9fe02bca2340997002b2182cdf40b + languageName: node + linkType: hard + +"@backstage/plugin-catalog-react@npm:^1.14.0": + version: 1.14.0 + resolution: "@backstage/plugin-catalog-react@npm:1.14.0" + dependencies: + "@backstage/catalog-client": ^1.7.1 + "@backstage/catalog-model": ^1.7.0 + "@backstage/core-compat-api": ^0.3.1 + "@backstage/core-components": ^0.15.1 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/errors": ^1.2.4 + "@backstage/frontend-plugin-api": ^0.9.0 + "@backstage/frontend-test-utils": ^0.2.1 + "@backstage/integration-react": ^1.2.0 + "@backstage/plugin-catalog-common": ^1.1.0 + "@backstage/plugin-permission-common": ^0.8.1 + "@backstage/plugin-permission-react": ^0.4.27 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.10 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.61 + "@react-hookz/web": ^24.0.0 + classnames: ^2.2.6 + lodash: ^4.17.21 + material-ui-popup-state: ^1.9.3 + qs: ^6.9.4 + react-use: ^17.2.4 + yaml: ^2.0.0 + zen-observable: ^0.10.0 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: bd358a3be0ed20c2e716b6aec8614884f815362380939d79704f9b667d982a7c5de375693506cec597d5c254cec1426c645ecab891bf81f154c0fda0b6117932 + languageName: node + linkType: hard + +"@backstage/plugin-catalog@npm:^1.24.0": + version: 1.24.0 + resolution: "@backstage/plugin-catalog@npm:1.24.0" + dependencies: + "@backstage/catalog-client": ^1.7.1 + "@backstage/catalog-model": ^1.7.0 + "@backstage/core-compat-api": ^0.3.1 + "@backstage/core-components": ^0.15.1 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/errors": ^1.2.4 + "@backstage/frontend-plugin-api": ^0.9.0 + "@backstage/integration-react": ^1.2.0 + "@backstage/plugin-catalog-common": ^1.1.0 + "@backstage/plugin-catalog-react": ^1.14.0 + "@backstage/plugin-permission-react": ^0.4.27 + "@backstage/plugin-scaffolder-common": ^1.5.6 + "@backstage/plugin-search-common": ^1.2.14 + "@backstage/plugin-search-react": ^1.8.1 + "@backstage/types": ^1.1.1 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.61 + "@mui/utils": ^5.14.15 + dataloader: ^2.0.0 + expiry-map: ^2.0.0 + history: ^5.0.0 + lodash: ^4.17.21 + pluralize: ^8.0.0 + react-use: ^17.2.4 + zen-observable: ^0.10.0 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 5b9dd519194168a57972bde2c73bb5bc111e2d13126e48ffe14a1e25e91a17755fb54c999d973e9a2e6e28e6edb4e25224d29981c4ceb00ea5aba5034fdb36f4 + languageName: node + linkType: hard + +"@backstage/plugin-events-node@npm:^0.4.2": + version: 0.4.2 + resolution: "@backstage/plugin-events-node@npm:0.4.2" + dependencies: + "@backstage/backend-plugin-api": ^1.0.1 + "@backstage/errors": ^1.2.4 + "@backstage/types": ^1.1.1 + cross-fetch: ^4.0.0 + uri-template: ^2.0.0 + checksum: 5a2da2a0e0ef916bf431a5ec78cd50d5b272437cfa6415624dbea466e511251af541b5be7d6cc7c3fb5077c0fe6e1e985f07382ce4c001233d19cbdc6ead69df + languageName: node + linkType: hard + +"@backstage/plugin-kubernetes-common@npm:^0.8.3": + version: 0.8.3 + resolution: "@backstage/plugin-kubernetes-common@npm:0.8.3" + dependencies: + "@backstage/catalog-model": ^1.7.0 + "@backstage/plugin-permission-common": ^0.8.1 + "@backstage/types": ^1.1.1 + "@kubernetes/client-node": 0.20.0 + kubernetes-models: ^4.3.1 + lodash: ^4.17.21 + luxon: ^3.0.0 + checksum: 948c473f9656e039ee74fd5c724d520b09899cabf6713ab89c587492e542648385892fb1fa0f3872260707cfb3cd71d81bbfcd737e468f71a4080bd503e710a2 + languageName: node + linkType: hard + +"@backstage/plugin-kubernetes-react@npm:^0.4.4": + version: 0.4.4 + resolution: "@backstage/plugin-kubernetes-react@npm:0.4.4" + dependencies: + "@backstage/catalog-model": ^1.7.0 + "@backstage/core-components": ^0.15.1 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-kubernetes-common": ^0.8.3 + "@backstage/types": ^1.1.1 + "@kubernetes-models/apimachinery": ^2.0.0 + "@kubernetes-models/base": ^5.0.0 + "@kubernetes/client-node": ^0.20.0 + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.11.3 + "@material-ui/lab": ^4.0.0-alpha.61 + cronstrue: ^2.32.0 + js-yaml: ^4.1.0 + kubernetes-models: ^4.3.1 + lodash: ^4.17.21 + luxon: ^3.0.0 + react-use: ^17.4.0 + xterm: ^5.3.0 + xterm-addon-attach: ^0.9.0 + xterm-addon-fit: ^0.8.0 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: bb96e77a798a701315424701f005786c753a29c4483d7b2a71c6e56a36e54b203874356edd52e203645f92a69bb72e48190d5934a6b328b6a34235319af26e38 + languageName: node + linkType: hard + +"@backstage/plugin-permission-backend@npm:^0.5.50": + version: 0.5.50 + resolution: "@backstage/plugin-permission-backend@npm:0.5.50" + dependencies: + "@backstage/backend-common": ^0.25.0 + "@backstage/backend-plugin-api": ^1.0.1 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-auth-node": ^0.5.3 + "@backstage/plugin-permission-common": ^0.8.1 + "@backstage/plugin-permission-node": ^0.8.4 + "@types/express": "*" + dataloader: ^2.0.0 + express: ^4.17.1 + express-promise-router: ^4.1.0 + lodash: ^4.17.21 + node-fetch: ^2.7.0 + yn: ^4.0.0 + zod: ^3.22.4 + checksum: aba76547dc8a7831edd0300016fa754dee933bcf9ac435ddcd9859cb5a870f30b4ca0433349af31df9aad1c9c6ff0787f7feed5472228d6d0cb622c3c6aae532 + languageName: node + linkType: hard + +"@backstage/plugin-permission-common@npm:^0.8.1": + version: 0.8.1 + resolution: "@backstage/plugin-permission-common@npm:0.8.1" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/types": ^1.1.1 + cross-fetch: ^4.0.0 + uuid: ^9.0.0 + zod: ^3.22.4 + zod-to-json-schema: ^3.20.4 + checksum: 00f71b998aecefcf413b335ef67897be2210f9cecb1f58bb28e466f68acd04276e3105f2e99ad242792dfd2902e4ae7ea023efb8cda92447aef92a10b83d87e5 + languageName: node + linkType: hard + +"@backstage/plugin-permission-node@npm:^0.8.4": + version: 0.8.4 + resolution: "@backstage/plugin-permission-node@npm:0.8.4" + dependencies: + "@backstage/backend-common": ^0.25.0 + "@backstage/backend-plugin-api": ^1.0.1 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/plugin-auth-node": ^0.5.3 + "@backstage/plugin-permission-common": ^0.8.1 + "@types/express": ^4.17.6 + express: ^4.17.1 + express-promise-router: ^4.1.0 + zod: ^3.22.4 + zod-to-json-schema: ^3.20.4 + checksum: a00c269e4777ff5e10db7a3859110f3a487c91e4c9422eb0650cbef3cfffc3dbd2f38e98fd793daef8242da2777ffbd87adb5715d21f0dff998b90c65261904e + languageName: node + linkType: hard + +"@backstage/plugin-permission-react@npm:^0.4.27": + version: 0.4.27 + resolution: "@backstage/plugin-permission-react@npm:0.4.27" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/plugin-permission-common": ^0.8.1 + swr: ^2.0.0 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 82e6ce34cd7634a08c61c3344840df295ef06b358cab93e3d42a0f304a9b8cbe635ca53a93b566889a1907d798149b1b9dc0ff82ecb9484c7f6ca4c314b6cb60 + languageName: node + linkType: hard + +"@backstage/plugin-scaffolder-common@npm:^1.5.6": + version: 1.5.6 + resolution: "@backstage/plugin-scaffolder-common@npm:1.5.6" + dependencies: + "@backstage/catalog-model": ^1.7.0 + "@backstage/plugin-permission-common": ^0.8.1 + "@backstage/types": ^1.1.1 + checksum: 0dcfa5089d92dd7e0e400df25d17e838846ee8bec60f84f268c77d7815f37a217226f9385dcbb6a816de0266ecce502f9f491208aec06f83ca8e5f5a84451e9a + languageName: node + linkType: hard + +"@backstage/plugin-search-common@npm:^1.2.14": + version: 1.2.14 + resolution: "@backstage/plugin-search-common@npm:1.2.14" + dependencies: + "@backstage/plugin-permission-common": ^0.8.1 + "@backstage/types": ^1.1.1 + checksum: e4e44c06aaabfa296c9f07b6d9537bebcdbc54c309dcfbe9b11ee2625193df1571f58469388b49379a5a0fa1cc6560b81347dc4020b239b118558ae4d0c79511 + languageName: node + linkType: hard + +"@backstage/plugin-search-react@npm:^1.8.1": + version: 1.8.1 + resolution: "@backstage/plugin-search-react@npm:1.8.1" + dependencies: + "@backstage/core-components": ^0.15.1 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/frontend-plugin-api": ^0.9.0 + "@backstage/plugin-search-common": ^1.2.14 + "@backstage/theme": ^0.6.0 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.10 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.61 + lodash: ^4.17.21 + qs: ^6.9.4 + react-use: ^17.3.2 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 2edb7d78501d4c21b4a9d9057332b69a01a8a5c8cc6cc688dffa10c39e031f5fd8d24f56415148a8f181eaf4a7ad8171e3b9644bc63461a7a7013e3ac3403ae9 + languageName: node + linkType: hard + +"@backstage/release-manifests@npm:^0.0.11": + version: 0.0.11 + resolution: "@backstage/release-manifests@npm:0.0.11" + dependencies: + cross-fetch: ^4.0.0 + checksum: c03a21524436f1e423a40ac15f685b7f13ce3205e2684ce859571db3b70c78d783b3e1702ba3ffb2ba2d446f7444e8c592c6696b7c618fbf6648e91cb4c4fe07 + languageName: node + linkType: hard + +"@backstage/repo-tools@npm:^0.10.0": + version: 0.10.0 + resolution: "@backstage/repo-tools@npm:0.10.0" + dependencies: + "@apidevtools/swagger-parser": ^10.1.0 + "@apisyouwonthate/style-guide": ^1.4.0 + "@backstage/backend-plugin-api": ^1.0.1 + "@backstage/catalog-model": ^1.7.0 + "@backstage/cli-common": ^0.1.14 + "@backstage/cli-node": ^0.2.9 + "@backstage/config-loader": ^1.9.1 + "@backstage/errors": ^1.2.4 + "@manypkg/get-packages": ^1.1.3 + "@microsoft/api-documenter": ^7.25.7 + "@microsoft/api-extractor": ^7.47.2 + "@openapitools/openapi-generator-cli": ^2.7.0 + "@stoplight/spectral-core": ^1.18.0 + "@stoplight/spectral-formatters": ^1.1.0 + "@stoplight/spectral-functions": ^1.7.2 + "@stoplight/spectral-parsers": ^1.0.2 + "@stoplight/spectral-rulesets": ^1.18.0 + "@stoplight/spectral-runtime": ^1.1.2 + "@stoplight/types": ^14.0.0 + "@useoptic/openapi-utilities": ^0.55.0 + chalk: ^4.0.0 + codeowners-utils: ^1.0.2 + command-exists: ^1.2.9 + commander: ^12.0.0 + fs-extra: ^11.2.0 + glob: ^8.0.3 + is-glob: ^4.0.3 + js-yaml: ^4.1.0 + lodash: ^4.17.21 + minimatch: ^9.0.0 + p-limit: ^3.0.2 + portfinder: ^1.0.32 + ts-morph: ^23.0.0 + yaml-diff-patch: ^2.0.0 + peerDependencies: + "@microsoft/api-extractor-model": "*" + "@microsoft/tsdoc": "*" + "@microsoft/tsdoc-config": "*" + "@useoptic/optic": ^1.0.0 + prettier: ^2.8.1 + typescript: "> 3.0.0" + peerDependenciesMeta: + prettier: + optional: true + bin: + backstage-repo-tools: bin/backstage-repo-tools + checksum: 8a68c69f053edadd087d829823880902d8e6d55e77d756d6b4ba4640af29493036ac5fa08cec08666b59ddfb27cacf4975fd24ac7ba785dab5b1d8a96df62bec + languageName: node + linkType: hard + +"@backstage/test-utils@npm:1.7.0, @backstage/test-utils@npm:^1.7.0": + version: 1.7.0 + resolution: "@backstage/test-utils@npm:1.7.0" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/core-app-api": ^1.15.1 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/plugin-permission-common": ^0.8.1 + "@backstage/plugin-permission-react": ^0.4.27 + "@backstage/theme": ^0.6.0 + "@backstage/types": ^1.1.1 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + cross-fetch: ^4.0.0 + i18next: ^22.4.15 + zen-observable: ^0.10.0 + peerDependencies: + "@testing-library/react": ^16.0.0 + "@types/jest": "*" + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/jest": + optional: true + "@types/react": + optional: true + checksum: a2510b9b2cde88ba636af2d09d30d8349b80167a9408e416b2245771f5138dfd56745a6b08ecad64ec7f9656d2b5a15d9ab15dbcd695d3981db49e272d8a3260 + languageName: node + linkType: hard + +"@backstage/theme@npm:^0.6.0": + version: 0.6.0 + resolution: "@backstage/theme@npm:0.6.0" + dependencies: + "@emotion/react": ^11.10.5 + "@emotion/styled": ^11.10.5 + "@mui/material": ^5.12.2 + peerDependencies: + "@material-ui/core": ^4.12.2 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: d1cdaa069bc6eac3a38f947504ddc45a7d1d49ef429fdb9ff99b606cb07a686b5b86c98bed76e0b2404d1b401fd7e6f12ab7a272ebb483a61ea3d07b8d467b6f + languageName: node + linkType: hard + +"@backstage/types@npm:1.1.1, @backstage/types@npm:^1.1.1": + version: 1.1.1 + resolution: "@backstage/types@npm:1.1.1" + checksum: 54bd9e53570cf2a7a8d9ae30e7181ee6b669b7f543949391a2168f616e1f7b13f0419f324941a87aa15f723d0313eda8f212db2077675421d6f91484f477c4f5 + languageName: node + linkType: hard + +"@backstage/version-bridge@npm:^1.0.10": + version: 1.0.10 + resolution: "@backstage/version-bridge@npm:1.0.10" + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: f24c02c071aecf5a9557602252dc458a6db93393960b9f4a51dc649c1886afcfaae3f3a46ce3c846721ea56a112c0c604de52782e19f495daec2b16ae7e72af4 + languageName: node + linkType: hard + +"@balena/dockerignore@npm:^1.0.2": + version: 1.0.2 + resolution: "@balena/dockerignore@npm:1.0.2" + checksum: 0d39f8fbcfd1a983a44bced54508471ab81aaaa40e2c62b46a9f97eac9d6b265790799f16919216db486331dedaacdde6ecbd6b7abe285d39bc50de111991699 + languageName: node + linkType: hard + +"@bcoe/v8-coverage@npm:^0.2.3": + version: 0.2.3 + resolution: "@bcoe/v8-coverage@npm:0.2.3" + checksum: 850f9305536d0f2bd13e9e0881cb5f02e4f93fad1189f7b2d4bebf694e3206924eadee1068130d43c11b750efcc9405f88a8e42ef098b6d75239c0f047de1a27 + languageName: node + linkType: hard + +"@changesets/apply-release-plan@npm:^7.0.5": + version: 7.0.5 + resolution: "@changesets/apply-release-plan@npm:7.0.5" + dependencies: + "@changesets/config": ^3.0.3 + "@changesets/get-version-range-type": ^0.4.0 + "@changesets/git": ^3.0.1 + "@changesets/should-skip-package": ^0.1.1 + "@changesets/types": ^6.0.0 + "@manypkg/get-packages": ^1.1.3 + detect-indent: ^6.0.0 + fs-extra: ^7.0.1 + lodash.startcase: ^4.4.0 + outdent: ^0.5.0 + prettier: ^2.7.1 + resolve-from: ^5.0.0 + semver: ^7.5.3 + checksum: f6a1b90d89fd08b46c11fad05d5fee510ff8a1888c163fd6221ccfb045eab013fa57c0c32c5697d6406852a39cf4df01b44f3ecca4746f30bd610bec54aa9abf + languageName: node + linkType: hard + +"@changesets/assemble-release-plan@npm:^6.0.4": + version: 6.0.4 + resolution: "@changesets/assemble-release-plan@npm:6.0.4" + dependencies: + "@changesets/errors": ^0.2.0 + "@changesets/get-dependents-graph": ^2.1.2 + "@changesets/should-skip-package": ^0.1.1 + "@changesets/types": ^6.0.0 + "@manypkg/get-packages": ^1.1.3 + semver: ^7.5.3 + checksum: 948066a8ca8e12390599f641a0439b6a4d6c1c2a9958f58596aa50cf68d7d594b28acc1eb6bd0ad17df2025f0614006e44728a2614fad2a3d54c669568bf6d65 + languageName: node + linkType: hard + +"@changesets/changelog-git@npm:^0.2.0": + version: 0.2.0 + resolution: "@changesets/changelog-git@npm:0.2.0" + dependencies: + "@changesets/types": ^6.0.0 + checksum: 132660f7fdabbdda00ac803cc822d6427a1a38a17a5f414e87ad32f6dc4cbef5280a147ecdc087a28dc06c8bd0762f8d6e7132d01b8a4142b59fbe1bc2177034 + languageName: node + linkType: hard + +"@changesets/cli@npm:^2.27.1": + version: 2.27.9 + resolution: "@changesets/cli@npm:2.27.9" + dependencies: + "@changesets/apply-release-plan": ^7.0.5 + "@changesets/assemble-release-plan": ^6.0.4 + "@changesets/changelog-git": ^0.2.0 + "@changesets/config": ^3.0.3 + "@changesets/errors": ^0.2.0 + "@changesets/get-dependents-graph": ^2.1.2 + "@changesets/get-release-plan": ^4.0.4 + "@changesets/git": ^3.0.1 + "@changesets/logger": ^0.1.1 + "@changesets/pre": ^2.0.1 + "@changesets/read": ^0.6.1 + "@changesets/should-skip-package": ^0.1.1 + "@changesets/types": ^6.0.0 + "@changesets/write": ^0.3.2 + "@manypkg/get-packages": ^1.1.3 + ansi-colors: ^4.1.3 + ci-info: ^3.7.0 + enquirer: ^2.3.0 + external-editor: ^3.1.0 + fs-extra: ^7.0.1 + mri: ^1.2.0 + p-limit: ^2.2.0 + package-manager-detector: ^0.2.0 + picocolors: ^1.1.0 + resolve-from: ^5.0.0 + semver: ^7.5.3 + spawndamnit: ^2.0.0 + term-size: ^2.1.0 + bin: + changeset: bin.js + checksum: 4bd36c152f9f93716b001f3ed849717588d2a9eb97f058e86f95ba6a43d8e4311073174251150aabb96f0a1ab5f8ab5ee6a32f85fc9248363f92b3826227cb9d + languageName: node + linkType: hard + +"@changesets/config@npm:^3.0.3": + version: 3.0.3 + resolution: "@changesets/config@npm:3.0.3" + dependencies: + "@changesets/errors": ^0.2.0 + "@changesets/get-dependents-graph": ^2.1.2 + "@changesets/logger": ^0.1.1 + "@changesets/types": ^6.0.0 + "@manypkg/get-packages": ^1.1.3 + fs-extra: ^7.0.1 + micromatch: ^4.0.2 + checksum: f216f497e09c0fcdd4c397fc3998d1651a171b89981d2bed2a6c23c0f55ffa4e240cadbd13902bf91c218686165689a5183674a5b4682d80d3d5b8b9c569f5f1 + languageName: node + linkType: hard + +"@changesets/errors@npm:^0.2.0": + version: 0.2.0 + resolution: "@changesets/errors@npm:0.2.0" + dependencies: + extendable-error: ^0.1.5 + checksum: 4b79373f92287af4f723e8dbbccaf0299aa8735fc043243d0ad587f04a7614615ea50180be575d4438b9f00aa82d1cf85e902b77a55bdd3e0a8dd97e77b18c60 + languageName: node + linkType: hard + +"@changesets/get-dependents-graph@npm:^2.1.2": + version: 2.1.2 + resolution: "@changesets/get-dependents-graph@npm:2.1.2" + dependencies: + "@changesets/types": ^6.0.0 + "@manypkg/get-packages": ^1.1.3 + picocolors: ^1.1.0 + semver: ^7.5.3 + checksum: 38446343e43f9b8731098e3b42d2525d5399d59cfccc09bdb62c9a48de60c7a893882050202badca3b5cab8405e6deb82e88258a56a318e42749fa60d96d874a + languageName: node + linkType: hard + +"@changesets/get-release-plan@npm:^4.0.4": + version: 4.0.4 + resolution: "@changesets/get-release-plan@npm:4.0.4" + dependencies: + "@changesets/assemble-release-plan": ^6.0.4 + "@changesets/config": ^3.0.3 + "@changesets/pre": ^2.0.1 + "@changesets/read": ^0.6.1 + "@changesets/types": ^6.0.0 + "@manypkg/get-packages": ^1.1.3 + checksum: 7217347f5bfaa56f97d3964e28e23a109d60c42b7c879c0cab6934feb30bdbdebb6dd0e81b4ecb5ec414be442d566b6af90d9224f6a48a52b6c5269c337f54a6 + languageName: node + linkType: hard + +"@changesets/get-version-range-type@npm:^0.4.0": + version: 0.4.0 + resolution: "@changesets/get-version-range-type@npm:0.4.0" + checksum: 2e8c511e658e193f48de7f09522649c4cf072932f0cbe0f252a7f2703d7775b0b90b632254526338795d0658e340be9dff3879cfc8eba4534b8cd6071efff8c9 + languageName: node + linkType: hard + +"@changesets/git@npm:^3.0.1": + version: 3.0.1 + resolution: "@changesets/git@npm:3.0.1" + dependencies: + "@changesets/errors": ^0.2.0 + "@manypkg/get-packages": ^1.1.3 + is-subdir: ^1.1.1 + micromatch: ^4.0.2 + spawndamnit: ^2.0.0 + checksum: 46d780fecd3dbdafde7c96dde7fe35a8461bc6edbff1de92d490971a99f021d60c5b4606a1d4fb778567146810090ede6610cf89407c14bde88edaa246801539 + languageName: node + linkType: hard + +"@changesets/logger@npm:^0.1.1": + version: 0.1.1 + resolution: "@changesets/logger@npm:0.1.1" + dependencies: + picocolors: ^1.1.0 + checksum: acca50ef6bf6e446b46eb576b32f1955bf4579dbf4bbc316768ed2c1d4ba4066c9c73b114eedefaa1b3e360b1060a020e6bd3dbdbc44b74da732df92307beab0 + languageName: node + linkType: hard + +"@changesets/parse@npm:^0.4.0": + version: 0.4.0 + resolution: "@changesets/parse@npm:0.4.0" + dependencies: + "@changesets/types": ^6.0.0 + js-yaml: ^3.13.1 + checksum: 3dd970b244479746233ebd357cfff3816cf9f344ebf2cf0c7c55ce8579adfd3f506978e86ad61222dc3acf1548a2105ffdd8b3e940b3f82b225741315cee2bf0 + languageName: node + linkType: hard + +"@changesets/pre@npm:^2.0.1": + version: 2.0.1 + resolution: "@changesets/pre@npm:2.0.1" + dependencies: + "@changesets/errors": ^0.2.0 + "@changesets/types": ^6.0.0 + "@manypkg/get-packages": ^1.1.3 + fs-extra: ^7.0.1 + checksum: fbe94283dce0223ee79c12fa221105752ac89eb885b77e300ec755682cb06cc0145e10335f4bc6cb26d63473e549556c2b1c8c866242419aee5e41986379652a + languageName: node + linkType: hard + +"@changesets/read@npm:^0.6.1": + version: 0.6.1 + resolution: "@changesets/read@npm:0.6.1" + dependencies: + "@changesets/git": ^3.0.1 + "@changesets/logger": ^0.1.1 + "@changesets/parse": ^0.4.0 + "@changesets/types": ^6.0.0 + fs-extra: ^7.0.1 + p-filter: ^2.1.0 + picocolors: ^1.1.0 + checksum: d00a18a3d04af5c76e7b763096650ebe16589864ab04eaf9e99c88aa77542f64de547b585037fc204d2055f9dd47fae94c789e2f173d3507a4e29dbe6609dd5b + languageName: node + linkType: hard + +"@changesets/should-skip-package@npm:^0.1.1": + version: 0.1.1 + resolution: "@changesets/should-skip-package@npm:0.1.1" + dependencies: + "@changesets/types": ^6.0.0 + "@manypkg/get-packages": ^1.1.3 + checksum: d187ef22495deb63e678d0ff65e8627701e2b52c25bd59dde10ce8646be8d605c0ed0a6af020dd825b137c2fc748fdc6cef52e7774bad4c7a4f404bf182a85cf + languageName: node + linkType: hard + +"@changesets/types@npm:^4.0.1": + version: 4.1.0 + resolution: "@changesets/types@npm:4.1.0" + checksum: 72c1f58044178ca867dd9349ecc4b7c233ce3781bb03b5b72a70c3166fbbab54a2f2cb19a81f96b4649ba004442c8734569fba238be4dd737fb4624a135c6098 + languageName: node + linkType: hard + +"@changesets/types@npm:^6.0.0": + version: 6.0.0 + resolution: "@changesets/types@npm:6.0.0" + checksum: d528b5d712f62c26ea422c7d34ccf6eac57a353c0733d96716db3c796ecd9bba5d496d48b37d5d46b784dc45b69c06ce3345fa3515df981bb68456cad68e6465 + languageName: node + linkType: hard + +"@changesets/write@npm:^0.3.2": + version: 0.3.2 + resolution: "@changesets/write@npm:0.3.2" + dependencies: + "@changesets/types": ^6.0.0 + fs-extra: ^7.0.1 + human-id: ^1.0.2 + prettier: ^2.7.1 + checksum: 553ed0ba6bd6397784f5e0e2921794bd7417a3c4fb810f1abb15e7072bf9d312af74308ff743161c6ea01478884cebcaf9cee02e5c70e2c7552b2774960ee07c + languageName: node + linkType: hard + +"@colors/colors@npm:1.6.0, @colors/colors@npm:^1.6.0": + version: 1.6.0 + resolution: "@colors/colors@npm:1.6.0" + checksum: aa209963e0c3218e80a4a20553ba8c0fbb6fa13140540b4e5f97923790be06801fc90172c1114fc8b7e888b3d012b67298cde6b9e81521361becfaee400c662f + languageName: node + linkType: hard + +"@cspotcode/source-map-support@npm:^0.8.0": + version: 0.8.1 + resolution: "@cspotcode/source-map-support@npm:0.8.1" + dependencies: + "@jridgewell/trace-mapping": 0.3.9 + checksum: 5718f267085ed8edb3e7ef210137241775e607ee18b77d95aa5bd7514f47f5019aa2d82d96b3bf342ef7aa890a346fa1044532ff7cc3009e7d24fce3ce6200fa + languageName: node + linkType: hard + +"@dabh/diagnostics@npm:^2.0.2": + version: 2.0.3 + resolution: "@dabh/diagnostics@npm:2.0.3" + dependencies: + colorspace: 1.1.x + enabled: 2.0.x + kuler: ^2.0.0 + checksum: 4879600c55c8315a0fb85fbb19057bad1adc08f0a080a8cb4e2b63f723c379bfc4283b68123a2b078d367b327dd8df12fcb27464efe791addc0a48b9df6d79a1 + languageName: node + linkType: hard + +"@dagrejs/graphlib@npm:^2.1.13": + version: 2.2.4 + resolution: "@dagrejs/graphlib@npm:2.2.4" + checksum: 02d2d8df07fd6234bfc82f03f0aebd718f1ffc2ba6579ef67a26e423d9ad25a437256d3342e6342e29ae8a5c5e448a07440d6e218c6e77e390bc57ee73b43d2d + languageName: node + linkType: hard + +"@date-io/core@npm:1.x, @date-io/core@npm:^1.3.13": + version: 1.3.13 + resolution: "@date-io/core@npm:1.3.13" + checksum: 5a9e9d1de20f0346a3c7d2d5946190caef4bfb0b64d82ba1f4c566657a9192667c94ebe7f438d11d4286d9c190974daad4fb2159294225cd8af4d9a140239879 + languageName: node + linkType: hard + +"@date-io/date-fns@npm:^1.3.13": + version: 1.3.13 + resolution: "@date-io/date-fns@npm:1.3.13" + dependencies: + "@date-io/core": ^1.3.13 + peerDependencies: + date-fns: ^2.0.0 + checksum: 0026c0e538ea4add57a11936ff6bdb07e99f25275f8bb28c4702bbb7e82c3a41b3e8124132aa719180d462c01a26a3b4801e41b7349cdb73813749d4bf5e8fbd + languageName: node + linkType: hard + +"@emotion/babel-plugin@npm:^11.12.0": + version: 11.12.0 + resolution: "@emotion/babel-plugin@npm:11.12.0" + dependencies: + "@babel/helper-module-imports": ^7.16.7 + "@babel/runtime": ^7.18.3 + "@emotion/hash": ^0.9.2 + "@emotion/memoize": ^0.9.0 + "@emotion/serialize": ^1.2.0 + babel-plugin-macros: ^3.1.0 + convert-source-map: ^1.5.0 + escape-string-regexp: ^4.0.0 + find-root: ^1.1.0 + source-map: ^0.5.7 + stylis: 4.2.0 + checksum: b5d4b3dfe97e6763794a42b5c3a027a560caa1aa6dcaf05c18e5969691368dd08245c077bad7397dcc720b53d29caeaaec1888121e68cfd9ab02ff52f6fef662 + languageName: node + linkType: hard + +"@emotion/cache@npm:^11.11.0, @emotion/cache@npm:^11.13.0": + version: 11.13.1 + resolution: "@emotion/cache@npm:11.13.1" + dependencies: + "@emotion/memoize": ^0.9.0 + "@emotion/sheet": ^1.4.0 + "@emotion/utils": ^1.4.0 + "@emotion/weak-memoize": ^0.4.0 + stylis: 4.2.0 + checksum: 94b161786a03a08a1e30257478fad9a9be1ac8585ddca0c6410d7411fd474fc8b0d6d1167d7d15bdb012d1fd8a1220ac2bbc79501ad9b292b83c17da0874d7de + languageName: node + linkType: hard + +"@emotion/hash@npm:^0.8.0": + version: 0.8.0 + resolution: "@emotion/hash@npm:0.8.0" + checksum: 4b35d88a97e67275c1d990c96d3b0450451d089d1508619488fc0acb882cb1ac91e93246d471346ebd1b5402215941ef4162efe5b51534859b39d8b3a0e3ffaa + languageName: node + linkType: hard + +"@emotion/hash@npm:^0.9.2": + version: 0.9.2 + resolution: "@emotion/hash@npm:0.9.2" + checksum: 379bde2830ccb0328c2617ec009642321c0e009a46aa383dfbe75b679c6aea977ca698c832d225a893901f29d7b3eef0e38cf341f560f6b2b56f1ff23c172387 + languageName: node + linkType: hard + +"@emotion/is-prop-valid@npm:^1.3.0": + version: 1.3.1 + resolution: "@emotion/is-prop-valid@npm:1.3.1" + dependencies: + "@emotion/memoize": ^0.9.0 + checksum: fe6549d54f389e1a17cb02d832af7ee85fb6ea126fc18d02ca47216e8ff19332c1983f4a0ba68602cfcd3b325ffd4ebf0b2d0c6270f1e7e6fe3fca4ba7741e1a + languageName: node + linkType: hard + +"@emotion/memoize@npm:^0.9.0": + version: 0.9.0 + resolution: "@emotion/memoize@npm:0.9.0" + checksum: 038132359397348e378c593a773b1148cd0cf0a2285ffd067a0f63447b945f5278860d9de718f906a74c7c940ba1783ac2ca18f1c06a307b01cc0e3944e783b1 + languageName: node + linkType: hard + +"@emotion/react@npm:^11.10.5": + version: 11.13.3 + resolution: "@emotion/react@npm:11.13.3" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.12.0 + "@emotion/cache": ^11.13.0 + "@emotion/serialize": ^1.3.1 + "@emotion/use-insertion-effect-with-fallbacks": ^1.1.0 + "@emotion/utils": ^1.4.0 + "@emotion/weak-memoize": ^0.4.0 + hoist-non-react-statics: ^3.3.1 + peerDependencies: + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 0b58374bf28de914b49881f0060acfb908989869ebab63a2287773fc5e91a39f15552632b03d376c3e9835c5b4f23a5ebac8b0963b29af164d46c0a77ac928f0 + languageName: node + linkType: hard + +"@emotion/serialize@npm:^1.2.0, @emotion/serialize@npm:^1.3.0, @emotion/serialize@npm:^1.3.1": + version: 1.3.2 + resolution: "@emotion/serialize@npm:1.3.2" + dependencies: + "@emotion/hash": ^0.9.2 + "@emotion/memoize": ^0.9.0 + "@emotion/unitless": ^0.10.0 + "@emotion/utils": ^1.4.1 + csstype: ^3.0.2 + checksum: 8051bafe32459e1aecf716cdb66a22b090060806104cca89d4e664893b56878d3e9bb94a4657df9b7b3fd183700a9be72f7144c959ddcbd3cf7b330206919237 + languageName: node + linkType: hard + +"@emotion/sheet@npm:^1.4.0": + version: 1.4.0 + resolution: "@emotion/sheet@npm:1.4.0" + checksum: eeb1212e3289db8e083e72e7e401cd6d1a84deece87e9ce184f7b96b9b5dbd6f070a89057255a6ff14d9865c3ce31f27c39248a053e4cdd875540359042586b4 + languageName: node + linkType: hard + +"@emotion/styled@npm:^11.10.5": + version: 11.13.0 + resolution: "@emotion/styled@npm:11.13.0" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.12.0 + "@emotion/is-prop-valid": ^1.3.0 + "@emotion/serialize": ^1.3.0 + "@emotion/use-insertion-effect-with-fallbacks": ^1.1.0 + "@emotion/utils": ^1.4.0 + peerDependencies: + "@emotion/react": ^11.0.0-rc.0 + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: f5b951059418f57bc8ea32b238afb25965ece3314f2ffd1b14ce049ba3c066a424990dfbfabbf57bb88e044eaa80bf19f620ac988adda3d2fc483177be6da05e + languageName: node + linkType: hard + +"@emotion/unitless@npm:^0.10.0": + version: 0.10.0 + resolution: "@emotion/unitless@npm:0.10.0" + checksum: d79346df31a933e6d33518e92636afeb603ce043f3857d0a39a2ac78a09ef0be8bedff40130930cb25df1beeee12d96ee38613963886fa377c681a89970b787c + languageName: node + linkType: hard + +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.1.0": + version: 1.1.0 + resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.1.0" + peerDependencies: + react: ">=16.8.0" + checksum: 63665191773b27de66807c53b90091ef0d10d5161381f62726cfceecfe1d8c944f18594b8021805fc81575b64246fd5ab9c75d60efabec92f940c1c410530949 + languageName: node + linkType: hard + +"@emotion/utils@npm:^1.4.0, @emotion/utils@npm:^1.4.1": + version: 1.4.1 + resolution: "@emotion/utils@npm:1.4.1" + checksum: 088f6844c735981f53c84a76101cf261422301e7895cb37fea6a47e7950247ffa8ca174ca2a15d9b29a47f0fa831b432017ca7683bccbb5cfd78dda82743d856 + languageName: node + linkType: hard + +"@emotion/weak-memoize@npm:^0.4.0": + version: 0.4.0 + resolution: "@emotion/weak-memoize@npm:0.4.0" + checksum: db5da0e89bd752c78b6bd65a1e56231f0abebe2f71c0bd8fc47dff96408f7065b02e214080f99924f6a3bfe7ee15afc48dad999d76df86b39b16e513f7a94f52 + languageName: node + linkType: hard + +"@esbuild/aix-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/aix-ppc64@npm:0.21.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/aix-ppc64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/aix-ppc64@npm:0.24.0" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm64@npm:0.21.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/android-arm64@npm:0.24.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm@npm:0.21.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/android-arm@npm:0.24.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-x64@npm:0.21.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/android-x64@npm:0.24.0" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-arm64@npm:0.21.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/darwin-arm64@npm:0.24.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-x64@npm:0.21.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/darwin-x64@npm:0.24.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-arm64@npm:0.21.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/freebsd-arm64@npm:0.24.0" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-x64@npm:0.21.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/freebsd-x64@npm:0.24.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm64@npm:0.21.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/linux-arm64@npm:0.24.0" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm@npm:0.21.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/linux-arm@npm:0.24.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ia32@npm:0.21.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/linux-ia32@npm:0.24.0" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-loong64@npm:0.21.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/linux-loong64@npm:0.24.0" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-mips64el@npm:0.21.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/linux-mips64el@npm:0.24.0" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ppc64@npm:0.21.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/linux-ppc64@npm:0.24.0" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-riscv64@npm:0.21.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/linux-riscv64@npm:0.24.0" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-s390x@npm:0.21.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/linux-s390x@npm:0.24.0" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-x64@npm:0.21.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/linux-x64@npm:0.24.0" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/netbsd-x64@npm:0.21.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/netbsd-x64@npm:0.24.0" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/openbsd-arm64@npm:0.24.0" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/openbsd-x64@npm:0.21.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/openbsd-x64@npm:0.24.0" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/sunos-x64@npm:0.21.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/sunos-x64@npm:0.24.0" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-arm64@npm:0.21.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/win32-arm64@npm:0.24.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-ia32@npm:0.21.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/win32-ia32@npm:0.24.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-x64@npm:0.21.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.24.0": + version: 0.24.0 + resolution: "@esbuild/win32-x64@npm:0.24.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": + version: 4.4.0 + resolution: "@eslint-community/eslint-utils@npm:4.4.0" + dependencies: + eslint-visitor-keys: ^3.3.0 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: cdfe3ae42b4f572cbfb46d20edafe6f36fc5fb52bf2d90875c58aefe226892b9677fef60820e2832caf864a326fe4fc225714c46e8389ccca04d5f9288aabd22 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.5.1, @eslint-community/regexpp@npm:^4.6.1": + version: 4.11.1 + resolution: "@eslint-community/regexpp@npm:4.11.1" + checksum: 6986685529d30e33c2640973c3d8e7ddd31bef3cc8cb10ad54ddc1dea12680779a2c23a45562aa1462c488137a3570e672d122fac7da22d82294382d915cec70 + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/eslintrc@npm:2.1.4" + dependencies: + ajv: ^6.12.4 + debug: ^4.3.2 + espree: ^9.6.0 + globals: ^13.19.0 + ignore: ^5.2.0 + import-fresh: ^3.2.1 + js-yaml: ^4.1.0 + minimatch: ^3.1.2 + strip-json-comments: ^3.1.1 + checksum: 10957c7592b20ca0089262d8c2a8accbad14b4f6507e35416c32ee6b4dbf9cad67dfb77096bbd405405e9ada2b107f3797fe94362e1c55e0b09d6e90dd149127 + languageName: node + linkType: hard + +"@eslint/js@npm:8.57.1": + version: 8.57.1 + resolution: "@eslint/js@npm:8.57.1" + checksum: 2afb77454c06e8316793d2e8e79a0154854d35e6782a1217da274ca60b5044d2c69d6091155234ed0551a1e408f86f09dd4ece02752c59568fa403e60611e880 + languageName: node + linkType: hard + +"@fastify/busboy@npm:^2.0.0": + version: 2.1.1 + resolution: "@fastify/busboy@npm:2.1.1" + checksum: 42c32ef75e906c9a4809c1e1930a5ca6d4ddc8d138e1a8c8ba5ea07f997db32210617d23b2e4a85fe376316a41a1a0439fc6ff2dedf5126d96f45a9d80754fb2 + languageName: node + linkType: hard + +"@gar/promisify@npm:^1.1.3": + version: 1.1.3 + resolution: "@gar/promisify@npm:1.1.3" + checksum: 4059f790e2d07bf3c3ff3e0fec0daa8144fe35c1f6e0111c9921bd32106adaa97a4ab096ad7dab1e28ee6a9060083c4d1a4ada42a7f5f3f7a96b8812e2b757c1 + languageName: node + linkType: hard + +"@google-cloud/paginator@npm:^5.0.0": + version: 5.0.2 + resolution: "@google-cloud/paginator@npm:5.0.2" + dependencies: + arrify: ^2.0.0 + extend: ^3.0.2 + checksum: eeb4a387807270ba9f69f22d7439d60c5bd6663573c2da9ea7d998c373d77671d77450b87f0f229c28418df654af4064e70554fa4dcde7edb3c0f5c05f208246 + languageName: node + linkType: hard + +"@google-cloud/projectify@npm:^4.0.0": + version: 4.0.0 + resolution: "@google-cloud/projectify@npm:4.0.0" + checksum: 973d28414ae200433333a3c315aebb881ced42ea4afe6f3f8520d2fecded75e76c913f5189fea8fb29ce6ca36117c4f44001b3c503eecdd3ac7f02597a98354a + languageName: node + linkType: hard + +"@google-cloud/promisify@npm:^4.0.0": + version: 4.0.0 + resolution: "@google-cloud/promisify@npm:4.0.0" + checksum: edd189398c5ed5b7b64a373177d77c87d076a248c31b8ae878bb91e2411d89860108bcb948c349f32628973a823bd131beb53ec008fd613a8cb466ef1d89de49 + languageName: node + linkType: hard + +"@google-cloud/storage@npm:^7.0.0": + version: 7.13.0 + resolution: "@google-cloud/storage@npm:7.13.0" + dependencies: + "@google-cloud/paginator": ^5.0.0 + "@google-cloud/projectify": ^4.0.0 + "@google-cloud/promisify": ^4.0.0 + abort-controller: ^3.0.0 + async-retry: ^1.3.3 + duplexify: ^4.1.3 + fast-xml-parser: ^4.4.1 + gaxios: ^6.0.2 + google-auth-library: ^9.6.3 + html-entities: ^2.5.2 + mime: ^3.0.0 + p-limit: ^3.0.1 + retry-request: ^7.0.0 + teeny-request: ^9.0.0 + uuid: ^8.0.0 + checksum: b5e61b3123f2924aae17f3b1e9aa97092e999f2097c00d90d85329212219cd6b6a63a65fd84d228791b534e4747e96d430007c4a507b37f3e1d6e42a98d4e7e2 + languageName: node + linkType: hard + +"@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0": + version: 9.3.0 + resolution: "@hapi/hoek@npm:9.3.0" + checksum: 4771c7a776242c3c022b168046af4e324d116a9d2e1d60631ee64f474c6e38d1bb07092d898bf95c7bc5d334c5582798a1456321b2e53ca817d4e7c88bc25b43 + languageName: node + linkType: hard + +"@hapi/topo@npm:^5.1.0": + version: 5.1.0 + resolution: "@hapi/topo@npm:5.1.0" + dependencies: + "@hapi/hoek": ^9.0.0 + checksum: 604dfd5dde76d5c334bd03f9001fce69c7ce529883acf92da96f4fe7e51221bf5e5110e964caca287a6a616ba027c071748ab636ff178ad750547fba611d6014 + languageName: node + linkType: hard + +"@humanwhocodes/config-array@npm:^0.13.0": + version: 0.13.0 + resolution: "@humanwhocodes/config-array@npm:0.13.0" + dependencies: + "@humanwhocodes/object-schema": ^2.0.3 + debug: ^4.3.1 + minimatch: ^3.0.5 + checksum: eae69ff9134025dd2924f0b430eb324981494be26f0fddd267a33c28711c4db643242cf9fddf7dadb9d16c96b54b2d2c073e60a56477df86e0173149313bd5d6 + languageName: node + linkType: hard + +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 0fd22007db8034a2cdf2c764b140d37d9020bbfce8a49d3ec5c05290e77d4b0263b1b972b752df8c89e5eaa94073408f2b7d977aed131faf6cf396ebb5d7fb61 + languageName: node + linkType: hard + +"@humanwhocodes/object-schema@npm:^2.0.3": + version: 2.0.3 + resolution: "@humanwhocodes/object-schema@npm:2.0.3" + checksum: d3b78f6c5831888c6ecc899df0d03bcc25d46f3ad26a11d7ea52944dc36a35ef543fad965322174238d677a43d5c694434f6607532cff7077062513ad7022631 + languageName: node + linkType: hard + +"@ianvs/prettier-plugin-sort-imports@npm:^4.3.1": + version: 4.3.1 + resolution: "@ianvs/prettier-plugin-sort-imports@npm:4.3.1" + dependencies: + "@babel/core": ^7.24.0 + "@babel/generator": ^7.23.6 + "@babel/parser": ^7.24.0 + "@babel/traverse": ^7.24.0 + "@babel/types": ^7.24.0 + semver: ^7.5.2 + peerDependencies: + "@vue/compiler-sfc": 2.7.x || 3.x + prettier: 2 || 3 + peerDependenciesMeta: + "@vue/compiler-sfc": + optional: true + checksum: 50af027d8b182893f247efa4c197259919c4796bea216b2a2323d0ced327c9f22785d7f8866b084566491fe7943640bb75457a724957bda92ee4c633fb2be9e6 + languageName: node + linkType: hard + +"@internal/rbac@workspace:.": + version: 0.0.0-use.local + resolution: "@internal/rbac@workspace:." + dependencies: + "@backstage/cli": ^0.28.0 + "@backstage/e2e-test-utils": ^0.1.1 + "@backstage/repo-tools": ^0.10.0 + "@changesets/cli": ^2.27.1 + "@ianvs/prettier-plugin-sort-imports": ^4.3.1 + "@spotify/prettier-config": ^12.0.0 + knip: ^5.27.4 + node-gyp: ^9.0.0 + prettier: ^2.3.2 + typescript: ~5.3.0 + languageName: unknown + linkType: soft + +"@ioredis/commands@npm:^1.1.1": + version: 1.2.0 + resolution: "@ioredis/commands@npm:1.2.0" + checksum: 9b20225ba36ef3e5caf69b3c0720597c3016cc9b1e157f519ea388f621dd9037177f84cfe7e25c4c32dad7dd90c70ff9123cd411f747e053cf292193c9c461e2 + languageName: node + linkType: hard + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: ^5.1.2 + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: ^7.0.1 + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: ^8.1.0 + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb + languageName: node + linkType: hard + +"@isaacs/fs-minipass@npm:^4.0.0": + version: 4.0.1 + resolution: "@isaacs/fs-minipass@npm:4.0.1" + dependencies: + minipass: ^7.0.4 + checksum: 5d36d289960e886484362d9eb6a51d1ea28baed5f5d0140bbe62b99bac52eaf06cc01c2bc0d3575977962f84f6b2c4387b043ee632216643d4787b0999465bf2 + languageName: node + linkType: hard + +"@istanbuljs/load-nyc-config@npm:^1.0.0": + version: 1.1.0 + resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" + dependencies: + camelcase: ^5.3.1 + find-up: ^4.1.0 + get-package-type: ^0.1.0 + js-yaml: ^3.13.1 + resolve-from: ^5.0.0 + checksum: d578da5e2e804d5c93228450a1380e1a3c691de4953acc162f387b717258512a3e07b83510a936d9fab03eac90817473917e24f5d16297af3867f59328d58568 + languageName: node + linkType: hard + +"@istanbuljs/schema@npm:^0.1.2, @istanbuljs/schema@npm:^0.1.3": + version: 0.1.3 + resolution: "@istanbuljs/schema@npm:0.1.3" + checksum: 5282759d961d61350f33d9118d16bcaed914ebf8061a52f4fa474b2cb08720c9c81d165e13b82f2e5a8a212cc5af482f0c6fc1ac27b9e067e5394c9a6ed186c9 + languageName: node + linkType: hard + +"@janus-idp/backstage-plugin-audit-log-node@npm:^1.7.0": + version: 1.7.0 + resolution: "@janus-idp/backstage-plugin-audit-log-node@npm:1.7.0" + dependencies: + lodash: ^4.17.21 + checksum: 22557f15cffc7a22bca592d9b0efaf917ac3cb8a2693957407ccad1f160060d349dc81fbc5f823c1b9e26cf2357d7566a005ac00d5b31b94b9ef952b13b301d0 + languageName: node + linkType: hard + +"@janus-idp/shared-react@npm:^2.13.0": + version: 2.13.0 + resolution: "@janus-idp/shared-react@npm:2.13.0" + dependencies: + "@backstage/catalog-model": ^1.7.0 + "@backstage/core-components": ^0.15.1 + "@backstage/core-plugin-api": ^1.10.0 + "@backstage/plugin-kubernetes-common": ^0.8.3 + "@backstage/plugin-kubernetes-react": ^0.4.4 + "@kubernetes/client-node": ^0.22.1 + "@material-ui/core": ^4.12.4 + "@mui/icons-material": 5.15.17 + classnames: ^2.3.2 + date-fns: ^2.30.0 + file-saver: ^2.0.5 + lodash: ^4.17.21 + mathjs: ^11.11.2 + react-use: ^17.5.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + checksum: 646bf1013f61926481b6c1457186d7b2f54df1f97c8249e40c66051d6ac81c360bfe1e7f4b3fb80596ee0f3e7a712724e564c96e3079e18fcb9e98a4f5bf1983 + languageName: node + linkType: hard + +"@jest/console@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/console@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + jest-message-util: ^29.7.0 + jest-util: ^29.7.0 + slash: ^3.0.0 + checksum: 0e3624e32c5a8e7361e889db70b170876401b7d70f509a2538c31d5cd50deb0c1ae4b92dc63fe18a0902e0a48c590c21d53787a0df41a52b34fa7cab96c384d6 + languageName: node + linkType: hard + +"@jest/core@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/core@npm:29.7.0" + dependencies: + "@jest/console": ^29.7.0 + "@jest/reporters": ^29.7.0 + "@jest/test-result": ^29.7.0 + "@jest/transform": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + ansi-escapes: ^4.2.1 + chalk: ^4.0.0 + ci-info: ^3.2.0 + exit: ^0.1.2 + graceful-fs: ^4.2.9 + jest-changed-files: ^29.7.0 + jest-config: ^29.7.0 + jest-haste-map: ^29.7.0 + jest-message-util: ^29.7.0 + jest-regex-util: ^29.6.3 + jest-resolve: ^29.7.0 + jest-resolve-dependencies: ^29.7.0 + jest-runner: ^29.7.0 + jest-runtime: ^29.7.0 + jest-snapshot: ^29.7.0 + jest-util: ^29.7.0 + jest-validate: ^29.7.0 + jest-watcher: ^29.7.0 + micromatch: ^4.0.4 + pretty-format: ^29.7.0 + slash: ^3.0.0 + strip-ansi: ^6.0.0 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: af759c9781cfc914553320446ce4e47775ae42779e73621c438feb1e4231a5d4862f84b1d8565926f2d1aab29b3ec3dcfdc84db28608bdf5f29867124ebcfc0d + languageName: node + linkType: hard + +"@jest/create-cache-key-function@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/create-cache-key-function@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + checksum: 681bc761fa1d6fa3dd77578d444f97f28296ea80755e90e46d1c8fa68661b9e67f54dd38b988742db636d26cf160450dc6011892cec98b3a7ceb58cad8ff3aae + languageName: node + linkType: hard + +"@jest/environment@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/environment@npm:29.7.0" + dependencies: + "@jest/fake-timers": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + jest-mock: ^29.7.0 + checksum: 6fb398143b2543d4b9b8d1c6dbce83fa5247f84f550330604be744e24c2bd2178bb893657d62d1b97cf2f24baf85c450223f8237cccb71192c36a38ea2272934 + languageName: node + linkType: hard + +"@jest/expect-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect-utils@npm:29.7.0" + dependencies: + jest-get-type: ^29.6.3 + checksum: 75eb177f3d00b6331bcaa057e07c0ccb0733a1d0a1943e1d8db346779039cb7f103789f16e502f888a3096fb58c2300c38d1f3748b36a7fa762eb6f6d1b160ed + languageName: node + linkType: hard + +"@jest/expect@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/expect@npm:29.7.0" + dependencies: + expect: ^29.7.0 + jest-snapshot: ^29.7.0 + checksum: a01cb85fd9401bab3370618f4b9013b90c93536562222d920e702a0b575d239d74cecfe98010aaec7ad464f67cf534a353d92d181646a4b792acaa7e912ae55e + languageName: node + linkType: hard + +"@jest/fake-timers@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/fake-timers@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + "@sinonjs/fake-timers": ^10.0.2 + "@types/node": "*" + jest-message-util: ^29.7.0 + jest-mock: ^29.7.0 + jest-util: ^29.7.0 + checksum: caf2bbd11f71c9241b458d1b5a66cbe95debc5a15d96442444b5d5c7ba774f523c76627c6931cca5e10e76f0d08761f6f1f01a608898f4751a0eee54fc3d8d00 + languageName: node + linkType: hard + +"@jest/globals@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/globals@npm:29.7.0" + dependencies: + "@jest/environment": ^29.7.0 + "@jest/expect": ^29.7.0 + "@jest/types": ^29.6.3 + jest-mock: ^29.7.0 + checksum: 97dbb9459135693ad3a422e65ca1c250f03d82b2a77f6207e7fa0edd2c9d2015fbe4346f3dc9ebff1678b9d8da74754d4d440b7837497f8927059c0642a22123 + languageName: node + linkType: hard + +"@jest/reporters@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/reporters@npm:29.7.0" + dependencies: + "@bcoe/v8-coverage": ^0.2.3 + "@jest/console": ^29.7.0 + "@jest/test-result": ^29.7.0 + "@jest/transform": ^29.7.0 + "@jest/types": ^29.6.3 + "@jridgewell/trace-mapping": ^0.3.18 + "@types/node": "*" + chalk: ^4.0.0 + collect-v8-coverage: ^1.0.0 + exit: ^0.1.2 + glob: ^7.1.3 + graceful-fs: ^4.2.9 + istanbul-lib-coverage: ^3.0.0 + istanbul-lib-instrument: ^6.0.0 + istanbul-lib-report: ^3.0.0 + istanbul-lib-source-maps: ^4.0.0 + istanbul-reports: ^3.1.3 + jest-message-util: ^29.7.0 + jest-util: ^29.7.0 + jest-worker: ^29.7.0 + slash: ^3.0.0 + string-length: ^4.0.1 + strip-ansi: ^6.0.0 + v8-to-istanbul: ^9.0.1 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 7eadabd62cc344f629024b8a268ecc8367dba756152b761bdcb7b7e570a3864fc51b2a9810cd310d85e0a0173ef002ba4528d5ea0329fbf66ee2a3ada9c40455 + languageName: node + linkType: hard + +"@jest/schemas@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/schemas@npm:29.6.3" + dependencies: + "@sinclair/typebox": ^0.27.8 + checksum: 910040425f0fc93cd13e68c750b7885590b8839066dfa0cd78e7def07bbb708ad869381f725945d66f2284de5663bbecf63e8fdd856e2ae6e261ba30b1687e93 + languageName: node + linkType: hard + +"@jest/source-map@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/source-map@npm:29.6.3" + dependencies: + "@jridgewell/trace-mapping": ^0.3.18 + callsites: ^3.0.0 + graceful-fs: ^4.2.9 + checksum: bcc5a8697d471396c0003b0bfa09722c3cd879ad697eb9c431e6164e2ea7008238a01a07193dfe3cbb48b1d258eb7251f6efcea36f64e1ebc464ea3c03ae2deb + languageName: node + linkType: hard + +"@jest/test-result@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-result@npm:29.7.0" + dependencies: + "@jest/console": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/istanbul-lib-coverage": ^2.0.0 + collect-v8-coverage: ^1.0.0 + checksum: 67b6317d526e335212e5da0e768e3b8ab8a53df110361b80761353ad23b6aea4432b7c5665bdeb87658ea373b90fb1afe02ed3611ef6c858c7fba377505057fa + languageName: node + linkType: hard + +"@jest/test-sequencer@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/test-sequencer@npm:29.7.0" + dependencies: + "@jest/test-result": ^29.7.0 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.7.0 + slash: ^3.0.0 + checksum: 73f43599017946be85c0b6357993b038f875b796e2f0950487a82f4ebcb115fa12131932dd9904026b4ad8be131fe6e28bd8d0aa93b1563705185f9804bff8bd + languageName: node + linkType: hard + +"@jest/transform@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/transform@npm:29.7.0" + dependencies: + "@babel/core": ^7.11.6 + "@jest/types": ^29.6.3 + "@jridgewell/trace-mapping": ^0.3.18 + babel-plugin-istanbul: ^6.1.1 + chalk: ^4.0.0 + convert-source-map: ^2.0.0 + fast-json-stable-stringify: ^2.1.0 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.7.0 + jest-regex-util: ^29.6.3 + jest-util: ^29.7.0 + micromatch: ^4.0.4 + pirates: ^4.0.4 + slash: ^3.0.0 + write-file-atomic: ^4.0.2 + checksum: 0f8ac9f413903b3cb6d240102db848f2a354f63971ab885833799a9964999dd51c388162106a807f810071f864302cdd8e3f0c241c29ce02d85a36f18f3f40ab + languageName: node + linkType: hard + +"@jest/types@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/types@npm:29.6.3" + dependencies: + "@jest/schemas": ^29.6.3 + "@types/istanbul-lib-coverage": ^2.0.0 + "@types/istanbul-reports": ^3.0.0 + "@types/node": "*" + "@types/yargs": ^17.0.8 + chalk: ^4.0.0 + checksum: a0bcf15dbb0eca6bdd8ce61a3fb055349d40268622a7670a3b2eb3c3dbafe9eb26af59938366d520b86907b9505b0f9b29b85cec11579a9e580694b87cd90fcc + languageName: node + linkType: hard + +"@jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.5 + resolution: "@jridgewell/gen-mapping@npm:0.3.5" + dependencies: + "@jridgewell/set-array": ^1.2.1 + "@jridgewell/sourcemap-codec": ^1.4.10 + "@jridgewell/trace-mapping": ^0.3.24 + checksum: ff7a1764ebd76a5e129c8890aa3e2f46045109dabde62b0b6c6a250152227647178ff2069ea234753a690d8f3c4ac8b5e7b267bbee272bffb7f3b0a370ab6e52 + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": + version: 3.1.2 + resolution: "@jridgewell/resolve-uri@npm:3.1.2" + checksum: 83b85f72c59d1c080b4cbec0fef84528963a1b5db34e4370fa4bd1e3ff64a0d80e0cee7369d11d73c704e0286fb2865b530acac7a871088fbe92b5edf1000870 + languageName: node + linkType: hard + +"@jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 + languageName: node + linkType: hard + +"@jridgewell/source-map@npm:^0.3.3": + version: 0.3.6 + resolution: "@jridgewell/source-map@npm:0.3.6" + dependencies: + "@jridgewell/gen-mapping": ^0.3.5 + "@jridgewell/trace-mapping": ^0.3.25 + checksum: c9dc7d899397df95e3c9ec287b93c0b56f8e4453cd20743e2b9c8e779b1949bc3cccf6c01bb302779e46560eb45f62ea38d19fedd25370d814734268450a9f30 + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0": + version: 1.5.0 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" + checksum: 05df4f2538b3b0f998ea4c1cd34574d0feba216fa5d4ccaef0187d12abf82eafe6021cec8b49f9bb4d90f2ba4582ccc581e72986a5fcf4176ae0cfeb04cf52ec + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9" + dependencies: + "@jridgewell/resolve-uri": ^3.0.3 + "@jridgewell/sourcemap-codec": ^1.4.10 + checksum: d89597752fd88d3f3480845691a05a44bd21faac18e2185b6f436c3b0fd0c5a859fbbd9aaa92050c4052caf325ad3e10e2e1d1b64327517471b7d51babc0ddef + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" + dependencies: + "@jridgewell/resolve-uri": ^3.1.0 + "@jridgewell/sourcemap-codec": ^1.4.14 + checksum: 9d3c40d225e139987b50c48988f8717a54a8c994d8a948ee42e1412e08988761d0754d7d10b803061cc3aebf35f92a5dbbab493bd0e1a9ef9e89a2130e83ba34 + languageName: node + linkType: hard + +"@jsdevtools/ono@npm:^7.1.3": + version: 7.1.3 + resolution: "@jsdevtools/ono@npm:7.1.3" + checksum: 2297fcd472ba810bffe8519d2249171132844c7174f3a16634f9260761c8c78bc0428a4190b5b6d72d45673c13918ab9844d706c3ed4ef8f62ab11a2627a08ad + languageName: node + linkType: hard + +"@jsep-plugin/assignment@npm:^1.2.1": + version: 1.2.1 + resolution: "@jsep-plugin/assignment@npm:1.2.1" + peerDependencies: + jsep: ^0.4.0||^1.0.0 + checksum: d56fd7423c59dd269c50b0a9c22ec05f099a789ec8e8980f2307782f496ab3f0740151f1bdc7a1f3a8ee9085cdeb6f5b4def0d6b312e6b93ab160e6489b400f2 + languageName: node + linkType: hard + +"@jsep-plugin/regex@npm:^1.0.1, @jsep-plugin/regex@npm:^1.0.3": + version: 1.0.3 + resolution: "@jsep-plugin/regex@npm:1.0.3" + peerDependencies: + jsep: ^0.4.0||^1.0.0 + checksum: a57718ae5c86bd10ff5de51843a771b96a10a9c6b5c5f4e02aa5318257c3d5fdec96f8b389fcbe129c7a6ad6b0746d9a0fd934c949b80882230fbc14b548c922 + languageName: node + linkType: hard + +"@jsep-plugin/ternary@npm:^1.0.2": + version: 1.1.3 + resolution: "@jsep-plugin/ternary@npm:1.1.3" + peerDependencies: + jsep: ^0.4.0||^1.0.0 + checksum: c05408b0302844723f98b90787425beb4e8ad14029df3d98e88b9d61343d81201a7f0bf3db5806dcf0378c7be69f5b4c9fcd04f055bda282c73f4d1b425e502a + languageName: node + linkType: hard + +"@jsonjoy.com/base64@npm:^1.1.1": + version: 1.1.2 + resolution: "@jsonjoy.com/base64@npm:1.1.2" + peerDependencies: + tslib: 2 + checksum: 00dbf9cbc6ecb3af0e58288a305cc4ee3dfca9efa24443d98061756e8f6de4d6d2d3764bdfde07f2b03e6ce56db27c8a59b490bd134bf3d8122b4c6b394c7010 + languageName: node + linkType: hard + +"@jsonjoy.com/json-pack@npm:^1.0.3": + version: 1.1.0 + resolution: "@jsonjoy.com/json-pack@npm:1.1.0" + dependencies: + "@jsonjoy.com/base64": ^1.1.1 + "@jsonjoy.com/util": ^1.1.2 + hyperdyperid: ^1.2.0 + thingies: ^1.20.0 + peerDependencies: + tslib: 2 + checksum: 5c89a01814d5a7464639c3cbd4dbbcbf19165e9e6d6cc3cc985f8a7594fc2c5ac3a29e4f49f9ddf029979ec26ab980960a250db044173798509d0ea388c2ae26 + languageName: node + linkType: hard + +"@jsonjoy.com/util@npm:^1.1.2, @jsonjoy.com/util@npm:^1.3.0": + version: 1.5.0 + resolution: "@jsonjoy.com/util@npm:1.5.0" + peerDependencies: + tslib: 2 + checksum: 62892928e1223798e3d910be8dde4fdceaddf2ebdd4bdc0c50495b8ee33503317adf7b5118cd8f5a63045e3f232d70e95fb0279828caf1ec392ffeeb7ea129b8 + languageName: node + linkType: hard + +"@keyv/memcache@npm:^1.3.5": + version: 1.4.1 + resolution: "@keyv/memcache@npm:1.4.1" + dependencies: + json-buffer: ^3.0.1 + memjs: ^1.3.2 + checksum: bee66686af965aa3bdd78ccd7c67658b424d32578936e894d3aa42ff616ef653f8ecc439f4ea28fc51ed04a68502e445fc8ff836bd142b38509787712b6ec04d + languageName: node + linkType: hard + +"@keyv/redis@npm:^2.5.3": + version: 2.8.5 + resolution: "@keyv/redis@npm:2.8.5" + dependencies: + ioredis: ^5.4.1 + checksum: 87ffec61d31fa9de128ba3e5a7b616535ddbdaa4d92cbc9e1a9fab143adf967135e9cca16e192e8f52cc1ba00ed2a7f10eca9944d7550385530dab95333e81ef + languageName: node + linkType: hard + +"@keyv/serialize@npm:*": + version: 1.0.1 + resolution: "@keyv/serialize@npm:1.0.1" + dependencies: + buffer: ^6.0.3 + checksum: ff3dd9a6246b17fca3d1b0aba312dea931059fdecc36027f4d8133e59dbb3554a0a516b1f3dfc7fb2b3ca7a3d6fa307804f299566ab214febd3fb9d0502eebed + languageName: node + linkType: hard + +"@kubernetes-models/apimachinery@npm:^2.0.0": + version: 2.0.0 + resolution: "@kubernetes-models/apimachinery@npm:2.0.0" + dependencies: + "@kubernetes-models/base": ^5.0.0 + "@kubernetes-models/validate": ^4.0.0 + "@swc/helpers": ^0.5.8 + checksum: 0e9ed8f05166221e2ccfe21a45a3aa480ab31c8e8b97b2dbb22d1d3c60477fb0b093def5d78727ba1037df4f14a9c7f38a7da0432c769fdc606097b4b6018dd6 + languageName: node + linkType: hard + +"@kubernetes-models/base@npm:^5.0.0": + version: 5.0.0 + resolution: "@kubernetes-models/base@npm:5.0.0" + dependencies: + "@kubernetes-models/validate": ^4.0.0 + is-plain-object: ^5.0.0 + tslib: ^2.4.0 + checksum: 3ac064e6c18d88f356b8e91d320e08ee31a12a3ecd47d6d86e23566203125640b88223bc9003d02d420b6a9a86f2990f65e4a26c32ed075ef0753d8599549c0b + languageName: node + linkType: hard + +"@kubernetes-models/validate@npm:^4.0.0": + version: 4.0.0 + resolution: "@kubernetes-models/validate@npm:4.0.0" + dependencies: + ajv: ^8.12.0 + ajv-formats: ^2.1.1 + ajv-formats-draft2019: ^1.6.1 + ajv-i18n: ^4.2.0 + is-cidr: ^4.0.0 + re2-wasm: ^1.0.2 + tslib: ^2.4.0 + dependenciesMeta: + re2-wasm: + optional: true + checksum: 983c873a674b16714b88c444fcfeba555a3349036b263e1e41441e80d58be8b783fee8ce8217b2e5a8fda6c9893ea377f9635d6a37d85c412b327c1bbad4fdef + languageName: node + linkType: hard + +"@kubernetes/client-node@npm:0.20.0, @kubernetes/client-node@npm:^0.20.0": + version: 0.20.0 + resolution: "@kubernetes/client-node@npm:0.20.0" + dependencies: + "@types/js-yaml": ^4.0.1 + "@types/node": ^20.1.1 + "@types/request": ^2.47.1 + "@types/ws": ^8.5.3 + byline: ^5.0.0 + isomorphic-ws: ^5.0.0 + js-yaml: ^4.1.0 + jsonpath-plus: ^7.2.0 + openid-client: ^5.3.0 + request: ^2.88.0 + rfc4648: ^1.3.0 + stream-buffers: ^3.0.2 + tar: ^6.1.11 + tslib: ^2.4.1 + ws: ^8.11.0 + dependenciesMeta: + openid-client: + optional: true + checksum: c7c2ec9c597b5579ec452bcc13647feeaa3eaf93601afa5d9a4e06b5fe91d2cafa444a1da07b5330a7596f0e07e107d6abe4acabc5998f7bedf43cd0ab8bf343 + languageName: node + linkType: hard + +"@kubernetes/client-node@npm:^0.22.1": + version: 0.22.1 + resolution: "@kubernetes/client-node@npm:0.22.1" + dependencies: + "@types/js-yaml": ^4.0.1 + "@types/node": ^22.0.0 + "@types/request": ^2.47.1 + "@types/ws": ^8.5.3 + byline: ^5.0.0 + isomorphic-ws: ^5.0.0 + js-yaml: ^4.1.0 + jsonpath-plus: ^10.0.0 + openid-client: ^5.3.0 + request: ^2.88.0 + rfc4648: ^1.3.0 + stream-buffers: ^3.0.2 + tar: ^7.0.0 + tslib: ^2.4.1 + ws: ^8.18.0 + dependenciesMeta: + openid-client: + optional: true + checksum: 501377ad70681df9e30885cf18e40f9b16fd452bc50d9a46688a6f667a2a7f490238269e528b1f1804a6eaf4347f8edda57e5129b1a28a52915fe7898ea84329 + languageName: node + linkType: hard + +"@leichtgewicht/ip-codec@npm:^2.0.1": + version: 2.0.5 + resolution: "@leichtgewicht/ip-codec@npm:2.0.5" + checksum: 4fcd025d0a923cb6b87b631a83436a693b255779c583158bbeacde6b4dd75b94cc1eba1c9c188de5fc36c218d160524ea08bfe4ef03a056b00ff14126d66f881 + languageName: node + linkType: hard + +"@lukeed/csprng@npm:^1.0.0": + version: 1.1.0 + resolution: "@lukeed/csprng@npm:1.1.0" + checksum: 926f5f7fc629470ca9a8af355bfcd0271d34535f7be3890f69902432bddc3262029bb5dbe9025542cf6c9883d878692eef2815fc2f3ba5b92e9da1f9eba2e51b + languageName: node + linkType: hard + +"@manypkg/find-root@npm:^1.1.0": + version: 1.1.0 + resolution: "@manypkg/find-root@npm:1.1.0" + dependencies: + "@babel/runtime": ^7.5.5 + "@types/node": ^12.7.1 + find-up: ^4.1.0 + fs-extra: ^8.1.0 + checksum: f0fd881a5a81a351cb6561cd24117e8ee9481bbf3b6d1c7d9d10bef1f4744ca2ba3d064713e83c0a0574416d1e5b4a4c6c414aad91913c4a1c6040d87283ac50 + languageName: node + linkType: hard + +"@manypkg/get-packages@npm:^1.1.3": + version: 1.1.3 + resolution: "@manypkg/get-packages@npm:1.1.3" + dependencies: + "@babel/runtime": ^7.5.5 + "@changesets/types": ^4.0.1 + "@manypkg/find-root": ^1.1.0 + fs-extra: ^8.1.0 + globby: ^11.0.0 + read-yaml-file: ^1.1.0 + checksum: f5a756e5a659e0e1c33f48852d56826d170d5b10a3cdea89ce4fcaa77678d8799aa4004b30e1985c87b73dbc390b95bb6411b78336dd1e0db87c08c74b5c0e74 + languageName: node + linkType: hard + +"@mapbox/node-pre-gyp@npm:^1.0.0": + version: 1.0.11 + resolution: "@mapbox/node-pre-gyp@npm:1.0.11" + dependencies: + detect-libc: ^2.0.0 + https-proxy-agent: ^5.0.0 + make-dir: ^3.1.0 + node-fetch: ^2.6.7 + nopt: ^5.0.0 + npmlog: ^5.0.1 + rimraf: ^3.0.2 + semver: ^7.3.5 + tar: ^6.1.11 + bin: + node-pre-gyp: bin/node-pre-gyp + checksum: b848f6abc531a11961d780db813cc510ca5a5b6bf3184d72134089c6875a91c44d571ba6c1879470020803f7803609e7b2e6e429651c026fe202facd11d444b8 + languageName: node + linkType: hard + +"@material-table/core@npm:^3.1.0": + version: 3.2.5 + resolution: "@material-table/core@npm:3.2.5" + dependencies: + "@babel/runtime": ^7.12.5 + "@date-io/date-fns": ^1.3.13 + "@material-ui/pickers": ^3.2.10 + "@material-ui/styles": ^4.11.4 + classnames: ^2.2.6 + date-fns: ^2.16.1 + debounce: ^1.2.0 + fast-deep-equal: ^3.1.3 + prop-types: ^15.7.2 + react-beautiful-dnd: ^13.0.0 + react-double-scrollbar: 0.0.15 + uuid: ^3.4.0 + peerDependencies: + "@date-io/core": ^1.3.13 + "@material-ui/core": ^4.11.2 + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 707e85cfcb8c1cfc8eb78ea6991509879f774081d7a54ad428f702fe00478b6d3707d0fd85f4ad443ebcfac0c0cab79c046c4d5083adcbc767615445667b50cf + languageName: node + linkType: hard + +"@material-ui/core@npm:^4.12.2, @material-ui/core@npm:^4.12.4, @material-ui/core@npm:^4.9.13": + version: 4.12.4 + resolution: "@material-ui/core@npm:4.12.4" + dependencies: + "@babel/runtime": ^7.4.4 + "@material-ui/styles": ^4.11.5 + "@material-ui/system": ^4.12.2 + "@material-ui/types": 5.1.0 + "@material-ui/utils": ^4.11.3 + "@types/react-transition-group": ^4.2.0 + clsx: ^1.0.4 + hoist-non-react-statics: ^3.3.2 + popper.js: 1.16.1-lts + prop-types: ^15.7.2 + react-is: ^16.8.0 || ^17.0.0 + react-transition-group: ^4.4.0 + peerDependencies: + "@types/react": ^16.8.6 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 96b48deccda87ced841b1db45bed2be6d2b6d1b4eae72cd5c9b931201cb72026330688e0fead54e715bcead40b267ea88bde781c9f1563b1a71a5c51bf187289 + languageName: node + linkType: hard + +"@material-ui/icons@npm:^4.11.3, @material-ui/icons@npm:^4.9.1": + version: 4.11.3 + resolution: "@material-ui/icons@npm:4.11.3" + dependencies: + "@babel/runtime": ^7.4.4 + peerDependencies: + "@material-ui/core": ^4.0.0 + "@types/react": ^16.8.6 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: f849a8c4fecddc112cfa94105a2c72e763ff76b9f8da74135b7bbadfd294ed6685897cbea6a2128099be0ce37843784893d8c64da6bde37d020956ab9067206c + languageName: node + linkType: hard + +"@material-ui/lab@npm:4.0.0-alpha.61, @material-ui/lab@npm:^4.0.0-alpha.45, @material-ui/lab@npm:^4.0.0-alpha.61": + version: 4.0.0-alpha.61 + resolution: "@material-ui/lab@npm:4.0.0-alpha.61" + dependencies: + "@babel/runtime": ^7.4.4 + "@material-ui/utils": ^4.11.3 + clsx: ^1.0.4 + prop-types: ^15.7.2 + react-is: ^16.8.0 || ^17.0.0 + peerDependencies: + "@material-ui/core": ^4.12.1 + "@types/react": ^16.8.6 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 8774a07d72615301e0099415580f87ea8f3d1d106f79e0b014738e302dd3e21959abf01d6c0a629e2e9afb8cb91abd8e9686c2886cddff06c27e6a8a8e063ea0 + languageName: node + linkType: hard + +"@material-ui/pickers@npm:^3.2.10": + version: 3.3.11 + resolution: "@material-ui/pickers@npm:3.3.11" + dependencies: + "@babel/runtime": ^7.6.0 + "@date-io/core": 1.x + "@types/styled-jsx": ^2.2.8 + clsx: ^1.0.2 + react-transition-group: ^4.0.0 + rifm: ^0.7.0 + peerDependencies: + "@date-io/core": ^1.3.6 + "@material-ui/core": ^4.0.0 + prop-types: ^15.6.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + checksum: c97822ae407877d1aa9ab7b14c335511d6879ca2546455ac7a3b156d70966b5678372a6d4d3470c2dced84e59857e2c1e1b2be61d26ab43f7f29806666f33064 + languageName: node + linkType: hard + +"@material-ui/styles@npm:^4.11.4, @material-ui/styles@npm:^4.11.5": + version: 4.11.5 + resolution: "@material-ui/styles@npm:4.11.5" + dependencies: + "@babel/runtime": ^7.4.4 + "@emotion/hash": ^0.8.0 + "@material-ui/types": 5.1.0 + "@material-ui/utils": ^4.11.3 + clsx: ^1.0.4 + csstype: ^2.5.2 + hoist-non-react-statics: ^3.3.2 + jss: ^10.5.1 + jss-plugin-camel-case: ^10.5.1 + jss-plugin-default-unit: ^10.5.1 + jss-plugin-global: ^10.5.1 + jss-plugin-nested: ^10.5.1 + jss-plugin-props-sort: ^10.5.1 + jss-plugin-rule-value-function: ^10.5.1 + jss-plugin-vendor-prefixer: ^10.5.1 + prop-types: ^15.7.2 + peerDependencies: + "@types/react": ^16.8.6 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: dbf3985ef57c1b7dae3fd916d5bfd61f2097afb93c9e1f64832cfcb8fc9bbf38a504c9632ed7b76eb5d235670083d9e66d35942bc976b7cd148c71d75b808e82 + languageName: node + linkType: hard + +"@material-ui/system@npm:^4.12.2": + version: 4.12.2 + resolution: "@material-ui/system@npm:4.12.2" + dependencies: + "@babel/runtime": ^7.4.4 + "@material-ui/utils": ^4.11.3 + csstype: ^2.5.2 + prop-types: ^15.7.2 + peerDependencies: + "@types/react": ^16.8.6 || ^17.0.0 + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: ebe6b3cc5f111034eacd763014f3260f7647b5e0cd132870f2ee18855cf3d51a996b4633035fe6f5f8965489944db4ac0cb3b71b84a765faa35a6861532ac9f6 + languageName: node + linkType: hard + +"@material-ui/types@npm:5.1.0": + version: 5.1.0 + resolution: "@material-ui/types@npm:5.1.0" + peerDependencies: + "@types/react": "*" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 64ac0938ee6f48011ba596f7422ab0660d9a8d9b4f5f183b39bd63185b1ce724209f65580f0af686d59b524603ffa57418ca2d443b69bec894303f80779c61f8 + languageName: node + linkType: hard + +"@material-ui/types@npm:^6.0.1": + version: 6.0.2 + resolution: "@material-ui/types@npm:6.0.2" + peerDependencies: + "@types/react": "*" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: cc1704059bc4cfc0296ead70d9bc8e58467b0699cdaba05b11b10d0119833ee635186a3acb202d11ed6c33d4872efafeed6cad23fca2b260eb5e94bd779be46f + languageName: node + linkType: hard + +"@material-ui/utils@npm:^4.11.3": + version: 4.11.3 + resolution: "@material-ui/utils@npm:4.11.3" + dependencies: + "@babel/runtime": ^7.4.4 + prop-types: ^15.7.2 + react-is: ^16.8.0 || ^17.0.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + checksum: 05ff67c982b33d3b4260cfaeaf566f3ccaecaebb231907ed626bcc30322d89d705bfe79b8805c0dda2f1dc2cfa98ca9d731ec8ae12868da7a98568a41c7dc231 + languageName: node + linkType: hard + +"@microsoft/api-documenter@npm:^7.25.7": + version: 7.25.21 + resolution: "@microsoft/api-documenter@npm:7.25.21" + dependencies: + "@microsoft/api-extractor-model": 7.29.8 + "@microsoft/tsdoc": ~0.15.0 + "@rushstack/node-core-library": 5.9.0 + "@rushstack/terminal": 0.14.2 + "@rushstack/ts-command-line": 4.23.0 + js-yaml: ~3.13.1 + resolve: ~1.22.1 + bin: + api-documenter: bin/api-documenter + checksum: a45c33f2b2f3425b8bfaea6fd6ca985c9df21b85b46531c1e028153d9553629e33f5c3b04c6dfd63873aaae4285dacfc541d10b9aa4a0756b3c440ebf70179cc + languageName: node + linkType: hard + +"@microsoft/api-extractor-model@npm:7.29.8": + version: 7.29.8 + resolution: "@microsoft/api-extractor-model@npm:7.29.8" + dependencies: + "@microsoft/tsdoc": ~0.15.0 + "@microsoft/tsdoc-config": ~0.17.0 + "@rushstack/node-core-library": 5.9.0 + checksum: 95a6b5df089d8bf44555f4565a6f0eda9323917266b2f4730b606aeb2c7f36df7c2cbcae9ca48a9198af7a33442cda8ce2c791e0f4c7c92f3bdaee6c3190b1f5 + languageName: node + linkType: hard + +"@microsoft/api-extractor@npm:^7.47.2": + version: 7.47.11 + resolution: "@microsoft/api-extractor@npm:7.47.11" + dependencies: + "@microsoft/api-extractor-model": 7.29.8 + "@microsoft/tsdoc": ~0.15.0 + "@microsoft/tsdoc-config": ~0.17.0 + "@rushstack/node-core-library": 5.9.0 + "@rushstack/rig-package": 0.5.3 + "@rushstack/terminal": 0.14.2 + "@rushstack/ts-command-line": 4.23.0 + lodash: ~4.17.15 + minimatch: ~3.0.3 + resolve: ~1.22.1 + semver: ~7.5.4 + source-map: ~0.6.1 + typescript: 5.4.2 + bin: + api-extractor: bin/api-extractor + checksum: 1ae7634c21e20fe191b5297a03b87547b03e9db4ee3439809363e554bcc7610ebd43dd71d30db5fbee573b7f84a2e1fa6ab3bdf320500a266b7c7c1ccc6049b2 + languageName: node + linkType: hard + +"@microsoft/tsdoc-config@npm:~0.17.0": + version: 0.17.0 + resolution: "@microsoft/tsdoc-config@npm:0.17.0" + dependencies: + "@microsoft/tsdoc": 0.15.0 + ajv: ~8.12.0 + jju: ~1.4.0 + resolve: ~1.22.2 + checksum: dd2de8247d0fc29608da83edf4ab73a21370f6ce10d089853303e91b135fdb1436ccec3bd1024f235dd3180dfe5dae7342989eadd03af55cf06f0e974e5fc213 + languageName: node + linkType: hard + +"@microsoft/tsdoc@npm:0.15.0, @microsoft/tsdoc@npm:~0.15.0": + version: 0.15.0 + resolution: "@microsoft/tsdoc@npm:0.15.0" + checksum: 3f693cff07b220b68563e3f86e9f94a9c8d0791a7446f76149c7d62ae5ed5cb4578bb48b9b5f9baa3dd9a9f77be81903c74654a41e0ca4ecf78936654952a8d4 + languageName: node + linkType: hard + +"@module-federation/bridge-react-webpack-plugin@npm:0.6.11": + version: 0.6.11 + resolution: "@module-federation/bridge-react-webpack-plugin@npm:0.6.11" + dependencies: + "@module-federation/sdk": 0.6.11 + "@types/semver": 7.5.8 + semver: 7.6.3 + checksum: 8797a7c52ec7baf8269c979a1d3f45dfa3e817a99a4c14602f53c2e891affbda1fb1a796480bb5b15e6b2df133f775d62e33df178dfec9d94bf0b1b28830cb09 + languageName: node + linkType: hard + +"@module-federation/data-prefetch@npm:0.6.11": + version: 0.6.11 + resolution: "@module-federation/data-prefetch@npm:0.6.11" + dependencies: + "@module-federation/runtime": 0.6.11 + "@module-federation/sdk": 0.6.11 + fs-extra: 9.1.0 + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: ca0f62c6e5103639e42ad0bcbaabc3aa4295eda90356888482bfa58bb961172939ce4b9689c7ef2ebdbebd4c886b3b11750af3da5189ad8646501ff251b08a8b + languageName: node + linkType: hard + +"@module-federation/dts-plugin@npm:0.6.11": + version: 0.6.11 + resolution: "@module-federation/dts-plugin@npm:0.6.11" + dependencies: + "@module-federation/managers": 0.6.11 + "@module-federation/sdk": 0.6.11 + "@module-federation/third-party-dts-extractor": 0.6.11 + adm-zip: ^0.5.10 + ansi-colors: ^4.1.3 + axios: ^1.7.4 + chalk: 3.0.0 + fs-extra: 9.1.0 + isomorphic-ws: 5.0.0 + koa: 2.15.3 + lodash.clonedeepwith: 4.5.0 + log4js: 6.9.1 + node-schedule: 2.1.1 + rambda: ^9.1.0 + ws: 8.18.0 + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: ">=1.0.24" + peerDependenciesMeta: + vue-tsc: + optional: true + checksum: 6d2aba51294f0e9ec84c4c4c05d6f589359157332ef2c4af580ea39079198d87ad9fdc3c4e666335a64d1580c4a7a01b439de2844f471404e2900562387ff7b0 + languageName: node + linkType: hard + +"@module-federation/enhanced@npm:^0.6.0": + version: 0.6.11 + resolution: "@module-federation/enhanced@npm:0.6.11" + dependencies: + "@module-federation/bridge-react-webpack-plugin": 0.6.11 + "@module-federation/data-prefetch": 0.6.11 + "@module-federation/dts-plugin": 0.6.11 + "@module-federation/managers": 0.6.11 + "@module-federation/manifest": 0.6.11 + "@module-federation/rspack": 0.6.11 + "@module-federation/runtime-tools": 0.6.11 + "@module-federation/sdk": 0.6.11 + btoa: ^1.2.1 + upath: 2.0.1 + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: ">=1.0.24" + webpack: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + webpack: + optional: true + checksum: f36ea6a4b70436ba365e92704f5acfcf2aec5faa2cf2dd18767f0243c0b2b901c19e2daed8a118f18668839691f42633577ba52231a1e58c12c1ccc42d842673 + languageName: node + linkType: hard + +"@module-federation/managers@npm:0.6.11": + version: 0.6.11 + resolution: "@module-federation/managers@npm:0.6.11" + dependencies: + "@module-federation/sdk": 0.6.11 + find-pkg: 2.0.0 + fs-extra: 9.1.0 + checksum: fdab486ca08060203c23acb078fce0402229d379e5b0666883ab0fdf204ebcd6e597a4204b023839db048809d40ddb0eaca43885bd3594bda39345c65af1522d + languageName: node + linkType: hard + +"@module-federation/manifest@npm:0.6.11": + version: 0.6.11 + resolution: "@module-federation/manifest@npm:0.6.11" + dependencies: + "@module-federation/dts-plugin": 0.6.11 + "@module-federation/managers": 0.6.11 + "@module-federation/sdk": 0.6.11 + chalk: 3.0.0 + find-pkg: 2.0.0 + checksum: b9f28ccfd527b676fa71d18d846ee49129a28156591c301d95f1820b6b5b2bb41b1fcb2a5d9e4d1f637c61b39e665818d7bec3444ea64058bf46e416c9b7cf09 + languageName: node + linkType: hard + +"@module-federation/rspack@npm:0.6.11": + version: 0.6.11 + resolution: "@module-federation/rspack@npm:0.6.11" + dependencies: + "@module-federation/bridge-react-webpack-plugin": 0.6.11 + "@module-federation/dts-plugin": 0.6.11 + "@module-federation/managers": 0.6.11 + "@module-federation/manifest": 0.6.11 + "@module-federation/runtime-tools": 0.6.11 + "@module-federation/sdk": 0.6.11 + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: ">=1.0.24" + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + checksum: 9e134f18fb0c5cf9fb411dd02709a2e936a630e7f39ac74f1fd07a7b8dfa231d9c001b15c52e9a8fbb8cd5688c2b58fa8594bd294ab561b47e283827a48b8656 + languageName: node + linkType: hard + +"@module-federation/runtime-tools@npm:0.6.11": + version: 0.6.11 + resolution: "@module-federation/runtime-tools@npm:0.6.11" + dependencies: + "@module-federation/runtime": 0.6.11 + "@module-federation/webpack-bundler-runtime": 0.6.11 + checksum: 15ab0bee1c4e02b0e07ee059410f2a166f0afc6b13b66293a9681e4fa9d185fa0aa232a762ebd3aa54f1c5f53ad0b367fdd09a5f6116f1b0fd6eadf4ca02a0a2 + languageName: node + linkType: hard + +"@module-federation/runtime@npm:0.6.11": + version: 0.6.11 + resolution: "@module-federation/runtime@npm:0.6.11" + dependencies: + "@module-federation/sdk": 0.6.11 + checksum: 3ddc92de3ad242177ba916f5f410b29f31017820b7c103defe8f964e8145ba8fb274b727ef8b92273c1ac4d53e1dd2cee61879c7710ea78180d8ff67387c0cf9 + languageName: node + linkType: hard + +"@module-federation/sdk@npm:0.6.11": + version: 0.6.11 + resolution: "@module-federation/sdk@npm:0.6.11" + checksum: d77811c08fcfce7e0922112593f1990a35eafe9e1fbfc0bdd99cb044fed29a66e33669b6f31ffafcdf7f4430b25f541dc79ca94d0e52b1f4bb0d7a7ec13ce765 + languageName: node + linkType: hard + +"@module-federation/third-party-dts-extractor@npm:0.6.11": + version: 0.6.11 + resolution: "@module-federation/third-party-dts-extractor@npm:0.6.11" + dependencies: + find-pkg: 2.0.0 + fs-extra: 9.1.0 + resolve: 1.22.8 + checksum: deb3a80ff83b983b483813e40362140b486ac6a10286117270f75d66bb7c27aa4f9ec4763faeda5e351785eac1a6f1c5f82df98a737708099e9a2a5075746575 + languageName: node + linkType: hard + +"@module-federation/webpack-bundler-runtime@npm:0.6.11": + version: 0.6.11 + resolution: "@module-federation/webpack-bundler-runtime@npm:0.6.11" + dependencies: + "@module-federation/runtime": 0.6.11 + "@module-federation/sdk": 0.6.11 + checksum: 3b4eea61b04f966d4ac74f6d59b43d75121692b2c2b7228a890646c68b356c03f7da1560cce159a1fbb5212d5ad5a6b8431b1eafeb19b4d37f38c03df2a6fb79 + languageName: node + linkType: hard + +"@mswjs/cookies@npm:^0.2.2": + version: 0.2.2 + resolution: "@mswjs/cookies@npm:0.2.2" + dependencies: + "@types/set-cookie-parser": ^2.4.0 + set-cookie-parser: ^2.4.6 + checksum: 23b1ef56d57efcc1b44600076f531a1fb703855af342a31e01bad4adaf0dab51f6d3b5595a95a7988c3f612ba075835f9a06c52833205284d101eb9a51dd72b0 + languageName: node + linkType: hard + +"@mswjs/interceptors@npm:^0.17.10": + version: 0.17.10 + resolution: "@mswjs/interceptors@npm:0.17.10" + dependencies: + "@open-draft/until": ^1.0.3 + "@types/debug": ^4.1.7 + "@xmldom/xmldom": ^0.8.3 + debug: ^4.3.3 + headers-polyfill: 3.2.5 + outvariant: ^1.2.1 + strict-event-emitter: ^0.2.4 + web-encoding: ^1.1.5 + checksum: 0e6d32f399144b5cefe6fd7620f2776c83adc9bbbbccf2eb4ea347332be059f585136c44168c09b544c41cd3d686f88e43432e10192227a24fbb0c98a2f52dc8 + languageName: node + linkType: hard + +"@mui/core-downloads-tracker@npm:^5.16.7": + version: 5.16.7 + resolution: "@mui/core-downloads-tracker@npm:5.16.7" + checksum: b65c48ba2bf6bba6435ba9f2d6c33db0c8a85b3ff7599136a9682b72205bec76470ab5ed5e6e625d5bd012ed9bcbc641ed677548be80d217c9fb5d0435567062 + languageName: node + linkType: hard + +"@mui/icons-material@npm:5.15.17": + version: 5.15.17 + resolution: "@mui/icons-material@npm:5.15.17" + dependencies: + "@babel/runtime": ^7.23.9 + peerDependencies: + "@mui/material": ^5.0.0 + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 6ac49529cbf6d2b2b6d955e4ade4f35fb52c4d25ca66159a5033919173ea8392ebf1ddbfe28a8d8609e40d638e08fc377d5c9fab4e016e3e1d3746cfba957d38 + languageName: node + linkType: hard + +"@mui/icons-material@npm:5.16.4": + version: 5.16.4 + resolution: "@mui/icons-material@npm:5.16.4" + dependencies: + "@babel/runtime": ^7.23.9 + peerDependencies: + "@mui/material": ^5.0.0 + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: b0559215a10819a082539a7ae43aedafb30bc109c5d6994ac6d748e46688d705e6b44b62693f89aea1fca3afdc6deb665d884c6bad0a2cffde9c5443027e3019 + languageName: node + linkType: hard + +"@mui/material@npm:^5.12.2, @mui/material@npm:^5.14.18": + version: 5.16.7 + resolution: "@mui/material@npm:5.16.7" + dependencies: + "@babel/runtime": ^7.23.9 + "@mui/core-downloads-tracker": ^5.16.7 + "@mui/system": ^5.16.7 + "@mui/types": ^7.2.15 + "@mui/utils": ^5.16.6 + "@popperjs/core": ^2.11.8 + "@types/react-transition-group": ^4.4.10 + clsx: ^2.1.0 + csstype: ^3.1.3 + prop-types: ^15.8.1 + react-is: ^18.3.1 + react-transition-group: ^4.4.5 + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 5057b48c3ce554247de9a8f675bda9bbda079bc83a696c500525f3ebbd63315a44f1c2a7c83c2025dbd02d2722892e397a0af10c1219d45f6534e41d91a43cc0 + languageName: node + linkType: hard + +"@mui/private-theming@npm:^5.16.6": + version: 5.16.6 + resolution: "@mui/private-theming@npm:5.16.6" + dependencies: + "@babel/runtime": ^7.23.9 + "@mui/utils": ^5.16.6 + prop-types: ^15.8.1 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 314ba598ab17cd425a36e4cab677ed26fe0939b23e53120da77cfbc3be6dada5428fa8e2a55cb697417599a4e3abfee6d4711de0a7318b9fb2c3a822b2d5b5a8 + languageName: node + linkType: hard + +"@mui/styled-engine@npm:^5.16.6": + version: 5.16.6 + resolution: "@mui/styled-engine@npm:5.16.6" + dependencies: + "@babel/runtime": ^7.23.9 + "@emotion/cache": ^11.11.0 + csstype: ^3.1.3 + prop-types: ^15.8.1 + peerDependencies: + "@emotion/react": ^11.4.1 + "@emotion/styled": ^11.3.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + checksum: 604f83b91801945336db211a8273061132668d01e9f456c30bb811a3b49cc5786b8b7dd8e0b5b89de15f6209abc900d9e679d3ae7a4651a6df45e323b6ed95c5 + languageName: node + linkType: hard + +"@mui/system@npm:^5.16.7": + version: 5.16.7 + resolution: "@mui/system@npm:5.16.7" + dependencies: + "@babel/runtime": ^7.23.9 + "@mui/private-theming": ^5.16.6 + "@mui/styled-engine": ^5.16.6 + "@mui/types": ^7.2.15 + "@mui/utils": ^5.16.6 + clsx: ^2.1.0 + csstype: ^3.1.3 + prop-types: ^15.8.1 + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 86cc11d062645b6742328178ca3a9e2aa2c6d064a559e4fb8c6c6bb8251794959b9dad385f9508fdcab2ae2764503c80f7c3d4f6eb1e0e8aa649f28d4f59133b + languageName: node + linkType: hard + +"@mui/types@npm:^7.2.15": + version: 7.2.18 + resolution: "@mui/types@npm:7.2.18" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: cf07ecc4bc8ad68a00b5afc87e4fb922664e3c34e83e9cfcc71e5de625481f652c2fd3982e77084acf47ca52e69577cd93641a9b185e7ef3afeec87f63252736 + languageName: node + linkType: hard + +"@mui/utils@npm:^5.14.15, @mui/utils@npm:^5.16.6": + version: 5.16.6 + resolution: "@mui/utils@npm:5.16.6" + dependencies: + "@babel/runtime": ^7.23.9 + "@mui/types": ^7.2.15 + "@types/prop-types": ^15.7.12 + clsx: ^2.1.1 + prop-types: ^15.8.1 + react-is: ^18.3.1 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 6f8068f07f60a842fcb2e2540eecbd9c5f04df695bcc427184720e8ae138ae689fefd3c20147ab7c76e809ede6e10f5e08d1c34cd3a8b09bd22d2020a666a96f + languageName: node + linkType: hard + +"@nestjs/axios@npm:3.1.1": + version: 3.1.1 + resolution: "@nestjs/axios@npm:3.1.1" + peerDependencies: + "@nestjs/common": ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + axios: ^1.3.1 + rxjs: ^6.0.0 || ^7.0.0 + checksum: a224cf85156b6a93ba7e4c15488160f5e904d7522ef63d7eef05a59fe109099117c5d565a8ace11393b01215417375ad1d79160256c7f280c94369f54603006d + languageName: node + linkType: hard + +"@nestjs/common@npm:10.4.6": + version: 10.4.6 + resolution: "@nestjs/common@npm:10.4.6" + dependencies: + iterare: 1.2.1 + tslib: 2.7.0 + uid: 2.0.2 + peerDependencies: + class-transformer: "*" + class-validator: "*" + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + checksum: 82478ce932867104af9a31b755af369bfb20931eb368874a22abfc553775e2c0fc3e55d07c8c948ab664a15f3ff6193bafb691658054b0b668a40a7e947033a2 + languageName: node + linkType: hard + +"@nestjs/core@npm:10.4.6": + version: 10.4.6 + resolution: "@nestjs/core@npm:10.4.6" + dependencies: + "@nuxtjs/opencollective": 0.3.2 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 3.3.0 + tslib: 2.7.0 + uid: 2.0.2 + peerDependencies: + "@nestjs/common": ^10.0.0 + "@nestjs/microservices": ^10.0.0 + "@nestjs/platform-express": ^10.0.0 + "@nestjs/websockets": ^10.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + "@nestjs/microservices": + optional: true + "@nestjs/platform-express": + optional: true + "@nestjs/websockets": + optional: true + checksum: d7d6e8129c344c3b7d5b41a98846de64614587842dd84b3050438da738530bdbea211efa0a08c65c274d2616ffb560096ab019254710b355ff57d92432ccb0ab + languageName: node + linkType: hard + +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": 2.0.5 + run-parallel: ^1.1.9 + checksum: a970d595bd23c66c880e0ef1817791432dbb7acbb8d44b7e7d0e7a22f4521260d4a83f7f9fd61d44fda4610105577f8f58a60718105fb38352baed612fd79e59 + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0 + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:1.2.8, @nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": 2.1.5 + fastq: ^1.6.0 + checksum: 190c643f156d8f8f277bf2a6078af1ffde1fd43f498f187c2db24d35b4b4b5785c02c7dc52e356497b9a1b65b13edc996de08de0b961c32844364da02986dc53 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^2.0.0": + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" + dependencies: + agent-base: ^7.1.0 + http-proxy-agent: ^7.0.0 + https-proxy-agent: ^7.0.1 + lru-cache: ^10.0.1 + socks-proxy-agent: ^8.0.3 + checksum: 67de7b88cc627a79743c88bab35e023e23daf13831a8aa4e15f998b92f5507b644d8ffc3788afc8e64423c612e0785a6a92b74782ce368f49a6746084b50d874 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^2.1.0": + version: 2.1.2 + resolution: "@npmcli/fs@npm:2.1.2" + dependencies: + "@gar/promisify": ^1.1.3 + semver: ^7.3.5 + checksum: 405074965e72d4c9d728931b64d2d38e6ea12066d4fad651ac253d175e413c06fe4350970c783db0d749181da8fe49c42d3880bd1cbc12cd68e3a7964d820225 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" + dependencies: + semver: ^7.3.5 + checksum: d960cab4b93adcb31ce223bfb75c5714edbd55747342efb67dcc2f25e023d930a7af6ece3e75f2f459b6f38fc14d031c766f116cd124fdc937fd33112579e820 + languageName: node + linkType: hard + +"@npmcli/move-file@npm:^2.0.0": + version: 2.0.1 + resolution: "@npmcli/move-file@npm:2.0.1" + dependencies: + mkdirp: ^1.0.4 + rimraf: ^3.0.2 + checksum: 52dc02259d98da517fae4cb3a0a3850227bdae4939dda1980b788a7670636ca2b4a01b58df03dd5f65c1e3cb70c50fa8ce5762b582b3f499ec30ee5ce1fd9380 + languageName: node + linkType: hard + +"@nuxtjs/opencollective@npm:0.3.2": + version: 0.3.2 + resolution: "@nuxtjs/opencollective@npm:0.3.2" + dependencies: + chalk: ^4.1.0 + consola: ^2.15.0 + node-fetch: ^2.6.1 + bin: + opencollective: bin/opencollective.js + checksum: fd3737c12edf55b5c2279674664c3ed5e756410ea82e9cd324c3f0e032ed5ccd8df1959ec69ea97f2f1c9c33c884aae3d7a7108a73ea0faa90d74ea47cf364d4 + languageName: node + linkType: hard + +"@octokit/auth-app@npm:^4.0.0": + version: 4.0.13 + resolution: "@octokit/auth-app@npm:4.0.13" + dependencies: + "@octokit/auth-oauth-app": ^5.0.0 + "@octokit/auth-oauth-user": ^2.0.0 + "@octokit/request": ^6.0.0 + "@octokit/request-error": ^3.0.0 + "@octokit/types": ^9.0.0 + deprecation: ^2.3.1 + lru-cache: ^9.0.0 + universal-github-app-jwt: ^1.1.1 + universal-user-agent: ^6.0.0 + checksum: 809004bc3e985fd4911cc42060fecd7b88e609e1334b90c4f79711aa27cade03fa1d930945ea8f7339ddd8d4514dd220a6ae8489faefa9e0ce6881519a02fc37 + languageName: node + linkType: hard + +"@octokit/auth-oauth-app@npm:^5.0.0": + version: 5.0.6 + resolution: "@octokit/auth-oauth-app@npm:5.0.6" + dependencies: + "@octokit/auth-oauth-device": ^4.0.0 + "@octokit/auth-oauth-user": ^2.0.0 + "@octokit/request": ^6.0.0 + "@octokit/types": ^9.0.0 + "@types/btoa-lite": ^1.0.0 + btoa-lite: ^1.0.0 + universal-user-agent: ^6.0.0 + checksum: 2101b70d148409ce24be3b7b5c033b03d92362a7b5786c441532187dac59826dba0ffbe245beb0c4cec55bc4b843b84b4b2ba0ad8ec46a31cc15451f80705b19 + languageName: node + linkType: hard + +"@octokit/auth-oauth-device@npm:^4.0.0": + version: 4.0.5 + resolution: "@octokit/auth-oauth-device@npm:4.0.5" + dependencies: + "@octokit/oauth-methods": ^2.0.0 + "@octokit/request": ^6.0.0 + "@octokit/types": ^9.0.0 + universal-user-agent: ^6.0.0 + checksum: 361824ba13c56beb05016b48b7d492f7439650abbb9e687c9f3e82ef4830790e1aae3d78c6e95dc317278146442c59821d87bf0b9b3c6d53f87117fe32b380d0 + languageName: node + linkType: hard + +"@octokit/auth-oauth-user@npm:^2.0.0": + version: 2.1.2 + resolution: "@octokit/auth-oauth-user@npm:2.1.2" + dependencies: + "@octokit/auth-oauth-device": ^4.0.0 + "@octokit/oauth-methods": ^2.0.0 + "@octokit/request": ^6.0.0 + "@octokit/types": ^9.0.0 + btoa-lite: ^1.0.0 + universal-user-agent: ^6.0.0 + checksum: cbb4994452b38fecebfd93bcf56b5ac7853f3bb880a42b00eec2fc6a9fdc6582293247cc8ead10814903f47195353c6450fe1a964184def7fe6e746da911b8bc + languageName: node + linkType: hard + +"@octokit/auth-token@npm:^3.0.0": + version: 3.0.4 + resolution: "@octokit/auth-token@npm:3.0.4" + checksum: 42f533a873d4192e6df406b3176141c1f95287423ebdc4cf23a38bb77ee00ccbc0e60e3fbd5874234fc2ed2e67bbc6035e3b0561dacc1d078adb5c4ced3579e3 + languageName: node + linkType: hard + +"@octokit/auth-unauthenticated@npm:^3.0.0": + version: 3.0.5 + resolution: "@octokit/auth-unauthenticated@npm:3.0.5" + dependencies: + "@octokit/request-error": ^3.0.0 + "@octokit/types": ^9.0.0 + checksum: 8372d732af9aeb09e51fc51c9aca00fb4522e182caf514898a27c5d7e33cfd8e39f9d00f7868cfc34ad437280a0fcafb312624a2968526110249e07b2b96b269 + languageName: node + linkType: hard + +"@octokit/core@npm:^4.0.0, @octokit/core@npm:^4.2.1": + version: 4.2.4 + resolution: "@octokit/core@npm:4.2.4" + dependencies: + "@octokit/auth-token": ^3.0.0 + "@octokit/graphql": ^5.0.0 + "@octokit/request": ^6.0.0 + "@octokit/request-error": ^3.0.0 + "@octokit/types": ^9.0.0 + before-after-hook: ^2.2.0 + universal-user-agent: ^6.0.0 + checksum: ac8ab47440a31b0228a034aacac6994b64d6b073ad5b688b4c5157fc5ee0d1af1c926e6087bf17fd7244ee9c5998839da89065a90819bde4a97cb77d4edf58a6 + languageName: node + linkType: hard + +"@octokit/endpoint@npm:^7.0.0": + version: 7.0.6 + resolution: "@octokit/endpoint@npm:7.0.6" + dependencies: + "@octokit/types": ^9.0.0 + is-plain-object: ^5.0.0 + universal-user-agent: ^6.0.0 + checksum: 7caebf30ceec50eb7f253341ed419df355232f03d4638a95c178ee96620400db7e4a5e15d89773fe14db19b8653d4ab4cc81b2e93ca0c760b4e0f7eb7ad80301 + languageName: node + linkType: hard + +"@octokit/graphql-schema@npm:^13.7.0": + version: 13.10.0 + resolution: "@octokit/graphql-schema@npm:13.10.0" + dependencies: + graphql: ^16.0.0 + graphql-tag: ^2.10.3 + checksum: fdec9c9a4df1f90b733ea0e24964744faceaf65e5d350b1727892e8e0e5821df1d29aec5cfa039925a044c6f56d4ed2028505108db7fbc0c68011053853c2411 + languageName: node + linkType: hard + +"@octokit/graphql@npm:^5.0.0": + version: 5.0.6 + resolution: "@octokit/graphql@npm:5.0.6" + dependencies: + "@octokit/request": ^6.0.0 + "@octokit/types": ^9.0.0 + universal-user-agent: ^6.0.0 + checksum: 7be545d348ef31dcab0a2478dd64d5746419a2f82f61459c774602bcf8a9b577989c18001f50b03f5f61a3d9e34203bdc021a4e4d75ff2d981e8c9c09cf8a65c + languageName: node + linkType: hard + +"@octokit/oauth-app@npm:^4.2.0": + version: 4.2.4 + resolution: "@octokit/oauth-app@npm:4.2.4" + dependencies: + "@octokit/auth-oauth-app": ^5.0.0 + "@octokit/auth-oauth-user": ^2.0.0 + "@octokit/auth-unauthenticated": ^3.0.0 + "@octokit/core": ^4.0.0 + "@octokit/oauth-authorization-url": ^5.0.0 + "@octokit/oauth-methods": ^2.0.0 + "@types/aws-lambda": ^8.10.83 + fromentries: ^1.3.1 + universal-user-agent: ^6.0.0 + checksum: 6d9798c9e63e84f3cb3031ac3f06f45c6ea053fd201be9a07a508786fd400479d7d9f6f85707d0fff7f094a265c7e966a2fa4c884001b99f02ddd927bf499d06 + languageName: node + linkType: hard + +"@octokit/oauth-authorization-url@npm:^5.0.0": + version: 5.0.0 + resolution: "@octokit/oauth-authorization-url@npm:5.0.0" + checksum: bc457c4af9559e9e8f752e643fc9d116247f4e4246e69959d99b9e39196c93d7af53c1c8e3bd946bd0e4fc29f7ba27efe9bced8525ffa41fe45ef56a8281014b + languageName: node + linkType: hard + +"@octokit/oauth-methods@npm:^2.0.0": + version: 2.0.6 + resolution: "@octokit/oauth-methods@npm:2.0.6" + dependencies: + "@octokit/oauth-authorization-url": ^5.0.0 + "@octokit/request": ^6.2.3 + "@octokit/request-error": ^3.0.3 + "@octokit/types": ^9.0.0 + btoa-lite: ^1.0.0 + checksum: 151b933d79d6fbf36fdfae8cdc868a3d43316352eaccf46cb8c420cfd238658275e41996d2d377177553bc0c637c3aefe8ca99c1ab7fd62054654b6119b7b1cc + languageName: node + linkType: hard + +"@octokit/openapi-types@npm:^18.0.0": + version: 18.1.1 + resolution: "@octokit/openapi-types@npm:18.1.1" + checksum: 94f42977fd2fcb9983c781fd199bc11218885a1226d492680bfb1268524a1b2af48a768eef90c63b80a2874437de641d59b3b7f640a5afa93e7c21fe1a79069a + languageName: node + linkType: hard + +"@octokit/plugin-paginate-rest@npm:^6.1.2": + version: 6.1.2 + resolution: "@octokit/plugin-paginate-rest@npm:6.1.2" + dependencies: + "@octokit/tsconfig": ^1.0.2 + "@octokit/types": ^9.2.3 + peerDependencies: + "@octokit/core": ">=4" + checksum: a7b3e686c7cbd27ec07871cde6e0b1dc96337afbcef426bbe3067152a17b535abd480db1861ca28c88d93db5f7bfdbcadd0919ead19818c28a69d0e194038065 + languageName: node + linkType: hard + +"@octokit/plugin-request-log@npm:^1.0.4": + version: 1.0.4 + resolution: "@octokit/plugin-request-log@npm:1.0.4" + peerDependencies: + "@octokit/core": ">=3" + checksum: 2086db00056aee0f8ebd79797b5b57149ae1014e757ea08985b71eec8c3d85dbb54533f4fd34b6b9ecaa760904ae6a7536be27d71e50a3782ab47809094bfc0c + languageName: node + linkType: hard + +"@octokit/plugin-rest-endpoint-methods@npm:^7.1.2": + version: 7.2.3 + resolution: "@octokit/plugin-rest-endpoint-methods@npm:7.2.3" + dependencies: + "@octokit/types": ^10.0.0 + peerDependencies: + "@octokit/core": ">=3" + checksum: 21dfb98514dbe900c29cddb13b335bbce43d613800c6b17eba3c1fd31d17e69c1960f3067f7bf864bb38fdd5043391f4a23edee42729d8c7fbabd00569a80336 + languageName: node + linkType: hard + +"@octokit/request-error@npm:^3.0.0, @octokit/request-error@npm:^3.0.3": + version: 3.0.3 + resolution: "@octokit/request-error@npm:3.0.3" + dependencies: + "@octokit/types": ^9.0.0 + deprecation: ^2.0.0 + once: ^1.4.0 + checksum: 5db0b514732686b627e6ed9ef1ccdbc10501f1b271a9b31f784783f01beee70083d7edcfeb35fbd7e569fa31fdd6762b1ff6b46101700d2d97e7e48e749520d0 + languageName: node + linkType: hard + +"@octokit/request@npm:^6.0.0, @octokit/request@npm:^6.2.3": + version: 6.2.8 + resolution: "@octokit/request@npm:6.2.8" + dependencies: + "@octokit/endpoint": ^7.0.0 + "@octokit/request-error": ^3.0.0 + "@octokit/types": ^9.0.0 + is-plain-object: ^5.0.0 + node-fetch: ^2.6.7 + universal-user-agent: ^6.0.0 + checksum: 3747106f50d7c462131ff995b13defdd78024b7becc40283f4ac9ea0af2391ff33a0bb476a05aa710346fe766d20254979079a1d6f626112015ba271fe38f3e2 + languageName: node + linkType: hard + +"@octokit/rest@npm:^19.0.3": + version: 19.0.13 + resolution: "@octokit/rest@npm:19.0.13" + dependencies: + "@octokit/core": ^4.2.1 + "@octokit/plugin-paginate-rest": ^6.1.2 + "@octokit/plugin-request-log": ^1.0.4 + "@octokit/plugin-rest-endpoint-methods": ^7.1.2 + checksum: ca1553e3fe46efabffef60e68e4a228d4cc0f0d545daf7f019560f666d3e934c6f3a6402a42bbd786af4f3c0a6e69380776312f01b7d52998fe1bbdd1b068f69 + languageName: node + linkType: hard + +"@octokit/tsconfig@npm:^1.0.2": + version: 1.0.2 + resolution: "@octokit/tsconfig@npm:1.0.2" + checksum: 74d56f3e9f326a8dd63700e9a51a7c75487180629c7a68bbafee97c612fbf57af8347369bfa6610b9268a3e8b833c19c1e4beb03f26db9a9dce31f6f7a19b5b1 + languageName: node + linkType: hard + +"@octokit/types@npm:^10.0.0": + version: 10.0.0 + resolution: "@octokit/types@npm:10.0.0" + dependencies: + "@octokit/openapi-types": ^18.0.0 + checksum: 8aafba2ff0cd2435fb70c291bf75ed071c0fa8a865cf6169648732068a35dec7b85a345851f18920ec5f3e94ee0e954988485caac0da09ec3f6781cc44fe153a + languageName: node + linkType: hard + +"@octokit/types@npm:^9.0.0, @octokit/types@npm:^9.2.3": + version: 9.3.2 + resolution: "@octokit/types@npm:9.3.2" + dependencies: + "@octokit/openapi-types": ^18.0.0 + checksum: f55d096aaed3e04b8308d4422104fb888f355988056ba7b7ef0a4c397b8a3e54290d7827b06774dbe0c9ce55280b00db486286954f9c265aa6b03091026d9da8 + languageName: node + linkType: hard + +"@open-draft/until@npm:^1.0.3": + version: 1.0.3 + resolution: "@open-draft/until@npm:1.0.3" + checksum: 323e92ebef0150ed0f8caedc7d219b68cdc50784fa4eba0377eef93533d3f46514eb2400ced83dda8c51bddc3d2c7b8e9cf95e5ec85ab7f62dfc015d174f62f2 + languageName: node + linkType: hard + +"@openapitools/openapi-generator-cli@npm:^2.7.0": + version: 2.15.3 + resolution: "@openapitools/openapi-generator-cli@npm:2.15.3" + dependencies: + "@nestjs/axios": 3.1.1 + "@nestjs/common": 10.4.6 + "@nestjs/core": 10.4.6 + "@nuxtjs/opencollective": 0.3.2 + axios: 1.7.7 + chalk: 4.1.2 + commander: 8.3.0 + compare-versions: 4.1.4 + concurrently: 6.5.1 + console.table: 0.10.0 + fs-extra: 10.1.0 + glob: 9.3.5 + inquirer: 8.2.6 + lodash: 4.17.21 + proxy-agent: 6.4.0 + reflect-metadata: 0.1.13 + rxjs: 7.8.1 + tslib: 2.8.1 + bin: + openapi-generator-cli: main.js + checksum: 7aaf394615cccea80ef37184e556134c61dee97ba777aecb3d14ac143f3a7390327511915e85b2e9108e8dbf56444678e8302e06009757581a3a0733531dd654 + languageName: node + linkType: hard + +"@opentelemetry/api@npm:^1.3.0": + version: 1.9.0 + resolution: "@opentelemetry/api@npm:1.9.0" + checksum: 9e88e59d53ced668f3daaecfd721071c5b85a67dd386f1c6f051d1be54375d850016c881f656ffbe9a03bedae85f7e89c2f2b635313f9c9b195ad033cdc31020 + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f + languageName: node + linkType: hard + +"@playwright/test@npm:1.45.3": + version: 1.45.3 + resolution: "@playwright/test@npm:1.45.3" + dependencies: + playwright: 1.45.3 + bin: + playwright: cli.js + checksum: 3e2c88d40f98cf94ab7947263804d1ee78c4bb21a35c8dbb64855eed5565ffc688509c5f07bda5438cba6c354374981448dcba3dbe326d1699b4fef75c9ce43d + languageName: node + linkType: hard + +"@pmmmwh/react-refresh-webpack-plugin@npm:^0.5.7": + version: 0.5.15 + resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.15" + dependencies: + ansi-html: ^0.0.9 + core-js-pure: ^3.23.3 + error-stack-parser: ^2.0.6 + html-entities: ^2.1.0 + loader-utils: ^2.0.4 + schema-utils: ^4.2.0 + source-map: ^0.7.3 + peerDependencies: + "@types/webpack": 4.x || 5.x + react-refresh: ">=0.10.0 <1.0.0" + sockjs-client: ^1.4.0 + type-fest: ">=0.17.0 <5.0.0" + webpack: ">=4.43.0 <6.0.0" + webpack-dev-server: 3.x || 4.x || 5.x + webpack-hot-middleware: 2.x + webpack-plugin-serve: 0.x || 1.x + peerDependenciesMeta: + "@types/webpack": + optional: true + sockjs-client: + optional: true + type-fest: + optional: true + webpack-dev-server: + optional: true + webpack-hot-middleware: + optional: true + webpack-plugin-serve: + optional: true + checksum: 82df6244146209d63a12f0ca2e70b05274ee058c7e6d6eb4ced1228afde3b039a7f3f3cc0c76f1bb4b28deadbcf08bc2821c814f0bfee06979128578300fff3d + languageName: node + linkType: hard + +"@popperjs/core@npm:^2.11.8": + version: 2.11.8 + resolution: "@popperjs/core@npm:2.11.8" + checksum: e5c69fdebf52a4012f6a1f14817ca8e9599cb1be73dd1387e1785e2ed5e5f0862ff817f420a87c7fc532add1f88a12e25aeb010ffcbdc98eace3d55ce2139cf0 + languageName: node + linkType: hard + +"@react-hookz/deep-equal@npm:^1.0.4": + version: 1.0.4 + resolution: "@react-hookz/deep-equal@npm:1.0.4" + checksum: 0923e364d309e32ee54e0850471a86488faf149d7a04ee838552cf5d54f493964623a8d742880ec82410cc1105530123f056e66dfc72b7da235d4cc93fad708f + languageName: node + linkType: hard + +"@react-hookz/web@npm:^24.0.0": + version: 24.0.4 + resolution: "@react-hookz/web@npm:24.0.4" + dependencies: + "@react-hookz/deep-equal": ^1.0.4 + peerDependencies: + js-cookie: ^3.0.5 + react: ^16.8 || ^17 || ^18 + react-dom: ^16.8 || ^17 || ^18 + peerDependenciesMeta: + js-cookie: + optional: true + checksum: 842dd51a2c875814c7468632315d756e79fcdff2882d7224e8e06c630f95ab788b6a59c29c0318cb049a18be97537803be8e3dbae12de34b2ae1290ababe266a + languageName: node + linkType: hard + +"@redhat-developer/red-hat-developer-hub-theme@npm:0.4.0": + version: 0.4.0 + resolution: "@redhat-developer/red-hat-developer-hub-theme@npm:0.4.0" + peerDependencies: + "@backstage/theme": ^0.5.2 + "@emotion/react": ^11.11.1 + "@emotion/styled": ^11.11.0 + "@material-ui/core": ^4.12.4 + "@material-ui/icons": ^4.11.3 + "@mui/icons-material": ^5.14.19 + "@mui/material": ^5.14.20 + checksum: 8684f8faa2fe87100dba2c19f1e1a306e808cf98bc65da829c3203a325ea6520a93e54c174e25a220ccf8280ecc2750a178a0d18b12f5ba354c7c415e8a9dc7b + languageName: node + linkType: hard + +"@remix-run/router@npm:1.20.0": + version: 1.20.0 + resolution: "@remix-run/router@npm:1.20.0" + checksum: 6bff41117eabb867b17c89baa727580f0a431368b309cd9a1f69767aafa68ea9cac95ff0eeb86d37c2c8655f5cd7c6283d37ae5e6d93e94f648c6112ddb24ede + languageName: node + linkType: hard + +"@rjsf/core@npm:^5.21.2": + version: 5.21.2 + resolution: "@rjsf/core@npm:5.21.2" + dependencies: + lodash: ^4.17.21 + lodash-es: ^4.17.21 + markdown-to-jsx: ^7.4.1 + nanoid: ^3.3.7 + prop-types: ^15.8.1 + peerDependencies: + "@rjsf/utils": ^5.20.x + react: ^16.14.0 || >=17 + checksum: ac5c4ff0e0cf74ba8cf6d58df314f8f17de6be5b00bb0ca14f79861347bbaa59f37b8f572d80f30388c5007de1d2dedfc3ff70e419eb874331d58f0ba9eeeb42 + languageName: node + linkType: hard + +"@rjsf/mui@npm:^5.21.2": + version: 5.21.2 + resolution: "@rjsf/mui@npm:5.21.2" + peerDependencies: + "@emotion/react": ^11.7.0 + "@emotion/styled": ^11.6.0 + "@mui/icons-material": ^5.2.0 || ^6.0.0 + "@mui/material": ^5.2.2 || ^6.0.0 + "@rjsf/core": ^5.20.x + "@rjsf/utils": ^5.20.x + react: ">=17" + checksum: 2ddd58eff962dffcc9df3cae518b2394b162b15856cebae5d521c17242ff4bd2a64021838cd018b5fd3f6f4ab07f99f98714d86f0c957db20d0d0ecd9f9f8776 + languageName: node + linkType: hard + +"@rjsf/utils@npm:^5.21.2": + version: 5.21.2 + resolution: "@rjsf/utils@npm:5.21.2" + dependencies: + json-schema-merge-allof: ^0.8.1 + jsonpointer: ^5.0.1 + lodash: ^4.17.21 + lodash-es: ^4.17.21 + react-is: ^18.2.0 + peerDependencies: + react: ^16.14.0 || >=17 + checksum: 05460f3c95e1a407001accaf2e9b90c0731433936cfea6a129ac01b49575f56ba336f1ae46e3930f0226580d06c6300c8622d1c3a56354c3e723caf3654f02e1 + languageName: node + linkType: hard + +"@rjsf/validator-ajv8@npm:^5.21.2": + version: 5.21.2 + resolution: "@rjsf/validator-ajv8@npm:5.21.2" + dependencies: + ajv: ^8.12.0 + ajv-formats: ^2.1.1 + lodash: ^4.17.21 + lodash-es: ^4.17.21 + peerDependencies: + "@rjsf/utils": ^5.20.x + checksum: 06d34e70e6595c5a0e999a3a2a651fccc7a36dbb2395f5805ce1ac6b47201111e6d84c9e122f3d336bbdbaca61875a90efd65e1839d9da3c9aafe282dcc03086 + languageName: node + linkType: hard + +"@rollup/plugin-commonjs@npm:^26.0.0": + version: 26.0.3 + resolution: "@rollup/plugin-commonjs@npm:26.0.3" + dependencies: + "@rollup/pluginutils": ^5.0.1 + commondir: ^1.0.1 + estree-walker: ^2.0.2 + glob: ^10.4.1 + is-reference: 1.2.1 + magic-string: ^0.30.3 + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 6f3ce53054f9b2edfd04a673c7572b5f8ba6b8416da55a7aef670a9b4630caf46e3e8d74b481d05e1d9f9cb98fa96228e23abad10ab2c95a6cc0b1a0065568e6 + languageName: node + linkType: hard + +"@rollup/plugin-json@npm:^6.0.0": + version: 6.1.0 + resolution: "@rollup/plugin-json@npm:6.1.0" + dependencies: + "@rollup/pluginutils": ^5.1.0 + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: cc018d20c80242a2b8b44fae61a968049cf31bb8406218187cc7cda35747616594e79452dd65722e7da6dd825b392e90d4599d43cd4461a02fefa2865945164e + languageName: node + linkType: hard + +"@rollup/plugin-node-resolve@npm:^15.0.0": + version: 15.3.0 + resolution: "@rollup/plugin-node-resolve@npm:15.3.0" + dependencies: + "@rollup/pluginutils": ^5.0.1 + "@types/resolve": 1.20.2 + deepmerge: ^4.2.2 + is-module: ^1.0.0 + resolve: ^1.22.1 + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 90e4e94b173e7edd57e374ac0cc0a69cc6f1b4507e83731132ac6fa1747d96a5648a48441e4452728429b6db5e67561439b7b2f4d2c6a941a33d38be56d871b4 + languageName: node + linkType: hard + +"@rollup/plugin-yaml@npm:^4.0.0": + version: 4.1.2 + resolution: "@rollup/plugin-yaml@npm:4.1.2" + dependencies: + "@rollup/pluginutils": ^5.0.1 + js-yaml: ^4.1.0 + tosource: ^2.0.0-alpha.3 + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: a044bb4568a10712465553ea5f31c13a2b7bc371a7f8382014e6b8048c0a264f5645f83f4d70ce9ab46b75117b94cdc032b597e9315fd2adcd8f30637f44bbea + languageName: node + linkType: hard + +"@rollup/pluginutils@npm:^4.2.1": + version: 4.2.1 + resolution: "@rollup/pluginutils@npm:4.2.1" + dependencies: + estree-walker: ^2.0.1 + picomatch: ^2.2.2 + checksum: 6bc41f22b1a0f1efec3043899e4d3b6b1497b3dea4d94292d8f83b4cf07a1073ecbaedd562a22d11913ff7659f459677b01b09e9598a98936e746780ecc93a12 + languageName: node + linkType: hard + +"@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.0.5, @rollup/pluginutils@npm:^5.1.0": + version: 5.1.2 + resolution: "@rollup/pluginutils@npm:5.1.2" + dependencies: + "@types/estree": ^1.0.0 + estree-walker: ^2.0.2 + picomatch: ^2.3.1 + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 16c8c154fef9a32c513b52bd79c92ac427edccd05a8dc3994f10c296063940c57bf809d05903b473d9d408aa5977d75b98c701f481dd1856d5ffc37187ac0060 + languageName: node + linkType: hard + +"@rollup/rollup-android-arm-eabi@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.24.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-android-arm64@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-android-arm64@npm:4.24.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-arm64@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.24.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-x64@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.24.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-gnueabihf@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.24.0" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-musleabihf@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.24.0" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.24.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-musl@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.24.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.24.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.24.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-s390x-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.24.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-gnu@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.24.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-musl@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.24.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.24.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-ia32-msvc@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.24.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.24.0": + version: 4.24.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.24.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rtsao/scc@npm:^1.1.0": + version: 1.1.0 + resolution: "@rtsao/scc@npm:1.1.0" + checksum: 17d04adf404e04c1e61391ed97bca5117d4c2767a76ae3e879390d6dec7b317fcae68afbf9e98badee075d0b64fa60f287729c4942021b4d19cd01db77385c01 + languageName: node + linkType: hard + +"@rushstack/node-core-library@npm:5.9.0": + version: 5.9.0 + resolution: "@rushstack/node-core-library@npm:5.9.0" + dependencies: + ajv: ~8.13.0 + ajv-draft-04: ~1.0.0 + ajv-formats: ~3.0.1 + fs-extra: ~7.0.1 + import-lazy: ~4.0.0 + jju: ~1.4.0 + resolve: ~1.22.1 + semver: ~7.5.4 + peerDependencies: + "@types/node": "*" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: beb558f118a796260f7df38b48b6669a94bbdb9711715785e0c5a426bd3a38c14721c03fc05e7a33883ec25a331ef0fb9e36438bb451ace021a7248a4f1fc74b + languageName: node + linkType: hard + +"@rushstack/rig-package@npm:0.5.3": + version: 0.5.3 + resolution: "@rushstack/rig-package@npm:0.5.3" + dependencies: + resolve: ~1.22.1 + strip-json-comments: ~3.1.1 + checksum: bf3eadfc434bff273893efd22b319fe159d0e3b95729cb32ce3ad9f4ab4b6fabe3c4dd7f03ee0ddc7b480f0d989e908349eae6d6dce3500f896728a085af7aab + languageName: node + linkType: hard + +"@rushstack/terminal@npm:0.14.2": + version: 0.14.2 + resolution: "@rushstack/terminal@npm:0.14.2" + dependencies: + "@rushstack/node-core-library": 5.9.0 + supports-color: ~8.1.1 + peerDependencies: + "@types/node": "*" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 90d38e6979737dcd97fdfdcebcc378194eed32a994341846235769273b6446b702e53e51e18fc8a373e8ed989c5622216aa6804198b8c7ae0e65cd6b103b90a1 + languageName: node + linkType: hard + +"@rushstack/ts-command-line@npm:4.23.0": + version: 4.23.0 + resolution: "@rushstack/ts-command-line@npm:4.23.0" + dependencies: + "@rushstack/terminal": 0.14.2 + "@types/argparse": 1.0.38 + argparse: ~1.0.9 + string-argv: ~0.3.1 + checksum: 4f3d77c5b2998bbc551d02e882f0c7b8e7aed0d97ad6e4ee45b2d6281a209087f738fc1a021397088ffbe666c4eae462c1d8c4a14dc031dddee2af055b12f794 + languageName: node + linkType: hard + +"@sideway/address@npm:^4.1.5": + version: 4.1.5 + resolution: "@sideway/address@npm:4.1.5" + dependencies: + "@hapi/hoek": ^9.0.0 + checksum: 3e3ea0f00b4765d86509282290368a4a5fd39a7995fdc6de42116ca19a96120858e56c2c995081def06e1c53e1f8bccc7d013f6326602bec9d56b72ee2772b9d + languageName: node + linkType: hard + +"@sideway/formula@npm:^3.0.1": + version: 3.0.1 + resolution: "@sideway/formula@npm:3.0.1" + checksum: e4beeebc9dbe2ff4ef0def15cec0165e00d1612e3d7cea0bc9ce5175c3263fc2c818b679bd558957f49400ee7be9d4e5ac90487e1625b4932e15c4aa7919c57a + languageName: node + linkType: hard + +"@sideway/pinpoint@npm:^2.0.0": + version: 2.0.0 + resolution: "@sideway/pinpoint@npm:2.0.0" + checksum: 0f4491e5897fcf5bf02c46f5c359c56a314e90ba243f42f0c100437935daa2488f20482f0f77186bd6bf43345095a95d8143ecf8b1f4d876a7bc0806aba9c3d2 + languageName: node + linkType: hard + +"@sinclair/typebox@npm:^0.27.8": + version: 0.27.8 + resolution: "@sinclair/typebox@npm:0.27.8" + checksum: 00bd7362a3439021aa1ea51b0e0d0a0e8ca1351a3d54c606b115fdcc49b51b16db6e5f43b4fe7a28c38688523e22a94d49dd31168868b655f0d4d50f032d07a1 + languageName: node + linkType: hard + +"@sinonjs/commons@npm:^3.0.0": + version: 3.0.1 + resolution: "@sinonjs/commons@npm:3.0.1" + dependencies: + type-detect: 4.0.8 + checksum: a7c3e7cc612352f4004873747d9d8b2d4d90b13a6d483f685598c945a70e734e255f1ca5dc49702515533c403b32725defff148177453b3f3915bcb60e9d4601 + languageName: node + linkType: hard + +"@sinonjs/fake-timers@npm:^10.0.2": + version: 10.3.0 + resolution: "@sinonjs/fake-timers@npm:10.3.0" + dependencies: + "@sinonjs/commons": ^3.0.0 + checksum: 614d30cb4d5201550c940945d44c9e0b6d64a888ff2cd5b357f95ad6721070d6b8839cd10e15b76bf5e14af0bcc1d8f9ec00d49a46318f1f669a4bec1d7f3148 + languageName: node + linkType: hard + +"@smithy/abort-controller@npm:^3.1.5": + version: 3.1.5 + resolution: "@smithy/abort-controller@npm:3.1.5" + dependencies: + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 538c88d6dfe84d92a7dead4eb149d48bc59857df8235057727c0481e851b0ceea6aabfd5cc059c9e37e66fbadead461c85d6a7c8436e2db6681f06333e814281 + languageName: node + linkType: hard + +"@smithy/chunked-blob-reader-native@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/chunked-blob-reader-native@npm:3.0.0" + dependencies: + "@smithy/util-base64": ^3.0.0 + tslib: ^2.6.2 + checksum: f97c0c0ce5e9bd2350883df3c232311aa82eb87eb387125f685900326f86fc3aca208e9004291f742f6978abf91a0c1112cc9a803cd0caf0dffbcfa9b6d0239e + languageName: node + linkType: hard + +"@smithy/chunked-blob-reader@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/chunked-blob-reader@npm:3.0.0" + dependencies: + tslib: ^2.6.2 + checksum: 6f520884ade14f1073adb640db2f03eb22a9920f342f37958df3e98327890b741cd909b16cbbc6f70c6c8dd250d6b3a8d76841b685d4871b0403f309267def4f + languageName: node + linkType: hard + +"@smithy/config-resolver@npm:^3.0.9": + version: 3.0.9 + resolution: "@smithy/config-resolver@npm:3.0.9" + dependencies: + "@smithy/node-config-provider": ^3.1.8 + "@smithy/types": ^3.5.0 + "@smithy/util-config-provider": ^3.0.0 + "@smithy/util-middleware": ^3.0.7 + tslib: ^2.6.2 + checksum: 87e61be2ae1690a69974c0860d455a87c696c2da163384d22b582ee0fbee322b73f5d69dea754a2d8681d1b70fd4b0ca8d993ecb13eecf54f28ba3ffabfa0c40 + languageName: node + linkType: hard + +"@smithy/core@npm:^2.4.8": + version: 2.4.8 + resolution: "@smithy/core@npm:2.4.8" + dependencies: + "@smithy/middleware-endpoint": ^3.1.4 + "@smithy/middleware-retry": ^3.0.23 + "@smithy/middleware-serde": ^3.0.7 + "@smithy/protocol-http": ^4.1.4 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-middleware": ^3.0.7 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: ab9e635f1622e870272f2950bb8f810ec942246b529aa94bc455265d6ba03deb82a0779b74fd3d666f1857fab228061642f90f2f60b73b8f09f52c39b11dc0f2 + languageName: node + linkType: hard + +"@smithy/credential-provider-imds@npm:^3.2.4": + version: 3.2.4 + resolution: "@smithy/credential-provider-imds@npm:3.2.4" + dependencies: + "@smithy/node-config-provider": ^3.1.8 + "@smithy/property-provider": ^3.1.7 + "@smithy/types": ^3.5.0 + "@smithy/url-parser": ^3.0.7 + tslib: ^2.6.2 + checksum: d416f85450aa2402f37ea26a1052e596f92a8a1f9164524313b43ba1ceb9abd3b986c817dbcd6f4fc984054b246ec739efa786ad66ff5604777648a34fc58d54 + languageName: node + linkType: hard + +"@smithy/eventstream-codec@npm:^3.1.6": + version: 3.1.6 + resolution: "@smithy/eventstream-codec@npm:3.1.6" + dependencies: + "@aws-crypto/crc32": 5.2.0 + "@smithy/types": ^3.5.0 + "@smithy/util-hex-encoding": ^3.0.0 + tslib: ^2.6.2 + checksum: 9b7ec78dd0b15c2950d5f89c1240adda5240cab252ecd0e68ed55ac4da5fca4802b237341d42e8fc638c4db93f31459c40c7eb79d8dfc0446e2a925c3fdc1ba2 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-browser@npm:^3.0.10": + version: 3.0.10 + resolution: "@smithy/eventstream-serde-browser@npm:3.0.10" + dependencies: + "@smithy/eventstream-serde-universal": ^3.0.9 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 292382ae41f5ca0d9d6b1791de2d7d91f93c6957c08ac7179b91d05afa1f116c754b260def0ead1d23ea8fd0f4359969db024470b74be976edadc69c931cb254 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-config-resolver@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/eventstream-serde-config-resolver@npm:3.0.7" + dependencies: + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: c1762b21c665a580bb3c89e8811e9b0a22122ebd8633db2a78693f40910b5788c3e5603c905773bec6a1a72bf0e9785a4c011fded658f6f6f2ba616fc4ac5dd6 + languageName: node + linkType: hard + +"@smithy/eventstream-serde-node@npm:^3.0.9": + version: 3.0.9 + resolution: "@smithy/eventstream-serde-node@npm:3.0.9" + dependencies: + "@smithy/eventstream-serde-universal": ^3.0.9 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 3f5dd216366f461d99c9100215d7e122fccf32ae78ffb6a5164277363ed1510c087bfcb3a31731f48368c179f57ea9b46ae2a19bbe3562da07cd6ada06a47e9c + languageName: node + linkType: hard + +"@smithy/eventstream-serde-universal@npm:^3.0.9": + version: 3.0.9 + resolution: "@smithy/eventstream-serde-universal@npm:3.0.9" + dependencies: + "@smithy/eventstream-codec": ^3.1.6 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: d247fdb9155063af562123dd1970f8d17a1871c3793355fc86c875bf3088aca44e6f3b17a704f4d9331a84ac9811b4592e3ecab54a90e600d6e717fc9f6781c6 + languageName: node + linkType: hard + +"@smithy/fetch-http-handler@npm:^3.2.9": + version: 3.2.9 + resolution: "@smithy/fetch-http-handler@npm:3.2.9" + dependencies: + "@smithy/protocol-http": ^4.1.4 + "@smithy/querystring-builder": ^3.0.7 + "@smithy/types": ^3.5.0 + "@smithy/util-base64": ^3.0.0 + tslib: ^2.6.2 + checksum: 3b8eed12bff9d39e23989ea424e112530e01c81f983f15a3bfc4265baa06feb230267d095588705c5a8002cc4a2bfcd834b0341bff60a6236dcc24599ecf8327 + languageName: node + linkType: hard + +"@smithy/hash-blob-browser@npm:^3.1.6": + version: 3.1.6 + resolution: "@smithy/hash-blob-browser@npm:3.1.6" + dependencies: + "@smithy/chunked-blob-reader": ^3.0.0 + "@smithy/chunked-blob-reader-native": ^3.0.0 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 4807ad388f552a5f27f168c4efa9cd88c14a2dc75a047137ccab88ef2dfb70729ef7800ca2ae12f2a41adb3149c5d4605eac81ef64880912766d6b59d258ad81 + languageName: node + linkType: hard + +"@smithy/hash-node@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/hash-node@npm:3.0.7" + dependencies: + "@smithy/types": ^3.5.0 + "@smithy/util-buffer-from": ^3.0.0 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: 7a3b432e498efc1d8f229d58a760fae92f1d8a434eb9865b2b4dccea521bd318a97a366e0fdd2e41e2eb02ee6c78c9d3a076a993d5c970e33b0051b4d209128b + languageName: node + linkType: hard + +"@smithy/hash-stream-node@npm:^3.1.6": + version: 3.1.6 + resolution: "@smithy/hash-stream-node@npm:3.1.6" + dependencies: + "@smithy/types": ^3.5.0 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: e6427f7865667ad3a72eb9aace0d19718100fd4b14fb9f1e85c09b68b0b7ed608e26d1c2b9c6829be2f89aaa3fa3c122b1a5d5beea43c1026a43f70e748d8483 + languageName: node + linkType: hard + +"@smithy/invalid-dependency@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/invalid-dependency@npm:3.0.7" + dependencies: + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 6ccfd995686c12cceedf4408021d30e83b88785d77f5ab2e0ee2fab034828a782464f47828acf76d282d37daf20ffff9f27bdd1ce0499926299e560143b28cad + languageName: node + linkType: hard + +"@smithy/is-array-buffer@npm:^2.2.0": + version: 2.2.0 + resolution: "@smithy/is-array-buffer@npm:2.2.0" + dependencies: + tslib: ^2.6.2 + checksum: cd12c2e27884fec89ca8966d33c9dc34d3234efe89b33a9b309c61ebcde463e6f15f6a02d31d4fddbfd6e5904743524ca5b95021b517b98fe10957c2da0cd5fc + languageName: node + linkType: hard + +"@smithy/is-array-buffer@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/is-array-buffer@npm:3.0.0" + dependencies: + tslib: ^2.6.2 + checksum: ce7440fcb1ce3c46722cff11c33e2f62a9df86d74fa2054a8e6b540302a91211cf6e4e3b1b7aac7030c6c8909158c1b6867c394201fa8afc6b631979956610e5 + languageName: node + linkType: hard + +"@smithy/md5-js@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/md5-js@npm:3.0.7" + dependencies: + "@smithy/types": ^3.5.0 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: d9badbd5361babc30103ef9e9a6c3b24b49d058de1ccd6765fbe1867753f9c8a97100e1ce88509fa50e1aec3135603b466c2ef21af5acba281f745a6eea0f034 + languageName: node + linkType: hard + +"@smithy/middleware-content-length@npm:^3.0.9": + version: 3.0.9 + resolution: "@smithy/middleware-content-length@npm:3.0.9" + dependencies: + "@smithy/protocol-http": ^4.1.4 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 0299e2573942b5f073d5dadf45778b61db530f79356e08594eb947060c603202282f45e6fd8c8f5e64f6184ca6b987cd3e8f55dfc8d189809af3d7b47230a2d7 + languageName: node + linkType: hard + +"@smithy/middleware-endpoint@npm:^3.1.4": + version: 3.1.4 + resolution: "@smithy/middleware-endpoint@npm:3.1.4" + dependencies: + "@smithy/middleware-serde": ^3.0.7 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/shared-ini-file-loader": ^3.1.8 + "@smithy/types": ^3.5.0 + "@smithy/url-parser": ^3.0.7 + "@smithy/util-middleware": ^3.0.7 + tslib: ^2.6.2 + checksum: 34cc4115fc57c9db90e6b74f4039e35e9e3cec94411173a3c0c14bacf99d86712ee51423b98b4d62695a5425a53d108fc0a2e11510df4b17a36f0496af03ddc1 + languageName: node + linkType: hard + +"@smithy/middleware-retry@npm:^3.0.23": + version: 3.0.23 + resolution: "@smithy/middleware-retry@npm:3.0.23" + dependencies: + "@smithy/node-config-provider": ^3.1.8 + "@smithy/protocol-http": ^4.1.4 + "@smithy/service-error-classification": ^3.0.7 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + "@smithy/util-middleware": ^3.0.7 + "@smithy/util-retry": ^3.0.7 + tslib: ^2.6.2 + uuid: ^9.0.1 + checksum: 8d991ce755f644d2e8934eeaef7af9a358dcabd452ed21533fa298a119919d1298f9211f23a9d291970a3ec7dd4c7479d0bdfbaef4ff4633d5375bdc289ff761 + languageName: node + linkType: hard + +"@smithy/middleware-serde@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/middleware-serde@npm:3.0.7" + dependencies: + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 6ec3a000049a5e3212c5814b5500b562669a75ef42f4efecf13f0726614982488b89bb3d55fd163eb655a1e58bf490e387f8f5d5bfb4fc51bb63dffd550e15e6 + languageName: node + linkType: hard + +"@smithy/middleware-stack@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/middleware-stack@npm:3.0.7" + dependencies: + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: f29af8abb52e58b9cbb59c5187e0758279dd7d50c350ae2ad3cf123277fb652976c72be44d0be459e6db42294a0dca24eaf0fa6aead33a9e4b7109437102246f + languageName: node + linkType: hard + +"@smithy/node-config-provider@npm:^3.1.8": + version: 3.1.8 + resolution: "@smithy/node-config-provider@npm:3.1.8" + dependencies: + "@smithy/property-provider": ^3.1.7 + "@smithy/shared-ini-file-loader": ^3.1.8 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 20b6d0e5e2487954a1a7235ca4bd4efa81e90f5cbd25b361e70e5d173807b346646109c62ace7c32d999938cb0825fa9aea54b597e487b18879dc433676d4e0c + languageName: node + linkType: hard + +"@smithy/node-http-handler@npm:^3.2.4": + version: 3.2.4 + resolution: "@smithy/node-http-handler@npm:3.2.4" + dependencies: + "@smithy/abort-controller": ^3.1.5 + "@smithy/protocol-http": ^4.1.4 + "@smithy/querystring-builder": ^3.0.7 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 658934366953828af04e5f8d0229f24e8ff783c1bd34b179203099321e4b41b19dfd921c3ef431d8067fc2d49a0c806d0c758fff6ea10606e092480dcf6b0f26 + languageName: node + linkType: hard + +"@smithy/property-provider@npm:^3.1.7": + version: 3.1.7 + resolution: "@smithy/property-provider@npm:3.1.7" + dependencies: + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: c0b9fdbfeb4100ddc27811f32af75d3b02496b2323b215f30a13f4de6f4d821597731b02123061cea23b6bb81fba91bc24ecc3cf0e8e035a8a100559b7d43e27 + languageName: node + linkType: hard + +"@smithy/protocol-http@npm:^4.1.4": + version: 4.1.4 + resolution: "@smithy/protocol-http@npm:4.1.4" + dependencies: + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: c0655e2031ec6ae96d63a125b76ca9bb46b3e4b8f4436ef0ea9bcf08303c1b6cdd4f0d17a1cd87cfdbe60bde34e5001d65f91d4e3eaa24cf560ed718967686de + languageName: node + linkType: hard + +"@smithy/querystring-builder@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/querystring-builder@npm:3.0.7" + dependencies: + "@smithy/types": ^3.5.0 + "@smithy/util-uri-escape": ^3.0.0 + tslib: ^2.6.2 + checksum: 0c41ce1993ce4b7dc509bc1fa50c42000a1cb5801601fc28d9113494349c337e88f77bff998f0debf0be0eba41d67d653a6648eea0f5b3b1e0f8a3cd57229631 + languageName: node + linkType: hard + +"@smithy/querystring-parser@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/querystring-parser@npm:3.0.7" + dependencies: + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 5ef80af89f1c1aed44ce263d91da5ba48f0858136d1f1b041524e6cbcc7d5c5345642ff6ef876fe1469107a3cd9815fc084057be2601bcafa6ff383c21dff5d0 + languageName: node + linkType: hard + +"@smithy/service-error-classification@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/service-error-classification@npm:3.0.7" + dependencies: + "@smithy/types": ^3.5.0 + checksum: a6370ee348f4b66698a193a680ab5c81e0ed4d5fac8204cbbd9967c869feceb0b6d129f8d0e4823538ab699d7f3ab3ff8151e791221ee5f97742423b0e76b321 + languageName: node + linkType: hard + +"@smithy/shared-ini-file-loader@npm:^3.1.8": + version: 3.1.8 + resolution: "@smithy/shared-ini-file-loader@npm:3.1.8" + dependencies: + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 0ad620cb4a641786f205e6f01ac00433afee6dbe5d14180458841cab3b9322b580caf18c9f9cf24d71d063bdf3b5716b159045e386f10f1c87847fff85272b70 + languageName: node + linkType: hard + +"@smithy/signature-v4@npm:^4.2.0": + version: 4.2.0 + resolution: "@smithy/signature-v4@npm:4.2.0" + dependencies: + "@smithy/is-array-buffer": ^3.0.0 + "@smithy/protocol-http": ^4.1.4 + "@smithy/types": ^3.5.0 + "@smithy/util-hex-encoding": ^3.0.0 + "@smithy/util-middleware": ^3.0.7 + "@smithy/util-uri-escape": ^3.0.0 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: edf0fa3ee5a65dbc132dd3a9f9ca6dcbeefa33b96e701dd7de4cb965ca3000ad706bf7ec87c50a9f71a86a6610fac5315ab96d5247e6b550b75548a3d9ecb667 + languageName: node + linkType: hard + +"@smithy/smithy-client@npm:^3.4.0": + version: 3.4.0 + resolution: "@smithy/smithy-client@npm:3.4.0" + dependencies: + "@smithy/middleware-endpoint": ^3.1.4 + "@smithy/middleware-stack": ^3.0.7 + "@smithy/protocol-http": ^4.1.4 + "@smithy/types": ^3.5.0 + "@smithy/util-stream": ^3.1.9 + tslib: ^2.6.2 + checksum: 4eb8387ca16064fc1c0c59d502f5d611fb3ee9c06e0ebd3c1a540bb8f1e709e0073bcc9aa9c3c337db1e3d4a799a376d2f29d3f90b008a431a6216805a217e6e + languageName: node + linkType: hard + +"@smithy/types@npm:^1.1.0": + version: 1.2.0 + resolution: "@smithy/types@npm:1.2.0" + dependencies: + tslib: ^2.5.0 + checksum: 376a1402d356a8dddd804af66ff2d273e57e332a3e9537a98039b47572684aae044d5fcd879ac6eee5cc08640ea00fbef0725a6a16026db5fb8d189473d44fe6 + languageName: node + linkType: hard + +"@smithy/types@npm:^3.5.0": + version: 3.5.0 + resolution: "@smithy/types@npm:3.5.0" + dependencies: + tslib: ^2.6.2 + checksum: 5d297005549991f6928daf038e0610c959423add6e435af970b8c8dcac988bf62b0cdbf4dd5df43197d9bc7af5c290792f17af6e2f5051be2ffa40dd98ab4659 + languageName: node + linkType: hard + +"@smithy/url-parser@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/url-parser@npm:3.0.7" + dependencies: + "@smithy/querystring-parser": ^3.0.7 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: b0e4939c95de0183d90335a173d642602267070748fb95030d0949f5d113b0048c397e949b0861ed352d9c9a45221348f18a0a636d3219262da56e139232b004 + languageName: node + linkType: hard + +"@smithy/util-base64@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-base64@npm:3.0.0" + dependencies: + "@smithy/util-buffer-from": ^3.0.0 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: 413f26046a7e98b2661a078f218a8d040c820fc5a02f5e364aff58c3957e28fde1ac4048c2ebbad5d87b9da4b9aa98a8d4a7fb0d2ce97def33738bd7d8d79aa0 + languageName: node + linkType: hard + +"@smithy/util-body-length-browser@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-body-length-browser@npm:3.0.0" + dependencies: + tslib: ^2.6.2 + checksum: b01d8258b9a25b262734fc49cefefe48583ba193c3eefd49a6f7fd5922c3015d23dda88b52f3dd9a16827cad16b5b9425eef01e91bd0c71bb5abc469d2952c07 + languageName: node + linkType: hard + +"@smithy/util-body-length-node@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-body-length-node@npm:3.0.0" + dependencies: + tslib: ^2.6.2 + checksum: da1baf4790609d3dc28c88385c7274fdf9b91a641fe3c5af22b78e18156df17bd470181348f43b2c739680936b1dafb1526158dfd817c3d9ecb71e653b4cbe3f + languageName: node + linkType: hard + +"@smithy/util-buffer-from@npm:^2.2.0": + version: 2.2.0 + resolution: "@smithy/util-buffer-from@npm:2.2.0" + dependencies: + "@smithy/is-array-buffer": ^2.2.0 + tslib: ^2.6.2 + checksum: 424c5b7368ae5880a8f2732e298d17879a19ca925f24ca45e1c6c005f717bb15b76eb28174d308d81631ad457ea0088aab0fd3255dd42f45a535c81944ad64d3 + languageName: node + linkType: hard + +"@smithy/util-buffer-from@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-buffer-from@npm:3.0.0" + dependencies: + "@smithy/is-array-buffer": ^3.0.0 + tslib: ^2.6.2 + checksum: 1bfc4ab093fe98132bbc1ccd36a0b9ad75a31ed26bac4b7e9350205513a2481eb190ae44679ab4fecc5e10d367b5e6592bbfbf792671579d17d17bd7f7f233f5 + languageName: node + linkType: hard + +"@smithy/util-config-provider@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-config-provider@npm:3.0.0" + dependencies: + tslib: ^2.6.2 + checksum: fc0f5f57d30261cf3a6693d8e338b9d269332c478ee18d905309a769844188190caf0564855d7e84f6c61e56aa556195dda89f65e8c30791951cf4999e4a70e7 + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-browser@npm:^3.0.23": + version: 3.0.23 + resolution: "@smithy/util-defaults-mode-browser@npm:3.0.23" + dependencies: + "@smithy/property-provider": ^3.1.7 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + bowser: ^2.11.0 + tslib: ^2.6.2 + checksum: 8b95eddff68fa1372ef4c2b076a928bee925ca04fcfc86de3a14956f297a69cd880b51176d5b008c093244c0776b6cd8d7bd355d6cfb609d99330f3996daea31 + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-node@npm:^3.0.23": + version: 3.0.23 + resolution: "@smithy/util-defaults-mode-node@npm:3.0.23" + dependencies: + "@smithy/config-resolver": ^3.0.9 + "@smithy/credential-provider-imds": ^3.2.4 + "@smithy/node-config-provider": ^3.1.8 + "@smithy/property-provider": ^3.1.7 + "@smithy/smithy-client": ^3.4.0 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 6e961b50a1c1141d301b1fc4bd006ae9430567737a382ffd8a20db01bc8ef42fa6d38539fe7b99c2504d2b69165987d1b8cdeefd263157292608ef2ebdfb86fa + languageName: node + linkType: hard + +"@smithy/util-endpoints@npm:^2.1.3": + version: 2.1.3 + resolution: "@smithy/util-endpoints@npm:2.1.3" + dependencies: + "@smithy/node-config-provider": ^3.1.8 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 63a362e1b521a63d9f535f4cfd4e4168e08be51f4e44a406adf840427b96f7295eee9343648a51c472a8fefa603b0f3644f876bc241b0a487d05343819f7aacf + languageName: node + linkType: hard + +"@smithy/util-hex-encoding@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-hex-encoding@npm:3.0.0" + dependencies: + tslib: ^2.6.2 + checksum: dd32fd71e915825987a18bf7c0f8f0c4956d0b17a0ee71592b5563bb20e04f24dbf81d36161aac07caab3bb5e535cc609fce20aa4a38f66b457c4c6f5c7748d9 + languageName: node + linkType: hard + +"@smithy/util-middleware@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/util-middleware@npm:3.0.7" + dependencies: + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: ed1f9751d650ba5d980a39e140f50780b655b8842b3a0f9de13aa38d87e327eabc2dda1a0b8f35fa633f46cadb223669837137ab2aa01b600753a0ddca7bcbfb + languageName: node + linkType: hard + +"@smithy/util-retry@npm:^3.0.7": + version: 3.0.7 + resolution: "@smithy/util-retry@npm:3.0.7" + dependencies: + "@smithy/service-error-classification": ^3.0.7 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 8af7ed849a7db65e9229a885490cd843c3f9b35248c661d6197a31d7cc0aa33c1790734b716e80e19b569d8149b1f6d8a3dfab4d887a155e64a3ea03bd7d504d + languageName: node + linkType: hard + +"@smithy/util-stream@npm:^3.1.9": + version: 3.1.9 + resolution: "@smithy/util-stream@npm:3.1.9" + dependencies: + "@smithy/fetch-http-handler": ^3.2.9 + "@smithy/node-http-handler": ^3.2.4 + "@smithy/types": ^3.5.0 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-buffer-from": ^3.0.0 + "@smithy/util-hex-encoding": ^3.0.0 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: 4a9777742034ce0f5a3403bbe99c54c84cb26aa55ad5255346a006a574e658ae36b9d001666e931ef485614d288c76e33e35c8966b0af52e3fa6a7ac9772de8b + languageName: node + linkType: hard + +"@smithy/util-uri-escape@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-uri-escape@npm:3.0.0" + dependencies: + tslib: ^2.6.2 + checksum: d7ee01c978e2b08d0a89a3b678f5d5e5d5bb4ab4ab85567a238b1a6195dff1bdaf9ae62497e7f32ff5121b3dc007c370bcb6e8ef79b01fe5acdec5bbce8c7ce4 + languageName: node + linkType: hard + +"@smithy/util-utf8@npm:^2.0.0": + version: 2.3.0 + resolution: "@smithy/util-utf8@npm:2.3.0" + dependencies: + "@smithy/util-buffer-from": ^2.2.0 + tslib: ^2.6.2 + checksum: 00e55d4b4e37d48be0eef3599082402b933c52a1407fed7e8e8ad76d94d81a0b30b8bfaf2047c59d9c3af31e5f20e7a8c959cb7ae270f894255e05a2229964f0 + languageName: node + linkType: hard + +"@smithy/util-utf8@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-utf8@npm:3.0.0" + dependencies: + "@smithy/util-buffer-from": ^3.0.0 + tslib: ^2.6.2 + checksum: d97be1748963263a1161ba80417d82318b977b38542f3fdf0379b0162461188be680e5bfb66a89d65652f0fad6ecf2ab23a43205979216e50602488f73434da3 + languageName: node + linkType: hard + +"@smithy/util-waiter@npm:^3.1.6": + version: 3.1.6 + resolution: "@smithy/util-waiter@npm:3.1.6" + dependencies: + "@smithy/abort-controller": ^3.1.5 + "@smithy/types": ^3.5.0 + tslib: ^2.6.2 + checksum: 8375e3530c19565f98e3a6ccbf2a332939f3d01817f0d100d8fcf6033eac2233df9debef181572dce2589e76aae140a3cc713d8715d4b29f73a294a48f857575 + languageName: node + linkType: hard + +"@snyk/github-codeowners@npm:1.1.0": + version: 1.1.0 + resolution: "@snyk/github-codeowners@npm:1.1.0" + dependencies: + commander: ^4.1.1 + ignore: ^5.1.8 + p-map: ^4.0.0 + bin: + github-codeowners: dist/cli.js + checksum: 133f867fa968f96229ebce724d8aedaa124218e20add96a3a7d39ea45e52007fee50cc90c39e406c9e662483d003da9326e00dc4d612afa5c2ca069d1cdab9d7 + languageName: node + linkType: hard + +"@spotify/eslint-config-base@npm:^15.0.0": + version: 15.0.0 + resolution: "@spotify/eslint-config-base@npm:15.0.0" + peerDependencies: + eslint: ">=7.x" + checksum: 265a4d807b5236030466a3a8373f41e51a9b4939b450d47ed2cb4704485004a5d64b2f9e024e865b4f5eea61ab6bbe439442e4ca2ac06e52a3b5c7e94c2d6b27 + languageName: node + linkType: hard + +"@spotify/eslint-config-react@npm:^15.0.0": + version: 15.0.0 + resolution: "@spotify/eslint-config-react@npm:15.0.0" + peerDependencies: + eslint: ">=8.x" + eslint-plugin-jsx-a11y: 6.x + eslint-plugin-react: ">=7.7.0 <8" + eslint-plugin-react-hooks: ^4.0.0 + checksum: 42e16f63d51b2230d2e4eba6524d2d9278d480827c5d2ab32f96253bafd4d8ceb87c37d8429601e36642ff30c86b92011ad4efd26c83db4037478ad118497cce + languageName: node + linkType: hard + +"@spotify/eslint-config-typescript@npm:^15.0.0": + version: 15.0.0 + resolution: "@spotify/eslint-config-typescript@npm:15.0.0" + peerDependencies: + "@typescript-eslint/eslint-plugin": ">=5" + "@typescript-eslint/parser": ">=5" + eslint: ">=8.x" + checksum: d30d07e1e2e0e18cc583a72ca74b5fdb80ee26e6529de26e1e85d1416ca5396c942efaccc2613287365c7ac3659378b0ba0cdda3df25c7e5cdbd7317f1cbe885 + languageName: node + linkType: hard + +"@spotify/prettier-config@npm:^12.0.0": + version: 12.0.0 + resolution: "@spotify/prettier-config@npm:12.0.0" + peerDependencies: + prettier: 2.x + checksum: 04732b96af895269bb8a988ba309e80bd7b87c785837e06f72ff938e8895c5a3a3211fa37b54c6a2b502e88587a437c2be3ccb486a84aff02c2f6fb4582a4a97 + languageName: node + linkType: hard + +"@spotify/prettier-config@npm:^15.0.0": + version: 15.0.0 + resolution: "@spotify/prettier-config@npm:15.0.0" + peerDependencies: + prettier: 2.x + checksum: aa5ec5739427f9acdb9d62ae6c04f04a344898567239f7ee45c75c6205ebdffbc61747ea8de6e83baf0bc3785359967de4b7097a8723c4b4063ff57dc5cb6c44 + languageName: node + linkType: hard + +"@sqltools/formatter@npm:^1.2.5": + version: 1.2.5 + resolution: "@sqltools/formatter@npm:1.2.5" + checksum: 9b8354e715467d660daa5afe044860b5686bbb1a5cb67a60866b932effafbf5e8b429f19a8ae67cd412065a4f067161f227e182f3664a0245339d5eb1e26e355 + languageName: node + linkType: hard + +"@stoplight/better-ajv-errors@npm:1.0.3": + version: 1.0.3 + resolution: "@stoplight/better-ajv-errors@npm:1.0.3" + dependencies: + jsonpointer: ^5.0.0 + leven: ^3.1.0 + peerDependencies: + ajv: ">=8" + checksum: 642fe5636a72a86de72e4ffc7bbf07499fc09d8446b386f31d3667b07dd1849d921c38a74c109a9e2554d405b6e90dc150728a0c455bf93f158ff139e0538ddd + languageName: node + linkType: hard + +"@stoplight/json-ref-readers@npm:1.2.2": + version: 1.2.2 + resolution: "@stoplight/json-ref-readers@npm:1.2.2" + dependencies: + node-fetch: ^2.6.0 + tslib: ^1.14.1 + checksum: 31b0e78b119f7afd7dd84a4fbb0c4aaceeb6e889179e785ddb9880ee548d4d161dce5743451ef6dad4b7a902d9f0711909c87b63ad794bede234a144bcf2b2b4 + languageName: node + linkType: hard + +"@stoplight/json-ref-resolver@npm:~3.1.6": + version: 3.1.6 + resolution: "@stoplight/json-ref-resolver@npm:3.1.6" + dependencies: + "@stoplight/json": ^3.21.0 + "@stoplight/path": ^1.3.2 + "@stoplight/types": ^12.3.0 || ^13.0.0 + "@types/urijs": ^1.19.19 + dependency-graph: ~0.11.0 + fast-memoize: ^2.5.2 + immer: ^9.0.6 + lodash: ^4.17.21 + tslib: ^2.6.0 + urijs: ^1.19.11 + checksum: 57c944cc8cee51b18fd8165aae7431eddf3b6ca96f2de7a264d890f18a869e5abb7750d48a77455ee1c688ac440efa4115bc8e912efce7c83140834bae49879e + languageName: node + linkType: hard + +"@stoplight/json@npm:^3.17.0, @stoplight/json@npm:^3.17.1, @stoplight/json@npm:^3.21.0, @stoplight/json@npm:~3.21.0": + version: 3.21.7 + resolution: "@stoplight/json@npm:3.21.7" + dependencies: + "@stoplight/ordered-object-literal": ^1.0.3 + "@stoplight/path": ^1.3.2 + "@stoplight/types": ^13.6.0 + jsonc-parser: ~2.2.1 + lodash: ^4.17.21 + safe-stable-stringify: ^1.1 + checksum: 5b0cd67e91e8f4cfac7ff0fe37c07e203611f429e8af7fce51cacb82f9c97150a3fa3aeda41daa9e65bc42d217b630bf01a8bf1f6db12b047079b0da9d7cd9af + languageName: node + linkType: hard + +"@stoplight/ordered-object-literal@npm:^1.0.3, @stoplight/ordered-object-literal@npm:^1.0.5": + version: 1.0.5 + resolution: "@stoplight/ordered-object-literal@npm:1.0.5" + checksum: 84fe385ed742c5298fd5bee3f95366bfe17a2b99ed52f9b323180756d3495078dfb3bf7e5f49f3c8dee7b79f2e8358b38fe4977b7b6475f0094765160d716bb5 + languageName: node + linkType: hard + +"@stoplight/path@npm:1.3.2, @stoplight/path@npm:^1.3.2": + version: 1.3.2 + resolution: "@stoplight/path@npm:1.3.2" + checksum: 8a1143cef9edcf9fd8cb24ca3f250693d475ce1f635f0dc95e5b045aad303fbf4d702c939f0c4ed8d28a04208d1aa4471fb10912ef1e3a94a9e6810878a7cfbb + languageName: node + linkType: hard + +"@stoplight/spectral-core@npm:^1.15.1, @stoplight/spectral-core@npm:^1.18.0, @stoplight/spectral-core@npm:^1.7.0, @stoplight/spectral-core@npm:^1.8.0, @stoplight/spectral-core@npm:^1.8.1": + version: 1.19.1 + resolution: "@stoplight/spectral-core@npm:1.19.1" + dependencies: + "@stoplight/better-ajv-errors": 1.0.3 + "@stoplight/json": ~3.21.0 + "@stoplight/path": 1.3.2 + "@stoplight/spectral-parsers": ^1.0.0 + "@stoplight/spectral-ref-resolver": ^1.0.4 + "@stoplight/spectral-runtime": ^1.0.0 + "@stoplight/types": ~13.6.0 + "@types/es-aggregate-error": ^1.0.2 + "@types/json-schema": ^7.0.11 + ajv: ^8.17.1 + ajv-errors: ~3.0.0 + ajv-formats: ~2.1.0 + es-aggregate-error: ^1.0.7 + jsonpath-plus: 7.1.0 + lodash: ~4.17.21 + lodash.topath: ^4.5.2 + minimatch: 3.1.2 + nimma: 0.2.2 + pony-cause: ^1.0.0 + simple-eval: 1.0.0 + tslib: ^2.3.0 + checksum: 35495c3f72eacd02d74b0913ad5a8cdad7573ab06c08cc9f6b44abd68e0c8b2229df9efee11cfe8a47ffeea802ce2b3bb17e378dffe5eab47504f70abd8b492c + languageName: node + linkType: hard + +"@stoplight/spectral-formats@npm:^1.2.0, @stoplight/spectral-formats@npm:^1.7.0": + version: 1.7.0 + resolution: "@stoplight/spectral-formats@npm:1.7.0" + dependencies: + "@stoplight/json": ^3.17.0 + "@stoplight/spectral-core": ^1.8.0 + "@types/json-schema": ^7.0.7 + tslib: ^2.3.1 + checksum: eccc2a6c099c7cbdd7c0b6c48b7fbfa334cdc2323958790496aa0295af27ef42ccae8b40e05c742aa3431da724b8d494c837af1af60f86d05189853b95b7c2c9 + languageName: node + linkType: hard + +"@stoplight/spectral-formatters@npm:^1.1.0": + version: 1.4.0 + resolution: "@stoplight/spectral-formatters@npm:1.4.0" + dependencies: + "@stoplight/path": ^1.3.2 + "@stoplight/spectral-core": ^1.15.1 + "@stoplight/spectral-runtime": ^1.1.0 + "@stoplight/types": ^13.15.0 + "@types/markdown-escape": ^1.1.3 + chalk: 4.1.2 + cliui: 7.0.4 + lodash: ^4.17.21 + markdown-escape: ^2.0.0 + node-sarif-builder: ^2.0.3 + strip-ansi: 6.0 + text-table: ^0.2.0 + tslib: ^2.5.0 + checksum: fd8b0c96df54b1afa1e2c325edac6a95df0ce4c9a14e3cd46786d229259417eeec7795e7c7ccf4e896a343c5dbd06e62f8692214985acd786d19485bd512958d + languageName: node + linkType: hard + +"@stoplight/spectral-functions@npm:^1.5.1, @stoplight/spectral-functions@npm:^1.6.1, @stoplight/spectral-functions@npm:^1.7.2": + version: 1.9.0 + resolution: "@stoplight/spectral-functions@npm:1.9.0" + dependencies: + "@stoplight/better-ajv-errors": 1.0.3 + "@stoplight/json": ^3.17.1 + "@stoplight/spectral-core": ^1.7.0 + "@stoplight/spectral-formats": ^1.7.0 + "@stoplight/spectral-runtime": ^1.1.0 + ajv: ^8.17.1 + ajv-draft-04: ~1.0.0 + ajv-errors: ~3.0.0 + ajv-formats: ~2.1.0 + lodash: ~4.17.21 + tslib: ^2.3.0 + checksum: 278dc6e84b3b4fdef73f6b2b2cc7071140ade604dbc938b3946203253f37c0977659a609dc148df6f73668ddcb84a809e279643a12fd3f4372e72e97973f0058 + languageName: node + linkType: hard + +"@stoplight/spectral-parsers@npm:^1.0.0, @stoplight/spectral-parsers@npm:^1.0.2": + version: 1.0.4 + resolution: "@stoplight/spectral-parsers@npm:1.0.4" + dependencies: + "@stoplight/json": ~3.21.0 + "@stoplight/types": ^14.1.1 + "@stoplight/yaml": ~4.3.0 + tslib: ^2.3.1 + checksum: ca88183661651d99b40da254316fec062c219253ea3054151b9379e7c492121cdeef49a2d1ac08cd89b2f89f7d16dbc4ecf9da6d7a7539979ac6418991fe804a + languageName: node + linkType: hard + +"@stoplight/spectral-ref-resolver@npm:^1.0.4": + version: 1.0.4 + resolution: "@stoplight/spectral-ref-resolver@npm:1.0.4" + dependencies: + "@stoplight/json-ref-readers": 1.2.2 + "@stoplight/json-ref-resolver": ~3.1.6 + "@stoplight/spectral-runtime": ^1.1.2 + dependency-graph: 0.11.0 + tslib: ^2.3.1 + checksum: 1e9b2e211d2724e0bab7d817a5128f7b6cab9f0f5281d07223ace1d541a51a0eb3901b9f7b02d4b0484df1cb2a3f7239ec33a974321438d3d08ce7996fd6fcc4 + languageName: node + linkType: hard + +"@stoplight/spectral-rulesets@npm:^1.18.0": + version: 1.20.2 + resolution: "@stoplight/spectral-rulesets@npm:1.20.2" + dependencies: + "@asyncapi/specs": ^4.1.0 + "@stoplight/better-ajv-errors": 1.0.3 + "@stoplight/json": ^3.17.0 + "@stoplight/spectral-core": ^1.8.1 + "@stoplight/spectral-formats": ^1.7.0 + "@stoplight/spectral-functions": ^1.5.1 + "@stoplight/spectral-runtime": ^1.1.1 + "@stoplight/types": ^13.6.0 + "@types/json-schema": ^7.0.7 + ajv: ^8.17.1 + ajv-formats: ~2.1.0 + json-schema-traverse: ^1.0.0 + leven: 3.1.0 + lodash: ~4.17.21 + tslib: ^2.3.0 + checksum: 53b8515864f7132cd727073886adbf61fcbe39543ddf6d975799c75d9ef13d91f3940d5585c81c72da3c94365fd735d8935e885a3ac388c67d03a943af37977f + languageName: node + linkType: hard + +"@stoplight/spectral-runtime@npm:^1.0.0, @stoplight/spectral-runtime@npm:^1.1.0, @stoplight/spectral-runtime@npm:^1.1.1, @stoplight/spectral-runtime@npm:^1.1.2": + version: 1.1.2 + resolution: "@stoplight/spectral-runtime@npm:1.1.2" + dependencies: + "@stoplight/json": ^3.17.0 + "@stoplight/path": ^1.3.2 + "@stoplight/types": ^12.3.0 + abort-controller: ^3.0.0 + lodash: ^4.17.21 + node-fetch: ^2.6.7 + tslib: ^2.3.1 + checksum: 35964a38f82384e6e0158988173a50ab7f473a2ed6e942073de023bd28fb696b5b913336a84d016b046346294be9cfa3a88c6a908c2622c0ceb36f16ca76e084 + languageName: node + linkType: hard + +"@stoplight/types@npm:^12.3.0": + version: 12.5.0 + resolution: "@stoplight/types@npm:12.5.0" + dependencies: + "@types/json-schema": ^7.0.4 + utility-types: ^3.10.0 + checksum: fe4a09df6e1c2f0cdb53f474b180cc7b8184e814e1ac4427d199642f10958335f597060530a908c0e5800ba2569d077afe124a51deaee466255ce942e1e03941 + languageName: node + linkType: hard + +"@stoplight/types@npm:^12.3.0 || ^13.0.0, @stoplight/types@npm:^13.15.0, @stoplight/types@npm:^13.6.0": + version: 13.20.0 + resolution: "@stoplight/types@npm:13.20.0" + dependencies: + "@types/json-schema": ^7.0.4 + utility-types: ^3.10.0 + checksum: b4c7ee22a8d4377aa9b2f901887c17b4a27d1009b2b9348962b2c6a72100ca954d11293a6dd2de01920e8fdc589e31b20ad84421eb0bf5edd9aeef5b5810f04b + languageName: node + linkType: hard + +"@stoplight/types@npm:^14.0.0, @stoplight/types@npm:^14.1.1": + version: 14.1.1 + resolution: "@stoplight/types@npm:14.1.1" + dependencies: + "@types/json-schema": ^7.0.4 + utility-types: ^3.10.0 + checksum: 1da2e683e88afe2f72c3b3af341537bc9bac153d224f65744ca60d44eade93609ce91172064ae27093e1ebfa7bcbf05fb232a1910d83b2aee5b1eed4bb726200 + languageName: node + linkType: hard + +"@stoplight/types@npm:~13.6.0": + version: 13.6.0 + resolution: "@stoplight/types@npm:13.6.0" + dependencies: + "@types/json-schema": ^7.0.4 + utility-types: ^3.10.0 + checksum: 4cc81cf29decc0392f15c71b21fd11cd806bcf99168ae4509ed41c2b7dbcfbd5a83c7f9f320edb5a518cc483fd18dd8794c54b232fb6a6f2a7b6e9fb6ca20269 + languageName: node + linkType: hard + +"@stoplight/yaml-ast-parser@npm:0.0.50": + version: 0.0.50 + resolution: "@stoplight/yaml-ast-parser@npm:0.0.50" + checksum: dd46f2e39cef4e3a56276202872282bc435c5f92ea7cf344abd6722fbdab62547ec7d2b84983c6c05aaa2776ac29efd53affe6d9753cce10ef37b4e15ce6ccdc + languageName: node + linkType: hard + +"@stoplight/yaml@npm:~4.3.0": + version: 4.3.0 + resolution: "@stoplight/yaml@npm:4.3.0" + dependencies: + "@stoplight/ordered-object-literal": ^1.0.5 + "@stoplight/types": ^14.1.1 + "@stoplight/yaml-ast-parser": 0.0.50 + tslib: ^2.2.0 + checksum: f113f600a62b75c76c96c27ce3713ba2c48be205fca73097699b66b6f861411c6917dcc5afa4dd08c17fe63f5181b49fa2be9c6500140ea5d05a107ffcb48a4f + languageName: node + linkType: hard + +"@sucrase/webpack-loader@npm:^2.0.0": + version: 2.0.0 + resolution: "@sucrase/webpack-loader@npm:2.0.0" + dependencies: + loader-utils: ^1.1.0 + peerDependencies: + sucrase: ^3 + checksum: 16578991b1b888ac5bec5628bd24db9e21651bbbe30de076aece8787f115d8971ac87a20bc75446187c73c3185851ec2233d5b6f18c4a2dd53fbbb1ed4e488b4 + languageName: node + linkType: hard + +"@svgr/babel-plugin-add-jsx-attribute@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-plugin-add-jsx-attribute@npm:6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: cab83832830a57735329ed68f67c03b57ca21fa037b0134847b0c5c0ef4beca89956d7dacfbf7b2a10fd901e7009e877512086db2ee918b8c69aee7742ae32c0 + languageName: node + linkType: hard + +"@svgr/babel-plugin-remove-jsx-attribute@npm:*": + version: 8.0.0 + resolution: "@svgr/babel-plugin-remove-jsx-attribute@npm:8.0.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ff992893c6c4ac802713ba3a97c13be34e62e6d981c813af40daabcd676df68a72a61bd1e692bb1eda3587f1b1d700ea462222ae2153bb0f46886632d4f88d08 + languageName: node + linkType: hard + +"@svgr/babel-plugin-remove-jsx-empty-expression@npm:*": + version: 8.0.0 + resolution: "@svgr/babel-plugin-remove-jsx-empty-expression@npm:8.0.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 0fb691b63a21bac00da3aa2dccec50d0d5a5b347ff408d60803b84410d8af168f2656e4ba1ee1f24dab0ae4e4af77901f2928752bb0434c1f6788133ec599ec8 + languageName: node + linkType: hard + +"@svgr/babel-plugin-replace-jsx-attribute-value@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-plugin-replace-jsx-attribute-value@npm:6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: b7d2125758e766e1ebd14b92216b800bdc976959bc696dbfa1e28682919147c1df4bb8b1b5fd037d7a83026e27e681fea3b8d3741af8d3cf4c9dfa3d412125df + languageName: node + linkType: hard + +"@svgr/babel-plugin-svg-dynamic-title@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-plugin-svg-dynamic-title@npm:6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 0fd42ebf127ae9163ef341e84972daa99bdcb9e6ed3f83aabd95ee173fddc43e40e02fa847fbc0a1058cf5549f72b7960a2c5e22c3e4ac18f7e3ac81277852ae + languageName: node + linkType: hard + +"@svgr/babel-plugin-svg-em-dimensions@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-plugin-svg-em-dimensions@npm:6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: c1550ee9f548526fa66fd171e3ffb5696bfc4e4cd108a631d39db492c7410dc10bba4eb5a190e9df824bf806130ccc586ae7d2e43c547e6a4f93bbb29a18f344 + languageName: node + linkType: hard + +"@svgr/babel-plugin-transform-react-native-svg@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-plugin-transform-react-native-svg@npm:6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 4c924af22b948b812629e80efb90ad1ec8faae26a232d8ca8a06b46b53e966a2c415a57806a3ff0ea806a622612e546422719b69ec6839717a7755dac19171d9 + languageName: node + linkType: hard + +"@svgr/babel-plugin-transform-svg-component@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-plugin-transform-svg-component@npm:6.5.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e496bb5ee871feb6bcab250b6e067322da7dd5c9c2b530b41e5586fe090f86611339b49d0a909c334d9b24cbca0fa755c949a2526c6ad03c6b5885666874cf5f + languageName: node + linkType: hard + +"@svgr/babel-preset@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/babel-preset@npm:6.5.1" + dependencies: + "@svgr/babel-plugin-add-jsx-attribute": ^6.5.1 + "@svgr/babel-plugin-remove-jsx-attribute": "*" + "@svgr/babel-plugin-remove-jsx-empty-expression": "*" + "@svgr/babel-plugin-replace-jsx-attribute-value": ^6.5.1 + "@svgr/babel-plugin-svg-dynamic-title": ^6.5.1 + "@svgr/babel-plugin-svg-em-dimensions": ^6.5.1 + "@svgr/babel-plugin-transform-react-native-svg": ^6.5.1 + "@svgr/babel-plugin-transform-svg-component": ^6.5.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 9f124be39a8e64f909162f925b3a63ddaa5a342a5e24fc0b7f7d9d4d7f7e3b916596c754fb557dc259928399cad5366a27cb231627a0d2dcc4b13ac521cf05af + languageName: node + linkType: hard + +"@svgr/core@npm:6.5.x, @svgr/core@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/core@npm:6.5.1" + dependencies: + "@babel/core": ^7.19.6 + "@svgr/babel-preset": ^6.5.1 + "@svgr/plugin-jsx": ^6.5.1 + camelcase: ^6.2.0 + cosmiconfig: ^7.0.1 + checksum: fd6d6d5da5aeb956703310480b626c1fb3e3973ad9fe8025efc1dcf3d895f857b70d100c63cf32cebb20eb83c9607bafa464c9436e18fe6fe4fafdc73ed6b1a5 + languageName: node + linkType: hard + +"@svgr/hast-util-to-babel-ast@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/hast-util-to-babel-ast@npm:6.5.1" + dependencies: + "@babel/types": ^7.20.0 + entities: ^4.4.0 + checksum: 37923cce1b3f4e2039077b0c570b6edbabe37d1cf1a6ee35e71e0fe00f9cffac450eec45e9720b1010418131a999cb0047331ba1b6d1d2c69af1b92ac785aacf + languageName: node + linkType: hard + +"@svgr/plugin-jsx@npm:6.5.x, @svgr/plugin-jsx@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/plugin-jsx@npm:6.5.1" + dependencies: + "@babel/core": ^7.19.6 + "@svgr/babel-preset": ^6.5.1 + "@svgr/hast-util-to-babel-ast": ^6.5.1 + svg-parser: ^2.0.4 + peerDependencies: + "@svgr/core": ^6.0.0 + checksum: 42f22847a6bdf930514d7bedd3c5e1fd8d53eb3594779f9db16cb94c762425907c375cd8ec789114e100a4d38068aca6c7ab5efea4c612fba63f0630c44cc859 + languageName: node + linkType: hard + +"@svgr/plugin-svgo@npm:6.5.x, @svgr/plugin-svgo@npm:^6.5.1": + version: 6.5.1 + resolution: "@svgr/plugin-svgo@npm:6.5.1" + dependencies: + cosmiconfig: ^7.0.1 + deepmerge: ^4.2.2 + svgo: ^2.8.0 + peerDependencies: + "@svgr/core": "*" + checksum: cd2833530ac0485221adc2146fd992ab20d79f4b12eebcd45fa859721dd779483158e11dfd9a534858fe468416b9412416e25cbe07ac7932c44ed5fa2021c72e + languageName: node + linkType: hard + +"@svgr/rollup@npm:6.5.x": + version: 6.5.1 + resolution: "@svgr/rollup@npm:6.5.1" + dependencies: + "@babel/core": ^7.19.6 + "@babel/plugin-transform-react-constant-elements": ^7.18.12 + "@babel/preset-env": ^7.19.4 + "@babel/preset-react": ^7.18.6 + "@babel/preset-typescript": ^7.18.6 + "@rollup/pluginutils": ^4.2.1 + "@svgr/core": ^6.5.1 + "@svgr/plugin-jsx": ^6.5.1 + "@svgr/plugin-svgo": ^6.5.1 + checksum: 809198a655c280b434d762829aeab0c48e545daaa7a520ac87d5e7cfe96402eb4d0c01f8b25959fcc37a2ce4aa1a53c9e1c4ccb1206cd5833883a34db5799dd4 + languageName: node + linkType: hard + +"@svgr/webpack@npm:6.5.x": + version: 6.5.1 + resolution: "@svgr/webpack@npm:6.5.1" + dependencies: + "@babel/core": ^7.19.6 + "@babel/plugin-transform-react-constant-elements": ^7.18.12 + "@babel/preset-env": ^7.19.4 + "@babel/preset-react": ^7.18.6 + "@babel/preset-typescript": ^7.18.6 + "@svgr/core": ^6.5.1 + "@svgr/plugin-jsx": ^6.5.1 + "@svgr/plugin-svgo": ^6.5.1 + checksum: d10582eb4fa82a5b6d314cb49f2c640af4fd3a60f5b76095d2b14e383ef6a43a6f4674b68774a21787dbde69dec0a251cfcfc3f9a96c82754ba5d5c6daf785f0 + languageName: node + linkType: hard + +"@swc/core-darwin-arm64@npm:1.7.36": + version: 1.7.36 + resolution: "@swc/core-darwin-arm64@npm:1.7.36" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-darwin-x64@npm:1.7.36": + version: 1.7.36 + resolution: "@swc/core-darwin-x64@npm:1.7.36" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@swc/core-linux-arm-gnueabihf@npm:1.7.36": + version: 1.7.36 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.7.36" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@swc/core-linux-arm64-gnu@npm:1.7.36": + version: 1.7.36 + resolution: "@swc/core-linux-arm64-gnu@npm:1.7.36" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-arm64-musl@npm:1.7.36": + version: 1.7.36 + resolution: "@swc/core-linux-arm64-musl@npm:1.7.36" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-linux-x64-gnu@npm:1.7.36": + version: 1.7.36 + resolution: "@swc/core-linux-x64-gnu@npm:1.7.36" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@swc/core-linux-x64-musl@npm:1.7.36": + version: 1.7.36 + resolution: "@swc/core-linux-x64-musl@npm:1.7.36" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@swc/core-win32-arm64-msvc@npm:1.7.36": + version: 1.7.36 + resolution: "@swc/core-win32-arm64-msvc@npm:1.7.36" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@swc/core-win32-ia32-msvc@npm:1.7.36": + version: 1.7.36 + resolution: "@swc/core-win32-ia32-msvc@npm:1.7.36" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@swc/core-win32-x64-msvc@npm:1.7.36": + version: 1.7.36 + resolution: "@swc/core-win32-x64-msvc@npm:1.7.36" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@swc/core@npm:^1.3.46": + version: 1.7.36 + resolution: "@swc/core@npm:1.7.36" + dependencies: + "@swc/core-darwin-arm64": 1.7.36 + "@swc/core-darwin-x64": 1.7.36 + "@swc/core-linux-arm-gnueabihf": 1.7.36 + "@swc/core-linux-arm64-gnu": 1.7.36 + "@swc/core-linux-arm64-musl": 1.7.36 + "@swc/core-linux-x64-gnu": 1.7.36 + "@swc/core-linux-x64-musl": 1.7.36 + "@swc/core-win32-arm64-msvc": 1.7.36 + "@swc/core-win32-ia32-msvc": 1.7.36 + "@swc/core-win32-x64-msvc": 1.7.36 + "@swc/counter": ^0.1.3 + "@swc/types": ^0.1.13 + peerDependencies: + "@swc/helpers": "*" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 848531930b7d179b2bb9ba38e0d39dd127d66b16cc5c9ee9ec318fd50156ada18bc1368f2d02e0ba745b8f43eec0f695eaf89b6a9f8e52a239c5ab3e4b07ee27 + languageName: node + linkType: hard + +"@swc/counter@npm:^0.1.3": + version: 0.1.3 + resolution: "@swc/counter@npm:0.1.3" + checksum: df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598 + languageName: node + linkType: hard + +"@swc/helpers@npm:^0.5.0, @swc/helpers@npm:^0.5.8": + version: 0.5.13 + resolution: "@swc/helpers@npm:0.5.13" + dependencies: + tslib: ^2.4.0 + checksum: d50c2c10da6ef940af423c6b03ad9c3c94cf9de59314b1e921a7d1bcc081a6074481c9d67b655fc8fe66a73288f98b25950743792a63882bfb5793b362494fc0 + languageName: node + linkType: hard + +"@swc/jest@npm:^0.2.22": + version: 0.2.36 + resolution: "@swc/jest@npm:0.2.36" + dependencies: + "@jest/create-cache-key-function": ^29.7.0 + "@swc/counter": ^0.1.3 + jsonc-parser: ^3.2.0 + peerDependencies: + "@swc/core": "*" + checksum: 14f2e696ac093e23dae1e2e57d894bbcde4de6fe80341a26c8d0d8cbae5aae31832f8fa32dc698529f128d19a76aeedf2227f59480de6dab5eb3f30bfdf9b71a + languageName: node + linkType: hard + +"@swc/types@npm:^0.1.13": + version: 0.1.13 + resolution: "@swc/types@npm:0.1.13" + dependencies: + "@swc/counter": ^0.1.3 + checksum: 4d9ef0fba20e410bee38b20b60eeb284a1284c1cf6b5f84754b6f5e467e5e0621e2db67dc31e22c524a8d63f36d0a1d530126cd97752a85f140d91bf53553e01 + languageName: node + linkType: hard + +"@testing-library/dom@npm:^10.0.0": + version: 10.4.0 + resolution: "@testing-library/dom@npm:10.4.0" + dependencies: + "@babel/code-frame": ^7.10.4 + "@babel/runtime": ^7.12.5 + "@types/aria-query": ^5.0.1 + aria-query: 5.3.0 + chalk: ^4.1.0 + dom-accessibility-api: ^0.5.9 + lz-string: ^1.5.0 + pretty-format: ^27.0.2 + checksum: bb128b90be0c8cd78c5f5e67aa45f53de614cc048a2b50b230e736ec710805ac6c73375af354b83c74d710b3928d52b83a273a4cb89de4eb3efe49e91e706837 + languageName: node + linkType: hard + +"@testing-library/jest-dom@npm:^6.0.0": + version: 6.6.3 + resolution: "@testing-library/jest-dom@npm:6.6.3" + dependencies: + "@adobe/css-tools": ^4.4.0 + aria-query: ^5.0.0 + chalk: ^3.0.0 + css.escape: ^1.5.1 + dom-accessibility-api: ^0.6.3 + lodash: ^4.17.21 + redent: ^3.0.0 + checksum: c1dc4260b05309a0084416639006cd105849acc5b102bef682a3b19bd6fce07ff6762085fc7f2599546c995a2fc66fdb1d70e50e22a634a0098524056cc9e511 + languageName: node + linkType: hard + +"@testing-library/react-hooks@npm:8.0.1": + version: 8.0.1 + resolution: "@testing-library/react-hooks@npm:8.0.1" + dependencies: + "@babel/runtime": ^7.12.5 + react-error-boundary: ^3.1.0 + peerDependencies: + "@types/react": ^16.9.0 || ^17.0.0 + react: ^16.9.0 || ^17.0.0 + react-dom: ^16.9.0 || ^17.0.0 + react-test-renderer: ^16.9.0 || ^17.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + react-dom: + optional: true + react-test-renderer: + optional: true + checksum: 7fe44352e920deb5cb1876f80d64e48615232072c9d5382f1e0284b3aab46bb1c659a040b774c45cdf084a5257b8fe463f7e08695ad8480d8a15635d4d3d1f6d + languageName: node + linkType: hard + +"@testing-library/react@npm:^15.0.0": + version: 15.0.7 + resolution: "@testing-library/react@npm:15.0.7" + dependencies: + "@babel/runtime": ^7.12.5 + "@testing-library/dom": ^10.0.0 + "@types/react-dom": ^18.0.0 + peerDependencies: + "@types/react": ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: eb33fd82eb811bb8612aa154e430a2c1c251d5ed45a477ef57fe20095db494ea7dcfa6b1e1e2bffb0c7ee10c86e408745d95a879be8ca8fbe301bb91e5f2e5db + languageName: node + linkType: hard + +"@testing-library/user-event@npm:14.5.2": + version: 14.5.2 + resolution: "@testing-library/user-event@npm:14.5.2" + peerDependencies: + "@testing-library/dom": ">=7.21.4" + checksum: d76937dffcf0082fbf3bb89eb2b81a31bf5448048dd61c33928c5f10e33a58e035321d39145cefd469bb5a499c68a5b4086b22f1a44e3e7c7e817dc5f6782867 + languageName: node + linkType: hard + +"@tootallnate/once@npm:2": + version: 2.0.0 + resolution: "@tootallnate/once@npm:2.0.0" + checksum: ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8 + languageName: node + linkType: hard + +"@tootallnate/quickjs-emscripten@npm:^0.23.0": + version: 0.23.0 + resolution: "@tootallnate/quickjs-emscripten@npm:0.23.0" + checksum: c350a2947ffb80b22e14ff35099fd582d1340d65723384a0fd0515e905e2534459ad2f301a43279a37308a27c99273c932e64649abd57d0bb3ca8c557150eccc + languageName: node + linkType: hard + +"@trysound/sax@npm:0.2.0": + version: 0.2.0 + resolution: "@trysound/sax@npm:0.2.0" + checksum: 11226c39b52b391719a2a92e10183e4260d9651f86edced166da1d95f39a0a1eaa470e44d14ac685ccd6d3df7e2002433782872c0feeb260d61e80f21250e65c + languageName: node + linkType: hard + +"@ts-morph/common@npm:~0.24.0": + version: 0.24.0 + resolution: "@ts-morph/common@npm:0.24.0" + dependencies: + fast-glob: ^3.3.2 + minimatch: ^9.0.4 + mkdirp: ^3.0.1 + path-browserify: ^1.0.1 + checksum: 793bc8a47c93ab55c6c036f94480d3b0e948661aef4bb7dbc29279b1dda2fc4fce809a88e221537867a313541842e12d1ecbd32b4769688abe1303807ec09db6 + languageName: node + linkType: hard + +"@tsconfig/node10@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node10@npm:1.0.11" + checksum: 51fe47d55fe1b80ec35e6e5ed30a13665fd3a531945350aa74a14a1e82875fb60b350c2f2a5e72a64831b1b6bc02acb6760c30b3738b54954ec2dea82db7a267 + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node12@npm:1.0.11" + checksum: 5ce29a41b13e7897a58b8e2df11269c5395999e588b9a467386f99d1d26f6c77d1af2719e407621412520ea30517d718d5192a32403b8dfcc163bf33e40a338a + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.3 + resolution: "@tsconfig/node14@npm:1.0.3" + checksum: 19275fe80c4c8d0ad0abed6a96dbf00642e88b220b090418609c4376e1cef81bf16237bf170ad1b341452feddb8115d8dd2e5acdfdea1b27422071163dc9ba9d + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.2": + version: 1.0.4 + resolution: "@tsconfig/node16@npm:1.0.4" + checksum: 202319785901f942a6e1e476b872d421baec20cf09f4b266a1854060efbf78cde16a4d256e8bc949d31e6cd9a90f1e8ef8fb06af96a65e98338a2b6b0de0a0ff + languageName: node + linkType: hard + +"@types/argparse@npm:1.0.38": + version: 1.0.38 + resolution: "@types/argparse@npm:1.0.38" + checksum: 26ed7e3f1e3595efdb883a852f5205f971b798e4c28b7e30a32c5298eee596e8b45834ce831f014d250b9730819ab05acff5b31229666d3af4ba465b4697d0eb + languageName: node + linkType: hard + +"@types/aria-query@npm:^5.0.1": + version: 5.0.4 + resolution: "@types/aria-query@npm:5.0.4" + checksum: ad8b87e4ad64255db5f0a73bc2b4da9b146c38a3a8ab4d9306154334e0fc67ae64e76bfa298eebd1e71830591fb15987e5de7111bdb36a2221bdc379e3415fb0 + languageName: node + linkType: hard + +"@types/autosuggest-highlight@npm:3.2.3": + version: 3.2.3 + resolution: "@types/autosuggest-highlight@npm:3.2.3" + checksum: de115f7a838482e5af750f2b51249fcbe1f3473d1c31119731d392dc1dfb119d6b7275f97db737cd4d01b626d3c32d88554377ba4aa55fac8bbae73ab7881756 + languageName: node + linkType: hard + +"@types/aws-lambda@npm:^8.10.83": + version: 8.10.145 + resolution: "@types/aws-lambda@npm:8.10.145" + checksum: 4beb4febe8eb7da3087e009b4d1df61de5e9a7336792424254ca1e24740e17ee701de21423a125dcd26afb499003557e717cc824e24c47c916d2de6b0c245482 + languageName: node + linkType: hard + +"@types/babel__core@npm:^7.1.14": + version: 7.20.5 + resolution: "@types/babel__core@npm:7.20.5" + dependencies: + "@babel/parser": ^7.20.7 + "@babel/types": ^7.20.7 + "@types/babel__generator": "*" + "@types/babel__template": "*" + "@types/babel__traverse": "*" + checksum: a3226f7930b635ee7a5e72c8d51a357e799d19cbf9d445710fa39ab13804f79ab1a54b72ea7d8e504659c7dfc50675db974b526142c754398d7413aa4bc30845 + languageName: node + linkType: hard + +"@types/babel__generator@npm:*": + version: 7.6.8 + resolution: "@types/babel__generator@npm:7.6.8" + dependencies: + "@babel/types": ^7.0.0 + checksum: 5b332ea336a2efffbdeedb92b6781949b73498606ddd4205462f7d96dafd45ff3618770b41de04c4881e333dd84388bfb8afbdf6f2764cbd98be550d85c6bb48 + languageName: node + linkType: hard + +"@types/babel__template@npm:*": + version: 7.4.4 + resolution: "@types/babel__template@npm:7.4.4" + dependencies: + "@babel/parser": ^7.1.0 + "@babel/types": ^7.0.0 + checksum: d7a02d2a9b67e822694d8e6a7ddb8f2b71a1d6962dfd266554d2513eefbb205b33ca71a0d163b1caea3981ccf849211f9964d8bd0727124d18ace45aa6c9ae29 + languageName: node + linkType: hard + +"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6": + version: 7.20.6 + resolution: "@types/babel__traverse@npm:7.20.6" + dependencies: + "@babel/types": ^7.20.7 + checksum: 2bdc65eb62232c2d5c1086adeb0c31e7980e6fd7e50a3483b4a724a1a1029c84d9cb59749cf8de612f9afa2bc14c85b8f50e64e21f8a4398fa77eb9059a4283c + languageName: node + linkType: hard + +"@types/body-parser@npm:*": + version: 1.19.5 + resolution: "@types/body-parser@npm:1.19.5" + dependencies: + "@types/connect": "*" + "@types/node": "*" + checksum: 1e251118c4b2f61029cc43b0dc028495f2d1957fe8ee49a707fb940f86a9bd2f9754230805598278fe99958b49e9b7e66eec8ef6a50ab5c1f6b93e1ba2aaba82 + languageName: node + linkType: hard + +"@types/bonjour@npm:^3.5.13": + version: 3.5.13 + resolution: "@types/bonjour@npm:3.5.13" + dependencies: + "@types/node": "*" + checksum: e827570e097bd7d625a673c9c208af2d1a22fa3885c0a1646533cf24394c839c3e5f60ac1bc60c0ddcc69c0615078c9fb2c01b42596c7c582d895d974f2409ee + languageName: node + linkType: hard + +"@types/btoa-lite@npm:^1.0.0": + version: 1.0.2 + resolution: "@types/btoa-lite@npm:1.0.2" + checksum: 4c46b163c881a75522c7556dd7a7df8a0d4c680a45e8bac34e50864e1c2d9df8dc90b99f75199154c60ef2faff90896b7e5f11df6936c94167a3e5e1c6f4d935 + languageName: node + linkType: hard + +"@types/caseless@npm:*": + version: 0.12.5 + resolution: "@types/caseless@npm:0.12.5" + checksum: f6a3628add76d27005495914c9c3873a93536957edaa5b69c63b46fe10b4649a6fecf16b676c1695f46aab851da47ec6047dcf3570fa8d9b6883492ff6d074e0 + languageName: node + linkType: hard + +"@types/connect-history-api-fallback@npm:^1.5.4": + version: 1.5.4 + resolution: "@types/connect-history-api-fallback@npm:1.5.4" + dependencies: + "@types/express-serve-static-core": "*" + "@types/node": "*" + checksum: e1dee43b8570ffac02d2d47a2b4ba80d3ca0dd1840632dafb221da199e59dbe3778d3d7303c9e23c6b401f37c076935a5bc2aeae1c4e5feaefe1c371fe2073fd + languageName: node + linkType: hard + +"@types/connect@npm:*": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" + dependencies: + "@types/node": "*" + checksum: 7eb1bc5342a9604facd57598a6c62621e244822442976c443efb84ff745246b10d06e8b309b6e80130026a396f19bf6793b7cecd7380169f369dac3bfc46fb99 + languageName: node + linkType: hard + +"@types/cookie@npm:^0.4.1": + version: 0.4.1 + resolution: "@types/cookie@npm:0.4.1" + checksum: 3275534ed69a76c68eb1a77d547d75f99fedc80befb75a3d1d03662fb08d697e6f8b1274e12af1a74c6896071b11510631ba891f64d30c78528d0ec45a9c1a18 + languageName: node + linkType: hard + +"@types/cookiejar@npm:^2.1.5": + version: 2.1.5 + resolution: "@types/cookiejar@npm:2.1.5" + checksum: 04d5990e87b6387532d15a87d9ec9b2eb783039291193863751dcfd7fc723a3b3aa30ce4c06b03975cba58632e933772f1ff031af23eaa3ac7f94e71afa6e073 + languageName: node + linkType: hard + +"@types/cors@npm:^2.8.6": + version: 2.8.17 + resolution: "@types/cors@npm:2.8.17" + dependencies: + "@types/node": "*" + checksum: 469bd85e29a35977099a3745c78e489916011169a664e97c4c3d6538143b0a16e4cc72b05b407dc008df3892ed7bf595f9b7c0f1f4680e169565ee9d64966bde + languageName: node + linkType: hard + +"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.7": + version: 4.1.12 + resolution: "@types/debug@npm:4.1.12" + dependencies: + "@types/ms": "*" + checksum: 47876a852de8240bfdaf7481357af2b88cb660d30c72e73789abf00c499d6bc7cd5e52f41c915d1b9cd8ec9fef5b05688d7b7aef17f7f272c2d04679508d1053 + languageName: node + linkType: hard + +"@types/docker-modem@npm:*": + version: 3.0.6 + resolution: "@types/docker-modem@npm:3.0.6" + dependencies: + "@types/node": "*" + "@types/ssh2": "*" + checksum: cc58e8189f6ec5a2b8ca890207402178a97ddac8c80d125dc65d8ab29034b5db736de15e99b91b2d74e66d14e26e73b6b8b33216613dd15fd3aa6b82c11a83ed + languageName: node + linkType: hard + +"@types/dockerode@npm:^3.3.0, @types/dockerode@npm:^3.3.29": + version: 3.3.31 + resolution: "@types/dockerode@npm:3.3.31" + dependencies: + "@types/docker-modem": "*" + "@types/node": "*" + "@types/ssh2": "*" + checksum: f634f18dc0633f8324faefcde53bcd3d8f3c4bd74d31078cbeb65d2e1597f9abcf12c2158abfaea13dc816bae0f5fa08d0bb570d4214ab0df1ded90db5ebabfe + languageName: node + linkType: hard + +"@types/es-aggregate-error@npm:^1.0.2": + version: 1.0.6 + resolution: "@types/es-aggregate-error@npm:1.0.6" + dependencies: + "@types/node": "*" + checksum: a5b2155f664a3460d3cbc1e84e76fc0f3e751c6cebb04bf79d38e2809f44a4ba6765b83761a1e5cc0bba1b7852f7ba4fae2231110dee6218405835024dd372ac + languageName: node + linkType: hard + +"@types/eslint@npm:^8.56.10": + version: 8.56.12 + resolution: "@types/eslint@npm:8.56.12" + dependencies: + "@types/estree": "*" + "@types/json-schema": "*" + checksum: 0f7710ee02a256c499514251f527f84de964bb29487db840408e4cde79283124a38935597636d2265756c34dd1d902e1b00ae78930d4a0b55111909cb7b80d84 + languageName: node + linkType: hard + +"@types/estree@npm:*, @types/estree@npm:1.0.6, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5": + version: 1.0.6 + resolution: "@types/estree@npm:1.0.6" + checksum: 8825d6e729e16445d9a1dd2fb1db2edc5ed400799064cd4d028150701031af012ba30d6d03fe9df40f4d7a437d0de6d2b256020152b7b09bde9f2e420afdffd9 + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^5.0.0": + version: 5.0.0 + resolution: "@types/express-serve-static-core@npm:5.0.0" + dependencies: + "@types/node": "*" + "@types/qs": "*" + "@types/range-parser": "*" + "@types/send": "*" + checksum: d4e2abfc961a908098290958e43a077504ef669f3ef3c49e871932453d2281e86f5d483ae99ec3aaecd13ada0b18025a99ad5413577660587570c4e21d91c263 + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:^4.17.33, @types/express-serve-static-core@npm:^4.17.5": + version: 4.19.6 + resolution: "@types/express-serve-static-core@npm:4.19.6" + dependencies: + "@types/node": "*" + "@types/qs": "*" + "@types/range-parser": "*" + "@types/send": "*" + checksum: b0576eddc2d25ccdf10e68ba09598b87a4d7b2ad04a81dc847cb39fe56beb0b6a5cc017b1e00aa0060cb3b38e700384ce96d291a116a0f1e54895564a104aae9 + languageName: node + linkType: hard + +"@types/express@npm:*": + version: 5.0.0 + resolution: "@types/express@npm:5.0.0" + dependencies: + "@types/body-parser": "*" + "@types/express-serve-static-core": ^5.0.0 + "@types/qs": "*" + "@types/serve-static": "*" + checksum: ef68d8e2b7593c930093b1e79bf4df15413773508c9acd6a1a933ed7017f2a4892a8d128b2222d7eab9a3fa43181067a378c2600d9258bd7ae917f170e962df4 + languageName: node + linkType: hard + +"@types/express@npm:4.17.21, @types/express@npm:^4.17.21, @types/express@npm:^4.17.6": + version: 4.17.21 + resolution: "@types/express@npm:4.17.21" + dependencies: + "@types/body-parser": "*" + "@types/express-serve-static-core": ^4.17.33 + "@types/qs": "*" + "@types/serve-static": "*" + checksum: fb238298630370a7392c7abdc80f495ae6c716723e114705d7e3fb67e3850b3859bbfd29391463a3fb8c0b32051847935933d99e719c0478710f8098ee7091c5 + languageName: node + linkType: hard + +"@types/graceful-fs@npm:^4.1.3": + version: 4.1.9 + resolution: "@types/graceful-fs@npm:4.1.9" + dependencies: + "@types/node": "*" + checksum: 79d746a8f053954bba36bd3d94a90c78de995d126289d656fb3271dd9f1229d33f678da04d10bce6be440494a5a73438e2e363e92802d16b8315b051036c5256 + languageName: node + linkType: hard + +"@types/hast@npm:^2.0.0": + version: 2.3.10 + resolution: "@types/hast@npm:2.3.10" + dependencies: + "@types/unist": ^2 + checksum: 41531b7fbf590b02452996fc63272479c20a07269e370bd6514982cbcd1819b4b84d3ea620f2410d1b9541a23d08ce2eeb0a592145d05e00e249c3d56700d460 + languageName: node + linkType: hard + +"@types/hoist-non-react-statics@npm:^3.3.0, @types/hoist-non-react-statics@npm:^3.3.1": + version: 3.3.5 + resolution: "@types/hoist-non-react-statics@npm:3.3.5" + dependencies: + "@types/react": "*" + hoist-non-react-statics: ^3.3.0 + checksum: b645b062a20cce6ab1245ada8274051d8e2e0b2ee5c6bd58215281d0ec6dae2f26631af4e2e7c8abe238cdcee73fcaededc429eef569e70908f82d0cc0ea31d7 + languageName: node + linkType: hard + +"@types/html-minifier-terser@npm:^6.0.0": + version: 6.1.0 + resolution: "@types/html-minifier-terser@npm:6.1.0" + checksum: eb843f6a8d662d44fb18ec61041117734c6aae77aa38df1be3b4712e8e50ffaa35f1e1c92fdd0fde14a5675fecf457abcd0d15a01fae7506c91926176967f452 + languageName: node + linkType: hard + +"@types/http-errors@npm:*": + version: 2.0.4 + resolution: "@types/http-errors@npm:2.0.4" + checksum: 1f3d7c3b32c7524811a45690881736b3ef741bf9849ae03d32ad1ab7062608454b150a4e7f1351f83d26a418b2d65af9bdc06198f1c079d75578282884c4e8e3 + languageName: node + linkType: hard + +"@types/http-proxy@npm:^1.17.8": + version: 1.17.15 + resolution: "@types/http-proxy@npm:1.17.15" + dependencies: + "@types/node": "*" + checksum: d96eaf4e22232b587b46256b89c20525c453216684481015cf50fb385b0b319b883749ccb77dee9af57d107e8440cdacd56f4234f65176d317e9777077ff5bf3 + languageName: node + linkType: hard + +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": + version: 2.0.6 + resolution: "@types/istanbul-lib-coverage@npm:2.0.6" + checksum: 3feac423fd3e5449485afac999dcfcb3d44a37c830af898b689fadc65d26526460bedb889db278e0d4d815a670331796494d073a10ee6e3a6526301fe7415778 + languageName: node + linkType: hard + +"@types/istanbul-lib-report@npm:*": + version: 3.0.3 + resolution: "@types/istanbul-lib-report@npm:3.0.3" + dependencies: + "@types/istanbul-lib-coverage": "*" + checksum: b91e9b60f865ff08cb35667a427b70f6c2c63e88105eadd29a112582942af47ed99c60610180aa8dcc22382fa405033f141c119c69b95db78c4c709fbadfeeb4 + languageName: node + linkType: hard + +"@types/istanbul-reports@npm:^3.0.0": + version: 3.0.4 + resolution: "@types/istanbul-reports@npm:3.0.4" + dependencies: + "@types/istanbul-lib-report": "*" + checksum: 93eb18835770b3431f68ae9ac1ca91741ab85f7606f310a34b3586b5a34450ec038c3eed7ab19266635499594de52ff73723a54a72a75b9f7d6a956f01edee95 + languageName: node + linkType: hard + +"@types/jest@npm:^29.5.11": + version: 29.5.13 + resolution: "@types/jest@npm:29.5.13" + dependencies: + expect: ^29.0.0 + pretty-format: ^29.0.0 + checksum: 875ac23c2398cdcf22aa56c6ba24560f11d2afda226d4fa23936322dde6202f9fdbd2b91602af51c27ecba223d9fc3c1e33c9df7e47b3bf0e2aefc6baf13ce53 + languageName: node + linkType: hard + +"@types/js-cookie@npm:^2.2.6": + version: 2.2.7 + resolution: "@types/js-cookie@npm:2.2.7" + checksum: 851f47e94ca1fc43661d8f51614d67a613e7810c91b876d0a3b311ce72f7df800107fd02a08cb6948184e12c120b4f058edca2f50424d8798bdcffd6627281e3 + languageName: node + linkType: hard + +"@types/js-levenshtein@npm:^1.1.1": + version: 1.1.3 + resolution: "@types/js-levenshtein@npm:1.1.3" + checksum: eb338696da976925ea8448a42d775d7615a14323dceeb08909f187d0b3d3b4c1f67a1c36ef586b1c2318b70ab141bba8fc58311ba1c816711704605aec09db8b + languageName: node + linkType: hard + +"@types/js-yaml@npm:^4.0.1": + version: 4.0.9 + resolution: "@types/js-yaml@npm:4.0.9" + checksum: e5e5e49b5789a29fdb1f7d204f82de11cb9e8f6cb24ab064c616da5d6e1b3ccfbf95aa5d1498a9fbd3b9e745564e69b4a20b6c530b5a8bbb2d4eb830cda9bc69 + languageName: node + linkType: hard + +"@types/jsdom@npm:^20.0.0": + version: 20.0.1 + resolution: "@types/jsdom@npm:20.0.1" + dependencies: + "@types/node": "*" + "@types/tough-cookie": "*" + parse5: ^7.0.0 + checksum: d55402c5256ef451f93a6e3d3881f98339fe73a5ac2030588df056d6835df8367b5a857b48d27528289057e26dcdd3f502edc00cb877c79174cb3a4c7f2198c1 + languageName: node + linkType: hard + +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.11, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": + version: 7.0.15 + resolution: "@types/json-schema@npm:7.0.15" + checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98 + languageName: node + linkType: hard + +"@types/json5@npm:^0.0.29": + version: 0.0.29 + resolution: "@types/json5@npm:0.0.29" + checksum: e60b153664572116dfea673c5bda7778dbff150498f44f998e34b5886d8afc47f16799280e4b6e241c0472aef1bc36add771c569c68fc5125fc2ae519a3eb9ac + languageName: node + linkType: hard + +"@types/jsonwebtoken@npm:^9.0.0": + version: 9.0.7 + resolution: "@types/jsonwebtoken@npm:9.0.7" + dependencies: + "@types/node": "*" + checksum: 872b62e2a50ec399d695402ccddfeb5cd66a6c3d28511f27453b932b6b67eb82c2d0ecaa864939848b88b3a8276c2492647bf5707bc82a6ac7e420d3412b9047 + languageName: node + linkType: hard + +"@types/keyv@npm:^4.2.0": + version: 4.2.0 + resolution: "@types/keyv@npm:4.2.0" + dependencies: + keyv: "*" + checksum: 8713da9382b9346d664866a6cab2f91b0fd479f61379af891303a618e9a2abad6f347adc38a0850540e3f2dad278427de24e7555339264fddb04d1d17d3b50e0 + languageName: node + linkType: hard + +"@types/lodash@npm:^4.14.151": + version: 4.17.13 + resolution: "@types/lodash@npm:4.17.13" + checksum: d0bf8fbd950be71946e0076b30fd40d492293baea75f05931b6b5b906fd62583708c6229abdb95b30205ad24ce1ed2f48bc9d419364f682320edd03405cc0c7e + languageName: node + linkType: hard + +"@types/luxon@npm:^3.0.0, @types/luxon@npm:~3.4.0": + version: 3.4.2 + resolution: "@types/luxon@npm:3.4.2" + checksum: 6f92d5bd02e89f310395753506bcd9cef3a56f5940f7a50db2a2b9822bce753553ac767d143cb5b4f9ed5ddd4a84e64f89ff538082ceb4d18739af7781b56925 + languageName: node + linkType: hard + +"@types/markdown-escape@npm:^1.1.3": + version: 1.1.3 + resolution: "@types/markdown-escape@npm:1.1.3" + checksum: cb2e410993271f0ccc526190391a08344f4f602be69e06fee989d36d5886866ba9ba2184054895d0ad2a12d57b02f3ccf86d7a1fe8904be48bcc1ee61b98e32f + languageName: node + linkType: hard + +"@types/mdast@npm:^3.0.0": + version: 3.0.15 + resolution: "@types/mdast@npm:3.0.15" + dependencies: + "@types/unist": ^2 + checksum: af85042a4e3af3f879bde4059fa9e76c71cb552dffc896cdcc6cf9dc1fd38e37035c2dbd6245cfa6535b433f1f0478f5549696234ccace47a64055a10c656530 + languageName: node + linkType: hard + +"@types/methods@npm:^1.1.4": + version: 1.1.4 + resolution: "@types/methods@npm:1.1.4" + checksum: ad2a7178486f2fd167750f3eb920ab032a947ff2e26f55c86670a6038632d790b46f52e5b6ead5823f1e53fc68028f1e9ddd15cfead7903e04517c88debd72b1 + languageName: node + linkType: hard + +"@types/mime@npm:^1": + version: 1.3.5 + resolution: "@types/mime@npm:1.3.5" + checksum: e29a5f9c4776f5229d84e525b7cd7dd960b51c30a0fb9a028c0821790b82fca9f672dab56561e2acd9e8eed51d431bde52eafdfef30f643586c4162f1aecfc78 + languageName: node + linkType: hard + +"@types/ms@npm:*": + version: 0.7.34 + resolution: "@types/ms@npm:0.7.34" + checksum: f38d36e7b6edecd9badc9cf50474159e9da5fa6965a75186cceaf883278611b9df6669dc3a3cc122b7938d317b68a9e3d573d316fcb35d1be47ec9e468c6bd8a + languageName: node + linkType: hard + +"@types/node-forge@npm:^1.3.0": + version: 1.3.11 + resolution: "@types/node-forge@npm:1.3.11" + dependencies: + "@types/node": "*" + checksum: 1e86bd55b92a492eaafd75f6d01f31e7d86a5cdadd0c6bcdc0b1df4103b7f99bb75b832efd5217c7ddda5c781095dc086a868e20b9de00f5a427ddad4c296cd5 + languageName: node + linkType: hard + +"@types/node@npm:*, @types/node@npm:^22.0.0": + version: 22.7.8 + resolution: "@types/node@npm:22.7.8" + dependencies: + undici-types: ~6.19.2 + checksum: c1dd36bd0bf82588e61f82edb29a792f21ce902f90cc5485591f9fd60cec3ea9172e044bf7b1c0849e7cf3a5a01da39516db260cb65cb0b94904010e00634a1c + languageName: node + linkType: hard + +"@types/node@npm:18.19.34": + version: 18.19.34 + resolution: "@types/node@npm:18.19.34" + dependencies: + undici-types: ~5.26.4 + checksum: ae6369baa1529ec3564da29611ec7eb8ccb219080d717292151b6b899820d25290243d01c9240f11a63d1a42e47198cd6310fab67b6d17bea723221fea07b644 + languageName: node + linkType: hard + +"@types/node@npm:^12.7.1": + version: 12.20.55 + resolution: "@types/node@npm:12.20.55" + checksum: e4f86785f4092706e0d3b0edff8dca5a13b45627e4b36700acd8dfe6ad53db71928c8dee914d4276c7fd3b6ccd829aa919811c9eb708a2c8e4c6eb3701178c37 + languageName: node + linkType: hard + +"@types/node@npm:^18.11.18, @types/node@npm:^18.11.9": + version: 18.19.56 + resolution: "@types/node@npm:18.19.56" + dependencies: + undici-types: ~5.26.4 + checksum: 2660aa2e50ddb77e4ea4e29e6b917e942ca49ecbd46b6333a7aeffdffafc5d7e48f8a731b6570a61bebdd0ba8dc104a250c432bea00e70aec5a4cb6e6f2df771 + languageName: node + linkType: hard + +"@types/node@npm:^20.1.1": + version: 20.16.12 + resolution: "@types/node@npm:20.16.12" + dependencies: + undici-types: ~6.19.2 + checksum: 648b2a35713157f1d5861123c29546cf316b543afd536ff2c02234d78a99cea07d2259559efd7f58cf2c45c6a2e80c1064ff03c5b5213bb99b91a7d598b7975f + languageName: node + linkType: hard + +"@types/parse-json@npm:^4.0.0": + version: 4.0.2 + resolution: "@types/parse-json@npm:4.0.2" + checksum: 5bf62eec37c332ad10059252fc0dab7e7da730764869c980b0714777ad3d065e490627be9f40fc52f238ffa3ac4199b19de4127196910576c2fe34dd47c7a470 + languageName: node + linkType: hard + +"@types/passport@npm:^1.0.3": + version: 1.0.16 + resolution: "@types/passport@npm:1.0.16" + dependencies: + "@types/express": "*" + checksum: e4a02fa338536eb82694ea548689a7214b1ca98df6a896080daa2b6a8859db02a1e6244eeefaf6f3cc9c268239bb4a7912049a9ed86192144a65c10e55219f80 + languageName: node + linkType: hard + +"@types/prop-types@npm:*, @types/prop-types@npm:^15.0.0, @types/prop-types@npm:^15.7.12, @types/prop-types@npm:^15.7.3": + version: 15.7.13 + resolution: "@types/prop-types@npm:15.7.13" + checksum: 8935cad87c683c665d09a055919d617fe951cb3b2d5c00544e3a913f861a2bd8d2145b51c9aa6d2457d19f3107ab40784c40205e757232f6a80cc8b1c815513c + languageName: node + linkType: hard + +"@types/qs@npm:*, @types/qs@npm:^6.9.6": + version: 6.9.16 + resolution: "@types/qs@npm:6.9.16" + checksum: 2e8918150c12735630f7ee16b770c72949274938c30306025f68aaf977227f41fe0c698ed93db1099e04916d582ac5a1faf7e3c7061c8d885d9169f59a184b6c + languageName: node + linkType: hard + +"@types/range-parser@npm:*": + version: 1.2.7 + resolution: "@types/range-parser@npm:1.2.7" + checksum: 95640233b689dfbd85b8c6ee268812a732cf36d5affead89e806fe30da9a430767af8ef2cd661024fd97e19d61f3dec75af2df5e80ec3bea000019ab7028629a + languageName: node + linkType: hard + +"@types/react-dom@npm:^18": + version: 18.3.1 + resolution: "@types/react-dom@npm:18.3.1" + dependencies: + "@types/react": "*" + checksum: ad28ecce3915d30dc76adc2a1373fda1745ba429cea290e16c6628df9a05fd80b6403c8e87d78b45e6c60e51df7a67add389ab62b90070fbfdc9bda8307d9953 + languageName: node + linkType: hard + +"@types/react-redux@npm:^7.1.20": + version: 7.1.34 + resolution: "@types/react-redux@npm:7.1.34" + dependencies: + "@types/hoist-non-react-statics": ^3.3.0 + "@types/react": "*" + hoist-non-react-statics: ^3.3.0 + redux: ^4.0.0 + checksum: ba0cc5f54b91bff162cc97cf5d82d0077944e2d744c276c3c8eb896a293aba00923b513f5cd6ad717a46bf0c128a099ad697c98672202acb25143602042c8e6c + languageName: node + linkType: hard + +"@types/react-sparklines@npm:^1.7.0": + version: 1.7.5 + resolution: "@types/react-sparklines@npm:1.7.5" + dependencies: + "@types/react": "*" + checksum: e79755fb1ed504d36ca0b6aec4e7ef54eba30448a27c275ef56b55132c37761c11d693f885e248e2e8ba80f294bf9475e7d0e15ce5f5bb2a2219f07f18488409 + languageName: node + linkType: hard + +"@types/react-transition-group@npm:^4.2.0, @types/react-transition-group@npm:^4.4.10": + version: 4.4.11 + resolution: "@types/react-transition-group@npm:4.4.11" + dependencies: + "@types/react": "*" + checksum: a6e3b2e4363cb019e256ae4f19dadf9d7eb199da1a5e4109bbbf6a132821884044d332e9c74b520b1e5321a7f545502443fd1ce0b18649c8b510fa4220b0e5c2 + languageName: node + linkType: hard + +"@types/react@npm:^18": + version: 18.3.11 + resolution: "@types/react@npm:18.3.11" + dependencies: + "@types/prop-types": "*" + csstype: ^3.0.2 + checksum: 6cbf36673b64e758dd61b16c24139d015f58530e0d476777de26ba83f24b55e142fbf64e3b8f6b3c7b05ed9ba548551b2a62d9ffb0f95743d0a368646a619163 + languageName: node + linkType: hard + +"@types/request@npm:^2.47.1, @types/request@npm:^2.48.8": + version: 2.48.12 + resolution: "@types/request@npm:2.48.12" + dependencies: + "@types/caseless": "*" + "@types/node": "*" + "@types/tough-cookie": "*" + form-data: ^2.5.0 + checksum: 20dfad0a46b4249bf42f09c51fbd4d02ec6738c5152194b5c7c69bab80b00eae9cc71df4489ffa929d0968d453ef7d0823d1f98871efed563a4fdb57bf0a4c58 + languageName: node + linkType: hard + +"@types/resolve@npm:1.20.2": + version: 1.20.2 + resolution: "@types/resolve@npm:1.20.2" + checksum: 61c2cad2499ffc8eab36e3b773945d337d848d3ac6b7b0a87c805ba814bc838ef2f262fc0f109bfd8d2e0898ff8bd80ad1025f9ff64f1f71d3d4294c9f14e5f6 + languageName: node + linkType: hard + +"@types/retry@npm:0.12.2": + version: 0.12.2 + resolution: "@types/retry@npm:0.12.2" + checksum: e5675035717b39ce4f42f339657cae9637cf0c0051cf54314a6a2c44d38d91f6544be9ddc0280587789b6afd056be5d99dbe3e9f4df68c286c36321579b1bf4a + languageName: node + linkType: hard + +"@types/sarif@npm:^2.1.4": + version: 2.1.7 + resolution: "@types/sarif@npm:2.1.7" + checksum: ee5d30f5a2678091502343fba7905e85d25dbb545f920de9fc8a7c6693509b491a043168970a16325730cc0c88de54d2b6b3de0c2caa31645c8ebf558c5553af + languageName: node + linkType: hard + +"@types/semver@npm:7.5.8, @types/semver@npm:^7.5.0": + version: 7.5.8 + resolution: "@types/semver@npm:7.5.8" + checksum: ea6f5276f5b84c55921785a3a27a3cd37afee0111dfe2bcb3e03c31819c197c782598f17f0b150a69d453c9584cd14c4c4d7b9a55d2c5e6cacd4d66fdb3b3663 + languageName: node + linkType: hard + +"@types/send@npm:*": + version: 0.17.4 + resolution: "@types/send@npm:0.17.4" + dependencies: + "@types/mime": ^1 + "@types/node": "*" + checksum: cf4db48251bbb03cd6452b4de6e8e09e2d75390a92fd798eca4a803df06444adc94ed050246c94c7ed46fb97be1f63607f0e1f13c3ce83d71788b3e08640e5e0 + languageName: node + linkType: hard + +"@types/serve-index@npm:^1.9.4": + version: 1.9.4 + resolution: "@types/serve-index@npm:1.9.4" + dependencies: + "@types/express": "*" + checksum: 72727c88d54da5b13275ebfb75dcdc4aa12417bbe9da1939e017c4c5f0c906fae843aa4e0fbfe360e7ee9df2f3d388c21abfc488f77ce58693fb57809f8ded92 + languageName: node + linkType: hard + +"@types/serve-static@npm:*, @types/serve-static@npm:^1.15.5": + version: 1.15.7 + resolution: "@types/serve-static@npm:1.15.7" + dependencies: + "@types/http-errors": "*" + "@types/node": "*" + "@types/send": "*" + checksum: bbbf00dbd84719da2250a462270dc68964006e8d62f41fe3741abd94504ba3688f420a49afb2b7478921a1544d3793183ffa097c5724167da777f4e0c7f1a7d6 + languageName: node + linkType: hard + +"@types/set-cookie-parser@npm:^2.4.0": + version: 2.4.10 + resolution: "@types/set-cookie-parser@npm:2.4.10" + dependencies: + "@types/node": "*" + checksum: 105cc90c7d7deeb344858f720b58bd137356586545ac00d1a448e050bfcc0f385553ff26bc9c674bd8c2e953a458149eadb1945ee3d1eee81e6c0656236ebc0a + languageName: node + linkType: hard + +"@types/sockjs@npm:^0.3.36": + version: 0.3.36 + resolution: "@types/sockjs@npm:0.3.36" + dependencies: + "@types/node": "*" + checksum: b4b5381122465d80ea8b158537c00bc82317222d3fb31fd7229ff25b31fa89134abfbab969118da55622236bf3d8fee75759f3959908b5688991f492008f29bc + languageName: node + linkType: hard + +"@types/ssh2-streams@npm:*": + version: 0.1.12 + resolution: "@types/ssh2-streams@npm:0.1.12" + dependencies: + "@types/node": "*" + checksum: aa0aa45e40cfca34b4443dafa8d28ff49196c05c71867cbf0a8cdd5127be4d8a3840819543fcad16535653ca8b0e29217671ed6500ff1e7a3ad2442c5d1b40a6 + languageName: node + linkType: hard + +"@types/ssh2@npm:*": + version: 1.15.1 + resolution: "@types/ssh2@npm:1.15.1" + dependencies: + "@types/node": ^18.11.18 + checksum: 6a10b4da60817f2939cac18006a7ccbc6421facf2370a263072fc5290b1f5d445b385c5f309e93ce447bb33ad92dac18f562ccda20f092076da1c1a55da299fb + languageName: node + linkType: hard + +"@types/ssh2@npm:^0.5.48": + version: 0.5.52 + resolution: "@types/ssh2@npm:0.5.52" + dependencies: + "@types/node": "*" + "@types/ssh2-streams": "*" + checksum: bc1c76ac727ad73ddd59ba849cf0ea3ed2e930439e7a363aff24f04f29b74f9b1976369b869dc9a018223c9fb8ad041c09a0f07aea8cf46a8c920049188cddae + languageName: node + linkType: hard + +"@types/stack-utils@npm:^2.0.0": + version: 2.0.3 + resolution: "@types/stack-utils@npm:2.0.3" + checksum: 72576cc1522090fe497337c2b99d9838e320659ac57fa5560fcbdcbafcf5d0216c6b3a0a8a4ee4fdb3b1f5e3420aa4f6223ab57b82fef3578bec3206425c6cf5 + languageName: node + linkType: hard + +"@types/styled-jsx@npm:^2.2.8": + version: 2.2.9 + resolution: "@types/styled-jsx@npm:2.2.9" + dependencies: + "@types/react": "*" + checksum: 0e7e9bce8435116168b2470c7599b3b6ad5775c678d5dc06b64b0bc4fe369c59603c794a7298e2ca4e209aa0135f98df89793a3a0778251c1907b34198c55e9e + languageName: node + linkType: hard + +"@types/superagent@npm:*": + version: 8.1.9 + resolution: "@types/superagent@npm:8.1.9" + dependencies: + "@types/cookiejar": ^2.1.5 + "@types/methods": ^1.1.4 + "@types/node": "*" + form-data: ^4.0.0 + checksum: 530d8c2e87706315c82c8c9696500c40621de3353bc54ea9b104947f3530243abf54d0a49a6ae219d4947606a102ceb94bedfc43b9cc49f74069a18cbb3be8e2 + languageName: node + linkType: hard + +"@types/supertest@npm:2.0.16": + version: 2.0.16 + resolution: "@types/supertest@npm:2.0.16" + dependencies: + "@types/superagent": "*" + checksum: 2fc998ea698e0467cdbe3bea0ebce2027ea3a45a13e51a6cecb0435f44b486faecf99c34d8702d2d7fe033e6e09fdd2b374af52ecc8d0c69a1deec66b8c0dd52 + languageName: node + linkType: hard + +"@types/tough-cookie@npm:*": + version: 4.0.5 + resolution: "@types/tough-cookie@npm:4.0.5" + checksum: f19409d0190b179331586365912920d192733112a195e870c7f18d20ac8adb7ad0b0ff69dad430dba8bc2be09593453a719cfea92dc3bda19748fd158fe1498d + languageName: node + linkType: hard + +"@types/triple-beam@npm:^1.3.2": + version: 1.3.5 + resolution: "@types/triple-beam@npm:1.3.5" + checksum: 519b6a1b30d4571965c9706ad5400a200b94e4050feca3e7856e3ea7ac00ec9903e32e9a10e2762d0f7e472d5d03e5f4b29c16c0bd8c1f77c8876c683b2231f1 + languageName: node + linkType: hard + +"@types/unist@npm:^2, @types/unist@npm:^2.0.0": + version: 2.0.11 + resolution: "@types/unist@npm:2.0.11" + checksum: 6d436e832bc35c6dde9f056ac515ebf2b3384a1d7f63679d12358766f9b313368077402e9c1126a14d827f10370a5485e628bf61aa91117cf4fc882423191a4e + languageName: node + linkType: hard + +"@types/urijs@npm:^1.19.19": + version: 1.19.25 + resolution: "@types/urijs@npm:1.19.25" + checksum: cce3fd2845d5e143f4130134a5f6ff7e02b4dfc05f4d13c7b28a404fd9420bb8a6483a572c0662693bb18c5b3d8f814270aa75f3fd539f32fae22d005e755b5d + languageName: node + linkType: hard + +"@types/uuid@npm:^9.0.1": + version: 9.0.8 + resolution: "@types/uuid@npm:9.0.8" + checksum: b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 + languageName: node + linkType: hard + +"@types/webpack-env@npm:^1.15.2": + version: 1.18.5 + resolution: "@types/webpack-env@npm:1.18.5" + checksum: 4ca8eb4c44e1e1807c3e245442fce7aaf2816a163056de9436bbac44cc47c8bc5b1c9a330dc05748d6616431b1fb5bd5379733fb1da0b78d03c59f4ec824c184 + languageName: node + linkType: hard + +"@types/ws@npm:^8.5.10, @types/ws@npm:^8.5.3": + version: 8.5.12 + resolution: "@types/ws@npm:8.5.12" + dependencies: + "@types/node": "*" + checksum: ddefb6ad1671f70ce73b38a5f47f471d4d493864fca7c51f002a86e5993d031294201c5dced6d5018fb8905ad46888d65c7f20dd54fc165910b69f42fba9a6d0 + languageName: node + linkType: hard + +"@types/yargs-parser@npm:*": + version: 21.0.3 + resolution: "@types/yargs-parser@npm:21.0.3" + checksum: ef236c27f9432983e91432d974243e6c4cdae227cb673740320eff32d04d853eed59c92ca6f1142a335cfdc0e17cccafa62e95886a8154ca8891cc2dec4ee6fc + languageName: node + linkType: hard + +"@types/yargs@npm:^17.0.8": + version: 17.0.33 + resolution: "@types/yargs@npm:17.0.33" + dependencies: + "@types/yargs-parser": "*" + checksum: ee013f257472ab643cb0584cf3e1ff9b0c44bca1c9ba662395300a7f1a6c55fa9d41bd40ddff42d99f5d95febb3907c9ff600fbcb92dadbec22c6a76de7e1236 + languageName: node + linkType: hard + +"@typescript-eslint/eslint-plugin@npm:^6.12.0": + version: 6.21.0 + resolution: "@typescript-eslint/eslint-plugin@npm:6.21.0" + dependencies: + "@eslint-community/regexpp": ^4.5.1 + "@typescript-eslint/scope-manager": 6.21.0 + "@typescript-eslint/type-utils": 6.21.0 + "@typescript-eslint/utils": 6.21.0 + "@typescript-eslint/visitor-keys": 6.21.0 + debug: ^4.3.4 + graphemer: ^1.4.0 + ignore: ^5.2.4 + natural-compare: ^1.4.0 + semver: ^7.5.4 + ts-api-utils: ^1.0.1 + peerDependencies: + "@typescript-eslint/parser": ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 5ef2c502255e643e98051e87eb682c2a257e87afd8ec3b9f6274277615e1c2caf3131b352244cfb1987b8b2c415645eeacb9113fa841fc4c9b2ac46e8aed6efd + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:^6.7.2": + version: 6.21.0 + resolution: "@typescript-eslint/parser@npm:6.21.0" + dependencies: + "@typescript-eslint/scope-manager": 6.21.0 + "@typescript-eslint/types": 6.21.0 + "@typescript-eslint/typescript-estree": 6.21.0 + "@typescript-eslint/visitor-keys": 6.21.0 + debug: ^4.3.4 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 162fe3a867eeeffda7328bce32dae45b52283c68c8cb23258fb9f44971f761991af61f71b8c9fe1aa389e93dfe6386f8509c1273d870736c507d76dd40647b68 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/scope-manager@npm:6.21.0" + dependencies: + "@typescript-eslint/types": 6.21.0 + "@typescript-eslint/visitor-keys": 6.21.0 + checksum: 71028b757da9694528c4c3294a96cc80bc7d396e383a405eab3bc224cda7341b88e0fc292120b35d3f31f47beac69f7083196c70616434072fbcd3d3e62d3376 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:8.10.0": + version: 8.10.0 + resolution: "@typescript-eslint/scope-manager@npm:8.10.0" + dependencies: + "@typescript-eslint/types": 8.10.0 + "@typescript-eslint/visitor-keys": 8.10.0 + checksum: 3df8df342e227b80514dcc9151774dea9a71bc649204f702d5b4a1b76a54b4814c5d5a970a6a9213462dd4df0d42342796fab35549e8663d4c0e5d84bd902bba + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/type-utils@npm:6.21.0" + dependencies: + "@typescript-eslint/typescript-estree": 6.21.0 + "@typescript-eslint/utils": 6.21.0 + debug: ^4.3.4 + ts-api-utils: ^1.0.1 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 77025473f4d80acf1fafcce99c5c283e557686a61861febeba9c9913331f8a41e930bf5cd8b7a54db502a57b6eb8ea6d155cbd4f41349ed00e3d7aeb1f477ddc + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/types@npm:6.21.0" + checksum: 9501b47d7403417af95fc1fb72b2038c5ac46feac0e1598a46bcb43e56a606c387e9dcd8a2a0abe174c91b509f2d2a8078b093786219eb9a01ab2fbf9ee7b684 + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:8.10.0": + version: 8.10.0 + resolution: "@typescript-eslint/types@npm:8.10.0" + checksum: 3839fd43b0f21b432a9f6090a39d5b2254ee48c1eecf14f8f66bea0cbaba9f2f33a7fc78aea37dfe8841442332d0a8f99cc65cd2d01ca43db99550d30d6f7fe8 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/typescript-estree@npm:6.21.0" + dependencies: + "@typescript-eslint/types": 6.21.0 + "@typescript-eslint/visitor-keys": 6.21.0 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + minimatch: 9.0.3 + semver: ^7.5.4 + ts-api-utils: ^1.0.1 + peerDependenciesMeta: + typescript: + optional: true + checksum: dec02dc107c4a541e14fb0c96148f3764b92117c3b635db3a577b5a56fc48df7a556fa853fb82b07c0663b4bf2c484c9f245c28ba3e17e5cb0918ea4cab2ea21 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:8.10.0": + version: 8.10.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.10.0" + dependencies: + "@typescript-eslint/types": 8.10.0 + "@typescript-eslint/visitor-keys": 8.10.0 + debug: ^4.3.4 + fast-glob: ^3.3.2 + is-glob: ^4.0.3 + minimatch: ^9.0.4 + semver: ^7.6.0 + ts-api-utils: ^1.3.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 3fc774f51d0a891a5e09bc77f5544b6aa268abec9c01cd9ec831f92dde9c9d61a5c818ca2800c124fb5d61d40ce7ac34740b347c21ba3493e756c052084afd65 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:6.21.0, @typescript-eslint/utils@npm:^6.0.0": + version: 6.21.0 + resolution: "@typescript-eslint/utils@npm:6.21.0" + dependencies: + "@eslint-community/eslint-utils": ^4.4.0 + "@types/json-schema": ^7.0.12 + "@types/semver": ^7.5.0 + "@typescript-eslint/scope-manager": 6.21.0 + "@typescript-eslint/types": 6.21.0 + "@typescript-eslint/typescript-estree": 6.21.0 + semver: ^7.5.4 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + checksum: b129b3a4aebec8468259f4589985cb59ea808afbfdb9c54f02fad11e17d185e2bf72bb332f7c36ec3c09b31f18fc41368678b076323e6e019d06f74ee93f7bf2 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0": + version: 8.10.0 + resolution: "@typescript-eslint/utils@npm:8.10.0" + dependencies: + "@eslint-community/eslint-utils": ^4.4.0 + "@typescript-eslint/scope-manager": 8.10.0 + "@typescript-eslint/types": 8.10.0 + "@typescript-eslint/typescript-estree": 8.10.0 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + checksum: db67603baacba9cccbbc625801a44e5320bc558be846646ff9962818c64a9ab07edcfdcad98b15a3f8954d3e398e3a41f085c1ec458f7169a1ce7b3674032d59 + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/visitor-keys@npm:6.21.0" + dependencies: + "@typescript-eslint/types": 6.21.0 + eslint-visitor-keys: ^3.4.1 + checksum: 67c7e6003d5af042d8703d11538fca9d76899f0119130b373402819ae43f0bc90d18656aa7add25a24427ccf1a0efd0804157ba83b0d4e145f06107d7d1b7433 + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:8.10.0": + version: 8.10.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.10.0" + dependencies: + "@typescript-eslint/types": 8.10.0 + eslint-visitor-keys: ^3.4.3 + checksum: 0b3060a036dd3b6acacc32b1d81b3ada1ac5523cc2d16a369ecffd3ab5b389cd98802b248bf65ee8a266a166125a9e38acd7e917d4dd26044bdf2c805537b7e3 + languageName: node + linkType: hard + +"@ungap/structured-clone@npm:^1.2.0": + version: 1.2.0 + resolution: "@ungap/structured-clone@npm:1.2.0" + checksum: 4f656b7b4672f2ce6e272f2427d8b0824ed11546a601d8d5412b9d7704e83db38a8d9f402ecdf2b9063fc164af842ad0ec4a55819f621ed7e7ea4d1efcc74524 + languageName: node + linkType: hard + +"@useoptic/json-pointer-helpers@npm:0.55.1": + version: 0.55.1 + resolution: "@useoptic/json-pointer-helpers@npm:0.55.1" + dependencies: + jsonpointer: ^5.0.1 + minimatch: 9.0.3 + checksum: 874db1e25c4abecf29faf95c51d39d127ac50ee9f1ad9654babb3a0257a7c321e54312bf66214ac188e3f92f7e9c342fd81565356c0472689c120ea40465b15d + languageName: node + linkType: hard + +"@useoptic/openapi-utilities@npm:^0.55.0": + version: 0.55.1 + resolution: "@useoptic/openapi-utilities@npm:0.55.1" + dependencies: + "@useoptic/json-pointer-helpers": 0.55.1 + ajv: ^8.6.0 + ajv-errors: ~3.0.0 + ajv-formats: ~2.1.0 + chalk: ^4.1.2 + fast-deep-equal: ^3.1.3 + is-url: ^1.2.4 + js-yaml: ^4.1.0 + json-stable-stringify: ^1.0.1 + lodash.groupby: ^4.6.0 + lodash.isequal: ^4.5.0 + lodash.omit: ^4.5.0 + node-machine-id: ^1.1.12 + openapi-types: ^12.0.2 + ts-invariant: ^0.9.3 + url-join: ^4.0.1 + yaml-ast-parser: ^0.0.43 + checksum: 53f9ebc645d69cb1ede0263bb3dc2786cfb18f7e5a3da734898c3d9cc2109d581cd6238cec77e6eaae2f478da4ed134abca8255977e68fd06418966a65a9bbe6 + languageName: node + linkType: hard + +"@webassemblyjs/ast@npm:1.12.1, @webassemblyjs/ast@npm:^1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/ast@npm:1.12.1" + dependencies: + "@webassemblyjs/helper-numbers": 1.11.6 + "@webassemblyjs/helper-wasm-bytecode": 1.11.6 + checksum: 31bcc64147236bd7b1b6d29d1f419c1f5845c785e1e42dc9e3f8ca2e05a029e9393a271b84f3a5bff2a32d35f51ff59e2181a6e5f953fe88576acd6750506202 + languageName: node + linkType: hard + +"@webassemblyjs/floating-point-hex-parser@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/floating-point-hex-parser@npm:1.11.6" + checksum: 29b08758841fd8b299c7152eda36b9eb4921e9c584eb4594437b5cd90ed6b920523606eae7316175f89c20628da14326801090167cc7fbffc77af448ac84b7e2 + languageName: node + linkType: hard + +"@webassemblyjs/helper-api-error@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-api-error@npm:1.11.6" + checksum: e8563df85161096343008f9161adb138a6e8f3c2cc338d6a36011aa55eabb32f2fd138ffe63bc278d009ada001cc41d263dadd1c0be01be6c2ed99076103689f + languageName: node + linkType: hard + +"@webassemblyjs/helper-buffer@npm:1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/helper-buffer@npm:1.12.1" + checksum: c3ffb723024130308db608e86e2bdccd4868bbb62dffb0a9a1530606496f79c87f8565bd8e02805ce64912b71f1a70ee5fb00307258b0c082c3abf961d097eca + languageName: node + linkType: hard + +"@webassemblyjs/helper-numbers@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-numbers@npm:1.11.6" + dependencies: + "@webassemblyjs/floating-point-hex-parser": 1.11.6 + "@webassemblyjs/helper-api-error": 1.11.6 + "@xtuc/long": 4.2.2 + checksum: f4b562fa219f84368528339e0f8d273ad44e047a07641ffcaaec6f93e5b76fd86490a009aa91a294584e1436d74b0a01fa9fde45e333a4c657b58168b04da424 + languageName: node + linkType: hard + +"@webassemblyjs/helper-wasm-bytecode@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-wasm-bytecode@npm:1.11.6" + checksum: 3535ef4f1fba38de3475e383b3980f4bbf3de72bbb631c2b6584c7df45be4eccd62c6ff48b5edd3f1bcff275cfd605a37679ec199fc91fd0a7705d7f1e3972dc + languageName: node + linkType: hard + +"@webassemblyjs/helper-wasm-section@npm:1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/helper-wasm-section@npm:1.12.1" + dependencies: + "@webassemblyjs/ast": 1.12.1 + "@webassemblyjs/helper-buffer": 1.12.1 + "@webassemblyjs/helper-wasm-bytecode": 1.11.6 + "@webassemblyjs/wasm-gen": 1.12.1 + checksum: c19810cdd2c90ff574139b6d8c0dda254d42d168a9e5b3d353d1bc085f1d7164ccd1b3c05592a45a939c47f7e403dc8d03572bb686642f06a3d02932f6f0bc8f + languageName: node + linkType: hard + +"@webassemblyjs/ieee754@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/ieee754@npm:1.11.6" + dependencies: + "@xtuc/ieee754": ^1.2.0 + checksum: 13574b8e41f6ca39b700e292d7edf102577db5650fe8add7066a320aa4b7a7c09a5056feccac7a74eb68c10dea9546d4461412af351f13f6b24b5f32379b49de + languageName: node + linkType: hard + +"@webassemblyjs/leb128@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/leb128@npm:1.11.6" + dependencies: + "@xtuc/long": 4.2.2 + checksum: 7ea942dc9777d4b18a5ebfa3a937b30ae9e1d2ce1fee637583ed7f376334dd1d4274f813d2e250056cca803e0952def4b954913f1a3c9068bcd4ab4ee5143bf0 + languageName: node + linkType: hard + +"@webassemblyjs/utf8@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/utf8@npm:1.11.6" + checksum: 807fe5b5ce10c390cfdd93e0fb92abda8aebabb5199980681e7c3743ee3306a75729bcd1e56a3903980e96c885ee53ef901fcbaac8efdfa480f9c0dae1d08713 + languageName: node + linkType: hard + +"@webassemblyjs/wasm-edit@npm:^1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/wasm-edit@npm:1.12.1" + dependencies: + "@webassemblyjs/ast": 1.12.1 + "@webassemblyjs/helper-buffer": 1.12.1 + "@webassemblyjs/helper-wasm-bytecode": 1.11.6 + "@webassemblyjs/helper-wasm-section": 1.12.1 + "@webassemblyjs/wasm-gen": 1.12.1 + "@webassemblyjs/wasm-opt": 1.12.1 + "@webassemblyjs/wasm-parser": 1.12.1 + "@webassemblyjs/wast-printer": 1.12.1 + checksum: ae23642303f030af888d30c4ef37b08dfec7eab6851a9575a616e65d1219f880d9223913a39056dd654e49049d76e97555b285d1f7e56935047abf578cce0692 + languageName: node + linkType: hard + +"@webassemblyjs/wasm-gen@npm:1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/wasm-gen@npm:1.12.1" + dependencies: + "@webassemblyjs/ast": 1.12.1 + "@webassemblyjs/helper-wasm-bytecode": 1.11.6 + "@webassemblyjs/ieee754": 1.11.6 + "@webassemblyjs/leb128": 1.11.6 + "@webassemblyjs/utf8": 1.11.6 + checksum: 5787626bb7f0b033044471ddd00ce0c9fe1ee4584e8b73e232051e3a4c99ba1a102700d75337151c8b6055bae77eefa4548960c610a5e4a504e356bd872138ff + languageName: node + linkType: hard + +"@webassemblyjs/wasm-opt@npm:1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/wasm-opt@npm:1.12.1" + dependencies: + "@webassemblyjs/ast": 1.12.1 + "@webassemblyjs/helper-buffer": 1.12.1 + "@webassemblyjs/wasm-gen": 1.12.1 + "@webassemblyjs/wasm-parser": 1.12.1 + checksum: 0e8fa8a0645304a1e18ff40d3db5a2e9233ebaa169b19fcc651d6fc9fe2cac0ce092ddee927318015ae735d9cd9c5d97c0cafb6a51dcd2932ac73587b62df991 + languageName: node + linkType: hard + +"@webassemblyjs/wasm-parser@npm:1.12.1, @webassemblyjs/wasm-parser@npm:^1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/wasm-parser@npm:1.12.1" + dependencies: + "@webassemblyjs/ast": 1.12.1 + "@webassemblyjs/helper-api-error": 1.11.6 + "@webassemblyjs/helper-wasm-bytecode": 1.11.6 + "@webassemblyjs/ieee754": 1.11.6 + "@webassemblyjs/leb128": 1.11.6 + "@webassemblyjs/utf8": 1.11.6 + checksum: 176015de3551ac068cd4505d837414f258d9ade7442bd71efb1232fa26c9f6d7d4e11a5c816caeed389943f409af7ebff6899289a992d7a70343cb47009d21a8 + languageName: node + linkType: hard + +"@webassemblyjs/wast-printer@npm:1.12.1": + version: 1.12.1 + resolution: "@webassemblyjs/wast-printer@npm:1.12.1" + dependencies: + "@webassemblyjs/ast": 1.12.1 + "@xtuc/long": 4.2.2 + checksum: 2974b5dda8d769145ba0efd886ea94a601e61fb37114c14f9a9a7606afc23456799af652ac3052f284909bd42edc3665a76bc9b50f95f0794c053a8a1757b713 + languageName: node + linkType: hard + +"@xmldom/xmldom@npm:^0.8.3": + version: 0.8.10 + resolution: "@xmldom/xmldom@npm:0.8.10" + checksum: 4c136aec31fb3b49aaa53b6fcbfe524d02a1dc0d8e17ee35bd3bf35e9ce1344560481cd1efd086ad1a4821541482528672306d5e37cdbd187f33d7fadd3e2cf0 + languageName: node + linkType: hard + +"@xobotyi/scrollbar-width@npm:^1.9.5": + version: 1.9.5 + resolution: "@xobotyi/scrollbar-width@npm:1.9.5" + checksum: e880c8696bd6c7eedaad4e89cc7bcfcd502c22dc6c061288ffa7f5a4fe5dab4aa2358bdd68e7357bf0334dc8b56724ed9bee05e010b60d83a3bb0d855f3d886f + languageName: node + linkType: hard + +"@xtuc/ieee754@npm:^1.2.0": + version: 1.2.0 + resolution: "@xtuc/ieee754@npm:1.2.0" + checksum: ac56d4ca6e17790f1b1677f978c0c6808b1900a5b138885d3da21732f62e30e8f0d9120fcf8f6edfff5100ca902b46f8dd7c1e3f903728634523981e80e2885a + languageName: node + linkType: hard + +"@xtuc/long@npm:4.2.2": + version: 4.2.2 + resolution: "@xtuc/long@npm:4.2.2" + checksum: 8ed0d477ce3bc9c6fe2bf6a6a2cc316bb9c4127c5a7827bae947fa8ec34c7092395c5a283cc300c05b5fa01cbbfa1f938f410a7bf75db7c7846fea41949989ec + languageName: node + linkType: hard + +"@yarnpkg/lockfile@npm:^1.1.0": + version: 1.1.0 + resolution: "@yarnpkg/lockfile@npm:1.1.0" + checksum: 05b881b4866a3546861fee756e6d3812776ea47fa6eb7098f983d6d0eefa02e12b66c3fff931574120f196286a7ad4879ce02743c8bb2be36c6a576c7852083a + languageName: node + linkType: hard + +"@yarnpkg/parsers@npm:^3.0.0": + version: 3.0.2 + resolution: "@yarnpkg/parsers@npm:3.0.2" + dependencies: + js-yaml: ^3.10.0 + tslib: ^2.4.0 + checksum: fb40a87ae7c9f3fc0b2a6b7d84375d1c69ae8304daf598c089b52966bfb4ac94fbd2dcd87ed041970416e03d34359cb5ff16be5f5601f48d1f936213a8edaf4d + languageName: node + linkType: hard + +"@zxing/text-encoding@npm:0.9.0": + version: 0.9.0 + resolution: "@zxing/text-encoding@npm:0.9.0" + checksum: c23b12aee7639382e4949961304a1294776afaffa40f579e09ffecd0e5e68cf26ef3edd75009de46da8a536e571448755ca68b3e2ea707d53793c0edb2e2c34a + languageName: node + linkType: hard + +"abab@npm:^2.0.6": + version: 2.0.6 + resolution: "abab@npm:2.0.6" + checksum: 6ffc1af4ff315066c62600123990d87551ceb0aafa01e6539da77b0f5987ac7019466780bf480f1787576d4385e3690c81ccc37cfda12819bf510b8ab47e5a3e + languageName: node + linkType: hard + +"abbrev@npm:1, abbrev@npm:^1.0.0": + version: 1.1.1 + resolution: "abbrev@npm:1.1.1" + checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17 + languageName: node + linkType: hard + +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: 0e994ad2aa6575f94670d8a2149afe94465de9cedaaaac364e7fb43a40c3691c980ff74899f682f4ca58fa96b4cbd7421a015d3a6defe43a442117d7821a2f36 + languageName: node + linkType: hard + +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: ^5.0.0 + checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75 + languageName: node + linkType: hard + +"accepts@npm:^1.3.5, accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": + version: 1.3.8 + resolution: "accepts@npm:1.3.8" + dependencies: + mime-types: ~2.1.34 + negotiator: 0.6.3 + checksum: 50c43d32e7b50285ebe84b613ee4a3aa426715a7d131b65b786e2ead0fd76b6b60091b9916d3478a75f11f162628a2139991b6c03ab3f1d9ab7c86075dc8eab4 + languageName: node + linkType: hard + +"acorn-globals@npm:^7.0.0": + version: 7.0.1 + resolution: "acorn-globals@npm:7.0.1" + dependencies: + acorn: ^8.1.0 + acorn-walk: ^8.0.2 + checksum: 2a2998a547af6d0db5f0cdb90acaa7c3cbca6709010e02121fb8b8617c0fbd8bab0b869579903fde358ac78454356a14fadcc1a672ecb97b04b1c2ccba955ce8 + languageName: node + linkType: hard + +"acorn-import-attributes@npm:^1.9.5": + version: 1.9.5 + resolution: "acorn-import-attributes@npm:1.9.5" + peerDependencies: + acorn: ^8 + checksum: 1c0c49b6a244503964ae46ae850baccf306e84caf99bc2010ed6103c69a423987b07b520a6c619f075d215388bd4923eccac995886a54309eda049ab78a4be95 + languageName: node + linkType: hard + +"acorn-jsx@npm:^5.3.2": + version: 5.3.2 + resolution: "acorn-jsx@npm:5.3.2" + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: c3d3b2a89c9a056b205b69530a37b972b404ee46ec8e5b341666f9513d3163e2a4f214a71f4dfc7370f5a9c07472d2fd1c11c91c3f03d093e37637d95da98950 + languageName: node + linkType: hard + +"acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1": + version: 8.3.4 + resolution: "acorn-walk@npm:8.3.4" + dependencies: + acorn: ^8.11.0 + checksum: 4ff03f42323e7cf90f1683e08606b0f460e1e6ac263d2730e3df91c7665b6f64e696db6ea27ee4bed18c2599569be61f28a8399fa170c611161a348c402ca19c + languageName: node + linkType: hard + +"acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": + version: 8.13.0 + resolution: "acorn@npm:8.13.0" + bin: + acorn: bin/acorn + checksum: f1541f05eb5d6ff67990d1927290809b1ebb663ac96d9c7057c935cf29c5bcaba6d39f37bd007f4bb814f162f142b0f2b2dd4b14128b8fcfaf9f0508a6f05f1c + languageName: node + linkType: hard + +"address@npm:^1.0.1, address@npm:^1.1.2": + version: 1.2.2 + resolution: "address@npm:1.2.2" + checksum: ace439960c1e3564d8f523aff23a841904bf33a2a7c2e064f7f60a064194075758b9690e65bd9785692a4ef698a998c57eb74d145881a1cecab8ba658ddb1607 + languageName: node + linkType: hard + +"adm-zip@npm:^0.5.10": + version: 0.5.16 + resolution: "adm-zip@npm:0.5.16" + checksum: 1f4104f3462b99e1b34d78ccfbdcf47e533a9cc7f894cedec6cd67b06cc6ad0b3a45241d66df5471050c7abbdd67e5707e3959fc76d75176ed6101a5b2a580d5 + languageName: node + linkType: hard + +"agent-base@npm:6, agent-base@npm:^6.0.2": + version: 6.0.2 + resolution: "agent-base@npm:6.0.2" + dependencies: + debug: 4 + checksum: f52b6872cc96fd5f622071b71ef200e01c7c4c454ee68bc9accca90c98cfb39f2810e3e9aa330435835eedc8c23f4f8a15267f67c6e245d2b33757575bdac49d + languageName: node + linkType: hard + +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" + dependencies: + debug: ^4.3.4 + checksum: 51c158769c5c051482f9ca2e6e1ec085ac72b5a418a9b31b4e82fe6c0a6699adb94c1c42d246699a587b3335215037091c79e0de512c516f73b6ea844202f037 + languageName: node + linkType: hard + +"agentkeepalive@npm:^4.2.1": + version: 4.5.0 + resolution: "agentkeepalive@npm:4.5.0" + dependencies: + humanize-ms: ^1.2.1 + checksum: 13278cd5b125e51eddd5079f04d6fe0914ac1b8b91c1f3db2c1822f99ac1a7457869068997784342fe455d59daaff22e14fb7b8c3da4e741896e7e31faf92481 + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: ^2.0.0 + indent-string: ^4.0.0 + checksum: 1101a33f21baa27a2fa8e04b698271e64616b886795fd43c31068c07533c7b3facfcaf4e9e0cab3624bd88f729a592f1c901a1a229c9e490eafce411a8644b79 + languageName: node + linkType: hard + +"ajv-draft-04@npm:^1.0.0, ajv-draft-04@npm:~1.0.0": + version: 1.0.0 + resolution: "ajv-draft-04@npm:1.0.0" + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 3f11fa0e7f7359bef6608657f02ab78e9cc62b1fb7bdd860db0d00351b3863a1189c1a23b72466d2d82726cab4eb20725c76f5e7c134a89865e2bfd0e6828137 + languageName: node + linkType: hard + +"ajv-errors@npm:~3.0.0": + version: 3.0.0 + resolution: "ajv-errors@npm:3.0.0" + peerDependencies: + ajv: ^8.0.1 + checksum: f3d1610a104fa776c2f90534acbe2113842a40d5ee446062da9e956ae6de6959afc997da1e3948c47316faa225255fc2d9d97aacd0803f47998fb38156d3d03c + languageName: node + linkType: hard + +"ajv-formats-draft2019@npm:^1.6.1": + version: 1.6.1 + resolution: "ajv-formats-draft2019@npm:1.6.1" + dependencies: + punycode: ^2.1.1 + schemes: ^1.4.0 + smtp-address-parser: ^1.0.3 + uri-js: ^4.4.1 + peerDependencies: + ajv: "*" + checksum: 281f802f76defbb5821fa19e60b38c7be13d2a89d5f915ee11dc32c4be631f4785bb67595edf2657c2b05697e6cb89d50ef4c5fecbd0c814c0e05132ed3a650c + languageName: node + linkType: hard + +"ajv-formats@npm:^2.1.1, ajv-formats@npm:~2.1.0": + version: 2.1.1 + resolution: "ajv-formats@npm:2.1.1" + dependencies: + ajv: ^8.0.0 + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 4a287d937f1ebaad4683249a4c40c0fa3beed30d9ddc0adba04859026a622da0d317851316ea64b3680dc60f5c3c708105ddd5d5db8fe595d9d0207fd19f90b7 + languageName: node + linkType: hard + +"ajv-formats@npm:~3.0.1": + version: 3.0.1 + resolution: "ajv-formats@npm:3.0.1" + dependencies: + ajv: ^8.0.0 + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: f4e1fe232d67fcafc02eafe373a7a9962351e0439dd0736647ca75c93c3da23b430b6502c255ab4315410ae330d4f3013ac9fe226c40b2524ca93a58e786d086 + languageName: node + linkType: hard + +"ajv-i18n@npm:^4.2.0": + version: 4.2.0 + resolution: "ajv-i18n@npm:4.2.0" + peerDependencies: + ajv: ^8.0.0-beta.0 + checksum: 6fa53eeafb13dc11dd99119039cd1e4394c949ca3ec407b2b421b8aae20ad2acd2f615f134ab9de64a636d66c6e0df1e15dcfca50b45683ebb9044e6667735e9 + languageName: node + linkType: hard + +"ajv-keywords@npm:^3.4.1, ajv-keywords@npm:^3.5.2": + version: 3.5.2 + resolution: "ajv-keywords@npm:3.5.2" + peerDependencies: + ajv: ^6.9.1 + checksum: 7dc5e5931677a680589050f79dcbe1fefbb8fea38a955af03724229139175b433c63c68f7ae5f86cf8f65d55eb7c25f75a046723e2e58296707617ca690feae9 + languageName: node + linkType: hard + +"ajv-keywords@npm:^5.1.0": + version: 5.1.0 + resolution: "ajv-keywords@npm:5.1.0" + dependencies: + fast-deep-equal: ^3.1.3 + peerDependencies: + ajv: ^8.8.2 + checksum: c35193940b853119242c6757787f09ecf89a2c19bcd36d03ed1a615e710d19d450cb448bfda407b939aba54b002368c8bff30529cc50a0536a8e10bcce300421 + languageName: node + linkType: hard + +"ajv@npm:^6.12.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": + version: 6.12.6 + resolution: "ajv@npm:6.12.6" + dependencies: + fast-deep-equal: ^3.1.1 + fast-json-stable-stringify: ^2.0.0 + json-schema-traverse: ^0.4.1 + uri-js: ^4.2.2 + checksum: 874972efe5c4202ab0a68379481fbd3d1b5d0a7bd6d3cc21d40d3536ebff3352a2a1fabb632d4fd2cc7fe4cbdcd5ed6782084c9bbf7f32a1536d18f9da5007d4 + languageName: node + linkType: hard + +"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.12.0, ajv@npm:^8.17.1, ajv@npm:^8.6.0, ajv@npm:^8.6.3, ajv@npm:^8.9.0": + version: 8.17.1 + resolution: "ajv@npm:8.17.1" + dependencies: + fast-deep-equal: ^3.1.3 + fast-uri: ^3.0.1 + json-schema-traverse: ^1.0.0 + require-from-string: ^2.0.2 + checksum: 1797bf242cfffbaf3b870d13565bd1716b73f214bb7ada9a497063aada210200da36e3ed40237285f3255acc4feeae91b1fb183625331bad27da95973f7253d9 + languageName: node + linkType: hard + +"ajv@npm:~8.12.0": + version: 8.12.0 + resolution: "ajv@npm:8.12.0" + dependencies: + fast-deep-equal: ^3.1.1 + json-schema-traverse: ^1.0.0 + require-from-string: ^2.0.2 + uri-js: ^4.2.2 + checksum: 4dc13714e316e67537c8b31bc063f99a1d9d9a497eb4bbd55191ac0dcd5e4985bbb71570352ad6f1e76684fb6d790928f96ba3b2d4fd6e10024be9612fe3f001 + languageName: node + linkType: hard + +"ajv@npm:~8.13.0": + version: 8.13.0 + resolution: "ajv@npm:8.13.0" + dependencies: + fast-deep-equal: ^3.1.3 + json-schema-traverse: ^1.0.0 + require-from-string: ^2.0.2 + uri-js: ^4.4.1 + checksum: 6de82d0b2073e645ca3300561356ddda0234f39b35d2125a8700b650509b296f41c00ab69f53178bbe25ad688bd6ac3747ab44101f2f4bd245952e8fd6ccc3c1 + languageName: node + linkType: hard + +"ansi-colors@npm:^4.1.1, ansi-colors@npm:^4.1.3": + version: 4.1.3 + resolution: "ansi-colors@npm:4.1.3" + checksum: a9c2ec842038a1fabc7db9ece7d3177e2fe1c5dc6f0c51ecfbf5f39911427b89c00b5dc6b8bd95f82a26e9b16aaae2e83d45f060e98070ce4d1333038edceb0e + languageName: node + linkType: hard + +"ansi-escapes@npm:^4.2.1": + version: 4.3.2 + resolution: "ansi-escapes@npm:4.3.2" + dependencies: + type-fest: ^0.21.3 + checksum: 93111c42189c0a6bed9cdb4d7f2829548e943827ee8479c74d6e0b22ee127b2a21d3f8b5ca57723b8ef78ce011fbfc2784350eb2bde3ccfccf2f575fa8489815 + languageName: node + linkType: hard + +"ansi-html-community@npm:^0.0.8": + version: 0.0.8 + resolution: "ansi-html-community@npm:0.0.8" + bin: + ansi-html: bin/ansi-html + checksum: 04c568e8348a636963f915e48eaa3e01218322e1169acafdd79c384f22e5558c003f79bbc480c1563865497482817c7eed025f0653ebc17642fededa5cb42089 + languageName: node + linkType: hard + +"ansi-html@npm:^0.0.9": + version: 0.0.9 + resolution: "ansi-html@npm:0.0.9" + bin: + ansi-html: bin/ansi-html + checksum: a03754d6f66bae33938ed8bb3dd98174b7f4895ebe45226185036ed4a1388a7aaf2f2b9581608f0626432ba7add92cfc590aa6475a78bbb90d9d1e1d1af8cbe6 + languageName: node + linkType: hard + +"ansi-regex@npm:^4.1.0": + version: 4.1.1 + resolution: "ansi-regex@npm:4.1.1" + checksum: b1a6ee44cb6ecdabaa770b2ed500542714d4395d71c7e5c25baa631f680fb2ad322eb9ba697548d498a6fd366949fc8b5bfcf48d49a32803611f648005b01888 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.1.0 + resolution: "ansi-regex@npm:6.1.0" + checksum: 495834a53b0856c02acd40446f7130cb0f8284f4a39afdab20d5dc42b2e198b1196119fe887beed8f9055c4ff2055e3b2f6d4641d0be018cdfb64fedf6fc1aac + languageName: node + linkType: hard + +"ansi-styles@npm:^3.2.1": + version: 3.2.1 + resolution: "ansi-styles@npm:3.2.1" + dependencies: + color-convert: ^1.9.0 + checksum: d85ade01c10e5dd77b6c89f34ed7531da5830d2cb5882c645f330079975b716438cd7ebb81d0d6e6b4f9c577f19ae41ab55f07f19786b02f9dfd9e0377395665 + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: ^2.0.1 + checksum: 513b44c3b2105dd14cc42a19271e80f386466c4be574bccf60b627432f9198571ebf4ab1e4c3ba17347658f4ee1711c163d574248c0c1cdc2d5917a0ad582ec4 + languageName: node + linkType: hard + +"ansi-styles@npm:^5.0.0": + version: 5.2.0 + resolution: "ansi-styles@npm:5.2.0" + checksum: d7f4e97ce0623aea6bc0d90dcd28881ee04cba06c570b97fd3391bd7a268eedfd9d5e2dd4fdcbdd82b8105df5faf6f24aaedc08eaf3da898e702db5948f63469 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 + languageName: node + linkType: hard + +"any-promise@npm:^1.0.0": + version: 1.3.0 + resolution: "any-promise@npm:1.3.0" + checksum: 0ee8a9bdbe882c90464d75d1f55cf027f5458650c4bd1f0467e65aec38ccccda07ca5844969ee77ed46d04e7dded3eaceb027e8d32f385688523fe305fa7e1de + languageName: node + linkType: hard + +"anymatch@npm:^3.0.3, anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: ^3.0.0 + picomatch: ^2.0.4 + checksum: 3e044fd6d1d26545f235a9fe4d7a534e2029d8e59fa7fd9f2a6eb21230f6b5380ea1eaf55136e60cbf8e613544b3b766e7a6fa2102e2a3a117505466e3025dc2 + languageName: node + linkType: hard + +"app-root-path@npm:^3.1.0": + version: 3.1.0 + resolution: "app-root-path@npm:3.1.0" + checksum: e3db3957aee197143a0f6c75e39fe89b19e7244f28b4f2944f7276a9c526d2a7ab2d115b4b2d70a51a65a9a3ca17506690e5b36f75a068a7e5a13f8c092389ba + languageName: node + linkType: hard + +"aproba@npm:^1.0.3 || ^2.0.0": + version: 2.0.0 + resolution: "aproba@npm:2.0.0" + checksum: 5615cadcfb45289eea63f8afd064ab656006361020e1735112e346593856f87435e02d8dcc7ff0d11928bc7d425f27bc7c2a84f6c0b35ab0ff659c814c138a24 + languageName: node + linkType: hard + +"archiver-utils@npm:^5.0.0, archiver-utils@npm:^5.0.2": + version: 5.0.2 + resolution: "archiver-utils@npm:5.0.2" + dependencies: + glob: ^10.0.0 + graceful-fs: ^4.2.0 + is-stream: ^2.0.1 + lazystream: ^1.0.0 + lodash: ^4.17.15 + normalize-path: ^3.0.0 + readable-stream: ^4.0.0 + checksum: 7dc4f3001dc373bd0fa7671ebf08edf6f815cbc539c78b5478a2eaa67e52e3fc0e92f562cdef2ba016c4dcb5468d3d069eb89535c6844da4a5bb0baf08ad5720 + languageName: node + linkType: hard + +"archiver@npm:^7.0.0, archiver@npm:^7.0.1": + version: 7.0.1 + resolution: "archiver@npm:7.0.1" + dependencies: + archiver-utils: ^5.0.2 + async: ^3.2.4 + buffer-crc32: ^1.0.0 + readable-stream: ^4.0.0 + readdir-glob: ^1.1.2 + tar-stream: ^3.0.0 + zip-stream: ^6.0.1 + checksum: f93bcc00f919e0bbb6bf38fddf111d6e4d1ed34721b73cc073edd37278303a7a9f67aa4abd6fd2beb80f6c88af77f2eb4f60276343f67605e3aea404e5ad93ea + languageName: node + linkType: hard + +"are-we-there-yet@npm:^2.0.0": + version: 2.0.0 + resolution: "are-we-there-yet@npm:2.0.0" + dependencies: + delegates: ^1.0.0 + readable-stream: ^3.6.0 + checksum: 6c80b4fd04ecee6ba6e737e0b72a4b41bdc64b7d279edfc998678567ff583c8df27e27523bc789f2c99be603ffa9eaa612803da1d886962d2086e7ff6fa90c7c + languageName: node + linkType: hard + +"are-we-there-yet@npm:^3.0.0": + version: 3.0.1 + resolution: "are-we-there-yet@npm:3.0.1" + dependencies: + delegates: ^1.0.0 + readable-stream: ^3.6.0 + checksum: 52590c24860fa7173bedeb69a4c05fb573473e860197f618b9a28432ee4379049336727ae3a1f9c4cb083114601c1140cee578376164d0e651217a9843f9fe83 + languageName: node + linkType: hard + +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3" + checksum: 544af8dd3f60546d3e4aff084d451b96961d2267d668670199692f8d054f0415d86fc5497d0e641e91546f0aa920e7c29e5250e99fc89f5552a34b5d93b77f43 + languageName: node + linkType: hard + +"arg@npm:^5.0.2": + version: 5.0.2 + resolution: "arg@npm:5.0.2" + checksum: 6c69ada1a9943d332d9e5382393e897c500908d91d5cb735a01120d5f71daf1b339b7b8980cbeaba8fd1afc68e658a739746179e4315a26e8a28951ff9930078 + languageName: node + linkType: hard + +"argparse@npm:^1.0.7, argparse@npm:~1.0.9": + version: 1.0.10 + resolution: "argparse@npm:1.0.10" + dependencies: + sprintf-js: ~1.0.2 + checksum: 7ca6e45583a28de7258e39e13d81e925cfa25d7d4aacbf806a382d3c02fcb13403a07fb8aeef949f10a7cfe4a62da0e2e807b348a5980554cc28ee573ef95945 + languageName: node + linkType: hard + +"argparse@npm:^2.0.1": + version: 2.0.1 + resolution: "argparse@npm:2.0.1" + checksum: 83644b56493e89a254bae05702abf3a1101b4fa4d0ca31df1c9985275a5a5bd47b3c27b7fa0b71098d41114d8ca000e6ed90cad764b306f8a503665e4d517ced + languageName: node + linkType: hard + +"aria-query@npm:5.3.0": + version: 5.3.0 + resolution: "aria-query@npm:5.3.0" + dependencies: + dequal: ^2.0.3 + checksum: 305bd73c76756117b59aba121d08f413c7ff5e80fa1b98e217a3443fcddb9a232ee790e24e432b59ae7625aebcf4c47cb01c2cac872994f0b426f5bdfcd96ba9 + languageName: node + linkType: hard + +"aria-query@npm:^5.0.0": + version: 5.3.2 + resolution: "aria-query@npm:5.3.2" + checksum: d971175c85c10df0f6d14adfe6f1292409196114ab3c62f238e208b53103686f46cc70695a4f775b73bc65f6a09b6a092fd963c4f3a5a7d690c8fc5094925717 + languageName: node + linkType: hard + +"aria-query@npm:~5.1.3": + version: 5.1.3 + resolution: "aria-query@npm:5.1.3" + dependencies: + deep-equal: ^2.0.5 + checksum: 929ff95f02857b650fb4cbcd2f41072eee2f46159a6605ea03bf63aa572e35ffdff43d69e815ddc462e16e07de8faba3978afc2813650b4448ee18c9895d982b + languageName: node + linkType: hard + +"array-buffer-byte-length@npm:^1.0.0, array-buffer-byte-length@npm:^1.0.1": + version: 1.0.1 + resolution: "array-buffer-byte-length@npm:1.0.1" + dependencies: + call-bind: ^1.0.5 + is-array-buffer: ^3.0.4 + checksum: 53524e08f40867f6a9f35318fafe467c32e45e9c682ba67b11943e167344d2febc0f6977a17e699b05699e805c3e8f073d876f8bbf1b559ed494ad2cd0fae09e + languageName: node + linkType: hard + +"array-flatten@npm:1.1.1": + version: 1.1.1 + resolution: "array-flatten@npm:1.1.1" + checksum: a9925bf3512d9dce202112965de90c222cd59a4fbfce68a0951d25d965cf44642931f40aac72309c41f12df19afa010ecadceb07cfff9ccc1621e99d89ab5f3b + languageName: node + linkType: hard + +"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8": + version: 3.1.8 + resolution: "array-includes@npm:3.1.8" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-abstract: ^1.23.2 + es-object-atoms: ^1.0.0 + get-intrinsic: ^1.2.4 + is-string: ^1.0.7 + checksum: eb39ba5530f64e4d8acab39297c11c1c5be2a4ea188ab2b34aba5fb7224d918f77717a9d57a3e2900caaa8440e59431bdaf5c974d5212ef65d97f132e38e2d91 + languageName: node + linkType: hard + +"array-union@npm:^2.1.0": + version: 2.1.0 + resolution: "array-union@npm:2.1.0" + checksum: 5bee12395cba82da674931df6d0fea23c4aa4660cb3b338ced9f828782a65caa232573e6bf3968f23e0c5eb301764a382cef2f128b170a9dc59de0e36c39f98d + languageName: node + linkType: hard + +"array.prototype.findlast@npm:^1.2.5": + version: 1.2.5 + resolution: "array.prototype.findlast@npm:1.2.5" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-abstract: ^1.23.2 + es-errors: ^1.3.0 + es-object-atoms: ^1.0.0 + es-shim-unscopables: ^1.0.2 + checksum: 83ce4ad95bae07f136d316f5a7c3a5b911ac3296c3476abe60225bc4a17938bf37541972fcc37dd5adbc99cbb9c928c70bbbfc1c1ce549d41a415144030bb446 + languageName: node + linkType: hard + +"array.prototype.findlastindex@npm:^1.2.5": + version: 1.2.5 + resolution: "array.prototype.findlastindex@npm:1.2.5" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-abstract: ^1.23.2 + es-errors: ^1.3.0 + es-object-atoms: ^1.0.0 + es-shim-unscopables: ^1.0.2 + checksum: 2c81cff2a75deb95bf1ed89b6f5f2bfbfb882211e3b7cc59c3d6b87df774cd9d6b36949a8ae39ac476e092c1d4a4905f5ee11a86a456abb10f35f8211ae4e710 + languageName: node + linkType: hard + +"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.2": + version: 1.3.2 + resolution: "array.prototype.flat@npm:1.3.2" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + es-shim-unscopables: ^1.0.0 + checksum: 5d6b4bf102065fb3f43764bfff6feb3295d372ce89591e6005df3d0ce388527a9f03c909af6f2a973969a4d178ab232ffc9236654149173e0e187ec3a1a6b87b + languageName: node + linkType: hard + +"array.prototype.flatmap@npm:^1.3.2": + version: 1.3.2 + resolution: "array.prototype.flatmap@npm:1.3.2" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + es-shim-unscopables: ^1.0.0 + checksum: ce09fe21dc0bcd4f30271f8144083aa8c13d4639074d6c8dc82054b847c7fc9a0c97f857491f4da19d4003e507172a78f4bcd12903098adac8b9cd374f734be3 + languageName: node + linkType: hard + +"array.prototype.tosorted@npm:^1.1.4": + version: 1.1.4 + resolution: "array.prototype.tosorted@npm:1.1.4" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-abstract: ^1.23.3 + es-errors: ^1.3.0 + es-shim-unscopables: ^1.0.2 + checksum: e4142d6f556bcbb4f393c02e7dbaea9af8f620c040450c2be137c9cbbd1a17f216b9c688c5f2c08fbb038ab83f55993fa6efdd9a05881d84693c7bcb5422127a + languageName: node + linkType: hard + +"arraybuffer.prototype.slice@npm:^1.0.3": + version: 1.0.3 + resolution: "arraybuffer.prototype.slice@npm:1.0.3" + dependencies: + array-buffer-byte-length: ^1.0.1 + call-bind: ^1.0.5 + define-properties: ^1.2.1 + es-abstract: ^1.22.3 + es-errors: ^1.2.1 + get-intrinsic: ^1.2.3 + is-array-buffer: ^3.0.4 + is-shared-array-buffer: ^1.0.2 + checksum: 352259cba534dcdd969c92ab002efd2ba5025b2e3b9bead3973150edbdf0696c629d7f4b3f061c5931511e8207bdc2306da614703c820b45dabce39e3daf7e3e + languageName: node + linkType: hard + +"arrify@npm:^2.0.0": + version: 2.0.1 + resolution: "arrify@npm:2.0.1" + checksum: 067c4c1afd182806a82e4c1cb8acee16ab8b5284fbca1ce29408e6e91281c36bb5b612f6ddfbd40a0f7a7e0c75bf2696eb94c027f6e328d6e9c52465c98e4209 + languageName: node + linkType: hard + +"asap@npm:^2.0.0": + version: 2.0.6 + resolution: "asap@npm:2.0.6" + checksum: b296c92c4b969e973260e47523207cd5769abd27c245a68c26dc7a0fe8053c55bb04360237cb51cab1df52be939da77150ace99ad331fb7fb13b3423ed73ff3d + languageName: node + linkType: hard + +"asn1.js@npm:^4.10.1": + version: 4.10.1 + resolution: "asn1.js@npm:4.10.1" + dependencies: + bn.js: ^4.0.0 + inherits: ^2.0.1 + minimalistic-assert: ^1.0.0 + checksum: 9289a1a55401238755e3142511d7b8f6fc32f08c86ff68bd7100da8b6c186179dd6b14234fba2f7f6099afcd6758a816708485efe44bc5b2a6ec87d9ceeddbb5 + languageName: node + linkType: hard + +"asn1@npm:^0.2.6, asn1@npm:~0.2.3": + version: 0.2.6 + resolution: "asn1@npm:0.2.6" + dependencies: + safer-buffer: ~2.1.0 + checksum: 39f2ae343b03c15ad4f238ba561e626602a3de8d94ae536c46a4a93e69578826305366dc09fbb9b56aec39b4982a463682f259c38e59f6fa380cd72cd61e493d + languageName: node + linkType: hard + +"assert-plus@npm:1.0.0, assert-plus@npm:^1.0.0": + version: 1.0.0 + resolution: "assert-plus@npm:1.0.0" + checksum: 19b4340cb8f0e6a981c07225eacac0e9d52c2644c080198765d63398f0075f83bbc0c8e95474d54224e297555ad0d631c1dcd058adb1ddc2437b41a6b424ac64 + languageName: node + linkType: hard + +"assert@npm:^1.1.1": + version: 1.5.1 + resolution: "assert@npm:1.5.1" + dependencies: + object.assign: ^4.1.4 + util: ^0.10.4 + checksum: bfc539da97545f9b2989395d6b85be40b70649ce57464f3cc6e61f4975fb097ba0689c386f95bdb4c3ab867931e40a565c9e193ae3c02263a8e92acb17c9dc93 + languageName: node + linkType: hard + +"ast-types-flow@npm:^0.0.8": + version: 0.0.8 + resolution: "ast-types-flow@npm:0.0.8" + checksum: 0a64706609a179233aac23817837abab614f3548c252a2d3d79ea1e10c74aa28a0846e11f466cf72771b6ed8713abc094dcf8c40c3ec4207da163efa525a94a8 + languageName: node + linkType: hard + +"ast-types@npm:^0.13.4": + version: 0.13.4 + resolution: "ast-types@npm:0.13.4" + dependencies: + tslib: ^2.0.1 + checksum: 5a51f7b70588ecced3601845a0e203279ca2f5fdc184416a0a1640c93ec0a267241d6090a328e78eebb8de81f8754754e0a4f1558ba2a3d638f8ccbd0b1f0eff + languageName: node + linkType: hard + +"astring@npm:^1.8.1": + version: 1.9.0 + resolution: "astring@npm:1.9.0" + bin: + astring: bin/astring + checksum: 69ffde3643f5280c6846231a995af878a94d3eab41d1a19a86b8c15f456453f63a7982cf5dd72d270b9f50dd26763a3e1e48377c961b7df16f550132b6dba805 + languageName: node + linkType: hard + +"async-lock@npm:^1.4.1": + version: 1.4.1 + resolution: "async-lock@npm:1.4.1" + checksum: 29e70cd892932b7c202437786cedc39ff62123cb6941014739bd3cabd6106326416e9e7c21285a5d1dc042cad239a0f7ec9c44658491ee4a615fd36a21c1d10a + languageName: node + linkType: hard + +"async-retry@npm:^1.3.3": + version: 1.3.3 + resolution: "async-retry@npm:1.3.3" + dependencies: + retry: 0.13.1 + checksum: 38a7152ff7265a9321ea214b9c69e8224ab1febbdec98efbbde6e562f17ff68405569b796b1c5271f354aef8783665d29953f051f68c1fc45306e61aec82fdc4 + languageName: node + linkType: hard + +"async@npm:^2.6.4": + version: 2.6.4 + resolution: "async@npm:2.6.4" + dependencies: + lodash: ^4.17.14 + checksum: a52083fb32e1ebe1d63e5c5624038bb30be68ff07a6c8d7dfe35e47c93fc144bd8652cbec869e0ac07d57dde387aa5f1386be3559cdee799cb1f789678d88e19 + languageName: node + linkType: hard + +"async@npm:^3.2.3, async@npm:^3.2.4": + version: 3.2.6 + resolution: "async@npm:3.2.6" + checksum: ee6eb8cd8a0ab1b58bd2a3ed6c415e93e773573a91d31df9d5ef559baafa9dab37d3b096fa7993e84585cac3697b2af6ddb9086f45d3ac8cae821bb2aab65682 + languageName: node + linkType: hard + +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 7b78c451df768adba04e2d02e63e2d0bf3b07adcd6e42b4cf665cb7ce899bedd344c69a1dcbce355b5f972d597b25aaa1c1742b52cffd9caccb22f348114f6be + languageName: node + linkType: hard + +"at-least-node@npm:^1.0.0": + version: 1.0.0 + resolution: "at-least-node@npm:1.0.0" + checksum: 463e2f8e43384f1afb54bc68485c436d7622acec08b6fad269b421cb1d29cebb5af751426793d0961ed243146fe4dc983402f6d5a51b720b277818dbf6f2e49e + languageName: node + linkType: hard + +"autosuggest-highlight@npm:^3.3.4": + version: 3.3.4 + resolution: "autosuggest-highlight@npm:3.3.4" + dependencies: + remove-accents: ^0.4.2 + checksum: ec89d69770c65f4a6615d1443307553ca1d1b6d397bce4e2e488610a3c42ffc534b986424c362c84089865922c5713cba94be9fd74ed30e13bd5bbf47162c61d + languageName: node + linkType: hard + +"available-typed-arrays@npm:^1.0.7": + version: 1.0.7 + resolution: "available-typed-arrays@npm:1.0.7" + dependencies: + possible-typed-array-names: ^1.0.0 + checksum: 1aa3ffbfe6578276996de660848b6e95669d9a95ad149e3dd0c0cda77db6ee1dbd9d1dd723b65b6d277b882dd0c4b91a654ae9d3cf9e1254b7e93e4908d78fd3 + languageName: node + linkType: hard + +"await-lock@npm:^2.0.1": + version: 2.2.2 + resolution: "await-lock@npm:2.2.2" + checksum: feb11f36768a8545879ed2d214b46aae484e6564ffa466af9212d5782897203770795cae01f813de04a46f66c0b8ee6bc690a0c435b04e00cad5a18ef0842e25 + languageName: node + linkType: hard + +"aws-sign2@npm:~0.7.0": + version: 0.7.0 + resolution: "aws-sign2@npm:0.7.0" + checksum: b148b0bb0778098ad8cf7e5fc619768bcb51236707ca1d3e5b49e41b171166d8be9fdc2ea2ae43d7decf02989d0aaa3a9c4caa6f320af95d684de9b548a71525 + languageName: node + linkType: hard + +"aws-ssl-profiles@npm:^1.1.1": + version: 1.1.2 + resolution: "aws-ssl-profiles@npm:1.1.2" + checksum: bfcf9b2cbb9788e24e4af39a1e8b4a8afbdf65773b6a1636e643c10068ffbae46b28277c4c9f9321179400b2080092db040ba4513f9143f22d1bb053a12dab2b + languageName: node + linkType: hard + +"aws4@npm:^1.8.0": + version: 1.13.2 + resolution: "aws4@npm:1.13.2" + checksum: 9ac924e4a91c088b4928ea86b68d8c4558b0e6289ccabaae0e3e96a611bd75277c2eab6e3965821028768700516f612b929a5ce822f33a8771f74ba2a8cedb9c + languageName: node + linkType: hard + +"axe-core@npm:^4.10.0": + version: 4.10.1 + resolution: "axe-core@npm:4.10.1" + checksum: 1e71bc4b7cdad6e99dad9e4098a174932ed69052e7400e0fb57b585fff1764cc541580db375c643755250da7d68f811ba05fe0636c31d4238aa16e3f31587869 + languageName: node + linkType: hard + +"axios@npm:1.7.7, axios@npm:^1.7.4, axios@npm:^1.7.7": + version: 1.7.7 + resolution: "axios@npm:1.7.7" + dependencies: + follow-redirects: ^1.15.6 + form-data: ^4.0.0 + proxy-from-env: ^1.1.0 + checksum: 882d4fe0ec694a07c7f5c1f68205eb6dc5a62aecdb632cc7a4a3d0985188ce3030e0b277e1a8260ac3f194d314ae342117660a151fabffdc5081ca0b5a8b47fe + languageName: node + linkType: hard + +"axobject-query@npm:^4.1.0": + version: 4.1.0 + resolution: "axobject-query@npm:4.1.0" + checksum: 7d1e87bf0aa7ae7a76cd39ab627b7c48fda3dc40181303d9adce4ba1d5b5ce73b5e5403ee6626ec8e91090448c887294d6144e24b6741a976f5be9347e3ae1df + languageName: node + linkType: hard + +"b4a@npm:^1.6.4": + version: 1.6.7 + resolution: "b4a@npm:1.6.7" + checksum: afe4e239b49c0ef62236fe0d788ac9bd9d7eac7e9855b0d1835593cd0efcc7be394f9cc28a747a2ed2cdcb0a48c3528a551a196f472eb625457c711169c9efa2 + languageName: node + linkType: hard + +"babel-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "babel-jest@npm:29.7.0" + dependencies: + "@jest/transform": ^29.7.0 + "@types/babel__core": ^7.1.14 + babel-plugin-istanbul: ^6.1.1 + babel-preset-jest: ^29.6.3 + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + slash: ^3.0.0 + peerDependencies: + "@babel/core": ^7.8.0 + checksum: ee6f8e0495afee07cac5e4ee167be705c711a8cc8a737e05a587a131fdae2b3c8f9aa55dfd4d9c03009ac2d27f2de63d8ba96d3e8460da4d00e8af19ef9a83f7 + languageName: node + linkType: hard + +"babel-plugin-istanbul@npm:^6.1.1": + version: 6.1.1 + resolution: "babel-plugin-istanbul@npm:6.1.1" + dependencies: + "@babel/helper-plugin-utils": ^7.0.0 + "@istanbuljs/load-nyc-config": ^1.0.0 + "@istanbuljs/schema": ^0.1.2 + istanbul-lib-instrument: ^5.0.4 + test-exclude: ^6.0.0 + checksum: cb4fd95738219f232f0aece1116628cccff16db891713c4ccb501cddbbf9272951a5df81f2f2658dfdf4b3e7b236a9d5cbcf04d5d8c07dd5077297339598061a + languageName: node + linkType: hard + +"babel-plugin-jest-hoist@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-plugin-jest-hoist@npm:29.6.3" + dependencies: + "@babel/template": ^7.3.3 + "@babel/types": ^7.3.3 + "@types/babel__core": ^7.1.14 + "@types/babel__traverse": ^7.0.6 + checksum: 51250f22815a7318f17214a9d44650ba89551e6d4f47a2dc259128428324b52f5a73979d010cefd921fd5a720d8c1d55ad74ff601cd94c7bd44d5f6292fde2d1 + languageName: node + linkType: hard + +"babel-plugin-macros@npm:^3.1.0": + version: 3.1.0 + resolution: "babel-plugin-macros@npm:3.1.0" + dependencies: + "@babel/runtime": ^7.12.5 + cosmiconfig: ^7.0.0 + resolve: ^1.19.0 + checksum: 765de4abebd3e4688ebdfbff8571ddc8cd8061f839bb6c3e550b0344a4027b04c60491f843296ce3f3379fb356cc873d57a9ee6694262547eb822c14a25be9a6 + languageName: node + linkType: hard + +"babel-plugin-polyfill-corejs2@npm:^0.4.10": + version: 0.4.11 + resolution: "babel-plugin-polyfill-corejs2@npm:0.4.11" + dependencies: + "@babel/compat-data": ^7.22.6 + "@babel/helper-define-polyfill-provider": ^0.6.2 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: f098353ce7c7dde1a1d2710858e01b471e85689110c9e37813e009072347eb8c55d5f84d20d3bf1cab31755f20078ba90f8855fdc4686a9daa826a95ff280bd7 + languageName: node + linkType: hard + +"babel-plugin-polyfill-corejs3@npm:^0.10.6": + version: 0.10.6 + resolution: "babel-plugin-polyfill-corejs3@npm:0.10.6" + dependencies: + "@babel/helper-define-polyfill-provider": ^0.6.2 + core-js-compat: ^3.38.0 + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: f762f29f7acca576897c63149c850f0a72babd3fb9ea436a2e36f0c339161c4b912a77828541d8188ce8a91e50965c6687120cf36071eabb1b7aa92f279e2164 + languageName: node + linkType: hard + +"babel-plugin-polyfill-regenerator@npm:^0.6.1": + version: 0.6.2 + resolution: "babel-plugin-polyfill-regenerator@npm:0.6.2" + dependencies: + "@babel/helper-define-polyfill-provider": ^0.6.2 + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 150233571072b6b3dfe946242da39cba8587b7f908d1c006f7545fc88b0e3c3018d445739beb61e7a75835f0c2751dbe884a94ff9b245ec42369d9267e0e1b3f + languageName: node + linkType: hard + +"babel-preset-current-node-syntax@npm:^1.0.0": + version: 1.1.0 + resolution: "babel-preset-current-node-syntax@npm:1.1.0" + dependencies: + "@babel/plugin-syntax-async-generators": ^7.8.4 + "@babel/plugin-syntax-bigint": ^7.8.3 + "@babel/plugin-syntax-class-properties": ^7.12.13 + "@babel/plugin-syntax-class-static-block": ^7.14.5 + "@babel/plugin-syntax-import-attributes": ^7.24.7 + "@babel/plugin-syntax-import-meta": ^7.10.4 + "@babel/plugin-syntax-json-strings": ^7.8.3 + "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + "@babel/plugin-syntax-numeric-separator": ^7.10.4 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + "@babel/plugin-syntax-private-property-in-object": ^7.14.5 + "@babel/plugin-syntax-top-level-await": ^7.14.5 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 9f93fac975eaba296c436feeca1031ca0539143c4066eaf5d1ba23525a31850f03b651a1049caea7287df837a409588c8252c15627ad3903f17864c8e25ed64b + languageName: node + linkType: hard + +"babel-preset-jest@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-preset-jest@npm:29.6.3" + dependencies: + babel-plugin-jest-hoist: ^29.6.3 + babel-preset-current-node-syntax: ^1.0.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: aa4ff2a8a728d9d698ed521e3461a109a1e66202b13d3494e41eea30729a5e7cc03b3a2d56c594423a135429c37bf63a9fa8b0b9ce275298be3095a88c69f6fb + languageName: node + linkType: hard + +"bail@npm:^2.0.0": + version: 2.0.2 + resolution: "bail@npm:2.0.2" + checksum: aab4e8ccdc8d762bf3fdfce8e706601695620c0c2eda256dd85088dc0be3cfd7ff126f6e99c2bee1f24f5d418414aacf09d7f9702f16d6963df2fa488cda8824 + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 + languageName: node + linkType: hard + +"bare-events@npm:^2.0.0, bare-events@npm:^2.2.0": + version: 2.5.0 + resolution: "bare-events@npm:2.5.0" + checksum: 5aa10716e7f33c5dfc471fd657eee2a33f2db0f78b3c83b5cdd1a45a7e7871114a69460ea96cd838807c55eb470b9e53dd0dfda8c83cced1352cc8253cebff48 + languageName: node + linkType: hard + +"bare-fs@npm:^2.1.1": + version: 2.3.5 + resolution: "bare-fs@npm:2.3.5" + dependencies: + bare-events: ^2.0.0 + bare-path: ^2.0.0 + bare-stream: ^2.0.0 + checksum: 071b1dff94a213eaf0b41693953959bf10af2deade597a56ff206a5d833579d56bc8530aa4614bb88bf39fd6d52f2404f7c36af4695109ffa756a13837ac3d91 + languageName: node + linkType: hard + +"bare-os@npm:^2.1.0": + version: 2.4.4 + resolution: "bare-os@npm:2.4.4" + checksum: e90088a7dc0307c020350a28df8ec5564cae5a4b7a213d8509d70831d7064308e2ed31de801b68f474cb004ad3a0a66bd28c38374d270484d9025ee71af20396 + languageName: node + linkType: hard + +"bare-path@npm:^2.0.0, bare-path@npm:^2.1.0": + version: 2.1.3 + resolution: "bare-path@npm:2.1.3" + dependencies: + bare-os: ^2.1.0 + checksum: 20301aeb05b735852a396515464908e51e896922c3bb353ef2a09ff54e81ced94e6ad857bb0a36d2ce659c42bd43dd5c3d5643edd8faaf910ee9950c4e137b88 + languageName: node + linkType: hard + +"bare-stream@npm:^2.0.0": + version: 2.3.1 + resolution: "bare-stream@npm:2.3.1" + dependencies: + streamx: ^2.20.0 + checksum: d4f7a303f8b0ffcbfbd52d581ff0e9687590217d6c37cc3224b8da91050ddc4d4037ae062e0fb86b883d73fb2bd81c1c33b0a006d4811fc031fdb9804141ae55 + languageName: node + linkType: hard + +"base64-js@npm:^1.0.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 + languageName: node + linkType: hard + +"base64-stream@npm:^1.0.0": + version: 1.0.0 + resolution: "base64-stream@npm:1.0.0" + checksum: 45ee0ffaa30350e21f7bd58eedeeeb4567297e2537eac71000e00cc38be8578bdaa7fda59c30302dc9ed58c18b235e440207425abb81bd89de9a3ef79348921b + languageName: node + linkType: hard + +"basic-auth@npm:~2.0.1": + version: 2.0.1 + resolution: "basic-auth@npm:2.0.1" + dependencies: + safe-buffer: 5.1.2 + checksum: 3419b805d5dfc518f3a05dcf42aa53aa9ce820e50b6df5097f9e186322e1bc733c36722b624802cd37e791035aa73b828ed814d8362333d42d7f5cd04d7a5e48 + languageName: node + linkType: hard + +"basic-ftp@npm:^5.0.2": + version: 5.0.5 + resolution: "basic-ftp@npm:5.0.5" + checksum: bc82d1c1c61cd838eaca96d68ece888bacf07546642fb6b9b8328ed410756f5935f8cf43a42cb44bb343e0565e28e908adc54c298bd2f1a6e0976871fb11fec6 + languageName: node + linkType: hard + +"batch@npm:0.6.1": + version: 0.6.1 + resolution: "batch@npm:0.6.1" + checksum: 61f9934c7378a51dce61b915586191078ef7f1c3eca707fdd58b96ff2ff56d9e0af2bdab66b1462301a73c73374239e6542d9821c0af787f3209a23365d07e7f + languageName: node + linkType: hard + +"bcrypt-pbkdf@npm:^1.0.0, bcrypt-pbkdf@npm:^1.0.2": + version: 1.0.2 + resolution: "bcrypt-pbkdf@npm:1.0.2" + dependencies: + tweetnacl: ^0.14.3 + checksum: 4edfc9fe7d07019609ccf797a2af28351736e9d012c8402a07120c4453a3b789a15f2ee1530dc49eee8f7eb9379331a8dd4b3766042b9e502f74a68e7f662291 + languageName: node + linkType: hard + +"before-after-hook@npm:^2.2.0": + version: 2.2.3 + resolution: "before-after-hook@npm:2.2.3" + checksum: a1a2430976d9bdab4cd89cb50d27fa86b19e2b41812bf1315923b0cba03371ebca99449809226425dd3bcef20e010db61abdaff549278e111d6480034bebae87 + languageName: node + linkType: hard + +"better-path-resolve@npm:1.0.0": + version: 1.0.0 + resolution: "better-path-resolve@npm:1.0.0" + dependencies: + is-windows: ^1.0.0 + checksum: 5392dbe04e7fe68b944eb37961d9dfa147aaac3ee9ee3f6e13d42e2c9fbe949e68d16e896c14ee9016fa5f8e6e53ec7fd8b5f01b50a32067a7d94ac9cfb9a050 + languageName: node + linkType: hard + +"better-sqlite3@npm:^11.0.0": + version: 11.4.0 + resolution: "better-sqlite3@npm:11.4.0" + dependencies: + bindings: ^1.5.0 + node-gyp: latest + prebuild-install: ^7.1.1 + checksum: d681ba16aae43e367ed9ab74fec3319a29bdffb6ca9f4aa7ac1e65304de395356f515e42f8259eed52aa667df9228c3b19bc6bef441cd5f21889d49042ab5f27 + languageName: node + linkType: hard + +"bfj@npm:^8.0.0": + version: 8.0.0 + resolution: "bfj@npm:8.0.0" + dependencies: + bluebird: ^3.7.2 + check-types: ^11.2.3 + hoopy: ^0.1.4 + jsonpath: ^1.1.1 + tryer: ^1.0.1 + checksum: f22d49cd2661a92e7526015edac0e02858a881a36438fe4e67df320dddc08cba09e197a7e128f282abc2c26127f5abb3ca8e8b7eff0737df20e5b8c4ee6273e9 + languageName: node + linkType: hard + +"big.js@npm:^5.2.2": + version: 5.2.2 + resolution: "big.js@npm:5.2.2" + checksum: b89b6e8419b097a8fb4ed2399a1931a68c612bce3cfd5ca8c214b2d017531191070f990598de2fc6f3f993d91c0f08aa82697717f6b3b8732c9731866d233c9e + languageName: node + linkType: hard + +"bignumber.js@npm:^9.0.0": + version: 9.1.2 + resolution: "bignumber.js@npm:9.1.2" + checksum: 582c03af77ec9cb0ebd682a373ee6c66475db94a4325f92299621d544aa4bd45cb45fd60001610e94aef8ae98a0905fa538241d9638d4422d57abbeeac6fadaf + languageName: node + linkType: hard + +"binary-extensions@npm:^2.0.0": + version: 2.3.0 + resolution: "binary-extensions@npm:2.3.0" + checksum: bcad01494e8a9283abf18c1b967af65ee79b0c6a9e6fcfafebfe91dbe6e0fc7272bafb73389e198b310516ae04f7ad17d79aacf6cb4c0d5d5202a7e2e52c7d98 + languageName: node + linkType: hard + +"bindings@npm:^1.5.0": + version: 1.5.0 + resolution: "bindings@npm:1.5.0" + dependencies: + file-uri-to-path: 1.0.0 + checksum: 65b6b48095717c2e6105a021a7da4ea435aa8d3d3cd085cb9e85bcb6e5773cf318c4745c3f7c504412855940b585bdf9b918236612a1c7a7942491de176f1ae7 + languageName: node + linkType: hard + +"bl@npm:^4.0.3, bl@npm:^4.1.0": + version: 4.1.0 + resolution: "bl@npm:4.1.0" + dependencies: + buffer: ^5.5.0 + inherits: ^2.0.4 + readable-stream: ^3.4.0 + checksum: 9e8521fa7e83aa9427c6f8ccdcba6e8167ef30cc9a22df26effcc5ab682ef91d2cbc23a239f945d099289e4bbcfae7a192e9c28c84c6202e710a0dfec3722662 + languageName: node + linkType: hard + +"bluebird@npm:3.7.2, bluebird@npm:^3.7.2": + version: 3.7.2 + resolution: "bluebird@npm:3.7.2" + checksum: 869417503c722e7dc54ca46715f70e15f4d9c602a423a02c825570862d12935be59ed9c7ba34a9b31f186c017c23cac6b54e35446f8353059c101da73eac22ef + languageName: node + linkType: hard + +"bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.9": + version: 4.12.0 + resolution: "bn.js@npm:4.12.0" + checksum: 39afb4f15f4ea537b55eaf1446c896af28ac948fdcf47171961475724d1bb65118cca49fa6e3d67706e4790955ec0e74de584e45c8f1ef89f46c812bee5b5a12 + languageName: node + linkType: hard + +"bn.js@npm:^5.2.1": + version: 5.2.1 + resolution: "bn.js@npm:5.2.1" + checksum: 3dd8c8d38055fedfa95c1d5fc3c99f8dd547b36287b37768db0abab3c239711f88ff58d18d155dd8ad902b0b0cee973747b7ae20ea12a09473272b0201c9edd3 + languageName: node + linkType: hard + +"body-parser@npm:1.20.3": + version: 1.20.3 + resolution: "body-parser@npm:1.20.3" + dependencies: + bytes: 3.1.2 + content-type: ~1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: ~1.6.18 + unpipe: 1.0.0 + checksum: 1a35c59a6be8d852b00946330141c4f142c6af0f970faa87f10ad74f1ee7118078056706a05ae3093c54dabca9cd3770fa62a170a85801da1a4324f04381167d + languageName: node + linkType: hard + +"bonjour-service@npm:^1.2.1": + version: 1.2.1 + resolution: "bonjour-service@npm:1.2.1" + dependencies: + fast-deep-equal: ^3.1.3 + multicast-dns: ^7.2.5 + checksum: b65b3e6e3a07e97f2da5806afb76f3946d5a6426b72e849a0236dc3c9d3612fb8c5359ebade4be7eb63f74a37670c53a53be2ff17f4f709811fda77f600eb25b + languageName: node + linkType: hard + +"boolbase@npm:^1.0.0": + version: 1.0.0 + resolution: "boolbase@npm:1.0.0" + checksum: 3e25c80ef626c3a3487c73dbfc70ac322ec830666c9ad915d11b701142fab25ec1e63eff2c450c74347acfd2de854ccde865cd79ef4db1683f7c7b046ea43bb0 + languageName: node + linkType: hard + +"boolean@npm:^3.0.1": + version: 3.2.0 + resolution: "boolean@npm:3.2.0" + checksum: fb29535b8bf710ef45279677a86d14f5185d604557204abd2ca5fa3fb2a5c80e04d695c8dbf13ab269991977a79bb6c04b048220a6b2a3849853faa94f4a7d77 + languageName: node + linkType: hard + +"bowser@npm:^2.11.0": + version: 2.11.0 + resolution: "bowser@npm:2.11.0" + checksum: 29c3f01f22e703fa6644fc3b684307442df4240b6e10f6cfe1b61c6ca5721073189ca97cdeedb376081148c8518e33b1d818a57f781d70b0b70e1f31fb48814f + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.11 + resolution: "brace-expansion@npm:1.1.11" + dependencies: + balanced-match: ^1.0.0 + concat-map: 0.0.1 + checksum: faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07 + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: ^1.0.0 + checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 + languageName: node + linkType: hard + +"braces@npm:^3.0.3, braces@npm:~3.0.2": + version: 3.0.3 + resolution: "braces@npm:3.0.3" + dependencies: + fill-range: ^7.1.1 + checksum: b95aa0b3bd909f6cd1720ffcf031aeaf46154dd88b4da01f9a1d3f7ea866a79eba76a6d01cbc3c422b2ee5cdc39a4f02491058d5df0d7bf6e6a162a832df1f69 + languageName: node + linkType: hard + +"brorand@npm:^1.0.1, brorand@npm:^1.1.0": + version: 1.1.0 + resolution: "brorand@npm:1.1.0" + checksum: 8a05c9f3c4b46572dec6ef71012b1946db6cae8c7bb60ccd4b7dd5a84655db49fe043ecc6272e7ef1f69dc53d6730b9e2a3a03a8310509a3d797a618cbee52be + languageName: node + linkType: hard + +"browserify-aes@npm:^1.0.4, browserify-aes@npm:^1.2.0": + version: 1.2.0 + resolution: "browserify-aes@npm:1.2.0" + dependencies: + buffer-xor: ^1.0.3 + cipher-base: ^1.0.0 + create-hash: ^1.1.0 + evp_bytestokey: ^1.0.3 + inherits: ^2.0.1 + safe-buffer: ^5.0.1 + checksum: 4a17c3eb55a2aa61c934c286f34921933086bf6d67f02d4adb09fcc6f2fc93977b47d9d884c25619144fccd47b3b3a399e1ad8b3ff5a346be47270114bcf7104 + languageName: node + linkType: hard + +"browserify-cipher@npm:^1.0.0": + version: 1.0.1 + resolution: "browserify-cipher@npm:1.0.1" + dependencies: + browserify-aes: ^1.0.4 + browserify-des: ^1.0.0 + evp_bytestokey: ^1.0.0 + checksum: 2d8500acf1ee535e6bebe808f7a20e4c3a9e2ed1a6885fff1facbfd201ac013ef030422bec65ca9ece8ffe82b03ca580421463f9c45af6c8415fd629f4118c13 + languageName: node + linkType: hard + +"browserify-des@npm:^1.0.0": + version: 1.0.2 + resolution: "browserify-des@npm:1.0.2" + dependencies: + cipher-base: ^1.0.1 + des.js: ^1.0.0 + inherits: ^2.0.1 + safe-buffer: ^5.1.2 + checksum: b15a3e358a1d78a3b62ddc06c845d02afde6fc826dab23f1b9c016e643e7b1fda41de628d2110b712f6a44fb10cbc1800bc6872a03ddd363fb50768e010395b7 + languageName: node + linkType: hard + +"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.1.0": + version: 4.1.1 + resolution: "browserify-rsa@npm:4.1.1" + dependencies: + bn.js: ^5.2.1 + randombytes: ^2.1.0 + safe-buffer: ^5.2.1 + checksum: 2628508646331791c29312bbf274c076a237437a17178ea9bdc75c577fb4164a0da0b137deaadf6ade623701332377c5c2ceb0ff6f991c744a576e790ec95852 + languageName: node + linkType: hard + +"browserify-sign@npm:^4.0.0": + version: 4.2.3 + resolution: "browserify-sign@npm:4.2.3" + dependencies: + bn.js: ^5.2.1 + browserify-rsa: ^4.1.0 + create-hash: ^1.2.0 + create-hmac: ^1.1.7 + elliptic: ^6.5.5 + hash-base: ~3.0 + inherits: ^2.0.4 + parse-asn1: ^5.1.7 + readable-stream: ^2.3.8 + safe-buffer: ^5.2.1 + checksum: 403a8061d229ae31266670345b4a7c00051266761d2c9bbeb68b1a9bcb05f68143b16110cf23a171a5d6716396a1f41296282b3e73eeec0a1871c77f0ff4ee6b + languageName: node + linkType: hard + +"browserify-zlib@npm:^0.2.0": + version: 0.2.0 + resolution: "browserify-zlib@npm:0.2.0" + dependencies: + pako: ~1.0.5 + checksum: 5cd9d6a665190fedb4a97dfbad8dabc8698d8a507298a03f42c734e96d58ca35d3c7d4085e283440bbca1cd1938cff85031728079bedb3345310c58ab1ec92d6 + languageName: node + linkType: hard + +"browserslist@npm:^4.0.0, browserslist@npm:^4.18.1, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0": + version: 4.24.0 + resolution: "browserslist@npm:4.24.0" + dependencies: + caniuse-lite: ^1.0.30001663 + electron-to-chromium: ^1.5.28 + node-releases: ^2.0.18 + update-browserslist-db: ^1.1.0 + bin: + browserslist: cli.js + checksum: de200d3eb8d6ed819dad99719099a28fb6ebeb88016a5ac42fbdc11607e910c236a84ca1b0bbf232477d4b88ab64e8ab6aa67557cdd40a73ca9c2834f92ccce0 + languageName: node + linkType: hard + +"bser@npm:2.1.1": + version: 2.1.1 + resolution: "bser@npm:2.1.1" + dependencies: + node-int64: ^0.4.0 + checksum: 9ba4dc58ce86300c862bffc3ae91f00b2a03b01ee07f3564beeeaf82aa243b8b03ba53f123b0b842c190d4399b94697970c8e7cf7b1ea44b61aa28c3526a4449 + languageName: node + linkType: hard + +"btoa-lite@npm:^1.0.0": + version: 1.0.0 + resolution: "btoa-lite@npm:1.0.0" + checksum: c2d61993b801f8e35a96f20692a45459c753d9baa29d86d1343e714f8d6bbe7069f1a20a5ae868488f3fb137d5bd0c560f6fbbc90b5a71050919d2d2c97c0475 + languageName: node + linkType: hard + +"btoa@npm:^1.2.1": + version: 1.2.1 + resolution: "btoa@npm:1.2.1" + bin: + btoa: bin/btoa.js + checksum: afbf004fb1b1d530e053ffa66ef5bd3878b101c59d808ac947fcff96810b4452abba2b54be687adadea2ba9efc7af48b04228742789bf824ef93f103767e690c + languageName: node + linkType: hard + +"buffer-crc32@npm:^1.0.0": + version: 1.0.0 + resolution: "buffer-crc32@npm:1.0.0" + checksum: bc114c0e02fe621249e0b5093c70e6f12d4c2b1d8ddaf3b1b7bbe3333466700100e6b1ebdc12c050d0db845bc582c4fce8c293da487cc483f97eea027c480b23 + languageName: node + linkType: hard + +"buffer-crc32@npm:~0.2.3": + version: 0.2.13 + resolution: "buffer-crc32@npm:0.2.13" + checksum: 06252347ae6daca3453b94e4b2f1d3754a3b146a111d81c68924c22d91889a40623264e95e67955b1cb4a68cbedf317abeabb5140a9766ed248973096db5ce1c + languageName: node + linkType: hard + +"buffer-equal-constant-time@npm:1.0.1": + version: 1.0.1 + resolution: "buffer-equal-constant-time@npm:1.0.1" + checksum: 80bb945f5d782a56f374b292770901065bad21420e34936ecbe949e57724b4a13874f735850dd1cc61f078773c4fb5493a41391e7bda40d1fa388d6bd80daaab + languageName: node + linkType: hard + +"buffer-from@npm:^1.0.0": + version: 1.1.2 + resolution: "buffer-from@npm:1.1.2" + checksum: 0448524a562b37d4d7ed9efd91685a5b77a50672c556ea254ac9a6d30e3403a517d8981f10e565db24e8339413b43c97ca2951f10e399c6125a0d8911f5679bb + languageName: node + linkType: hard + +"buffer-xor@npm:^1.0.3": + version: 1.0.3 + resolution: "buffer-xor@npm:1.0.3" + checksum: 10c520df29d62fa6e785e2800e586a20fc4f6dfad84bcdbd12e1e8a83856de1cb75c7ebd7abe6d036bbfab738a6cf18a3ae9c8e5a2e2eb3167ca7399ce65373a + languageName: node + linkType: hard + +"buffer@npm:^4.3.0": + version: 4.9.2 + resolution: "buffer@npm:4.9.2" + dependencies: + base64-js: ^1.0.2 + ieee754: ^1.1.4 + isarray: ^1.0.0 + checksum: 8801bc1ba08539f3be70eee307a8b9db3d40f6afbfd3cf623ab7ef41dffff1d0a31de0addbe1e66e0ca5f7193eeb667bfb1ecad3647f8f1b0750de07c13295c3 + languageName: node + linkType: hard + +"buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: ^1.3.1 + ieee754: ^1.1.13 + checksum: e2cf8429e1c4c7b8cbd30834ac09bd61da46ce35f5c22a78e6c2f04497d6d25541b16881e30a019c6fd3154150650ccee27a308eff3e26229d788bbdeb08ab84 + languageName: node + linkType: hard + +"buffer@npm:^6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: ^1.3.1 + ieee754: ^1.2.1 + checksum: 5ad23293d9a731e4318e420025800b42bf0d264004c0286c8cc010af7a270c7a0f6522e84f54b9ad65cbd6db20b8badbfd8d2ebf4f80fa03dab093b89e68c3f9 + languageName: node + linkType: hard + +"buildcheck@npm:~0.0.6": + version: 0.0.6 + resolution: "buildcheck@npm:0.0.6" + checksum: ad61759dc98d62e931df2c9f54ccac7b522e600c6e13bdcfdc2c9a872a818648c87765ee209c850f022174da4dd7c6a450c00357c5391705d26b9c5807c2a076 + languageName: node + linkType: hard + +"builtin-status-codes@npm:^3.0.0": + version: 3.0.0 + resolution: "builtin-status-codes@npm:3.0.0" + checksum: 1119429cf4b0d57bf76b248ad6f529167d343156ebbcc4d4e4ad600484f6bc63002595cbb61b67ad03ce55cd1d3c4711c03bbf198bf24653b8392420482f3773 + languageName: node + linkType: hard + +"bundle-name@npm:^4.1.0": + version: 4.1.0 + resolution: "bundle-name@npm:4.1.0" + dependencies: + run-applescript: ^7.0.0 + checksum: 1d966c8d2dbf4d9d394e53b724ac756c2414c45c01340b37743621f59cc565a435024b394ddcb62b9b335d1c9a31f4640eb648c3fec7f97ee74dc0694c9beb6c + languageName: node + linkType: hard + +"byline@npm:^5.0.0": + version: 5.0.0 + resolution: "byline@npm:5.0.0" + checksum: 737ca83e8eda2976728dae62e68bc733aea095fab08db4c6f12d3cee3cf45b6f97dce45d1f6b6ff9c2c947736d10074985b4425b31ce04afa1985a4ef3d334a7 + languageName: node + linkType: hard + +"bytes@npm:3.0.0": + version: 3.0.0 + resolution: "bytes@npm:3.0.0" + checksum: a2b386dd8188849a5325f58eef69c3b73c51801c08ffc6963eddc9be244089ba32d19347caf6d145c86f315ae1b1fc7061a32b0c1aa6379e6a719090287ed101 + languageName: node + linkType: hard + +"bytes@npm:3.1.2": + version: 3.1.2 + resolution: "bytes@npm:3.1.2" + checksum: e4bcd3948d289c5127591fbedf10c0b639ccbf00243504e4e127374a15c3bc8eed0d28d4aaab08ff6f1cf2abc0cce6ba3085ed32f4f90e82a5683ce0014e1b6e + languageName: node + linkType: hard + +"cacache@npm:^16.1.0": + version: 16.1.3 + resolution: "cacache@npm:16.1.3" + dependencies: + "@npmcli/fs": ^2.1.0 + "@npmcli/move-file": ^2.0.0 + chownr: ^2.0.0 + fs-minipass: ^2.1.0 + glob: ^8.0.1 + infer-owner: ^1.0.4 + lru-cache: ^7.7.1 + minipass: ^3.1.6 + minipass-collect: ^1.0.2 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + mkdirp: ^1.0.4 + p-map: ^4.0.0 + promise-inflight: ^1.0.1 + rimraf: ^3.0.2 + ssri: ^9.0.0 + tar: ^6.1.11 + unique-filename: ^2.0.0 + checksum: d91409e6e57d7d9a3a25e5dcc589c84e75b178ae8ea7de05cbf6b783f77a5fae938f6e8fda6f5257ed70000be27a681e1e44829251bfffe4c10216002f8f14e6 + languageName: node + linkType: hard + +"cacache@npm:^18.0.0": + version: 18.0.4 + resolution: "cacache@npm:18.0.4" + dependencies: + "@npmcli/fs": ^3.1.0 + fs-minipass: ^3.0.0 + glob: ^10.2.2 + lru-cache: ^10.0.1 + minipass: ^7.0.3 + minipass-collect: ^2.0.1 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + p-map: ^4.0.0 + ssri: ^10.0.0 + tar: ^6.1.11 + unique-filename: ^3.0.0 + checksum: b7422c113b4ec750f33beeca0f426a0024c28e3172f332218f48f963e5b970647fa1ac05679fe5bb448832c51efea9fda4456b9a95c3a1af1105fe6c1833cde2 + languageName: node + linkType: hard + +"cache-content-type@npm:^1.0.0": + version: 1.0.1 + resolution: "cache-content-type@npm:1.0.1" + dependencies: + mime-types: ^2.1.18 + ylru: ^1.2.0 + checksum: 18db4d59452669ccbfd7146a1510a37eb28e9eccf18ca7a4eb603dff2edc5cccdca7498fc3042a2978f76f11151fba486eb9eb69d9afa3fb124957870aef4fd3 + languageName: node + linkType: hard + +"call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7": + version: 1.0.7 + resolution: "call-bind@npm:1.0.7" + dependencies: + es-define-property: ^1.0.0 + es-errors: ^1.3.0 + function-bind: ^1.1.2 + get-intrinsic: ^1.2.4 + set-function-length: ^1.2.1 + checksum: 295c0c62b90dd6522e6db3b0ab1ce26bdf9e7404215bda13cfee25b626b5ff1a7761324d58d38b1ef1607fc65aca2d06e44d2e18d0dfc6c14b465b00d8660029 + languageName: node + linkType: hard + +"call-me-maybe@npm:^1.0.1": + version: 1.0.2 + resolution: "call-me-maybe@npm:1.0.2" + checksum: 42ff2d0bed5b207e3f0122589162eaaa47ba618f79ad2382fe0ba14d9e49fbf901099a6227440acc5946f86a4953e8aa2d242b330b0a5de4d090bb18f8935cae + languageName: node + linkType: hard + +"callsites@npm:^3.0.0": + version: 3.1.0 + resolution: "callsites@npm:3.1.0" + checksum: 072d17b6abb459c2ba96598918b55868af677154bec7e73d222ef95a8fdb9bbf7dae96a8421085cdad8cd190d86653b5b6dc55a4484f2e5b2e27d5e0c3fc15b3 + languageName: node + linkType: hard + +"camel-case@npm:^4.1.2": + version: 4.1.2 + resolution: "camel-case@npm:4.1.2" + dependencies: + pascal-case: ^3.1.2 + tslib: ^2.0.3 + checksum: bcbd25cd253b3cbc69be3f535750137dbf2beb70f093bdc575f73f800acc8443d34fd52ab8f0a2413c34f1e8203139ffc88428d8863e4dfe530cfb257a379ad6 + languageName: node + linkType: hard + +"camelcase@npm:^5.3.1": + version: 5.3.1 + resolution: "camelcase@npm:5.3.1" + checksum: e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b + languageName: node + linkType: hard + +"camelcase@npm:^6.2.0": + version: 6.3.0 + resolution: "camelcase@npm:6.3.0" + checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d + languageName: node + linkType: hard + +"caniuse-api@npm:^3.0.0": + version: 3.0.0 + resolution: "caniuse-api@npm:3.0.0" + dependencies: + browserslist: ^4.0.0 + caniuse-lite: ^1.0.0 + lodash.memoize: ^4.1.2 + lodash.uniq: ^4.5.0 + checksum: db2a229383b20d0529b6b589dde99d7b6cb56ba371366f58cbbfa2929c9f42c01f873e2b6ef641d4eda9f0b4118de77dbb2805814670bdad4234bf08e720b0b4 + languageName: node + linkType: hard + +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001663": + version: 1.0.30001669 + resolution: "caniuse-lite@npm:1.0.30001669" + checksum: 8ed0c69d0c6aa3b1cbc5ba4e5f5330943e7b7165e257f6955b8b73f043d07ad922265261f2b54d9bbaf02886bbdba5e6f5b16662310a13f91f17035af3212de1 + languageName: node + linkType: hard + +"canvas@npm:^2.11.2": + version: 2.11.2 + resolution: "canvas@npm:2.11.2" + dependencies: + "@mapbox/node-pre-gyp": ^1.0.0 + nan: ^2.17.0 + node-gyp: latest + simple-get: ^3.0.3 + checksum: 61e554aef80022841dc836964534082ec21435928498032562089dfb7736215f039c7d99ee546b0cf10780232d9bf310950f8b4d489dc394e0fb6f6adfc97994 + languageName: node + linkType: hard + +"casbin@npm:^5.27.0, casbin@npm:^5.27.1": + version: 5.31.0 + resolution: "casbin@npm:5.31.0" + dependencies: + await-lock: ^2.0.1 + buffer: ^6.0.3 + csv-parse: ^5.3.5 + expression-eval: ^5.0.0 + minimatch: ^7.4.2 + checksum: 3bdf15d71d4bc7af69227ffb794f91f3cc0caaeaccf35a7092d95b7a00cbe9fcd084e6f6a3a5f0100dbbe86bb33b2c9da00f8b638dd5b1c00157b7e3932728b8 + languageName: node + linkType: hard + +"caseless@npm:~0.12.0": + version: 0.12.0 + resolution: "caseless@npm:0.12.0" + checksum: b43bd4c440aa1e8ee6baefee8063b4850fd0d7b378f6aabc796c9ec8cb26d27fb30b46885350777d9bd079c5256c0e1329ad0dc7c2817e0bb466810ebb353751 + languageName: node + linkType: hard + +"ccount@npm:^2.0.0": + version: 2.0.1 + resolution: "ccount@npm:2.0.1" + checksum: 48193dada54c9e260e0acf57fc16171a225305548f9ad20d5471e0f7a8c026aedd8747091dccb0d900cde7df4e4ddbd235df0d8de4a64c71b12f0d3303eeafd4 + languageName: node + linkType: hard + +"chalk@npm:2.4.2, chalk@npm:^2.4.2": + version: 2.4.2 + resolution: "chalk@npm:2.4.2" + dependencies: + ansi-styles: ^3.2.1 + escape-string-regexp: ^1.0.5 + supports-color: ^5.3.0 + checksum: ec3661d38fe77f681200f878edbd9448821924e0f93a9cefc0e26a33b145f1027a2084bf19967160d11e1f03bfe4eaffcabf5493b89098b2782c3fe0b03d80c2 + languageName: node + linkType: hard + +"chalk@npm:3.0.0, chalk@npm:^3.0.0": + version: 3.0.0 + resolution: "chalk@npm:3.0.0" + dependencies: + ansi-styles: ^4.1.0 + supports-color: ^7.1.0 + checksum: 8e3ddf3981c4da405ddbd7d9c8d91944ddf6e33d6837756979f7840a29272a69a5189ecae0ff84006750d6d1e92368d413335eab4db5476db6e6703a1d1e0505 + languageName: node + linkType: hard + +"chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: ^4.1.0 + supports-color: ^7.1.0 + checksum: fe75c9d5c76a7a98d45495b91b2172fa3b7a09e0cc9370e5c8feb1c567b85c4288e2b3fded7cfdd7359ac28d6b3844feb8b82b8686842e93d23c827c417e83fc + languageName: node + linkType: hard + +"char-regex@npm:^1.0.2": + version: 1.0.2 + resolution: "char-regex@npm:1.0.2" + checksum: b563e4b6039b15213114626621e7a3d12f31008bdce20f9c741d69987f62aeaace7ec30f6018890ad77b2e9b4d95324c9f5acfca58a9441e3b1dcdd1e2525d17 + languageName: node + linkType: hard + +"character-entities-legacy@npm:^1.0.0": + version: 1.1.4 + resolution: "character-entities-legacy@npm:1.1.4" + checksum: fe03a82c154414da3a0c8ab3188e4237ec68006cbcd681cf23c7cfb9502a0e76cd30ab69a2e50857ca10d984d57de3b307680fff5328ccd427f400e559c3a811 + languageName: node + linkType: hard + +"character-entities@npm:^1.0.0": + version: 1.2.4 + resolution: "character-entities@npm:1.2.4" + checksum: e1545716571ead57beac008433c1ff69517cd8ca5b336889321c5b8ff4a99c29b65589a701e9c086cda8a5e346a67295e2684f6c7ea96819fe85cbf49bf8686d + languageName: node + linkType: hard + +"character-entities@npm:^2.0.0": + version: 2.0.2 + resolution: "character-entities@npm:2.0.2" + checksum: cf1643814023697f725e47328fcec17923b8f1799102a8a79c1514e894815651794a2bffd84bb1b3a4b124b050154e4529ed6e81f7c8068a734aecf07a6d3def + languageName: node + linkType: hard + +"character-reference-invalid@npm:^1.0.0": + version: 1.1.4 + resolution: "character-reference-invalid@npm:1.1.4" + checksum: 20274574c70e05e2f81135f3b93285536bc8ff70f37f0809b0d17791a832838f1e49938382899ed4cb444e5bbd4314ca1415231344ba29f4222ce2ccf24fea0b + languageName: node + linkType: hard + +"chardet@npm:^0.7.0": + version: 0.7.0 + resolution: "chardet@npm:0.7.0" + checksum: 6fd5da1f5d18ff5712c1e0aed41da200d7c51c28f11b36ee3c7b483f3696dabc08927fc6b227735eb8f0e1215c9a8abd8154637f3eff8cada5959df7f58b024d + languageName: node + linkType: hard + +"check-more-types@npm:2.24.0": + version: 2.24.0 + resolution: "check-more-types@npm:2.24.0" + checksum: b09080ec3404d20a4b0ead828994b2e5913236ef44ed3033a27062af0004cf7d2091fbde4b396bf13b7ce02fb018bc9960b48305e6ab2304cd82d73ed7a51ef4 + languageName: node + linkType: hard + +"check-types@npm:^11.2.3": + version: 11.2.3 + resolution: "check-types@npm:11.2.3" + checksum: f99ff09ae65e63cfcfa40a1275c0a70d8c43ffbf9ac35095f3bf030cc70361c92e075a9975a1144329e50b4fe4620be6bedb4568c18abc96071a3e23aed3ed8e + languageName: node + linkType: hard + +"chokidar@npm:^3.3.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.2, chokidar@npm:^3.5.3, chokidar@npm:^3.6.0": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: ~3.1.2 + braces: ~3.0.2 + fsevents: ~2.3.2 + glob-parent: ~5.1.2 + is-binary-path: ~2.1.0 + is-glob: ~4.0.1 + normalize-path: ~3.0.0 + readdirp: ~3.6.0 + dependenciesMeta: + fsevents: + optional: true + checksum: d2f29f499705dcd4f6f3bbed79a9ce2388cf530460122eed3b9c48efeab7a4e28739c6551fd15bec9245c6b9eeca7a32baa64694d64d9b6faeb74ddb8c4a413d + languageName: node + linkType: hard + +"chownr@npm:^1.1.1": + version: 1.1.4 + resolution: "chownr@npm:1.1.4" + checksum: 115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f + languageName: node + linkType: hard + +"chownr@npm:^3.0.0": + version: 3.0.0 + resolution: "chownr@npm:3.0.0" + checksum: fd73a4bab48b79e66903fe1cafbdc208956f41ea4f856df883d0c7277b7ab29fd33ee65f93b2ec9192fc0169238f2f8307b7735d27c155821d886b84aa97aa8d + languageName: node + linkType: hard + +"chrome-trace-event@npm:^1.0.2": + version: 1.0.4 + resolution: "chrome-trace-event@npm:1.0.4" + checksum: fcbbd9dd0cd5b48444319007cc0c15870fd8612cc0df320908aa9d5e8a244084d48571eb28bf3c58c19327d2c5838f354c2d89fac3956d8e992273437401ac19 + languageName: node + linkType: hard + +"ci-info@npm:^3.2.0, ci-info@npm:^3.7.0": + version: 3.9.0 + resolution: "ci-info@npm:3.9.0" + checksum: 6b19dc9b2966d1f8c2041a838217299718f15d6c4b63ae36e4674edd2bee48f780e94761286a56aa59eb305a85fbea4ddffb7630ec063e7ec7e7e5ad42549a87 + languageName: node + linkType: hard + +"cidr-regex@npm:^3.1.1": + version: 3.1.1 + resolution: "cidr-regex@npm:3.1.1" + dependencies: + ip-regex: ^4.1.0 + checksum: ef9306d086928ee82b3f841b3bdab6e072230f3623a57cf19e06174946f2cbfeb70ca52bc106b127db27a628b9e84fb39284f5851003898ffdb957fe330478ee + languageName: node + linkType: hard + +"cipher-base@npm:^1.0.0, cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3": + version: 1.0.4 + resolution: "cipher-base@npm:1.0.4" + dependencies: + inherits: ^2.0.1 + safe-buffer: ^5.0.1 + checksum: 47d3568dbc17431a339bad1fe7dff83ac0891be8206911ace3d3b818fc695f376df809bea406e759cdea07fff4b454fa25f1013e648851bec790c1d75763032e + languageName: node + linkType: hard + +"cjs-module-lexer@npm:^1.0.0": + version: 1.4.1 + resolution: "cjs-module-lexer@npm:1.4.1" + checksum: 2556807a99aec1f9daac60741af96cd613a707f343174ae7967da46402c91dced411bf830d209f2e93be4cecea46fc75cecf1f17c799d7d8a9e1dd6204bfcd22 + languageName: node + linkType: hard + +"classnames@npm:^2.2.6, classnames@npm:^2.3.2": + version: 2.5.1 + resolution: "classnames@npm:2.5.1" + checksum: da424a8a6f3a96a2e87d01a432ba19315503294ac7e025f9fece656db6b6a0f7b5003bb1fbb51cbb0d9624d964f1b9bb35a51c73af9b2434c7b292c42231c1e5 + languageName: node + linkType: hard + +"clean-css@npm:^5.2.2": + version: 5.3.3 + resolution: "clean-css@npm:5.3.3" + dependencies: + source-map: ~0.6.0 + checksum: 941987c14860dd7d346d5cf121a82fd2caf8344160b1565c5387f7ccca4bbcaf885bace961be37c4f4713ce2d8c488dd89483c1add47bb779790edbfdcc79cbc + languageName: node + linkType: hard + +"clean-git-ref@npm:^2.0.1": + version: 2.0.1 + resolution: "clean-git-ref@npm:2.0.1" + checksum: b25f585ed47040ea5d699d40a2bb84d1f35afd651f3fcc05fb077224358ffd3d7509fc9edbfc4570f1fc732c987e03ac7d8ec31524ac503ac35c53cb1f5e3bf9 + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 2ac8cd2b2f5ec986a3c743935ec85b07bc174d5421a5efc8017e1f146a1cf5f781ae962618f416352103b32c9cd7e203276e8c28241bbe946160cab16149fb68 + languageName: node + linkType: hard + +"cli-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "cli-cursor@npm:3.1.0" + dependencies: + restore-cursor: ^3.1.0 + checksum: 2692784c6cd2fd85cfdbd11f53aea73a463a6d64a77c3e098b2b4697a20443f430c220629e1ca3b195ea5ac4a97a74c2ee411f3807abf6df2b66211fec0c0a29 + languageName: node + linkType: hard + +"cli-highlight@npm:^2.1.11": + version: 2.1.11 + resolution: "cli-highlight@npm:2.1.11" + dependencies: + chalk: ^4.0.0 + highlight.js: ^10.7.1 + mz: ^2.4.0 + parse5: ^5.1.1 + parse5-htmlparser2-tree-adapter: ^6.0.0 + yargs: ^16.0.0 + bin: + highlight: bin/highlight + checksum: 0a60e60545e39efea78c1732a25b91692017ec40fb6e9497208dc0eeeae69991d3923a8d6e4edd0543db3c395ed14529a33dd4d0353f1679c5b6dded792a8496 + languageName: node + linkType: hard + +"cli-spinners@npm:^2.5.0": + version: 2.9.2 + resolution: "cli-spinners@npm:2.9.2" + checksum: 1bd588289b28432e4676cb5d40505cfe3e53f2e4e10fbe05c8a710a154d6fe0ce7836844b00d6858f740f2ffe67cdc36e0fce9c7b6a8430e80e6388d5aa4956c + languageName: node + linkType: hard + +"cli-width@npm:^3.0.0": + version: 3.0.0 + resolution: "cli-width@npm:3.0.0" + checksum: 4c94af3769367a70e11ed69aa6095f1c600c0ff510f3921ab4045af961820d57c0233acfa8b6396037391f31b4c397e1f614d234294f979ff61430a6c166c3f6 + languageName: node + linkType: hard + +"client-only@npm:^0.0.1": + version: 0.0.1 + resolution: "client-only@npm:0.0.1" + checksum: 0c16bf660dadb90610553c1d8946a7fdfb81d624adea073b8440b7d795d5b5b08beb3c950c6a2cf16279365a3265158a236876d92bce16423c485c322d7dfaf8 + languageName: node + linkType: hard + +"cliui@npm:7.0.4, cliui@npm:^7.0.2": + version: 7.0.4 + resolution: "cliui@npm:7.0.4" + dependencies: + string-width: ^4.2.0 + strip-ansi: ^6.0.0 + wrap-ansi: ^7.0.0 + checksum: ce2e8f578a4813806788ac399b9e866297740eecd4ad1823c27fd344d78b22c5f8597d548adbcc46f0573e43e21e751f39446c5a5e804a12aace402b7a315d7f + languageName: node + linkType: hard + +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: ^4.2.0 + strip-ansi: ^6.0.1 + wrap-ansi: ^7.0.0 + checksum: 79648b3b0045f2e285b76fb2e24e207c6db44323581e421c3acbd0e86454cba1b37aea976ab50195a49e7384b871e6dfb2247ad7dec53c02454ac6497394cb56 + languageName: node + linkType: hard + +"clone@npm:^1.0.2": + version: 1.0.4 + resolution: "clone@npm:1.0.4" + checksum: d06418b7335897209e77bdd430d04f882189582e67bd1f75a04565f3f07f5b3f119a9d670c943b6697d0afb100f03b866b3b8a1f91d4d02d72c4ecf2bb64b5dd + languageName: node + linkType: hard + +"clsx@npm:^1.0.2, clsx@npm:^1.0.4": + version: 1.2.1 + resolution: "clsx@npm:1.2.1" + checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12 + languageName: node + linkType: hard + +"clsx@npm:^2.1.0, clsx@npm:^2.1.1": + version: 2.1.1 + resolution: "clsx@npm:2.1.1" + checksum: acd3e1ab9d8a433ecb3cc2f6a05ab95fe50b4a3cfc5ba47abb6cbf3754585fcb87b84e90c822a1f256c4198e3b41c7f6c391577ffc8678ad587fc0976b24fd57 + languageName: node + linkType: hard + +"cluster-key-slot@npm:^1.1.0": + version: 1.1.2 + resolution: "cluster-key-slot@npm:1.1.2" + checksum: be0ad2d262502adc998597e83f9ded1b80f827f0452127c5a37b22dfca36bab8edf393f7b25bb626006fb9fb2436106939ede6d2d6ecf4229b96a47f27edd681 + languageName: node + linkType: hard + +"co@npm:^4.6.0": + version: 4.6.0 + resolution: "co@npm:4.6.0" + checksum: 5210d9223010eb95b29df06a91116f2cf7c8e0748a9013ed853b53f362ea0e822f1e5bb054fb3cefc645239a4cf966af1f6133a3b43f40d591f3b68ed6cf0510 + languageName: node + linkType: hard + +"code-block-writer@npm:^13.0.1": + version: 13.0.3 + resolution: "code-block-writer@npm:13.0.3" + checksum: 8e234f0ec2db9625d5efb9f05bdae79da6559bb4d9df94a6aa79a89a7b5ae25093b70d309fc5122840c9c07995cb14b4dd3f98a30f8878e3a3372e177df79454 + languageName: node + linkType: hard + +"codeowners-utils@npm:^1.0.2": + version: 1.0.2 + resolution: "codeowners-utils@npm:1.0.2" + dependencies: + cross-spawn: ^7.0.2 + find-up: ^4.1.0 + ignore: ^5.1.4 + locate-path: ^5.0.0 + checksum: 1e1c1f271ad4d4b4b25f6d19fc61f177f010bfb95de9af26662bb09c2f4f5572c1f3c8e9552aff15924f1c97058812bd5b5064d1eea721cc70e17490dae3fb02 + languageName: node + linkType: hard + +"collect-v8-coverage@npm:^1.0.0": + version: 1.0.2 + resolution: "collect-v8-coverage@npm:1.0.2" + checksum: c10f41c39ab84629d16f9f6137bc8a63d332244383fc368caf2d2052b5e04c20cd1fd70f66fcf4e2422b84c8226598b776d39d5f2d2a51867cc1ed5d1982b4da + languageName: node + linkType: hard + +"color-convert@npm:^1.9.0, color-convert@npm:^1.9.3": + version: 1.9.3 + resolution: "color-convert@npm:1.9.3" + dependencies: + color-name: 1.1.3 + checksum: fd7a64a17cde98fb923b1dd05c5f2e6f7aefda1b60d67e8d449f9328b4e53b228a428fd38bfeaeb2db2ff6b6503a776a996150b80cdf224062af08a5c8a3a203 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: ~1.1.4 + checksum: 79e6bdb9fd479a205c71d89574fccfb22bd9053bd98c6c4d870d65c132e5e904e6034978e55b43d69fcaa7433af2016ee203ce76eeba9cfa554b373e7f7db336 + languageName: node + linkType: hard + +"color-name@npm:1.1.3": + version: 1.1.3 + resolution: "color-name@npm:1.1.3" + checksum: 09c5d3e33d2105850153b14466501f2bfb30324a2f76568a408763a3b7433b0e50e5b4ab1947868e65cb101bb7cb75029553f2c333b6d4b8138a73fcc133d69d + languageName: node + linkType: hard + +"color-name@npm:^1.0.0, color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 + languageName: node + linkType: hard + +"color-string@npm:^1.6.0": + version: 1.9.1 + resolution: "color-string@npm:1.9.1" + dependencies: + color-name: ^1.0.0 + simple-swizzle: ^0.2.2 + checksum: c13fe7cff7885f603f49105827d621ce87f4571d78ba28ef4a3f1a104304748f620615e6bf065ecd2145d0d9dad83a3553f52bb25ede7239d18e9f81622f1cc5 + languageName: node + linkType: hard + +"color-support@npm:^1.1.2, color-support@npm:^1.1.3": + version: 1.1.3 + resolution: "color-support@npm:1.1.3" + bin: + color-support: bin.js + checksum: 9b7356817670b9a13a26ca5af1c21615463b500783b739b7634a0c2047c16cef4b2865d7576875c31c3cddf9dd621fa19285e628f20198b233a5cfdda6d0793b + languageName: node + linkType: hard + +"color@npm:^3.1.3": + version: 3.2.1 + resolution: "color@npm:3.2.1" + dependencies: + color-convert: ^1.9.3 + color-string: ^1.6.0 + checksum: f81220e8b774d35865c2561be921f5652117638dcda7ca4029262046e37fc2444ac7bbfdd110cf1fd9c074a4ee5eda8f85944ffbdda26186b602dd9bb05f6400 + languageName: node + linkType: hard + +"colord@npm:^2.9.1": + version: 2.9.3 + resolution: "colord@npm:2.9.3" + checksum: 95d909bfbcfd8d5605cbb5af56f2d1ce2b323990258fd7c0d2eb0e6d3bb177254d7fb8213758db56bb4ede708964f78c6b992b326615f81a18a6aaf11d64c650 + languageName: node + linkType: hard + +"colorette@npm:2.0.19": + version: 2.0.19 + resolution: "colorette@npm:2.0.19" + checksum: 888cf5493f781e5fcf54ce4d49e9d7d698f96ea2b2ef67906834bb319a392c667f9ec69f4a10e268d2946d13a9503d2d19b3abaaaf174e3451bfe91fb9d82427 + languageName: node + linkType: hard + +"colorette@npm:^2.0.10": + version: 2.0.20 + resolution: "colorette@npm:2.0.20" + checksum: 0c016fea2b91b733eb9f4bcdb580018f52c0bc0979443dad930e5037a968237ac53d9beb98e218d2e9235834f8eebce7f8e080422d6194e957454255bde71d3d + languageName: node + linkType: hard + +"colorspace@npm:1.1.x": + version: 1.1.4 + resolution: "colorspace@npm:1.1.4" + dependencies: + color: ^3.1.3 + text-hex: 1.0.x + checksum: bb3934ef3c417e961e6d03d7ca60ea6e175947029bfadfcdb65109b01881a1c0ecf9c2b0b59abcd0ee4a0d7c1eae93beed01b0e65848936472270a0b341ebce8 + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8, combined-stream@npm:~1.0.6": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: ~1.0.0 + checksum: 49fa4aeb4916567e33ea81d088f6584749fc90c7abec76fd516bf1c5aa5c79f3584b5ba3de6b86d26ddd64bae5329c4c7479343250cfe71c75bb366eae53bb7c + languageName: node + linkType: hard + +"comma-separated-tokens@npm:^1.0.0": + version: 1.0.8 + resolution: "comma-separated-tokens@npm:1.0.8" + checksum: 0adcb07174fa4d08cf0f5c8e3aec40a36b5ff0c2c720e5e23f50fe02e6789d1d00a67036c80e0c1e1539f41d3e7f0101b074039dd833b4e4a59031b659d6ca0d + languageName: node + linkType: hard + +"comma-separated-tokens@npm:^2.0.0": + version: 2.0.3 + resolution: "comma-separated-tokens@npm:2.0.3" + checksum: e3bf9e0332a5c45f49b90e79bcdb4a7a85f28d6a6f0876a94f1bb9b2bfbdbbb9292aac50e1e742d8c0db1e62a0229a106f57917e2d067fca951d81737651700d + languageName: node + linkType: hard + +"command-exists@npm:^1.2.9": + version: 1.2.9 + resolution: "command-exists@npm:1.2.9" + checksum: 729ae3d88a2058c93c58840f30341b7f82688a573019535d198b57a4d8cb0135ced0ad7f52b591e5b28a90feb2c675080ce916e56254a0f7c15cb2395277cac3 + languageName: node + linkType: hard + +"commander@npm:8.3.0, commander@npm:^8.3.0": + version: 8.3.0 + resolution: "commander@npm:8.3.0" + checksum: 0f82321821fc27b83bd409510bb9deeebcfa799ff0bf5d102128b500b7af22872c0c92cb6a0ebc5a4cf19c6b550fba9cedfa7329d18c6442a625f851377bacf0 + languageName: node + linkType: hard + +"commander@npm:^10.0.0": + version: 10.0.1 + resolution: "commander@npm:10.0.1" + checksum: 436901d64a818295803c1996cd856621a74f30b9f9e28a588e726b2b1670665bccd7c1a77007ebf328729f0139838a88a19265858a0fa7a8728c4656796db948 + languageName: node + linkType: hard + +"commander@npm:^12.0.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 68e9818b00fc1ed9cdab9eb16905551c2b768a317ae69a5e3c43924c2b20ac9bb65b27e1cab36aeda7b6496376d4da908996ba2c0b5d79463e0fb1e77935d514 + languageName: node + linkType: hard + +"commander@npm:^2.19.0, commander@npm:^2.20.0": + version: 2.20.3 + resolution: "commander@npm:2.20.3" + checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e + languageName: node + linkType: hard + +"commander@npm:^4.0.0, commander@npm:^4.1.1": + version: 4.1.1 + resolution: "commander@npm:4.1.1" + checksum: d7b9913ff92cae20cb577a4ac6fcc121bd6223319e54a40f51a14740a681ad5c574fd29a57da478a5f234a6fa6c52cbf0b7c641353e03c648b1ae85ba670b977 + languageName: node + linkType: hard + +"commander@npm:^7.2.0": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 53501cbeee61d5157546c0bef0fedb6cdfc763a882136284bed9a07225f09a14b82d2a84e7637edfd1a679fb35ed9502fd58ef1d091e6287f60d790147f68ddc + languageName: node + linkType: hard + +"commondir@npm:^1.0.1": + version: 1.0.1 + resolution: "commondir@npm:1.0.1" + checksum: 59715f2fc456a73f68826285718503340b9f0dd89bfffc42749906c5cf3d4277ef11ef1cca0350d0e79204f00f1f6d83851ececc9095dc88512a697ac0b9bdcb + languageName: node + linkType: hard + +"compare-versions@npm:4.1.4": + version: 4.1.4 + resolution: "compare-versions@npm:4.1.4" + checksum: c1617544b79c2f36a1d543c50efd0da1a994040294c8923218080bc0df46da83ca414e3378282e93cab073744995124946417d130d8987e8efb5d1a73c0c4ba6 + languageName: node + linkType: hard + +"complex.js@npm:^2.1.1": + version: 2.3.0 + resolution: "complex.js@npm:2.3.0" + checksum: 8a230999a0ba8bce1dfdd046245775ec38f3cf0615c365c1eae8e6a7e8a193714074c02bff5b65d48ae9b72d2824fc584fb1b345920cf83aa0d78d87b920927a + languageName: node + linkType: hard + +"component-emitter@npm:^1.3.0": + version: 1.3.1 + resolution: "component-emitter@npm:1.3.1" + checksum: 94550aa462c7bd5a61c1bc480e28554aa306066930152d1b1844a0dd3845d4e5db7e261ddec62ae184913b3e59b55a2ad84093b9d3596a8f17c341514d6c483d + languageName: node + linkType: hard + +"compress-commons@npm:^6.0.2": + version: 6.0.2 + resolution: "compress-commons@npm:6.0.2" + dependencies: + crc-32: ^1.2.0 + crc32-stream: ^6.0.0 + is-stream: ^2.0.1 + normalize-path: ^3.0.0 + readable-stream: ^4.0.0 + checksum: 37d79a54f91344ecde352588e0a128f28ce619b085acd4f887defd76978a0640e3454a42c7dcadb0191bb3f971724ae4b1f9d6ef9620034aa0427382099ac946 + languageName: node + linkType: hard + +"compressible@npm:~2.0.16": + version: 2.0.18 + resolution: "compressible@npm:2.0.18" + dependencies: + mime-db: ">= 1.43.0 < 2" + checksum: 58321a85b375d39230405654721353f709d0c1442129e9a17081771b816302a012471a9b8f4864c7dbe02eef7f2aaac3c614795197092262e94b409c9be108f0 + languageName: node + linkType: hard + +"compression@npm:^1.7.4": + version: 1.7.4 + resolution: "compression@npm:1.7.4" + dependencies: + accepts: ~1.3.5 + bytes: 3.0.0 + compressible: ~2.0.16 + debug: 2.6.9 + on-headers: ~1.0.2 + safe-buffer: 5.1.2 + vary: ~1.1.2 + checksum: 35c0f2eb1f28418978615dc1bc02075b34b1568f7f56c62d60f4214d4b7cc00d0f6d282b5f8a954f59872396bd770b6b15ffd8aa94c67d4bce9b8887b906999b + languageName: node + linkType: hard + +"compute-gcd@npm:^1.2.1": + version: 1.2.1 + resolution: "compute-gcd@npm:1.2.1" + dependencies: + validate.io-array: ^1.0.3 + validate.io-function: ^1.0.2 + validate.io-integer-array: ^1.0.0 + checksum: 51cf33b75f7c8db5142fcb99a9d84a40260993fed8e02a7ab443834186c3ab99b3fd20b30ad9075a6a9d959d69df6da74dd3be8a59c78d9f2fe780ebda8242e1 + languageName: node + linkType: hard + +"compute-lcm@npm:^1.1.2": + version: 1.1.2 + resolution: "compute-lcm@npm:1.1.2" + dependencies: + compute-gcd: ^1.2.1 + validate.io-array: ^1.0.3 + validate.io-function: ^1.0.2 + validate.io-integer-array: ^1.0.0 + checksum: d499ab57dcb48e8d0fd233b99844a06d1cc56115602c920c586e998ebba60293731f5b6976e8a1e83ae6cbfe86716f62d9432e8d94913fed8bd8352f447dc917 + languageName: node + linkType: hard + +"concat-map@npm:0.0.1": + version: 0.0.1 + resolution: "concat-map@npm:0.0.1" + checksum: 902a9f5d8967a3e2faf138d5cb784b9979bad2e6db5357c5b21c568df4ebe62bcb15108af1b2253744844eb964fc023fbd9afbbbb6ddd0bcc204c6fb5b7bf3af + languageName: node + linkType: hard + +"concat-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "concat-stream@npm:2.0.0" + dependencies: + buffer-from: ^1.0.0 + inherits: ^2.0.3 + readable-stream: ^3.0.2 + typedarray: ^0.0.6 + checksum: d7f75d48f0ecd356c1545d87e22f57b488172811b1181d96021c7c4b14ab8855f5313280263dca44bb06e5222f274d047da3e290a38841ef87b59719bde967c7 + languageName: node + linkType: hard + +"concat-with-sourcemaps@npm:^1.1.0": + version: 1.1.0 + resolution: "concat-with-sourcemaps@npm:1.1.0" + dependencies: + source-map: ^0.6.1 + checksum: 57faa6f4a6f38a1846a58f96b2745ec8435755e0021f069e89085c651d091b78d9bc20807ea76c38c85021acca80dc2fa4cedda666aade169b602604215d25b9 + languageName: node + linkType: hard + +"concurrently@npm:6.5.1": + version: 6.5.1 + resolution: "concurrently@npm:6.5.1" + dependencies: + chalk: ^4.1.0 + date-fns: ^2.16.1 + lodash: ^4.17.21 + rxjs: ^6.6.3 + spawn-command: ^0.0.2-1 + supports-color: ^8.1.0 + tree-kill: ^1.2.2 + yargs: ^16.2.0 + bin: + concurrently: bin/concurrently.js + checksum: 3f4d89b464fa5c9fb6f9489b46594c30ba54eff6ff10ab3cb5f30f64b74c83be664623a0f0cc731a3cb3f057a1f4a3292f7d3470c012a292c44aca31f214a3fa + languageName: node + linkType: hard + +"connect-history-api-fallback@npm:^2.0.0": + version: 2.0.0 + resolution: "connect-history-api-fallback@npm:2.0.0" + checksum: dc5368690f4a5c413889792f8df70d5941ca9da44523cde3f87af0745faee5ee16afb8195434550f0504726642734f2683d6c07f8b460f828a12c45fbd4c9a68 + languageName: node + linkType: hard + +"consola@npm:^2.15.0": + version: 2.15.3 + resolution: "consola@npm:2.15.3" + checksum: 8ef7a09b703ec67ac5c389a372a33b6dc97eda6c9876443a60d76a3076eea0259e7f67a4e54fd5a52f97df73690822d090cf8b7e102b5761348afef7c6d03e28 + languageName: node + linkType: hard + +"console-browserify@npm:^1.1.0": + version: 1.2.0 + resolution: "console-browserify@npm:1.2.0" + checksum: 226591eeff8ed68e451dffb924c1fb750c654d54b9059b3b261d360f369d1f8f70650adecf2c7136656236a4bfeb55c39281b5d8a55d792ebbb99efd3d848d52 + languageName: node + linkType: hard + +"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0": + version: 1.1.0 + resolution: "console-control-strings@npm:1.1.0" + checksum: 8755d76787f94e6cf79ce4666f0c5519906d7f5b02d4b884cf41e11dcd759ed69c57da0670afd9236d229a46e0f9cf519db0cd829c6dca820bb5a5c3def584ed + languageName: node + linkType: hard + +"console.table@npm:0.10.0": + version: 0.10.0 + resolution: "console.table@npm:0.10.0" + dependencies: + easy-table: 1.1.0 + checksum: 4c1460e3105a5f7df5bfa372844104a20e487fc0fccc5821c169a39def3249759554fc132621074ad6695664a1a8d558dd385c0e7f290acb2eaca51466474bb9 + languageName: node + linkType: hard + +"constants-browserify@npm:^1.0.0": + version: 1.0.0 + resolution: "constants-browserify@npm:1.0.0" + checksum: f7ac8c6d0b6e4e0c77340a1d47a3574e25abd580bfd99ad707b26ff7618596cf1a5e5ce9caf44715e9e01d4a5d12cb3b4edaf1176f34c19adb2874815a56e64f + languageName: node + linkType: hard + +"content-disposition@npm:0.5.4, content-disposition@npm:~0.5.2": + version: 0.5.4 + resolution: "content-disposition@npm:0.5.4" + dependencies: + safe-buffer: 5.2.1 + checksum: afb9d545e296a5171d7574fcad634b2fdf698875f4006a9dd04a3e1333880c5c0c98d47b560d01216fb6505a54a2ba6a843ee3a02ec86d7e911e8315255f56c3 + languageName: node + linkType: hard + +"content-type@npm:^1.0.4, content-type@npm:~1.0.4, content-type@npm:~1.0.5": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: 566271e0a251642254cde0f845f9dd4f9856e52d988f4eb0d0dcffbb7a1f8ec98de7a5215fc628f3bce30fe2fb6fd2bc064b562d721658c59b544e2d34ea2766 + languageName: node + linkType: hard + +"convert-source-map@npm:^1.5.0": + version: 1.9.0 + resolution: "convert-source-map@npm:1.9.0" + checksum: dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 + languageName: node + linkType: hard + +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 63ae9933be5a2b8d4509daca5124e20c14d023c820258e484e32dc324d34c2754e71297c94a05784064ad27615037ef677e3f0c00469fb55f409d2bb21261035 + languageName: node + linkType: hard + +"cookie-signature@npm:1.0.6": + version: 1.0.6 + resolution: "cookie-signature@npm:1.0.6" + checksum: f4e1b0a98a27a0e6e66fd7ea4e4e9d8e038f624058371bf4499cfcd8f3980be9a121486995202ba3fca74fbed93a407d6d54d43a43f96fd28d0bd7a06761591a + languageName: node + linkType: hard + +"cookie@npm:0.7.1": + version: 0.7.1 + resolution: "cookie@npm:0.7.1" + checksum: cec5e425549b3650eb5c3498a9ba3cde0b9cd419e3b36e4b92739d30b4d89e0b678b98c1ddc209ce7cf958cd3215671fd6ac47aec21f10c2a0cc68abd399d8a7 + languageName: node + linkType: hard + +"cookie@npm:^0.4.2": + version: 0.4.2 + resolution: "cookie@npm:0.4.2" + checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b + languageName: node + linkType: hard + +"cookie@npm:^0.7.0": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 9bf8555e33530affd571ea37b615ccad9b9a34febbf2c950c86787088eb00a8973690833b0f8ebd6b69b753c62669ea60cec89178c1fb007bf0749abed74f93e + languageName: node + linkType: hard + +"cookiejar@npm:^2.1.4": + version: 2.1.4 + resolution: "cookiejar@npm:2.1.4" + checksum: c4442111963077dc0e5672359956d6556a195d31cbb35b528356ce5f184922b99ac48245ac05ed86cf993f7df157c56da10ab3efdadfed79778a0d9b1b092d5b + languageName: node + linkType: hard + +"cookies@npm:~0.9.0": + version: 0.9.1 + resolution: "cookies@npm:0.9.1" + dependencies: + depd: ~2.0.0 + keygrip: ~1.1.0 + checksum: 213e4d14847b582fbd8a003203d3621a4b9fa792a315c37954e89332d38fac5bcc34ba92ef316ad6d5fe28f0187aaa115927fbbe2080744ad1707a93b4313247 + languageName: node + linkType: hard + +"copy-to-clipboard@npm:^3.3.1": + version: 3.3.3 + resolution: "copy-to-clipboard@npm:3.3.3" + dependencies: + toggle-selection: ^1.0.6 + checksum: e0a325e39b7615108e6c1c8ac110ae7b829cdc4ee3278b1df6a0e4228c490442cc86444cd643e2da344fbc424b3aab8909e2fec82f8bc75e7e5b190b7c24eecf + languageName: node + linkType: hard + +"core-js-compat@npm:^3.38.0, core-js-compat@npm:^3.38.1": + version: 3.38.1 + resolution: "core-js-compat@npm:3.38.1" + dependencies: + browserslist: ^4.23.3 + checksum: a0a5673bcd59f588f0cd0b59cdacd4712b82909738a87406d334dd412eb3d273ae72b275bdd8e8fef63fca9ef12b42ed651be139c7c44c8a1acb423c8906992e + languageName: node + linkType: hard + +"core-js-pure@npm:^3.23.3": + version: 3.38.1 + resolution: "core-js-pure@npm:3.38.1" + checksum: 95ca2e75df371571b0d41cba81e1f6335a2ba1f080e80f8edfa124ad3041880fe72e10f2144527a700a3d993dbf9f7cada3e04a927a66604bc49d0c4951567fb + languageName: node + linkType: hard + +"core-util-is@npm:1.0.2": + version: 1.0.2 + resolution: "core-util-is@npm:1.0.2" + checksum: 7a4c925b497a2c91421e25bf76d6d8190f0b2359a9200dbeed136e63b2931d6294d3b1893eda378883ed363cd950f44a12a401384c609839ea616befb7927dab + languageName: node + linkType: hard + +"core-util-is@npm:~1.0.0": + version: 1.0.3 + resolution: "core-util-is@npm:1.0.3" + checksum: 9de8597363a8e9b9952491ebe18167e3b36e7707569eed0ebf14f8bba773611376466ae34575bca8cfe3c767890c859c74056084738f09d4e4a6f902b2ad7d99 + languageName: node + linkType: hard + +"cors@npm:^2.8.5": + version: 2.8.5 + resolution: "cors@npm:2.8.5" + dependencies: + object-assign: ^4 + vary: ^1 + checksum: ced838404ccd184f61ab4fdc5847035b681c90db7ac17e428f3d81d69e2989d2b680cc254da0e2554f5ed4f8a341820a1ce3d1c16b499f6e2f47a1b9b07b5006 + languageName: node + linkType: hard + +"cosmiconfig@npm:^6.0.0": + version: 6.0.0 + resolution: "cosmiconfig@npm:6.0.0" + dependencies: + "@types/parse-json": ^4.0.0 + import-fresh: ^3.1.0 + parse-json: ^5.0.0 + path-type: ^4.0.0 + yaml: ^1.7.2 + checksum: 8eed7c854b91643ecb820767d0deb038b50780ecc3d53b0b19e03ed8aabed4ae77271198d1ae3d49c3b110867edf679f5faad924820a8d1774144a87cb6f98fc + languageName: node + linkType: hard + +"cosmiconfig@npm:^7.0.0, cosmiconfig@npm:^7.0.1": + version: 7.1.0 + resolution: "cosmiconfig@npm:7.1.0" + dependencies: + "@types/parse-json": ^4.0.0 + import-fresh: ^3.2.1 + parse-json: ^5.0.0 + path-type: ^4.0.0 + yaml: ^1.10.0 + checksum: c53bf7befc1591b2651a22414a5e786cd5f2eeaa87f3678a3d49d6069835a9d8d1aef223728e98aa8fec9a95bf831120d245096db12abe019fecb51f5696c96f + languageName: node + linkType: hard + +"cosmiconfig@npm:^8.2.0": + version: 8.3.6 + resolution: "cosmiconfig@npm:8.3.6" + dependencies: + import-fresh: ^3.3.0 + js-yaml: ^4.1.0 + parse-json: ^5.2.0 + path-type: ^4.0.0 + peerDependencies: + typescript: ">=4.9.5" + peerDependenciesMeta: + typescript: + optional: true + checksum: dc339ebea427898c9e03bf01b56ba7afbac07fc7d2a2d5a15d6e9c14de98275a9565da949375aee1809591c152c0a3877bb86dbeaf74d5bd5aaa79955ad9e7a0 + languageName: node + linkType: hard + +"cpu-features@npm:~0.0.10": + version: 0.0.10 + resolution: "cpu-features@npm:0.0.10" + dependencies: + buildcheck: ~0.0.6 + nan: ^2.19.0 + node-gyp: latest + checksum: ab17e25cea0b642bdcfd163d3d872be4cc7d821e854d41048557799e990d672ee1cc7bd1d4e7c4de0309b1683d4c001d36ba8569b5035d1e7e2ff2d681f681d7 + languageName: node + linkType: hard + +"crc-32@npm:^1.2.0": + version: 1.2.2 + resolution: "crc-32@npm:1.2.2" + bin: + crc32: bin/crc32.njs + checksum: ad2d0ad0cbd465b75dcaeeff0600f8195b686816ab5f3ba4c6e052a07f728c3e70df2e3ca9fd3d4484dc4ba70586e161ca5a2334ec8bf5a41bf022a6103ff243 + languageName: node + linkType: hard + +"crc32-stream@npm:^6.0.0": + version: 6.0.0 + resolution: "crc32-stream@npm:6.0.0" + dependencies: + crc-32: ^1.2.0 + readable-stream: ^4.0.0 + checksum: e6edc2f81bc387daef6d18b2ac18c2ffcb01b554d3b5c7d8d29b177505aafffba574658fdd23922767e8dab1183d1962026c98c17e17fb272794c33293ef607c + languageName: node + linkType: hard + +"create-ecdh@npm:^4.0.0": + version: 4.0.4 + resolution: "create-ecdh@npm:4.0.4" + dependencies: + bn.js: ^4.1.0 + elliptic: ^6.5.3 + checksum: 0dd7fca9711d09e152375b79acf1e3f306d1a25ba87b8ff14c2fd8e68b83aafe0a7dd6c4e540c9ffbdd227a5fa1ad9b81eca1f233c38bb47770597ba247e614b + languageName: node + linkType: hard + +"create-hash@npm:^1.1.0, create-hash@npm:^1.1.2, create-hash@npm:^1.2.0": + version: 1.2.0 + resolution: "create-hash@npm:1.2.0" + dependencies: + cipher-base: ^1.0.1 + inherits: ^2.0.1 + md5.js: ^1.3.4 + ripemd160: ^2.0.1 + sha.js: ^2.4.0 + checksum: 02a6ae3bb9cd4afee3fabd846c1d8426a0e6b495560a977ba46120c473cb283be6aa1cace76b5f927cf4e499c6146fb798253e48e83d522feba807d6b722eaa9 + languageName: node + linkType: hard + +"create-hmac@npm:^1.1.0, create-hmac@npm:^1.1.4, create-hmac@npm:^1.1.7": + version: 1.1.7 + resolution: "create-hmac@npm:1.1.7" + dependencies: + cipher-base: ^1.0.3 + create-hash: ^1.1.0 + inherits: ^2.0.1 + ripemd160: ^2.0.0 + safe-buffer: ^5.0.1 + sha.js: ^2.4.8 + checksum: ba12bb2257b585a0396108c72830e85f882ab659c3320c83584b1037f8ab72415095167ced80dc4ce8e446a8ecc4b2acf36d87befe0707d73b26cf9dc77440ed + languageName: node + linkType: hard + +"create-jest@npm:^29.7.0": + version: 29.7.0 + resolution: "create-jest@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + chalk: ^4.0.0 + exit: ^0.1.2 + graceful-fs: ^4.2.9 + jest-config: ^29.7.0 + jest-util: ^29.7.0 + prompts: ^2.0.1 + bin: + create-jest: bin/create-jest.js + checksum: 1427d49458adcd88547ef6fa39041e1fe9033a661293aa8d2c3aa1b4967cb5bf4f0c00436c7a61816558f28ba2ba81a94d5c962e8022ea9a883978fc8e1f2945 + languageName: node + linkType: hard + +"create-require@npm:^1.1.0": + version: 1.1.1 + resolution: "create-require@npm:1.1.1" + checksum: a9a1503d4390d8b59ad86f4607de7870b39cad43d929813599a23714831e81c520bddf61bcdd1f8e30f05fd3a2b71ae8538e946eb2786dc65c2bbc520f692eff + languageName: node + linkType: hard + +"cron-parser@npm:^4.2.0": + version: 4.9.0 + resolution: "cron-parser@npm:4.9.0" + dependencies: + luxon: ^3.2.1 + checksum: 3cf248fc5cae6c19ec7124962b1cd84b76f02b9bc4f58976b3bd07624db3ef10aaf1548efcc2d2dcdab0dad4f12029d640a55ecce05ea5e1596af9db585502cf + languageName: node + linkType: hard + +"cron@npm:^3.0.0": + version: 3.1.7 + resolution: "cron@npm:3.1.7" + dependencies: + "@types/luxon": ~3.4.0 + luxon: ~3.4.0 + checksum: d98ee5297543c138221d96dd49270bf6576db80134e6041f4ce4a3c0cb6060863d76910209b34fee66fbf134461449ec3bd283d6a76d1c50da220cde7fc10c65 + languageName: node + linkType: hard + +"cronstrue@npm:^2.32.0": + version: 2.50.0 + resolution: "cronstrue@npm:2.50.0" + bin: + cronstrue: bin/cli.js + checksum: bf6e51c4b9ab28d7ba928a392a76b7d97bd3c3dc8da5618db8424480dc6213cafed658ea835925675767fe5497931d1325e51634eeb8e2556f0630a62eb29cc3 + languageName: node + linkType: hard + +"cross-fetch@npm:^4.0.0": + version: 4.0.0 + resolution: "cross-fetch@npm:4.0.0" + dependencies: + node-fetch: ^2.6.12 + checksum: ecca4f37ffa0e8283e7a8a590926b66713a7ef7892757aa36c2d20ffa27b0ac5c60dcf453119c809abe5923fc0bae3702a4d896bfb406ef1077b0d0018213e24 + languageName: node + linkType: hard + +"cross-spawn@npm:^5.1.0": + version: 5.1.0 + resolution: "cross-spawn@npm:5.1.0" + dependencies: + lru-cache: ^4.0.1 + shebang-command: ^1.2.0 + which: ^1.2.9 + checksum: 726939c9954fc70c20e538923feaaa33bebc253247d13021737c3c7f68cdc3e0a57f720c0fe75057c0387995349f3f12e20e9bfdbf12274db28019c7ea4ec166 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: ^3.1.0 + shebang-command: ^2.0.0 + which: ^2.0.1 + checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 + languageName: node + linkType: hard + +"crypto-browserify@npm:^3.11.0": + version: 3.12.0 + resolution: "crypto-browserify@npm:3.12.0" + dependencies: + browserify-cipher: ^1.0.0 + browserify-sign: ^4.0.0 + create-ecdh: ^4.0.0 + create-hash: ^1.1.0 + create-hmac: ^1.1.0 + diffie-hellman: ^5.0.0 + inherits: ^2.0.1 + pbkdf2: ^3.0.3 + public-encrypt: ^4.0.0 + randombytes: ^2.0.0 + randomfill: ^1.0.3 + checksum: c1609af82605474262f3eaa07daa0b2140026bd264ab316d4bf1170272570dbe02f0c49e29407fe0d3634f96c507c27a19a6765fb856fed854a625f9d15618e2 + languageName: node + linkType: hard + +"css-box-model@npm:^1.2.0": + version: 1.2.1 + resolution: "css-box-model@npm:1.2.1" + dependencies: + tiny-invariant: ^1.0.6 + checksum: 4d113f26fed6b9150e2c314502d00dabe06f12ae43a01a7e9b6e57f3de49b4281dbb0dc46a1158a7349618f8f34d9250af57cb43d7337e9485e73e6b821e470e + languageName: node + linkType: hard + +"css-declaration-sorter@npm:^6.3.1": + version: 6.4.1 + resolution: "css-declaration-sorter@npm:6.4.1" + peerDependencies: + postcss: ^8.0.9 + checksum: cbdc9e0d481011b1a28fd5b60d4eb55fe204391d31a0b1b490b2cecf4baa85810f9b8c48adab4df644f4718104ed3ed72c64a9745e3216173767bf4aeca7f9b8 + languageName: node + linkType: hard + +"css-in-js-utils@npm:^3.1.0": + version: 3.1.0 + resolution: "css-in-js-utils@npm:3.1.0" + dependencies: + hyphenate-style-name: ^1.0.3 + checksum: 066318e918c04a5e5bce46b38fe81052ea6ac051bcc6d3c369a1d59ceb1546cb2b6086901ab5d22be084122ee3732169996a3dfb04d3406eaee205af77aec61b + languageName: node + linkType: hard + +"css-loader@npm:^6.5.1": + version: 6.11.0 + resolution: "css-loader@npm:6.11.0" + dependencies: + icss-utils: ^5.1.0 + postcss: ^8.4.33 + postcss-modules-extract-imports: ^3.1.0 + postcss-modules-local-by-default: ^4.0.5 + postcss-modules-scope: ^3.2.0 + postcss-modules-values: ^4.0.0 + postcss-value-parser: ^4.2.0 + semver: ^7.5.4 + peerDependencies: + "@rspack/core": 0.x || 1.x + webpack: ^5.0.0 + peerDependenciesMeta: + "@rspack/core": + optional: true + webpack: + optional: true + checksum: 5c8d35975a7121334905394e88e28f05df72f037dbed2fb8fec4be5f0b313ae73a13894ba791867d4a4190c35896da84a7fd0c54fb426db55d85ba5e714edbe3 + languageName: node + linkType: hard + +"css-select@npm:^4.1.3": + version: 4.3.0 + resolution: "css-select@npm:4.3.0" + dependencies: + boolbase: ^1.0.0 + css-what: ^6.0.1 + domhandler: ^4.3.1 + domutils: ^2.8.0 + nth-check: ^2.0.1 + checksum: d6202736839194dd7f910320032e7cfc40372f025e4bf21ca5bf6eb0a33264f322f50ba9c0adc35dadd342d3d6fae5ca244779a4873afbfa76561e343f2058e0 + languageName: node + linkType: hard + +"css-tree@npm:^1.1.2, css-tree@npm:^1.1.3": + version: 1.1.3 + resolution: "css-tree@npm:1.1.3" + dependencies: + mdn-data: 2.0.14 + source-map: ^0.6.1 + checksum: 79f9b81803991b6977b7fcb1588799270438274d89066ce08f117f5cdb5e20019b446d766c61506dd772c839df84caa16042d6076f20c97187f5abe3b50e7d1f + languageName: node + linkType: hard + +"css-vendor@npm:^2.0.8": + version: 2.0.8 + resolution: "css-vendor@npm:2.0.8" + dependencies: + "@babel/runtime": ^7.8.3 + is-in-browser: ^1.0.2 + checksum: 647cd4ea5e401c65c59376255aa2b708e92bf84fba9ce2b3ff5ecb94bf51d74ac374052b1cf9956ef7419b8ebf07fcea9a7683d2d2459127b2ca747ab5b98745 + languageName: node + linkType: hard + +"css-what@npm:^6.0.1": + version: 6.1.0 + resolution: "css-what@npm:6.1.0" + checksum: b975e547e1e90b79625918f84e67db5d33d896e6de846c9b584094e529f0c63e2ab85ee33b9daffd05bff3a146a1916bec664e18bb76dd5f66cbff9fc13b2bbe + languageName: node + linkType: hard + +"css.escape@npm:^1.5.1": + version: 1.5.1 + resolution: "css.escape@npm:1.5.1" + checksum: f6d38088d870a961794a2580b2b2af1027731bb43261cfdce14f19238a88664b351cc8978abc20f06cc6bbde725699dec8deb6fe9816b139fc3f2af28719e774 + languageName: node + linkType: hard + +"cssesc@npm:^3.0.0": + version: 3.0.0 + resolution: "cssesc@npm:3.0.0" + bin: + cssesc: bin/cssesc + checksum: f8c4ababffbc5e2ddf2fa9957dda1ee4af6048e22aeda1869d0d00843223c1b13ad3f5d88b51caa46c994225eacb636b764eb807a8883e2fb6f99b4f4e8c48b2 + languageName: node + linkType: hard + +"cssnano-preset-default@npm:^5.2.14": + version: 5.2.14 + resolution: "cssnano-preset-default@npm:5.2.14" + dependencies: + css-declaration-sorter: ^6.3.1 + cssnano-utils: ^3.1.0 + postcss-calc: ^8.2.3 + postcss-colormin: ^5.3.1 + postcss-convert-values: ^5.1.3 + postcss-discard-comments: ^5.1.2 + postcss-discard-duplicates: ^5.1.0 + postcss-discard-empty: ^5.1.1 + postcss-discard-overridden: ^5.1.0 + postcss-merge-longhand: ^5.1.7 + postcss-merge-rules: ^5.1.4 + postcss-minify-font-values: ^5.1.0 + postcss-minify-gradients: ^5.1.1 + postcss-minify-params: ^5.1.4 + postcss-minify-selectors: ^5.2.1 + postcss-normalize-charset: ^5.1.0 + postcss-normalize-display-values: ^5.1.0 + postcss-normalize-positions: ^5.1.1 + postcss-normalize-repeat-style: ^5.1.1 + postcss-normalize-string: ^5.1.0 + postcss-normalize-timing-functions: ^5.1.0 + postcss-normalize-unicode: ^5.1.1 + postcss-normalize-url: ^5.1.0 + postcss-normalize-whitespace: ^5.1.1 + postcss-ordered-values: ^5.1.3 + postcss-reduce-initial: ^5.1.2 + postcss-reduce-transforms: ^5.1.0 + postcss-svgo: ^5.1.0 + postcss-unique-selectors: ^5.1.1 + peerDependencies: + postcss: ^8.2.15 + checksum: d3bbbe3d50c6174afb28d0bdb65b511fdab33952ec84810aef58b87189f3891c34aaa8b6a6101acd5314f8acded839b43513e39a75f91a698ddc985a1b1d9e95 + languageName: node + linkType: hard + +"cssnano-utils@npm:^3.1.0": + version: 3.1.0 + resolution: "cssnano-utils@npm:3.1.0" + peerDependencies: + postcss: ^8.2.15 + checksum: 975c84ce9174cf23bb1da1e9faed8421954607e9ea76440cd3bb0c1bea7e17e490d800fca5ae2812d1d9e9d5524eef23ede0a3f52497d7ccc628e5d7321536f2 + languageName: node + linkType: hard + +"cssnano@npm:^5.0.1": + version: 5.1.15 + resolution: "cssnano@npm:5.1.15" + dependencies: + cssnano-preset-default: ^5.2.14 + lilconfig: ^2.0.3 + yaml: ^1.10.2 + peerDependencies: + postcss: ^8.2.15 + checksum: ca9e1922178617c66c2f1548824b2c7af2ecf69cc3a187fc96bf8d29251c2e84d9e4966c69cf64a2a6a057a37dff7d6d057bc8a2a0957e6ea382e452ae9d0bbb + languageName: node + linkType: hard + +"csso@npm:^4.2.0": + version: 4.2.0 + resolution: "csso@npm:4.2.0" + dependencies: + css-tree: ^1.1.2 + checksum: 380ba9663da3bcea58dee358a0d8c4468bb6539be3c439dc266ac41c047217f52fd698fb7e4b6b6ccdfb8cf53ef4ceed8cc8ceccb8dfca2aa628319826b5b998 + languageName: node + linkType: hard + +"cssom@npm:^0.5.0": + version: 0.5.0 + resolution: "cssom@npm:0.5.0" + checksum: 823471aa30091c59e0a305927c30e7768939b6af70405808f8d2ce1ca778cddcb24722717392438329d1691f9a87cb0183b64b8d779b56a961546d54854fde01 + languageName: node + linkType: hard + +"cssom@npm:~0.3.6": + version: 0.3.8 + resolution: "cssom@npm:0.3.8" + checksum: 24beb3087c76c0d52dd458be9ee1fbc80ac771478a9baef35dd258cdeb527c68eb43204dd439692bb2b1ae5272fa5f2946d10946edab0d04f1078f85e06bc7f6 + languageName: node + linkType: hard + +"cssstyle@npm:^2.3.0": + version: 2.3.0 + resolution: "cssstyle@npm:2.3.0" + dependencies: + cssom: ~0.3.6 + checksum: 5f05e6fd2e3df0b44695c2f08b9ef38b011862b274e320665176467c0725e44a53e341bc4959a41176e83b66064ab786262e7380fd1cabeae6efee0d255bb4e3 + languageName: node + linkType: hard + +"csstype@npm:^2.5.2": + version: 2.6.21 + resolution: "csstype@npm:2.6.21" + checksum: 2ce8bc832375146eccdf6115a1f8565a27015b74cce197c35103b4494955e9516b246140425ad24103864076aa3e1257ac9bab25a06c8d931dd87a6428c9dccf + languageName: node + linkType: hard + +"csstype@npm:^3.0.2, csstype@npm:^3.1.2, csstype@npm:^3.1.3": + version: 3.1.3 + resolution: "csstype@npm:3.1.3" + checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7 + languageName: node + linkType: hard + +"csv-parse@npm:^5.3.5, csv-parse@npm:^5.5.5": + version: 5.5.6 + resolution: "csv-parse@npm:5.5.6" + checksum: ee06f97f674487dc1d001b360de8ea510a41b9d971abf43bcf9c3be22c83a3634df0d3ebfbe52fd49d145077066be7ff9f25de3fc6b71aefb973099b04147a25 + languageName: node + linkType: hard + +"ctrlc-windows@npm:^2.1.0": + version: 2.1.0 + resolution: "ctrlc-windows@npm:2.1.0" + checksum: 0f0582ba9516290d3e90ea7b91710f8b9b110e1ed29b7c84ebd44c16368b2553722b86a17226120ca3ea0ef679ac3596f48104cc113cfb7c3d07260f6c92e38b + languageName: node + linkType: hard + +"d3-color@npm:1 - 3": + version: 3.1.0 + resolution: "d3-color@npm:3.1.0" + checksum: 4931fbfda5d7c4b5cfa283a13c91a954f86e3b69d75ce588d06cde6c3628cebfc3af2069ccf225e982e8987c612aa7948b3932163ce15eb3c11cd7c003f3ee3b + languageName: node + linkType: hard + +"d3-dispatch@npm:1 - 3": + version: 3.0.1 + resolution: "d3-dispatch@npm:3.0.1" + checksum: fdfd4a230f46463e28e5b22a45dd76d03be9345b605e1b5dc7d18bd7ebf504e6c00ae123fd6d03e23d9e2711e01f0e14ea89cd0632545b9f0c00b924ba4be223 + languageName: node + linkType: hard + +"d3-drag@npm:2 - 3": + version: 3.0.0 + resolution: "d3-drag@npm:3.0.0" + dependencies: + d3-dispatch: 1 - 3 + d3-selection: 3 + checksum: d297231e60ecd633b0d076a63b4052b436ddeb48b5a3a11ff68c7e41a6774565473a6b064c5e9256e88eca6439a917ab9cea76032c52d944ddbf4fd289e31111 + languageName: node + linkType: hard + +"d3-ease@npm:1 - 3": + version: 3.0.1 + resolution: "d3-ease@npm:3.0.1" + checksum: 06e2ee5326d1e3545eab4e2c0f84046a123dcd3b612e68858219aa034da1160333d9ce3da20a1d3486d98cb5c2a06f7d233eee1bc19ce42d1533458bd85dedcd + languageName: node + linkType: hard + +"d3-interpolate@npm:1 - 3": + version: 3.0.1 + resolution: "d3-interpolate@npm:3.0.1" + dependencies: + d3-color: 1 - 3 + checksum: a42ba314e295e95e5365eff0f604834e67e4a3b3c7102458781c477bd67e9b24b6bb9d8e41ff5521050a3f2c7c0c4bbbb6e187fd586daa3980943095b267e78b + languageName: node + linkType: hard + +"d3-path@npm:^3.1.0": + version: 3.1.0 + resolution: "d3-path@npm:3.1.0" + checksum: 2306f1bd9191e1eac895ec13e3064f732a85f243d6e627d242a313f9777756838a2215ea11562f0c7630c7c3b16a19ec1fe0948b1c82f3317fac55882f6ee5d8 + languageName: node + linkType: hard + +"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-selection@npm:3.0.0" + checksum: f4e60e133309115b99f5b36a79ae0a19d71ee6e2d5e3c7216ef3e75ebd2cb1e778c2ed2fa4c01bef35e0dcbd96c5428f5bd6ca2184fe2957ed582fde6841cbc5 + languageName: node + linkType: hard + +"d3-shape@npm:^3.0.0": + version: 3.2.0 + resolution: "d3-shape@npm:3.2.0" + dependencies: + d3-path: ^3.1.0 + checksum: de2af5fc9a93036a7b68581ca0bfc4aca2d5a328aa7ba7064c11aedd44d24f310c20c40157cb654359d4c15c3ef369f95ee53d71221017276e34172c7b719cfa + languageName: node + linkType: hard + +"d3-timer@npm:1 - 3": + version: 3.0.1 + resolution: "d3-timer@npm:3.0.1" + checksum: 1cfddf86d7bca22f73f2c427f52dfa35c49f50d64e187eb788dcad6e927625c636aa18ae4edd44d084eb9d1f81d8ca4ec305dae7f733c15846a824575b789d73 + languageName: node + linkType: hard + +"d3-transition@npm:2 - 3": + version: 3.0.1 + resolution: "d3-transition@npm:3.0.1" + dependencies: + d3-color: 1 - 3 + d3-dispatch: 1 - 3 + d3-ease: 1 - 3 + d3-interpolate: 1 - 3 + d3-timer: 1 - 3 + peerDependencies: + d3-selection: 2 - 3 + checksum: cb1e6e018c3abf0502fe9ff7b631ad058efb197b5e14b973a410d3935aead6e3c07c67d726cfab258e4936ef2667c2c3d1cd2037feb0765f0b4e1d3b8788c0ea + languageName: node + linkType: hard + +"d3-zoom@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-zoom@npm:3.0.0" + dependencies: + d3-dispatch: 1 - 3 + d3-drag: 2 - 3 + d3-interpolate: 1 - 3 + d3-selection: 2 - 3 + d3-transition: 2 - 3 + checksum: 8056e3527281cfd1ccbcbc458408f86973b0583e9dac00e51204026d1d36803ca437f970b5736f02fafed9f2b78f145f72a5dbc66397e02d4d95d4c594b8ff54 + languageName: node + linkType: hard + +"dagre@npm:^0.8.5": + version: 0.8.5 + resolution: "dagre@npm:0.8.5" + dependencies: + graphlib: ^2.1.8 + lodash: ^4.17.15 + checksum: b9fabd425466d7b662381c2e457b1adda996bc4169aa60121d4de50250d83a6bb4b77d559e2f887c9c564caea781c2a377fd4de2a76c15f8f04ec3d086ca95f9 + languageName: node + linkType: hard + +"damerau-levenshtein@npm:^1.0.8": + version: 1.0.8 + resolution: "damerau-levenshtein@npm:1.0.8" + checksum: d240b7757544460ae0586a341a53110ab0a61126570ef2d8c731e3eab3f0cb6e488e2609e6a69b46727635de49be20b071688698744417ff1b6c1d7ccd03e0de + languageName: node + linkType: hard + +"dashdash@npm:^1.12.0": + version: 1.14.1 + resolution: "dashdash@npm:1.14.1" + dependencies: + assert-plus: ^1.0.0 + checksum: 3634c249570f7f34e3d34f866c93f866c5b417f0dd616275decae08147dcdf8fccfaa5947380ccfb0473998ea3a8057c0b4cd90c875740ee685d0624b2983598 + languageName: node + linkType: hard + +"data-uri-to-buffer@npm:^6.0.2": + version: 6.0.2 + resolution: "data-uri-to-buffer@npm:6.0.2" + checksum: 8b6927c33f9b54037f442856be0aa20e5fd49fa6c9c8ceece408dc306445d593ad72d207d57037c529ce65f413b421da800c6827b1dbefb607b8056f17123a61 + languageName: node + linkType: hard + +"data-urls@npm:^3.0.2": + version: 3.0.2 + resolution: "data-urls@npm:3.0.2" + dependencies: + abab: ^2.0.6 + whatwg-mimetype: ^3.0.0 + whatwg-url: ^11.0.0 + checksum: 033fc3dd0fba6d24bc9a024ddcf9923691dd24f90a3d26f6545d6a2f71ec6956f93462f2cdf2183cc46f10dc01ed3bcb36731a8208456eb1a08147e571fe2a76 + languageName: node + linkType: hard + +"data-view-buffer@npm:^1.0.1": + version: 1.0.1 + resolution: "data-view-buffer@npm:1.0.1" + dependencies: + call-bind: ^1.0.6 + es-errors: ^1.3.0 + is-data-view: ^1.0.1 + checksum: ce24348f3c6231223b216da92e7e6a57a12b4af81a23f27eff8feabdf06acfb16c00639c8b705ca4d167f761cfc756e27e5f065d0a1f840c10b907fdaf8b988c + languageName: node + linkType: hard + +"data-view-byte-length@npm:^1.0.1": + version: 1.0.1 + resolution: "data-view-byte-length@npm:1.0.1" + dependencies: + call-bind: ^1.0.7 + es-errors: ^1.3.0 + is-data-view: ^1.0.1 + checksum: dbb3200edcb7c1ef0d68979834f81d64fd8cab2f7691b3a4c6b97e67f22182f3ec2c8602efd7b76997b55af6ff8bce485829c1feda4fa2165a6b71fb7baa4269 + languageName: node + linkType: hard + +"data-view-byte-offset@npm:^1.0.0": + version: 1.0.0 + resolution: "data-view-byte-offset@npm:1.0.0" + dependencies: + call-bind: ^1.0.6 + es-errors: ^1.3.0 + is-data-view: ^1.0.1 + checksum: 7f0bf8720b7414ca719eedf1846aeec392f2054d7af707c5dc9a753cc77eb8625f067fa901e0b5127e831f9da9056138d894b9c2be79c27a21f6db5824f009c2 + languageName: node + linkType: hard + +"dataloader@npm:^2.0.0": + version: 2.2.2 + resolution: "dataloader@npm:2.2.2" + checksum: 4dabd247089c29f194e94d5434d504f99156c5c214a03463c20f3f17f40398d7e179edee69a27c16e315519ac8739042a810090087ae26449a0e685156a02c65 + languageName: node + linkType: hard + +"date-fns@npm:^2.16.1, date-fns@npm:^2.30.0": + version: 2.30.0 + resolution: "date-fns@npm:2.30.0" + dependencies: + "@babel/runtime": ^7.21.0 + checksum: f7be01523282e9bb06c0cd2693d34f245247a29098527d4420628966a2d9aad154bd0e90a6b1cf66d37adcb769cd108cf8a7bd49d76db0fb119af5cdd13644f4 + languageName: node + linkType: hard + +"date-format@npm:^4.0.14": + version: 4.0.14 + resolution: "date-format@npm:4.0.14" + checksum: dfe5139df6af5759b9dd3c007b899b3f60d45a9240ffeee6314ab74e6ab52e9b519a44ccf285888bdd6b626c66ee9b4c8a523075fa1140617b5beb1cbb9b18d1 + languageName: node + linkType: hard + +"dayjs@npm:^1.11.9": + version: 1.11.13 + resolution: "dayjs@npm:1.11.13" + checksum: f388db88a6aa93956c1f6121644e783391c7b738b73dbc54485578736565c8931bdfba4bb94e9b1535c6e509c97d5deb918bbe1ae6b34358d994de735055cca9 + languageName: node + linkType: hard + +"debounce@npm:^1.2.0": + version: 1.2.1 + resolution: "debounce@npm:1.2.1" + checksum: 682a89506d9e54fb109526f4da255c5546102fbb8e3ae75eef3b04effaf5d4853756aee97475cd4650641869794e44f410eeb20ace2b18ea592287ab2038519e + languageName: node + linkType: hard + +"debug@npm:2.6.9, debug@npm:^2.6.0": + version: 2.6.9 + resolution: "debug@npm:2.6.9" + dependencies: + ms: 2.0.0 + checksum: d2f51589ca66df60bf36e1fa6e4386b318c3f1e06772280eea5b1ae9fd3d05e9c2b7fd8a7d862457d00853c75b00451aa2d7459b924629ee385287a650f58fe6 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:4.3.7, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5": + version: 4.3.7 + resolution: "debug@npm:4.3.7" + dependencies: + ms: ^2.1.3 + peerDependenciesMeta: + supports-color: + optional: true + checksum: 822d74e209cd910ef0802d261b150314bbcf36c582ccdbb3e70f0894823c17e49a50d3e66d96b633524263975ca16b6a833f3e3b7e030c157169a5fabac63160 + languageName: node + linkType: hard + +"debug@npm:4.3.4": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: 2.1.2 + peerDependenciesMeta: + supports-color: + optional: true + checksum: 3dbad3f94ea64f34431a9cbf0bafb61853eda57bff2880036153438f50fb5a84f27683ba0d8e5426bf41a8c6ff03879488120cf5b3a761e77953169c0600a708 + languageName: node + linkType: hard + +"debug@npm:^3.2.7": + version: 3.2.7 + resolution: "debug@npm:3.2.7" + dependencies: + ms: ^2.1.1 + checksum: b3d8c5940799914d30314b7c3304a43305fd0715581a919dacb8b3176d024a782062368405b47491516d2091d6462d4d11f2f4974a405048094f8bfebfa3071c + languageName: node + linkType: hard + +"decimal.js@npm:^10.4.2, decimal.js@npm:^10.4.3": + version: 10.4.3 + resolution: "decimal.js@npm:10.4.3" + checksum: 796404dcfa9d1dbfdc48870229d57f788b48c21c603c3f6554a1c17c10195fc1024de338b0cf9e1efe0c7c167eeb18f04548979bcc5fdfabebb7cc0ae3287bae + languageName: node + linkType: hard + +"decode-named-character-reference@npm:^1.0.0": + version: 1.0.2 + resolution: "decode-named-character-reference@npm:1.0.2" + dependencies: + character-entities: ^2.0.0 + checksum: f4c71d3b93105f20076052f9cb1523a22a9c796b8296cd35eef1ca54239c78d182c136a848b83ff8da2071e3ae2b1d300bf29d00650a6d6e675438cc31b11d78 + languageName: node + linkType: hard + +"decompress-response@npm:^4.2.0": + version: 4.2.1 + resolution: "decompress-response@npm:4.2.1" + dependencies: + mimic-response: ^2.0.0 + checksum: 4e783ca4dfe9417354d61349750fe05236f565a4415a6ca20983a311be2371debaedd9104c0b0e7b36e5f167aeaae04f84f1a0b3f8be4162f1d7d15598b8fdba + languageName: node + linkType: hard + +"decompress-response@npm:^6.0.0": + version: 6.0.0 + resolution: "decompress-response@npm:6.0.0" + dependencies: + mimic-response: ^3.1.0 + checksum: d377cf47e02d805e283866c3f50d3d21578b779731e8c5072d6ce8c13cc31493db1c2f6784da9d1d5250822120cefa44f1deab112d5981015f2e17444b763812 + languageName: node + linkType: hard + +"dedent@npm:^1.0.0": + version: 1.5.3 + resolution: "dedent@npm:1.5.3" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: 045b595557b2a8ea2eb9b0b4623d764e9a87326486fe2b61191b4342ed93dc01245644d8a09f3108a50c0ee7965f1eedd92e4a3a503ed89ea8e810566ea27f9a + languageName: node + linkType: hard + +"deep-equal@npm:^2.0.5": + version: 2.2.3 + resolution: "deep-equal@npm:2.2.3" + dependencies: + array-buffer-byte-length: ^1.0.0 + call-bind: ^1.0.5 + es-get-iterator: ^1.1.3 + get-intrinsic: ^1.2.2 + is-arguments: ^1.1.1 + is-array-buffer: ^3.0.2 + is-date-object: ^1.0.5 + is-regex: ^1.1.4 + is-shared-array-buffer: ^1.0.2 + isarray: ^2.0.5 + object-is: ^1.1.5 + object-keys: ^1.1.1 + object.assign: ^4.1.4 + regexp.prototype.flags: ^1.5.1 + side-channel: ^1.0.4 + which-boxed-primitive: ^1.0.2 + which-collection: ^1.0.1 + which-typed-array: ^1.1.13 + checksum: ee8852f23e4d20a5626c13b02f415ba443a1b30b4b3d39eaf366d59c4a85e6545d7ec917db44d476a85ae5a86064f7e5f7af7479f38f113995ba869f3a1ddc53 + languageName: node + linkType: hard + +"deep-equal@npm:~1.0.1": + version: 1.0.1 + resolution: "deep-equal@npm:1.0.1" + checksum: 5af8cbfcebf190491878a498caccc7dc9592f8ebd1685b976eacc3825619d222b5e929923163b92c4f414494e2b884f7ebf00c022e8198e8292deb70dd9785f4 + languageName: node + linkType: hard + +"deep-extend@npm:^0.6.0": + version: 0.6.0 + resolution: "deep-extend@npm:0.6.0" + checksum: 7be7e5a8d468d6b10e6a67c3de828f55001b6eb515d014f7aeb9066ce36bd5717161eb47d6a0f7bed8a9083935b465bc163ee2581c8b128d29bf61092fdf57a7 + languageName: node + linkType: hard + +"deep-is@npm:^0.1.3, deep-is@npm:~0.1.3": + version: 0.1.4 + resolution: "deep-is@npm:0.1.4" + checksum: edb65dd0d7d1b9c40b2f50219aef30e116cedd6fc79290e740972c132c09106d2e80aa0bc8826673dd5a00222d4179c84b36a790eef63a4c4bca75a37ef90804 + languageName: node + linkType: hard + +"deepmerge@npm:^2.1.1": + version: 2.2.1 + resolution: "deepmerge@npm:2.2.1" + checksum: 284b71065079e66096229f735a9a0222463c9ca9ee9dda7d5e9a0545bf254906dbc7377e3499ca3b2212073672b1a430d80587993b43b87d8de17edc6af649a8 + languageName: node + linkType: hard + +"deepmerge@npm:^4.2.2": + version: 4.3.1 + resolution: "deepmerge@npm:4.3.1" + checksum: 2024c6a980a1b7128084170c4cf56b0fd58a63f2da1660dcfe977415f27b17dbe5888668b59d0b063753f3220719d5e400b7f113609489c90160bb9a5518d052 + languageName: node + linkType: hard + +"default-browser-id@npm:^5.0.0": + version: 5.0.0 + resolution: "default-browser-id@npm:5.0.0" + checksum: 185bfaecec2c75fa423544af722a3469b20704c8d1942794a86e4364fe7d9e8e9f63241a5b769d61c8151993bc65833a5b959026fa1ccea343b3db0a33aa6deb + languageName: node + linkType: hard + +"default-browser@npm:^5.2.1": + version: 5.2.1 + resolution: "default-browser@npm:5.2.1" + dependencies: + bundle-name: ^4.1.0 + default-browser-id: ^5.0.0 + checksum: afab7eff7b7f5f7a94d9114d1ec67273d3fbc539edf8c0f80019879d53aa71e867303c6f6d7cffeb10a6f3cfb59d4f963dba3f9c96830b4540cc7339a1bf9840 + languageName: node + linkType: hard + +"defaults@npm:^1.0.3": + version: 1.0.4 + resolution: "defaults@npm:1.0.4" + dependencies: + clone: ^1.0.2 + checksum: 3a88b7a587fc076b84e60affad8b85245c01f60f38fc1d259e7ac1d89eb9ce6abb19e27215de46b98568dd5bc48471730b327637e6f20b0f1bc85cf00440c80a + languageName: node + linkType: hard + +"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": + version: 1.1.4 + resolution: "define-data-property@npm:1.1.4" + dependencies: + es-define-property: ^1.0.0 + es-errors: ^1.3.0 + gopd: ^1.0.1 + checksum: 8068ee6cab694d409ac25936eb861eea704b7763f7f342adbdfe337fc27c78d7ae0eff2364b2917b58c508d723c7a074326d068eef2e45c4edcd85cf94d0313b + languageName: node + linkType: hard + +"define-lazy-prop@npm:^2.0.0": + version: 2.0.0 + resolution: "define-lazy-prop@npm:2.0.0" + checksum: 0115fdb065e0490918ba271d7339c42453d209d4cb619dfe635870d906731eff3e1ade8028bb461ea27ce8264ec5e22c6980612d332895977e89c1bbc80fcee2 + languageName: node + linkType: hard + +"define-lazy-prop@npm:^3.0.0": + version: 3.0.0 + resolution: "define-lazy-prop@npm:3.0.0" + checksum: 54884f94caac0791bf6395a3ec530ce901cf71c47b0196b8754f3fd17edb6c0e80149c1214429d851873bb0d689dbe08dcedbb2306dc45c8534a5934723851b6 + languageName: node + linkType: hard + +"define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": + version: 1.2.1 + resolution: "define-properties@npm:1.2.1" + dependencies: + define-data-property: ^1.0.1 + has-property-descriptors: ^1.0.0 + object-keys: ^1.1.1 + checksum: b4ccd00597dd46cb2d4a379398f5b19fca84a16f3374e2249201992f36b30f6835949a9429669ee6b41b6e837205a163eadd745e472069e70dfc10f03e5fcc12 + languageName: node + linkType: hard + +"degenerator@npm:^5.0.0": + version: 5.0.1 + resolution: "degenerator@npm:5.0.1" + dependencies: + ast-types: ^0.13.4 + escodegen: ^2.1.0 + esprima: ^4.0.1 + checksum: a64fa39cdf6c2edd75188157d32338ee9de7193d7dbb2aeb4acb1eb30fa4a15ed80ba8dae9bd4d7b085472cf174a5baf81adb761aaa8e326771392c922084152 + languageName: node + linkType: hard + +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020 + languageName: node + linkType: hard + +"delegates@npm:^1.0.0": + version: 1.0.0 + resolution: "delegates@npm:1.0.0" + checksum: a51744d9b53c164ba9c0492471a1a2ffa0b6727451bdc89e31627fdf4adda9d51277cfcbfb20f0a6f08ccb3c436f341df3e92631a3440226d93a8971724771fd + languageName: node + linkType: hard + +"denque@npm:^2.1.0": + version: 2.1.0 + resolution: "denque@npm:2.1.0" + checksum: 1d4ae1d05e59ac3a3481e7b478293f4b4c813819342273f3d5b826c7ffa9753c520919ba264f377e09108d24ec6cf0ec0ac729a5686cbb8f32d797126c5dae74 + languageName: node + linkType: hard + +"depd@npm:2.0.0, depd@npm:^2.0.0, depd@npm:~2.0.0": + version: 2.0.0 + resolution: "depd@npm:2.0.0" + checksum: abbe19c768c97ee2eed6282d8ce3031126662252c58d711f646921c9623f9052e3e1906443066beec1095832f534e57c523b7333f8e7e0d93051ab6baef5ab3a + languageName: node + linkType: hard + +"depd@npm:~1.1.2": + version: 1.1.2 + resolution: "depd@npm:1.1.2" + checksum: 6b406620d269619852885ce15965272b829df6f409724415e0002c8632ab6a8c0a08ec1f0bd2add05dc7bd7507606f7e2cc034fa24224ab829580040b835ecd9 + languageName: node + linkType: hard + +"dependency-graph@npm:0.11.0, dependency-graph@npm:~0.11.0": + version: 0.11.0 + resolution: "dependency-graph@npm:0.11.0" + checksum: 477204beaa9be69e642bc31ffe7a8c383d0cf48fa27acbc91c5df01431ab913e65c154213d2ef83d034c98d77280743ec85e5da018a97a18dd43d3c0b78b28cd + languageName: node + linkType: hard + +"deprecation@npm:^2.0.0, deprecation@npm:^2.3.1": + version: 2.3.1 + resolution: "deprecation@npm:2.3.1" + checksum: f56a05e182c2c195071385455956b0c4106fe14e36245b00c689ceef8e8ab639235176a96977ba7c74afb173317fac2e0ec6ec7a1c6d1e6eaa401c586c714132 + languageName: node + linkType: hard + +"dequal@npm:^2.0.0, dequal@npm:^2.0.3": + version: 2.0.3 + resolution: "dequal@npm:2.0.3" + checksum: 8679b850e1a3d0ebbc46ee780d5df7b478c23f335887464023a631d1b9af051ad4a6595a44220f9ff8ff95a8ddccf019b5ad778a976fd7bbf77383d36f412f90 + languageName: node + linkType: hard + +"des.js@npm:^1.0.0": + version: 1.1.0 + resolution: "des.js@npm:1.1.0" + dependencies: + inherits: ^2.0.1 + minimalistic-assert: ^1.0.0 + checksum: 0e9c1584b70d31e20f20a613fc9ef60fbc6a147dfec9e448a168794a4b97ac04d8dc47ea008f1fa93b0f8aaf7c1ead632a5e59ce1913a6079d2d244c9f5ebe33 + languageName: node + linkType: hard + +"destroy@npm:1.2.0, destroy@npm:^1.0.4": + version: 1.2.0 + resolution: "destroy@npm:1.2.0" + checksum: 0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38 + languageName: node + linkType: hard + +"detect-indent@npm:^6.0.0": + version: 6.1.0 + resolution: "detect-indent@npm:6.1.0" + checksum: ab953a73c72dbd4e8fc68e4ed4bfd92c97eb6c43734af3900add963fd3a9316f3bc0578b018b24198d4c31a358571eff5f0656e81a1f3b9ad5c547d58b2d093d + languageName: node + linkType: hard + +"detect-libc@npm:^2.0.0": + version: 2.0.3 + resolution: "detect-libc@npm:2.0.3" + checksum: 2ba6a939ae55f189aea996ac67afceb650413c7a34726ee92c40fb0deb2400d57ef94631a8a3f052055eea7efb0f99a9b5e6ce923415daa3e68221f963cfc27d + languageName: node + linkType: hard + +"detect-newline@npm:^3.0.0": + version: 3.1.0 + resolution: "detect-newline@npm:3.1.0" + checksum: ae6cd429c41ad01b164c59ea36f264a2c479598e61cba7c99da24175a7ab80ddf066420f2bec9a1c57a6bead411b4655ff15ad7d281c000a89791f48cbe939e7 + languageName: node + linkType: hard + +"detect-node@npm:^2.0.4": + version: 2.1.0 + resolution: "detect-node@npm:2.1.0" + checksum: 832184ec458353e41533ac9c622f16c19f7c02d8b10c303dfd3a756f56be93e903616c0bb2d4226183c9351c15fc0b3dba41a17a2308262afabcfa3776e6ae6e + languageName: node + linkType: hard + +"detect-port-alt@npm:^1.1.6": + version: 1.1.6 + resolution: "detect-port-alt@npm:1.1.6" + dependencies: + address: ^1.0.1 + debug: ^2.6.0 + bin: + detect: ./bin/detect-port + detect-port: ./bin/detect-port + checksum: 9dc37b1fa4a9dd6d4889e1045849b8d841232b598d1ca888bf712f4035b07a17cf6d537465a0d7323250048d3a5a0540e3b7cf89457efc222f96f77e2c40d16a + languageName: node + linkType: hard + +"dezalgo@npm:^1.0.4": + version: 1.0.4 + resolution: "dezalgo@npm:1.0.4" + dependencies: + asap: ^2.0.0 + wrappy: 1 + checksum: 895389c6aead740d2ab5da4d3466d20fa30f738010a4d3f4dcccc9fc645ca31c9d10b7e1804ae489b1eb02c7986f9f1f34ba132d409b043082a86d9a4e745624 + languageName: node + linkType: hard + +"diff-sequences@npm:^29.6.3": + version: 29.6.3 + resolution: "diff-sequences@npm:29.6.3" + checksum: f4914158e1f2276343d98ff5b31fc004e7304f5470bf0f1adb2ac6955d85a531a6458d33e87667f98f6ae52ebd3891bb47d420bb48a5bd8b7a27ee25b20e33aa + languageName: node + linkType: hard + +"diff3@npm:0.0.3": + version: 0.0.3 + resolution: "diff3@npm:0.0.3" + checksum: 28d883f1057b9873dfcb38cd2750337e6b32bf184bb1c0fb3292efeb83c597f1ce9b8f508bdd0d623a58b9ca1c917b1f297b90cb7fce3a62b26b0dde496f70e6 + languageName: node + linkType: hard + +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2" + checksum: f2c09b0ce4e6b301c221addd83bf3f454c0bc00caa3dd837cf6c127d6edf7223aa2bbe3b688feea110b7f262adbfc845b757c44c8a9f8c0c5b15d8fa9ce9d20d + languageName: node + linkType: hard + +"diff@npm:^5.0.0": + version: 5.2.0 + resolution: "diff@npm:5.2.0" + checksum: 12b63ca9c36c72bafa3effa77121f0581b4015df18bc16bac1f8e263597735649f1a173c26f7eba17fb4162b073fee61788abe49610e6c70a2641fe1895443fd + languageName: node + linkType: hard + +"diffie-hellman@npm:^5.0.0": + version: 5.0.3 + resolution: "diffie-hellman@npm:5.0.3" + dependencies: + bn.js: ^4.1.0 + miller-rabin: ^4.0.0 + randombytes: ^2.0.0 + checksum: 0e620f322170c41076e70181dd1c24e23b08b47dbb92a22a644f3b89b6d3834b0f8ee19e37916164e5eb1ee26d2aa836d6129f92723995267250a0b541811065 + languageName: node + linkType: hard + +"dir-glob@npm:^3.0.1": + version: 3.0.1 + resolution: "dir-glob@npm:3.0.1" + dependencies: + path-type: ^4.0.0 + checksum: fa05e18324510d7283f55862f3161c6759a3f2f8dbce491a2fc14c8324c498286c54282c1f0e933cb930da8419b30679389499b919122952a4f8592362ef4615 + languageName: node + linkType: hard + +"discontinuous-range@npm:1.0.0": + version: 1.0.0 + resolution: "discontinuous-range@npm:1.0.0" + checksum: 8ee88d7082445b6eadc7c03bebe6dc978f96760c45e9f65d16ca66174d9e086a9e3855ee16acf65625e1a07a846a17de674f02a5964a6aebe5963662baf8b5c8 + languageName: node + linkType: hard + +"dns-packet@npm:^5.2.2": + version: 5.6.1 + resolution: "dns-packet@npm:5.6.1" + dependencies: + "@leichtgewicht/ip-codec": ^2.0.1 + checksum: 64c06457f0c6e143f7a0946e0aeb8de1c5f752217cfa143ef527467c00a6d78db1835cfdb6bb68333d9f9a4963cf23f410439b5262a8935cce1236f45e344b81 + languageName: node + linkType: hard + +"docker-compose@npm:^0.24.8": + version: 0.24.8 + resolution: "docker-compose@npm:0.24.8" + dependencies: + yaml: ^2.2.2 + checksum: 48f3564c46490f1f51899a144deb546b61450a76bffddb378379ac7702aa34b055e0237e0dc77507df94d7ad6f1f7daeeac27730230bce9aafe2e35efeda6b45 + languageName: node + linkType: hard + +"docker-modem@npm:^3.0.0": + version: 3.0.8 + resolution: "docker-modem@npm:3.0.8" + dependencies: + debug: ^4.1.1 + readable-stream: ^3.5.0 + split-ca: ^1.0.1 + ssh2: ^1.11.0 + checksum: e3675c9b1ad800be8fb1cb9c5621fbef20a75bfedcd6e01b69808eadd7f0165681e4e30d1700897b788a67dbf4769964fcccd19c3d66f6d2499bb7aede6b34df + languageName: node + linkType: hard + +"docker-modem@npm:^5.0.3": + version: 5.0.3 + resolution: "docker-modem@npm:5.0.3" + dependencies: + debug: ^4.1.1 + readable-stream: ^3.5.0 + split-ca: ^1.0.1 + ssh2: ^1.15.0 + checksum: 68f4948591622860ca95c10a01cae7f53ff2b2e8435b73b901698083b24ceb24208da12c1db2c47f073d48bc2f64a274cbf30e3c73979734f6fb3fbdf5bdb72e + languageName: node + linkType: hard + +"dockerode@npm:^3.3.5": + version: 3.3.5 + resolution: "dockerode@npm:3.3.5" + dependencies: + "@balena/dockerignore": ^1.0.2 + docker-modem: ^3.0.0 + tar-fs: ~2.0.1 + checksum: 7f6650422b07fa7ea9d5801f04b1a432634446b5fe37b995b8302b953b64e93abf1bb4596c2fb574ba47aafee685ef2ab959cc86c9654add5a26d09541bbbcc6 + languageName: node + linkType: hard + +"dockerode@npm:^4.0.0": + version: 4.0.2 + resolution: "dockerode@npm:4.0.2" + dependencies: + "@balena/dockerignore": ^1.0.2 + docker-modem: ^5.0.3 + tar-fs: ~2.0.1 + checksum: 4d36633d04ac5f662b0322d2fa4fe51fb1dd5a45f00b07379196ee5ff5dae13688a9ec1adf1edeaefab5eb22f3ae2219f62026241555a8bcf7edb396bbb5a92f + languageName: node + linkType: hard + +"doctrine@npm:^2.1.0": + version: 2.1.0 + resolution: "doctrine@npm:2.1.0" + dependencies: + esutils: ^2.0.2 + checksum: a45e277f7feaed309fe658ace1ff286c6e2002ac515af0aaf37145b8baa96e49899638c7cd47dccf84c3d32abfc113246625b3ac8f552d1046072adee13b0dc8 + languageName: node + linkType: hard + +"doctrine@npm:^3.0.0": + version: 3.0.0 + resolution: "doctrine@npm:3.0.0" + dependencies: + esutils: ^2.0.2 + checksum: fd7673ca77fe26cd5cba38d816bc72d641f500f1f9b25b83e8ce28827fe2da7ad583a8da26ab6af85f834138cf8dae9f69b0cd6ab925f52ddab1754db44d99ce + languageName: node + linkType: hard + +"dom-accessibility-api@npm:^0.5.9": + version: 0.5.16 + resolution: "dom-accessibility-api@npm:0.5.16" + checksum: 005eb283caef57fc1adec4d5df4dd49189b628f2f575af45decb210e04d634459e3f1ee64f18b41e2dcf200c844bc1d9279d80807e686a30d69a4756151ad248 + languageName: node + linkType: hard + +"dom-accessibility-api@npm:^0.6.3": + version: 0.6.3 + resolution: "dom-accessibility-api@npm:0.6.3" + checksum: c325b5144bb406df23f4affecffc117dbaec9af03daad9ee6b510c5be647b14d28ef0a4ea5ca06d696d8ab40bb777e5fed98b985976fdef9d8790178fa1d573f + languageName: node + linkType: hard + +"dom-converter@npm:^0.2.0": + version: 0.2.0 + resolution: "dom-converter@npm:0.2.0" + dependencies: + utila: ~0.4 + checksum: ea52fe303f5392e48dea563abef0e6fb3a478b8dbe3c599e99bb5d53981c6c38fc4944e56bb92a8ead6bb989d10b7914722ae11febbd2fd0910e33b9fc4aaa77 + languageName: node + linkType: hard + +"dom-helpers@npm:^5.0.1": + version: 5.2.1 + resolution: "dom-helpers@npm:5.2.1" + dependencies: + "@babel/runtime": ^7.8.7 + csstype: ^3.0.2 + checksum: 863ba9e086f7093df3376b43e74ce4422571d404fc9828bf2c56140963d5edf0e56160f9b2f3bb61b282c07f8fc8134f023c98fd684bddcb12daf7b0f14d951c + languageName: node + linkType: hard + +"dom-serializer@npm:^1.0.1": + version: 1.4.1 + resolution: "dom-serializer@npm:1.4.1" + dependencies: + domelementtype: ^2.0.1 + domhandler: ^4.2.0 + entities: ^2.0.0 + checksum: fbb0b01f87a8a2d18e6e5a388ad0f7ec4a5c05c06d219377da1abc7bb0f674d804f4a8a94e3f71ff15f6cb7dcfc75704a54b261db672b9b3ab03da6b758b0b22 + languageName: node + linkType: hard + +"domain-browser@npm:^1.1.1": + version: 1.2.0 + resolution: "domain-browser@npm:1.2.0" + checksum: 8f1235c7f49326fb762f4675795246a6295e7dd566b4697abec24afdba2460daa7dfbd1a73d31efbf5606b3b7deadb06ce47cf06f0a476e706153d62a4ff2b90 + languageName: node + linkType: hard + +"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0": + version: 2.3.0 + resolution: "domelementtype@npm:2.3.0" + checksum: ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6 + languageName: node + linkType: hard + +"domexception@npm:^4.0.0": + version: 4.0.0 + resolution: "domexception@npm:4.0.0" + dependencies: + webidl-conversions: ^7.0.0 + checksum: ddbc1268edf33a8ba02ccc596735ede80375ee0cf124b30d2f05df5b464ba78ef4f49889b6391df4a04954e63d42d5631c7fcf8b1c4f12bc531252977a5f13d5 + languageName: node + linkType: hard + +"domhandler@npm:^4.0.0, domhandler@npm:^4.2.0, domhandler@npm:^4.3.1": + version: 4.3.1 + resolution: "domhandler@npm:4.3.1" + dependencies: + domelementtype: ^2.2.0 + checksum: 4c665ceed016e1911bf7d1dadc09dc888090b64dee7851cccd2fcf5442747ec39c647bb1cb8c8919f8bbdd0f0c625a6bafeeed4b2d656bbecdbae893f43ffaaa + languageName: node + linkType: hard + +"domutils@npm:^2.5.2, domutils@npm:^2.8.0": + version: 2.8.0 + resolution: "domutils@npm:2.8.0" + dependencies: + dom-serializer: ^1.0.1 + domelementtype: ^2.2.0 + domhandler: ^4.2.0 + checksum: abf7434315283e9aadc2a24bac0e00eab07ae4313b40cc239f89d84d7315ebdfd2fb1b5bf750a96bc1b4403d7237c7b2ebf60459be394d625ead4ca89b934391 + languageName: node + linkType: hard + +"dot-case@npm:^3.0.4": + version: 3.0.4 + resolution: "dot-case@npm:3.0.4" + dependencies: + no-case: ^3.0.4 + tslib: ^2.0.3 + checksum: a65e3519414856df0228b9f645332f974f2bf5433370f544a681122eab59e66038fc3349b4be1cdc47152779dac71a5864f1ccda2f745e767c46e9c6543b1169 + languageName: node + linkType: hard + +"dotenv@npm:^16.0.3": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 301a12c3d44fd49888b74eb9ccf9f07a1f5df43f489e7fcb89647a2edcd84c42d6bc349dc8df099cd18f07c35c7b04685c1a4f3e6a6a9e6b30f8d48c15b7f49c + languageName: node + linkType: hard + +"duplexer@npm:^0.1.2, duplexer@npm:~0.1.1": + version: 0.1.2 + resolution: "duplexer@npm:0.1.2" + checksum: 62ba61a830c56801db28ff6305c7d289b6dc9f859054e8c982abd8ee0b0a14d2e9a8e7d086ffee12e868d43e2bbe8a964be55ddbd8c8957714c87373c7a4f9b0 + languageName: node + linkType: hard + +"duplexify@npm:^4.1.3": + version: 4.1.3 + resolution: "duplexify@npm:4.1.3" + dependencies: + end-of-stream: ^1.4.1 + inherits: ^2.0.3 + readable-stream: ^3.1.1 + stream-shift: ^1.0.2 + checksum: 9636a027345de3dd3c801594d01a7c73d9ce260019538beb1ee650bba7544e72f40a4d4902b52e1ab283dc32a06f210d42748773af02ff15e3064a9659deab7f + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed + languageName: node + linkType: hard + +"easy-table@npm:1.1.0": + version: 1.1.0 + resolution: "easy-table@npm:1.1.0" + dependencies: + wcwidth: ">=1.0.1" + dependenciesMeta: + wcwidth: + optional: true + checksum: 49b960fefe5670076773824386f22070dce185ebc0a99542035496700cc39a0b9346f65fd4307f5fe3dbbe7e6d9c4b59966e77e32f915e0fe71de71c3d0efcf7 + languageName: node + linkType: hard + +"easy-table@npm:1.2.0": + version: 1.2.0 + resolution: "easy-table@npm:1.2.0" + dependencies: + ansi-regex: ^5.0.1 + wcwidth: ^1.0.1 + dependenciesMeta: + wcwidth: + optional: true + checksum: 66961b19751a68d2d30ce9b74ef750c374cc3112bbcac3d1ed5a939e43c035ecf6b1954098df2d5b05f1e853ab2b67de893794390dcbf0abe1f157fddeb52174 + languageName: node + linkType: hard + +"ecc-jsbn@npm:~0.1.1": + version: 0.1.2 + resolution: "ecc-jsbn@npm:0.1.2" + dependencies: + jsbn: ~0.1.0 + safer-buffer: ^2.1.0 + checksum: 22fef4b6203e5f31d425f5b711eb389e4c6c2723402e389af394f8411b76a488fa414d309d866e2b577ce3e8462d344205545c88a8143cc21752a5172818888a + languageName: node + linkType: hard + +"ecdsa-sig-formatter@npm:1.0.11, ecdsa-sig-formatter@npm:^1.0.11": + version: 1.0.11 + resolution: "ecdsa-sig-formatter@npm:1.0.11" + dependencies: + safe-buffer: ^5.0.1 + checksum: 207f9ab1c2669b8e65540bce29506134613dd5f122cccf1e6a560f4d63f2732d427d938f8481df175505aad94583bcb32c688737bb39a6df0625f903d6d93c03 + languageName: node + linkType: hard + +"ee-first@npm:1.1.1": + version: 1.1.1 + resolution: "ee-first@npm:1.1.1" + checksum: 1b4cac778d64ce3b582a7e26b218afe07e207a0f9bfe13cc7395a6d307849cfe361e65033c3251e00c27dd060cab43014c2d6b2647676135e18b77d2d05b3f4f + languageName: node + linkType: hard + +"electron-to-chromium@npm:^1.5.28": + version: 1.5.41 + resolution: "electron-to-chromium@npm:1.5.41" + checksum: 942cc53beabeb0647598d432155e2c21bed0de3dfd46576112aeed4157ea59543875c8a99038c5b05e8843fb3b91f14278ed4ea2bf4943845b26456ec20d2c9b + languageName: node + linkType: hard + +"elliptic@npm:^6.5.3, elliptic@npm:^6.5.5": + version: 6.5.7 + resolution: "elliptic@npm:6.5.7" + dependencies: + bn.js: ^4.11.9 + brorand: ^1.1.0 + hash.js: ^1.0.0 + hmac-drbg: ^1.0.1 + inherits: ^2.0.4 + minimalistic-assert: ^1.0.1 + minimalistic-crypto-utils: ^1.0.1 + checksum: af0ffddffdbc2fea4eeec74388cd73e62ed5a0eac6711568fb28071566319785df529c968b0bf1250ba4bc628e074b2d64c54a633e034aa6f0c6b152ceb49ab8 + languageName: node + linkType: hard + +"emittery@npm:^0.13.1": + version: 0.13.1 + resolution: "emittery@npm:0.13.1" + checksum: 2b089ab6306f38feaabf4f6f02792f9ec85fc054fda79f44f6790e61bbf6bc4e1616afb9b232e0c5ec5289a8a452f79bfa6d905a6fd64e94b49981f0934001c6 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: d4c5c39d5a9868b5fa152f00cada8a936868fd3367f33f71be515ecee4c803132d11b31a6222b2571b1e5f7e13890156a94880345594d0ce7e3c9895f560f192 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601 + languageName: node + linkType: hard + +"emojis-list@npm:^3.0.0": + version: 3.0.0 + resolution: "emojis-list@npm:3.0.0" + checksum: ddaaa02542e1e9436c03970eeed445f4ed29a5337dfba0fe0c38dfdd2af5da2429c2a0821304e8a8d1cadf27fdd5b22ff793571fa803ae16852a6975c65e8e70 + languageName: node + linkType: hard + +"enabled@npm:2.0.x": + version: 2.0.0 + resolution: "enabled@npm:2.0.0" + checksum: 9d256d89f4e8a46ff988c6a79b22fa814b4ffd82826c4fdacd9b42e9b9465709d3b748866d0ab4d442dfc6002d81de7f7b384146ccd1681f6a7f868d2acca063 + languageName: node + linkType: hard + +"encodeurl@npm:^1.0.2, encodeurl@npm:~1.0.2": + version: 1.0.2 + resolution: "encodeurl@npm:1.0.2" + checksum: e50e3d508cdd9c4565ba72d2012e65038e5d71bdc9198cb125beb6237b5b1ade6c0d343998da9e170fb2eae52c1bed37d4d6d98a46ea423a0cddbed5ac3f780c + languageName: node + linkType: hard + +"encodeurl@npm:~2.0.0": + version: 2.0.0 + resolution: "encodeurl@npm:2.0.0" + checksum: abf5cd51b78082cf8af7be6785813c33b6df2068ce5191a40ca8b1afe6a86f9230af9a9ce694a5ce4665955e5c1120871826df9c128a642e09c58d592e2807fe + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: ^0.6.2 + checksum: bb98632f8ffa823996e508ce6a58ffcf5856330fde839ae42c9e1f436cc3b5cc651d4aeae72222916545428e54fd0f6aa8862fd8d25bdbcc4589f1e3f3715e7f + languageName: node + linkType: hard + +"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": + version: 1.4.4 + resolution: "end-of-stream@npm:1.4.4" + dependencies: + once: ^1.4.0 + checksum: 530a5a5a1e517e962854a31693dbb5c0b2fc40b46dad2a56a2deec656ca040631124f4795823acc68238147805f8b021abbe221f4afed5ef3c8e8efc2024908b + languageName: node + linkType: hard + +"enhanced-resolve@npm:^5.17.1": + version: 5.17.1 + resolution: "enhanced-resolve@npm:5.17.1" + dependencies: + graceful-fs: ^4.2.4 + tapable: ^2.2.0 + checksum: 4bc38cf1cea96456f97503db7280394177d1bc46f8f87c267297d04f795ac5efa81e48115a2f5b6273c781027b5b6bfc5f62b54df629e4d25fa7001a86624f59 + languageName: node + linkType: hard + +"enquirer@npm:^2.3.0": + version: 2.4.1 + resolution: "enquirer@npm:2.4.1" + dependencies: + ansi-colors: ^4.1.1 + strip-ansi: ^6.0.1 + checksum: f080f11a74209647dbf347a7c6a83c8a47ae1ebf1e75073a808bc1088eb780aa54075bfecd1bcdb3e3c724520edb8e6ee05da031529436b421b71066fcc48cb5 + languageName: node + linkType: hard + +"entities@npm:^2.0.0": + version: 2.2.0 + resolution: "entities@npm:2.2.0" + checksum: 19010dacaf0912c895ea262b4f6128574f9ccf8d4b3b65c7e8334ad0079b3706376360e28d8843ff50a78aabcb8f08f0a32dbfacdc77e47ed77ca08b713669b3 + languageName: node + linkType: hard + +"entities@npm:^4.4.0, entities@npm:^4.5.0": + version: 4.5.0 + resolution: "entities@npm:4.5.0" + checksum: 853f8ebd5b425d350bffa97dd6958143179a5938352ccae092c62d1267c4e392a039be1bae7d51b6e4ffad25f51f9617531fedf5237f15df302ccfb452cbf2d7 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 8b7b1be20d2de12d2255c0bc2ca638b7af5171142693299416e6a9339bd7d88fc8d7707d913d78e0993176005405a236b066b45666b27b797252c771156ace54 + languageName: node + linkType: hard + +"error-ex@npm:^1.3.1": + version: 1.3.2 + resolution: "error-ex@npm:1.3.2" + dependencies: + is-arrayish: ^0.2.1 + checksum: c1c2b8b65f9c91b0f9d75f0debaa7ec5b35c266c2cac5de412c1a6de86d4cbae04ae44e510378cb14d032d0645a36925d0186f8bb7367bcc629db256b743a001 + languageName: node + linkType: hard + +"error-stack-parser@npm:^2.0.6": + version: 2.1.4 + resolution: "error-stack-parser@npm:2.1.4" + dependencies: + stackframe: ^1.3.4 + checksum: 3b916d2d14c6682f287c8bfa28e14672f47eafe832701080e420e7cdbaebb2c50293868256a95706ac2330fe078cf5664713158b49bc30d7a5f2ac229ded0e18 + languageName: node + linkType: hard + +"es-abstract@npm:^1.17.5, es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3, es-abstract@npm:^1.23.0, es-abstract@npm:^1.23.1, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3": + version: 1.23.3 + resolution: "es-abstract@npm:1.23.3" + dependencies: + array-buffer-byte-length: ^1.0.1 + arraybuffer.prototype.slice: ^1.0.3 + available-typed-arrays: ^1.0.7 + call-bind: ^1.0.7 + data-view-buffer: ^1.0.1 + data-view-byte-length: ^1.0.1 + data-view-byte-offset: ^1.0.0 + es-define-property: ^1.0.0 + es-errors: ^1.3.0 + es-object-atoms: ^1.0.0 + es-set-tostringtag: ^2.0.3 + es-to-primitive: ^1.2.1 + function.prototype.name: ^1.1.6 + get-intrinsic: ^1.2.4 + get-symbol-description: ^1.0.2 + globalthis: ^1.0.3 + gopd: ^1.0.1 + has-property-descriptors: ^1.0.2 + has-proto: ^1.0.3 + has-symbols: ^1.0.3 + hasown: ^2.0.2 + internal-slot: ^1.0.7 + is-array-buffer: ^3.0.4 + is-callable: ^1.2.7 + is-data-view: ^1.0.1 + is-negative-zero: ^2.0.3 + is-regex: ^1.1.4 + is-shared-array-buffer: ^1.0.3 + is-string: ^1.0.7 + is-typed-array: ^1.1.13 + is-weakref: ^1.0.2 + object-inspect: ^1.13.1 + object-keys: ^1.1.1 + object.assign: ^4.1.5 + regexp.prototype.flags: ^1.5.2 + safe-array-concat: ^1.1.2 + safe-regex-test: ^1.0.3 + string.prototype.trim: ^1.2.9 + string.prototype.trimend: ^1.0.8 + string.prototype.trimstart: ^1.0.8 + typed-array-buffer: ^1.0.2 + typed-array-byte-length: ^1.0.1 + typed-array-byte-offset: ^1.0.2 + typed-array-length: ^1.0.6 + unbox-primitive: ^1.0.2 + which-typed-array: ^1.1.15 + checksum: f840cf161224252512f9527306b57117192696571e07920f777cb893454e32999206198b4f075516112af6459daca282826d1735c450528470356d09eff3a9ae + languageName: node + linkType: hard + +"es-aggregate-error@npm:^1.0.7": + version: 1.0.13 + resolution: "es-aggregate-error@npm:1.0.13" + dependencies: + define-data-property: ^1.1.4 + define-properties: ^1.2.1 + es-abstract: ^1.23.2 + es-errors: ^1.3.0 + function-bind: ^1.1.2 + globalthis: ^1.0.3 + has-property-descriptors: ^1.0.2 + set-function-name: ^2.0.2 + checksum: f29596a9267220850fd77cc32abec369ffdea8ccc05de3ca387e55cf1711db2d1f6cdd1384f5bb968dbfb3ae8371919e82a61edb7219123caa41b924f31f1821 + languageName: node + linkType: hard + +"es-define-property@npm:^1.0.0": + version: 1.0.0 + resolution: "es-define-property@npm:1.0.0" + dependencies: + get-intrinsic: ^1.2.4 + checksum: f66ece0a887b6dca71848fa71f70461357c0e4e7249696f81bad0a1f347eed7b31262af4a29f5d726dc026426f085483b6b90301855e647aa8e21936f07293c6 + languageName: node + linkType: hard + +"es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: ec1414527a0ccacd7f15f4a3bc66e215f04f595ba23ca75cdae0927af099b5ec865f9f4d33e9d7e86f512f252876ac77d4281a7871531a50678132429b1271b5 + languageName: node + linkType: hard + +"es-get-iterator@npm:^1.1.3": + version: 1.1.3 + resolution: "es-get-iterator@npm:1.1.3" + dependencies: + call-bind: ^1.0.2 + get-intrinsic: ^1.1.3 + has-symbols: ^1.0.3 + is-arguments: ^1.1.1 + is-map: ^2.0.2 + is-set: ^2.0.2 + is-string: ^1.0.7 + isarray: ^2.0.5 + stop-iteration-iterator: ^1.0.0 + checksum: 8fa118da42667a01a7c7529f8a8cca514feeff243feec1ce0bb73baaa3514560bd09d2b3438873cf8a5aaec5d52da248131de153b28e2638a061b6e4df13267d + languageName: node + linkType: hard + +"es-iterator-helpers@npm:^1.0.19": + version: 1.1.0 + resolution: "es-iterator-helpers@npm:1.1.0" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-abstract: ^1.23.3 + es-errors: ^1.3.0 + es-set-tostringtag: ^2.0.3 + function-bind: ^1.1.2 + get-intrinsic: ^1.2.4 + globalthis: ^1.0.4 + has-property-descriptors: ^1.0.2 + has-proto: ^1.0.3 + has-symbols: ^1.0.3 + internal-slot: ^1.0.7 + iterator.prototype: ^1.1.3 + safe-array-concat: ^1.1.2 + checksum: 4ba3a32ab7ba05b85f0ae30604feeb8ffd801fe762e9df9577bd220a96b9eaa2e90af8e6bdc498e523051f293955e2f7d2bddd34de71e1428a1b8ff3fd961016 + languageName: node + linkType: hard + +"es-module-lexer@npm:^1.2.1, es-module-lexer@npm:^1.3.1": + version: 1.5.4 + resolution: "es-module-lexer@npm:1.5.4" + checksum: a0cf04fb92d052647ac7d818d1913b98d3d3d0f5b9d88f0eafb993436e4c3e2c958599db68839d57f2dfa281fdf0f60e18d448eb78fc292c33c0f25635b6854f + languageName: node + linkType: hard + +"es-object-atoms@npm:^1.0.0": + version: 1.0.0 + resolution: "es-object-atoms@npm:1.0.0" + dependencies: + es-errors: ^1.3.0 + checksum: 26f0ff78ab93b63394e8403c353842b2272836968de4eafe97656adfb8a7c84b9099bf0fe96ed58f4a4cddc860f6e34c77f91649a58a5daa4a9c40b902744e3c + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.0.3": + version: 2.0.3 + resolution: "es-set-tostringtag@npm:2.0.3" + dependencies: + get-intrinsic: ^1.2.4 + has-tostringtag: ^1.0.2 + hasown: ^2.0.1 + checksum: 7227fa48a41c0ce83e0377b11130d324ac797390688135b8da5c28994c0165be8b252e15cd1de41e1325e5a5412511586960213e88f9ab4a5e7d028895db5129 + languageName: node + linkType: hard + +"es-shim-unscopables@npm:^1.0.0, es-shim-unscopables@npm:^1.0.2": + version: 1.0.2 + resolution: "es-shim-unscopables@npm:1.0.2" + dependencies: + hasown: ^2.0.0 + checksum: 432bd527c62065da09ed1d37a3f8e623c423683285e6188108286f4a1e8e164a5bcbfbc0051557c7d14633cd2a41ce24c7048e6bbb66a985413fd32f1be72626 + languageName: node + linkType: hard + +"es-to-primitive@npm:^1.2.1": + version: 1.2.1 + resolution: "es-to-primitive@npm:1.2.1" + dependencies: + is-callable: ^1.1.4 + is-date-object: ^1.0.1 + is-symbol: ^1.0.2 + checksum: 4ead6671a2c1402619bdd77f3503991232ca15e17e46222b0a41a5d81aebc8740a77822f5b3c965008e631153e9ef0580540007744521e72de8e33599fca2eed + languageName: node + linkType: hard + +"es6-error@npm:^4.1.1": + version: 4.1.1 + resolution: "es6-error@npm:4.1.1" + checksum: ae41332a51ec1323da6bbc5d75b7803ccdeddfae17c41b6166ebbafc8e8beb7a7b80b884b7fab1cc80df485860ac3c59d78605e860bb4f8cd816b3d6ade0d010 + languageName: node + linkType: hard + +"esbuild-loader@npm:^4.0.0": + version: 4.2.2 + resolution: "esbuild-loader@npm:4.2.2" + dependencies: + esbuild: ^0.21.0 + get-tsconfig: ^4.7.0 + loader-utils: ^2.0.4 + webpack-sources: ^1.4.3 + peerDependencies: + webpack: ^4.40.0 || ^5.0.0 + checksum: 793d2482693c1c66298f63d7fdb62f2f3e314b006ade1dc3c46b46ade39777c5fba5930c2fa2752636c511997faa08d4a0f5d5b8a734b9046b3626aa6d5ab8e3 + languageName: node + linkType: hard + +"esbuild@npm:^0.21.0": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": 0.21.5 + "@esbuild/android-arm": 0.21.5 + "@esbuild/android-arm64": 0.21.5 + "@esbuild/android-x64": 0.21.5 + "@esbuild/darwin-arm64": 0.21.5 + "@esbuild/darwin-x64": 0.21.5 + "@esbuild/freebsd-arm64": 0.21.5 + "@esbuild/freebsd-x64": 0.21.5 + "@esbuild/linux-arm": 0.21.5 + "@esbuild/linux-arm64": 0.21.5 + "@esbuild/linux-ia32": 0.21.5 + "@esbuild/linux-loong64": 0.21.5 + "@esbuild/linux-mips64el": 0.21.5 + "@esbuild/linux-ppc64": 0.21.5 + "@esbuild/linux-riscv64": 0.21.5 + "@esbuild/linux-s390x": 0.21.5 + "@esbuild/linux-x64": 0.21.5 + "@esbuild/netbsd-x64": 0.21.5 + "@esbuild/openbsd-x64": 0.21.5 + "@esbuild/sunos-x64": 0.21.5 + "@esbuild/win32-arm64": 0.21.5 + "@esbuild/win32-ia32": 0.21.5 + "@esbuild/win32-x64": 0.21.5 + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 2911c7b50b23a9df59a7d6d4cdd3a4f85855787f374dce751148dbb13305e0ce7e880dde1608c2ab7a927fc6cec3587b80995f7fc87a64b455f8b70b55fd8ec1 + languageName: node + linkType: hard + +"esbuild@npm:^0.24.0": + version: 0.24.0 + resolution: "esbuild@npm:0.24.0" + dependencies: + "@esbuild/aix-ppc64": 0.24.0 + "@esbuild/android-arm": 0.24.0 + "@esbuild/android-arm64": 0.24.0 + "@esbuild/android-x64": 0.24.0 + "@esbuild/darwin-arm64": 0.24.0 + "@esbuild/darwin-x64": 0.24.0 + "@esbuild/freebsd-arm64": 0.24.0 + "@esbuild/freebsd-x64": 0.24.0 + "@esbuild/linux-arm": 0.24.0 + "@esbuild/linux-arm64": 0.24.0 + "@esbuild/linux-ia32": 0.24.0 + "@esbuild/linux-loong64": 0.24.0 + "@esbuild/linux-mips64el": 0.24.0 + "@esbuild/linux-ppc64": 0.24.0 + "@esbuild/linux-riscv64": 0.24.0 + "@esbuild/linux-s390x": 0.24.0 + "@esbuild/linux-x64": 0.24.0 + "@esbuild/netbsd-x64": 0.24.0 + "@esbuild/openbsd-arm64": 0.24.0 + "@esbuild/openbsd-x64": 0.24.0 + "@esbuild/sunos-x64": 0.24.0 + "@esbuild/win32-arm64": 0.24.0 + "@esbuild/win32-ia32": 0.24.0 + "@esbuild/win32-x64": 0.24.0 + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: dd386d92a05c7eb03078480522cdd8b40c434777b5f08487c27971d30933ecaae3f08bd221958dd8f9c66214915cdc85f844283ca9bdbf8ee703d889ae526edd + languageName: node + linkType: hard + +"escalade@npm:^3.1.1, escalade@npm:^3.2.0": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 47b029c83de01b0d17ad99ed766347b974b0d628e848de404018f3abee728e987da0d2d370ad4574aa3d5b5bfc368754fd085d69a30f8e75903486ec4b5b709e + languageName: node + linkType: hard + +"escape-html@npm:^1.0.3, escape-html@npm:~1.0.3": + version: 1.0.3 + resolution: "escape-html@npm:1.0.3" + checksum: 6213ca9ae00d0ab8bccb6d8d4e0a98e76237b2410302cf7df70aaa6591d509a2a37ce8998008cbecae8fc8ffaadf3fb0229535e6a145f3ce0b211d060decbb24 + languageName: node + linkType: hard + +"escape-latex@npm:^1.2.0": + version: 1.2.0 + resolution: "escape-latex@npm:1.2.0" + checksum: 73a787319f0965ecb8244bb38bf3a3cba872f0b9a5d3da8821140e9f39fe977045dc953a62b1a2bed4d12bfccbe75a7d8ec786412bf00739eaa2f627d0a8e0d6 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^1.0.5": + version: 1.0.5 + resolution: "escape-string-regexp@npm:1.0.5" + checksum: 6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^2.0.0": + version: 2.0.0 + resolution: "escape-string-regexp@npm:2.0.0" + checksum: 9f8a2d5743677c16e85c810e3024d54f0c8dea6424fad3c79ef6666e81dd0846f7437f5e729dfcdac8981bc9e5294c39b4580814d114076b8d36318f46ae4395 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^5.0.0": + version: 5.0.0 + resolution: "escape-string-regexp@npm:5.0.0" + checksum: 20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e + languageName: node + linkType: hard + +"escodegen@npm:^1.8.1": + version: 1.14.3 + resolution: "escodegen@npm:1.14.3" + dependencies: + esprima: ^4.0.1 + estraverse: ^4.2.0 + esutils: ^2.0.2 + optionator: ^0.8.1 + source-map: ~0.6.1 + dependenciesMeta: + source-map: + optional: true + bin: + escodegen: bin/escodegen.js + esgenerate: bin/esgenerate.js + checksum: 381cdc4767ecdb221206bbbab021b467bbc2a6f5c9a99c9e6353040080bdd3dfe73d7604ad89a47aca6ea7d58bc635f6bd3fbc8da9a1998e9ddfa8372362ccd0 + languageName: node + linkType: hard + +"escodegen@npm:^2.0.0, escodegen@npm:^2.1.0": + version: 2.1.0 + resolution: "escodegen@npm:2.1.0" + dependencies: + esprima: ^4.0.1 + estraverse: ^5.2.0 + esutils: ^2.0.2 + source-map: ~0.6.1 + dependenciesMeta: + source-map: + optional: true + bin: + escodegen: bin/escodegen.js + esgenerate: bin/esgenerate.js + checksum: 096696407e161305cd05aebb95134ad176708bc5cb13d0dcc89a5fcbb959b8ed757e7f2591a5f8036f8f4952d4a724de0df14cd419e29212729fa6df5ce16bf6 + languageName: node + linkType: hard + +"eslint-config-prettier@npm:^9.0.0": + version: 9.1.0 + resolution: "eslint-config-prettier@npm:9.1.0" + peerDependencies: + eslint: ">=7.0.0" + bin: + eslint-config-prettier: bin/cli.js + checksum: 9229b768c879f500ee54ca05925f31b0c0bafff3d9f5521f98ff05127356de78c81deb9365c86a5ec4efa990cb72b74df8612ae15965b14136044c73e1f6a907 + languageName: node + linkType: hard + +"eslint-formatter-friendly@npm:^7.0.0": + version: 7.0.0 + resolution: "eslint-formatter-friendly@npm:7.0.0" + dependencies: + "@babel/code-frame": 7.0.0 + chalk: 2.4.2 + extend: 3.0.2 + strip-ansi: 5.2.0 + text-table: 0.2.0 + checksum: e318768ac919993a234d38798544c5cf8e40ce05d6f2c028e4d0a4ac5c503a31609590ed67ceb31c98fae899b87950c6c805ad9e8c3a9060776daecda1bf1545 + languageName: node + linkType: hard + +"eslint-import-resolver-node@npm:^0.3.9": + version: 0.3.9 + resolution: "eslint-import-resolver-node@npm:0.3.9" + dependencies: + debug: ^3.2.7 + is-core-module: ^2.13.0 + resolve: ^1.22.4 + checksum: 439b91271236b452d478d0522a44482e8c8540bf9df9bd744062ebb89ab45727a3acd03366a6ba2bdbcde8f9f718bab7fe8db64688aca75acf37e04eafd25e22 + languageName: node + linkType: hard + +"eslint-module-utils@npm:^2.12.0": + version: 2.12.0 + resolution: "eslint-module-utils@npm:2.12.0" + dependencies: + debug: ^3.2.7 + peerDependenciesMeta: + eslint: + optional: true + checksum: be3ac52e0971c6f46daeb1a7e760e45c7c45f820c8cc211799f85f10f04ccbf7afc17039165d56cb2da7f7ca9cec2b3a777013cddf0b976784b37eb9efa24180 + languageName: node + linkType: hard + +"eslint-plugin-deprecation@npm:^2.0.0": + version: 2.0.0 + resolution: "eslint-plugin-deprecation@npm:2.0.0" + dependencies: + "@typescript-eslint/utils": ^6.0.0 + tslib: ^2.3.1 + tsutils: ^3.21.0 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: ^4.2.4 || ^5.0.0 + checksum: d79611e902ac419a21e51eab582fcdbcf8170aff820c5e5197e7d242e7ca6bda59c0077d88404970c25993017398dd65c96df7d31a833e332d45dd330935324b + languageName: node + linkType: hard + +"eslint-plugin-import@npm:^2.25.4": + version: 2.31.0 + resolution: "eslint-plugin-import@npm:2.31.0" + dependencies: + "@rtsao/scc": ^1.1.0 + array-includes: ^3.1.8 + array.prototype.findlastindex: ^1.2.5 + array.prototype.flat: ^1.3.2 + array.prototype.flatmap: ^1.3.2 + debug: ^3.2.7 + doctrine: ^2.1.0 + eslint-import-resolver-node: ^0.3.9 + eslint-module-utils: ^2.12.0 + hasown: ^2.0.2 + is-core-module: ^2.15.1 + is-glob: ^4.0.3 + minimatch: ^3.1.2 + object.fromentries: ^2.0.8 + object.groupby: ^1.0.3 + object.values: ^1.2.0 + semver: ^6.3.1 + string.prototype.trimend: ^1.0.8 + tsconfig-paths: ^3.15.0 + peerDependencies: + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + checksum: b1d2ac268b3582ff1af2a72a2c476eae4d250c100f2e335b6e102036e4a35efa530b80ec578dfc36761fabb34a635b9bf5ab071abe9d4404a4bb054fdf22d415 + languageName: node + linkType: hard + +"eslint-plugin-jest@npm:^28.0.0": + version: 28.8.3 + resolution: "eslint-plugin-jest@npm:28.8.3" + dependencies: + "@typescript-eslint/utils": ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependencies: + "@typescript-eslint/eslint-plugin": ^6.0.0 || ^7.0.0 || ^8.0.0 + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + jest: "*" + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + jest: + optional: true + checksum: e371fcbe2127a403824b6c23b66f6b2e2cc54074c3c70a9965d48bdcdfb461670965a7d7cdddab68f09e703d3a09a281d05591b1cb4315f5246d27fd8baa84ac + languageName: node + linkType: hard + +"eslint-plugin-jsx-a11y@npm:^6.5.1": + version: 6.10.0 + resolution: "eslint-plugin-jsx-a11y@npm:6.10.0" + dependencies: + aria-query: ~5.1.3 + array-includes: ^3.1.8 + array.prototype.flatmap: ^1.3.2 + ast-types-flow: ^0.0.8 + axe-core: ^4.10.0 + axobject-query: ^4.1.0 + damerau-levenshtein: ^1.0.8 + emoji-regex: ^9.2.2 + es-iterator-helpers: ^1.0.19 + hasown: ^2.0.2 + jsx-ast-utils: ^3.3.5 + language-tags: ^1.0.9 + minimatch: ^3.1.2 + object.fromentries: ^2.0.8 + safe-regex-test: ^1.0.3 + string.prototype.includes: ^2.0.0 + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + checksum: 1009deca12ddbe3624586bc5fc3534ca98d00a5841a2563cb6abd9339b984f0a99075dc2a703a517f4087eb84d659c87e60beda17645883de2ba1d86f2b20c96 + languageName: node + linkType: hard + +"eslint-plugin-react-hooks@npm:^4.3.0": + version: 4.6.2 + resolution: "eslint-plugin-react-hooks@npm:4.6.2" + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + checksum: 395c433610f59577cfcf3f2e42bcb130436c8a0b3777ac64f441d88c5275f4fcfc89094cedab270f2822daf29af1079151a7a6579a8e9ea8cee66540ba0384c4 + languageName: node + linkType: hard + +"eslint-plugin-react@npm:^7.28.0": + version: 7.37.1 + resolution: "eslint-plugin-react@npm:7.37.1" + dependencies: + array-includes: ^3.1.8 + array.prototype.findlast: ^1.2.5 + array.prototype.flatmap: ^1.3.2 + array.prototype.tosorted: ^1.1.4 + doctrine: ^2.1.0 + es-iterator-helpers: ^1.0.19 + estraverse: ^5.3.0 + hasown: ^2.0.2 + jsx-ast-utils: ^2.4.1 || ^3.0.0 + minimatch: ^3.1.2 + object.entries: ^1.1.8 + object.fromentries: ^2.0.8 + object.values: ^1.2.0 + prop-types: ^15.8.1 + resolve: ^2.0.0-next.5 + semver: ^6.3.1 + string.prototype.matchall: ^4.0.11 + string.prototype.repeat: ^1.0.0 + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + checksum: 22d1bdf0dd4cdbf8c57ce563c58d43c5f5e1da0b08d27d0a69d7126d9e8afcb74a5befae97dab4019b4c6029ae617b6a0af1709cb9e0439d5757b01b392d2ca7 + languageName: node + linkType: hard + +"eslint-plugin-unused-imports@npm:^3.0.0": + version: 3.2.0 + resolution: "eslint-plugin-unused-imports@npm:3.2.0" + dependencies: + eslint-rule-composer: ^0.3.0 + peerDependencies: + "@typescript-eslint/eslint-plugin": 6 - 7 + eslint: 8 + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + checksum: e85ae4f3af489294ef5e0969ab904fa87f9fa7c959ca0804f30845438db4aeb0428ddad7ab06a70608e93121626799977241b442fdf126a4d0667be57390c3d6 + languageName: node + linkType: hard + +"eslint-rule-composer@npm:^0.3.0": + version: 0.3.0 + resolution: "eslint-rule-composer@npm:0.3.0" + checksum: c2f57cded8d1c8f82483e0ce28861214347e24fd79fd4144667974cd334d718f4ba05080aaef2399e3bbe36f7d6632865110227e6b176ed6daa2d676df9281b1 + languageName: node + linkType: hard + +"eslint-scope@npm:5.1.1": + version: 5.1.1 + resolution: "eslint-scope@npm:5.1.1" + dependencies: + esrecurse: ^4.3.0 + estraverse: ^4.1.1 + checksum: 47e4b6a3f0cc29c7feedee6c67b225a2da7e155802c6ea13bbef4ac6b9e10c66cd2dcb987867ef176292bf4e64eccc680a49e35e9e9c669f4a02bac17e86abdb + languageName: node + linkType: hard + +"eslint-scope@npm:^7.2.2": + version: 7.2.2 + resolution: "eslint-scope@npm:7.2.2" + dependencies: + esrecurse: ^4.3.0 + estraverse: ^5.2.0 + checksum: ec97dbf5fb04b94e8f4c5a91a7f0a6dd3c55e46bfc7bbcd0e3138c3a76977570e02ed89a1810c778dcd72072ff0e9621ba1379b4babe53921d71e2e4486fda3e + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": + version: 3.4.3 + resolution: "eslint-visitor-keys@npm:3.4.3" + checksum: 36e9ef87fca698b6fd7ca5ca35d7b2b6eeaaf106572e2f7fd31c12d3bfdaccdb587bba6d3621067e5aece31c8c3a348b93922ab8f7b2cbc6aaab5e1d89040c60 + languageName: node + linkType: hard + +"eslint-webpack-plugin@npm:^4.0.0": + version: 4.2.0 + resolution: "eslint-webpack-plugin@npm:4.2.0" + dependencies: + "@types/eslint": ^8.56.10 + jest-worker: ^29.7.0 + micromatch: ^4.0.5 + normalize-path: ^3.0.0 + schema-utils: ^4.2.0 + peerDependencies: + eslint: ^8.0.0 || ^9.0.0 + webpack: ^5.0.0 + checksum: 51538d60d0d0f3dd5774a4291af4620884a45a40270e2878c2f7c8dbff3584ef8588ffded8de696a4bbcee45bee219eba442eb503f5eddcc79aefeb4845985ae + languageName: node + linkType: hard + +"eslint@npm:^8.6.0": + version: 8.57.1 + resolution: "eslint@npm:8.57.1" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@eslint-community/regexpp": ^4.6.1 + "@eslint/eslintrc": ^2.1.4 + "@eslint/js": 8.57.1 + "@humanwhocodes/config-array": ^0.13.0 + "@humanwhocodes/module-importer": ^1.0.1 + "@nodelib/fs.walk": ^1.2.8 + "@ungap/structured-clone": ^1.2.0 + ajv: ^6.12.4 + chalk: ^4.0.0 + cross-spawn: ^7.0.2 + debug: ^4.3.2 + doctrine: ^3.0.0 + escape-string-regexp: ^4.0.0 + eslint-scope: ^7.2.2 + eslint-visitor-keys: ^3.4.3 + espree: ^9.6.1 + esquery: ^1.4.2 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^6.0.1 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + globals: ^13.19.0 + graphemer: ^1.4.0 + ignore: ^5.2.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + is-path-inside: ^3.0.3 + js-yaml: ^4.1.0 + json-stable-stringify-without-jsonify: ^1.0.1 + levn: ^0.4.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.3 + strip-ansi: ^6.0.1 + text-table: ^0.2.0 + bin: + eslint: bin/eslint.js + checksum: e2489bb7f86dd2011967759a09164e65744ef7688c310bc990612fc26953f34cc391872807486b15c06833bdff737726a23e9b4cdba5de144c311377dc41d91b + languageName: node + linkType: hard + +"esm@npm:^3.2.25": + version: 3.2.25 + resolution: "esm@npm:3.2.25" + checksum: 978aabe2de83541c105605a6d60a26ed8e627ef6bb0a7605fe15a95bbdea6b8348bd045255cb22219c054dd09a81a94823df00843d9e97f42419c92015ce3a64 + languageName: node + linkType: hard + +"espree@npm:^9.6.0, espree@npm:^9.6.1": + version: 9.6.1 + resolution: "espree@npm:9.6.1" + dependencies: + acorn: ^8.9.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^3.4.1 + checksum: eb8c149c7a2a77b3f33a5af80c10875c3abd65450f60b8af6db1bfcfa8f101e21c1e56a561c6dc13b848e18148d43469e7cd208506238554fb5395a9ea5a1ab9 + languageName: node + linkType: hard + +"esprima@npm:1.2.2": + version: 1.2.2 + resolution: "esprima@npm:1.2.2" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: 4f10006f0e315f2f7d8cf6630e465f183512f1ab2e862b11785a133ce37ed1696573deefb5256e510eaa4368342b13b393334477f6ccdcdb8f10e782b0f5e6dc + languageName: node + linkType: hard + +"esprima@npm:^4.0.0, esprima@npm:^4.0.1": + version: 4.0.1 + resolution: "esprima@npm:4.0.1" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: b45bc805a613dbea2835278c306b91aff6173c8d034223fa81498c77dcbce3b2931bf6006db816f62eacd9fd4ea975dfd85a5b7f3c6402cfd050d4ca3c13a628 + languageName: node + linkType: hard + +"esquery@npm:^1.4.2": + version: 1.6.0 + resolution: "esquery@npm:1.6.0" + dependencies: + estraverse: ^5.1.0 + checksum: 08ec4fe446d9ab27186da274d979558557fbdbbd10968fa9758552482720c54152a5640e08b9009e5a30706b66aba510692054d4129d32d0e12e05bbc0b96fb2 + languageName: node + linkType: hard + +"esrecurse@npm:^4.3.0": + version: 4.3.0 + resolution: "esrecurse@npm:4.3.0" + dependencies: + estraverse: ^5.2.0 + checksum: ebc17b1a33c51cef46fdc28b958994b1dc43cd2e86237515cbc3b4e5d2be6a811b2315d0a1a4d9d340b6d2308b15322f5c8291059521cc5f4802f65e7ec32837 + languageName: node + linkType: hard + +"estraverse@npm:^4.1.1, estraverse@npm:^4.2.0": + version: 4.3.0 + resolution: "estraverse@npm:4.3.0" + checksum: a6299491f9940bb246124a8d44b7b7a413a8336f5436f9837aaa9330209bd9ee8af7e91a654a3545aee9c54b3308e78ee360cef1d777d37cfef77d2fa33b5827 + languageName: node + linkType: hard + +"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0, estraverse@npm:^5.3.0": + version: 5.3.0 + resolution: "estraverse@npm:5.3.0" + checksum: 072780882dc8416ad144f8fe199628d2b3e7bbc9989d9ed43795d2c90309a2047e6bc5979d7e2322a341163d22cfad9e21f4110597fe487519697389497e4e2b + languageName: node + linkType: hard + +"estree-walker@npm:^0.6.1": + version: 0.6.1 + resolution: "estree-walker@npm:0.6.1" + checksum: 9d6f82a4921f11eec18f8089fb3cce6e53bcf45a8e545c42a2674d02d055fb30f25f90495f8be60803df6c39680c80dcee7f944526867eb7aa1fc9254883b23d + languageName: node + linkType: hard + +"estree-walker@npm:^2.0.1, estree-walker@npm:^2.0.2": + version: 2.0.2 + resolution: "estree-walker@npm:2.0.2" + checksum: 6151e6f9828abe2259e57f5fd3761335bb0d2ebd76dc1a01048ccee22fabcfef3c0859300f6d83ff0d1927849368775ec5a6d265dde2f6de5a1be1721cd94efc + languageName: node + linkType: hard + +"esutils@npm:^2.0.2": + version: 2.0.3 + resolution: "esutils@npm:2.0.3" + checksum: 22b5b08f74737379a840b8ed2036a5fb35826c709ab000683b092d9054e5c2a82c27818f12604bfc2a9a76b90b6834ef081edbc1c7ae30d1627012e067c6ec87 + languageName: node + linkType: hard + +"etag@npm:~1.8.1": + version: 1.8.1 + resolution: "etag@npm:1.8.1" + checksum: 571aeb3dbe0f2bbd4e4fadbdb44f325fc75335cd5f6f6b6a091e6a06a9f25ed5392f0863c5442acb0646787446e816f13cbfc6edce5b07658541dff573cab1ff + languageName: node + linkType: hard + +"event-stream@npm:=3.3.4": + version: 3.3.4 + resolution: "event-stream@npm:3.3.4" + dependencies: + duplexer: ~0.1.1 + from: ~0 + map-stream: ~0.1.0 + pause-stream: 0.0.11 + split: 0.3 + stream-combiner: ~0.0.4 + through: ~2.3.1 + checksum: 80b467820b6daf824d9fb4345d2daf115a056e5c104463f2e98534e92d196a27f2df5ea2aa085624db26f4c45698905499e881d13bc7c01f7a13eac85be72a22 + languageName: node + linkType: hard + +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 1ffe3bb22a6d51bdeb6bf6f7cf97d2ff4a74b017ad12284cc9e6a279e727dc30a5de6bb613e5596ff4dc3e517841339ad09a7eec44266eccb1aa201a30448166 + languageName: node + linkType: hard + +"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.4": + version: 4.0.7 + resolution: "eventemitter3@npm:4.0.7" + checksum: 1875311c42fcfe9c707b2712c32664a245629b42bb0a5a84439762dd0fd637fc54d078155ea83c2af9e0323c9ac13687e03cfba79b03af9f40c89b4960099374 + languageName: node + linkType: hard + +"events@npm:^3.0.0, events@npm:^3.2.0, events@npm:^3.3.0": + version: 3.3.0 + resolution: "events@npm:3.3.0" + checksum: f6f487ad2198aa41d878fa31452f1a3c00958f46e9019286ff4787c84aac329332ab45c9cdc8c445928fc6d7ded294b9e005a7fce9426488518017831b272780 + languageName: node + linkType: hard + +"evp_bytestokey@npm:^1.0.0, evp_bytestokey@npm:^1.0.3": + version: 1.0.3 + resolution: "evp_bytestokey@npm:1.0.3" + dependencies: + md5.js: ^1.3.4 + node-gyp: latest + safe-buffer: ^5.1.1 + checksum: ad4e1577f1a6b721c7800dcc7c733fe01f6c310732bb5bf2240245c2a5b45a38518b91d8be2c610611623160b9d1c0e91f1ce96d639f8b53e8894625cf20fa45 + languageName: node + linkType: hard + +"execa@npm:5.1.1, execa@npm:^5.0.0": + version: 5.1.1 + resolution: "execa@npm:5.1.1" + dependencies: + cross-spawn: ^7.0.3 + get-stream: ^6.0.0 + human-signals: ^2.1.0 + is-stream: ^2.0.0 + merge-stream: ^2.0.0 + npm-run-path: ^4.0.1 + onetime: ^5.1.2 + signal-exit: ^3.0.3 + strip-final-newline: ^2.0.0 + checksum: fba9022c8c8c15ed862847e94c252b3d946036d7547af310e344a527e59021fd8b6bb0723883ea87044dc4f0201f949046993124a42ccb0855cae5bf8c786343 + languageName: node + linkType: hard + +"exit@npm:^0.1.2": + version: 0.1.2 + resolution: "exit@npm:0.1.2" + checksum: abc407f07a875c3961e4781dfcb743b58d6c93de9ab263f4f8c9d23bb6da5f9b7764fc773f86b43dd88030444d5ab8abcb611cb680fba8ca075362b77114bba3 + languageName: node + linkType: hard + +"expand-template@npm:^2.0.3": + version: 2.0.3 + resolution: "expand-template@npm:2.0.3" + checksum: 588c19847216421ed92befb521767b7018dc88f88b0576df98cb242f20961425e96a92cbece525ef28cc5becceae5d544ae0f5b9b5e2aa05acb13716ca5b3099 + languageName: node + linkType: hard + +"expand-tilde@npm:^2.0.0, expand-tilde@npm:^2.0.2": + version: 2.0.2 + resolution: "expand-tilde@npm:2.0.2" + dependencies: + homedir-polyfill: ^1.0.1 + checksum: 2efe6ed407d229981b1b6ceb552438fbc9e5c7d6a6751ad6ced3e0aa5cf12f0b299da695e90d6c2ac79191b5c53c613e508f7149e4573abfbb540698ddb7301a + languageName: node + linkType: hard + +"expect@npm:^29.0.0, expect@npm:^29.7.0": + version: 29.7.0 + resolution: "expect@npm:29.7.0" + dependencies: + "@jest/expect-utils": ^29.7.0 + jest-get-type: ^29.6.3 + jest-matcher-utils: ^29.7.0 + jest-message-util: ^29.7.0 + jest-util: ^29.7.0 + checksum: 9257f10288e149b81254a0fda8ffe8d54a7061cd61d7515779998b012579d2b8c22354b0eb901daf0145f347403da582f75f359f4810c007182ad3fb318b5c0c + languageName: node + linkType: hard + +"expiry-map@npm:^2.0.0": + version: 2.0.0 + resolution: "expiry-map@npm:2.0.0" + dependencies: + map-age-cleaner: ^0.2.0 + checksum: 9be8662e1a5c1084fb6d0ddc5402658dd06101c330454062b2f5efbf1477259d272e54ec16663d7d12a93d08ed510535781c36acb214696c5bc3a690a02a7a9d + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 3d21519a4f8207c99f7457287291316306255a328770d320b401114ec8481986e4e467e854cb9914dd965e0a1ca810a23ccb559c642c88f4c7f55c55778a9b48 + languageName: node + linkType: hard + +"express-promise-router@npm:^4.1.0": + version: 4.1.1 + resolution: "express-promise-router@npm:4.1.1" + dependencies: + is-promise: ^4.0.0 + lodash.flattendeep: ^4.0.0 + methods: ^1.0.0 + peerDependencies: + "@types/express": ^4.0.0 + express: ^4.0.0 + peerDependenciesMeta: + "@types/express": + optional: true + checksum: e69ee7eb2c70470d5be71d34cd9275c26aae157c1ef16a21ecf53141e512fd4a6b5a68db89b30f745df941518505d00ec0a5e13f0becbd53ad63ffce3ed885f3 + languageName: node + linkType: hard + +"express@npm:^4.17.1, express@npm:^4.18.2, express@npm:^4.19.2": + version: 4.21.1 + resolution: "express@npm:4.21.1" + dependencies: + accepts: ~1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: ~1.0.4 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: ~2.0.0 + escape-html: ~1.0.3 + etag: ~1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: ~1.1.2 + on-finished: 2.4.1 + parseurl: ~1.3.3 + path-to-regexp: 0.1.10 + proxy-addr: ~2.0.7 + qs: 6.13.0 + range-parser: ~1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: ~1.6.18 + utils-merge: 1.0.1 + vary: ~1.1.2 + checksum: 5ac2b26d8aeddda5564fc0907227d29c100f90c0ead2ead9d474dc5108e8fb306c2de2083c4e3ba326e0906466f2b73417dbac16961f4075ff9f03785fd940fe + languageName: node + linkType: hard + +"expression-eval@npm:^5.0.0": + version: 5.0.1 + resolution: "expression-eval@npm:5.0.1" + dependencies: + jsep: ^0.3.0 + checksum: 1318726337fa5ef980a565cd02800aadb3e0b09a01378dd5a33d6525a6a726667d931a5851de9c792a6043857409d06d6ce48d62dbf61636ba9eba46fc8f6dde + languageName: node + linkType: hard + +"extend@npm:3.0.2, extend@npm:^3.0.0, extend@npm:^3.0.2, extend@npm:~3.0.2": + version: 3.0.2 + resolution: "extend@npm:3.0.2" + checksum: a50a8309ca65ea5d426382ff09f33586527882cf532931cb08ca786ea3146c0553310bda688710ff61d7668eba9f96b923fe1420cdf56a2c3eaf30fcab87b515 + languageName: node + linkType: hard + +"extendable-error@npm:^0.1.5": + version: 0.1.7 + resolution: "extendable-error@npm:0.1.7" + checksum: 80478be7429a1675d2085f701239796bab3230ed6f2fb1b138fbabec24bea6516b7c5ceb6e9c209efcc9c089948d93715703845653535f8e8a49655066a9255e + languageName: node + linkType: hard + +"external-editor@npm:^3.0.3, external-editor@npm:^3.1.0": + version: 3.1.0 + resolution: "external-editor@npm:3.1.0" + dependencies: + chardet: ^0.7.0 + iconv-lite: ^0.4.24 + tmp: ^0.0.33 + checksum: 1c2a616a73f1b3435ce04030261bed0e22d4737e14b090bb48e58865da92529c9f2b05b893de650738d55e692d071819b45e1669259b2b354bc3154d27a698c7 + languageName: node + linkType: hard + +"extsprintf@npm:1.3.0": + version: 1.3.0 + resolution: "extsprintf@npm:1.3.0" + checksum: cee7a4a1e34cffeeec18559109de92c27517e5641991ec6bab849aa64e3081022903dd53084f2080d0d2530803aa5ee84f1e9de642c365452f9e67be8f958ce2 + languageName: node + linkType: hard + +"extsprintf@npm:^1.2.0": + version: 1.4.1 + resolution: "extsprintf@npm:1.4.1" + checksum: a2f29b241914a8d2bad64363de684821b6b1609d06ae68d5b539e4de6b28659715b5bea94a7265201603713b7027d35399d10b0548f09071c5513e65e8323d33 + languageName: node + linkType: hard + +"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": + version: 3.1.3 + resolution: "fast-deep-equal@npm:3.1.3" + checksum: e21a9d8d84f53493b6aa15efc9cfd53dd5b714a1f23f67fb5dc8f574af80df889b3bce25dc081887c6d25457cce704e636395333abad896ccdec03abaf1f3f9d + languageName: node + linkType: hard + +"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": + version: 1.3.2 + resolution: "fast-fifo@npm:1.3.2" + checksum: 6bfcba3e4df5af7be3332703b69a7898a8ed7020837ec4395bb341bd96cc3a6d86c3f6071dd98da289618cf2234c70d84b2a6f09a33dd6f988b1ff60d8e54275 + languageName: node + linkType: hard + +"fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": + version: 3.3.2 + resolution: "fast-glob@npm:3.3.2" + dependencies: + "@nodelib/fs.stat": ^2.0.2 + "@nodelib/fs.walk": ^1.2.3 + glob-parent: ^5.1.2 + merge2: ^1.3.0 + micromatch: ^4.0.4 + checksum: 900e4979f4dbc3313840078419245621259f349950411ca2fa445a2f9a1a6d98c3b5e7e0660c5ccd563aa61abe133a21765c6c0dec8e57da1ba71d8000b05ec1 + languageName: node + linkType: hard + +"fast-json-patch@npm:^3.1.0": + version: 3.1.1 + resolution: "fast-json-patch@npm:3.1.1" + checksum: c4525b61b2471df60d4b025b4118b036d99778a93431aa44d1084218182841d82ce93056f0f3bbd731a24e6a8e69820128adf1873eb2199a26c62ef58d137833 + languageName: node + linkType: hard + +"fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": + version: 2.1.0 + resolution: "fast-json-stable-stringify@npm:2.1.0" + checksum: b191531e36c607977e5b1c47811158733c34ccb3bfde92c44798929e9b4154884378536d26ad90dfecd32e1ffc09c545d23535ad91b3161a27ddbb8ebe0cbecb + languageName: node + linkType: hard + +"fast-levenshtein@npm:^2.0.6, fast-levenshtein@npm:~2.0.6": + version: 2.0.6 + resolution: "fast-levenshtein@npm:2.0.6" + checksum: 92cfec0a8dfafd9c7a15fba8f2cc29cd0b62b85f056d99ce448bbcd9f708e18ab2764bda4dd5158364f4145a7c72788538994f0d1787b956ef0d1062b0f7c24c + languageName: node + linkType: hard + +"fast-memoize@npm:^2.5.2": + version: 2.5.2 + resolution: "fast-memoize@npm:2.5.2" + checksum: 79fa759719ba4eac7e8c22fb3b0eb3f18f4a31e218c00b1eb4a5b53c5781921133a6b84472d59ec5a6ea8f26ad57b43cd99a350c0547ccce51489bc9a5f0b28d + languageName: node + linkType: hard + +"fast-safe-stringify@npm:2.1.1, fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: a851cbddc451745662f8f00ddb622d6766f9bd97642dabfd9a405fb0d646d69fc0b9a1243cbf67f5f18a39f40f6fa821737651ff1bceeba06c9992ca2dc5bd3d + languageName: node + linkType: hard + +"fast-shallow-equal@npm:^1.0.0": + version: 1.0.0 + resolution: "fast-shallow-equal@npm:1.0.0" + checksum: ae89318ce43c0c46410d9511ac31520d59cfe675bad3d0b1cb5f900b2d635943d788b8370437178e91ae0d0412decc394229c03e69925ade929a8c02da241610 + languageName: node + linkType: hard + +"fast-uri@npm:^3.0.1": + version: 3.0.3 + resolution: "fast-uri@npm:3.0.3" + checksum: c52e6c86465f5c240e84a4485fb001088cc743d261a4b54b0050ce4758b1648bdbe53da1328ef9620149dca1435e3de64184f226d7c0a3656cb5837b3491e149 + languageName: node + linkType: hard + +"fast-xml-parser@npm:4.4.1": + version: 4.4.1 + resolution: "fast-xml-parser@npm:4.4.1" + dependencies: + strnum: ^1.0.5 + bin: + fxparser: src/cli/cli.js + checksum: f440c01cd141b98789ae777503bcb6727393296094cc82924ae9f88a5b971baa4eec7e65306c7e07746534caa661fc83694ff437d9012dc84dee39dfbfaab947 + languageName: node + linkType: hard + +"fast-xml-parser@npm:^4.4.1": + version: 4.5.0 + resolution: "fast-xml-parser@npm:4.5.0" + dependencies: + strnum: ^1.0.5 + bin: + fxparser: src/cli/cli.js + checksum: 696dc98da46f0f48eb26dfe1640a53043ea64f2420056374e62abbb5e620f092f8df3c3ff3195505a2eefab2057db3bf0ebaac63557f277934f6cce4e7da027c + languageName: node + linkType: hard + +"fastest-stable-stringify@npm:^2.0.2": + version: 2.0.2 + resolution: "fastest-stable-stringify@npm:2.0.2" + checksum: 5e2cb166c7bb6f16ac25a1e4be17f6b8d2923234c80739e12c9d21dea376b3128b2c63f90aa2aae7746cfec4dcf188d1d4eb6a964bb484ca133f17c8e9acfacc + languageName: node + linkType: hard + +"fastq@npm:^1.6.0": + version: 1.17.1 + resolution: "fastq@npm:1.17.1" + dependencies: + reusify: ^1.0.4 + checksum: a8c5b26788d5a1763f88bae56a8ddeee579f935a831c5fe7a8268cea5b0a91fbfe705f612209e02d639b881d7b48e461a50da4a10cfaa40da5ca7cc9da098d88 + languageName: node + linkType: hard + +"fault@npm:^1.0.0": + version: 1.0.4 + resolution: "fault@npm:1.0.4" + dependencies: + format: ^0.2.0 + checksum: 5ac610d8b09424e0f2fa8cf913064372f2ee7140a203a79957f73ed557c0e79b1a3d096064d7f40bde8132a69204c1fe25ec23634c05c6da2da2039cff26c4e7 + languageName: node + linkType: hard + +"faye-websocket@npm:^0.11.3": + version: 0.11.4 + resolution: "faye-websocket@npm:0.11.4" + dependencies: + websocket-driver: ">=0.5.1" + checksum: d49a62caf027f871149fc2b3f3c7104dc6d62744277eb6f9f36e2d5714e847d846b9f7f0d0b7169b25a012e24a594cde11a93034b30732e4c683f20b8a5019fa + languageName: node + linkType: hard + +"fb-watchman@npm:^2.0.0": + version: 2.0.2 + resolution: "fb-watchman@npm:2.0.2" + dependencies: + bser: 2.1.1 + checksum: b15a124cef28916fe07b400eb87cbc73ca082c142abf7ca8e8de6af43eca79ca7bd13eb4d4d48240b3bd3136eaac40d16e42d6edf87a8e5d1dd8070626860c78 + languageName: node + linkType: hard + +"fecha@npm:^4.2.0": + version: 4.2.3 + resolution: "fecha@npm:4.2.3" + checksum: f94e2fb3acf5a7754165d04549460d3ae6c34830394d20c552197e3e000035d69732d74af04b9bed3283bf29fe2a9ebdcc0085e640b0be3cc3658b9726265e31 + languageName: node + linkType: hard + +"figures@npm:^3.0.0": + version: 3.2.0 + resolution: "figures@npm:3.2.0" + dependencies: + escape-string-regexp: ^1.0.5 + checksum: 85a6ad29e9aca80b49b817e7c89ecc4716ff14e3779d9835af554db91bac41c0f289c418923519392a1e582b4d10482ad282021330cd045bb7b80c84152f2a2b + languageName: node + linkType: hard + +"file-entry-cache@npm:^6.0.1": + version: 6.0.1 + resolution: "file-entry-cache@npm:6.0.1" + dependencies: + flat-cache: ^3.0.4 + checksum: f49701feaa6314c8127c3c2f6173cfefff17612f5ed2daaafc6da13b5c91fd43e3b2a58fd0d63f9f94478a501b167615931e7200e31485e320f74a33885a9c74 + languageName: node + linkType: hard + +"file-saver@npm:^2.0.5": + version: 2.0.5 + resolution: "file-saver@npm:2.0.5" + checksum: c62d96e5cebc58b4bdf3ae8a60d5cf9607ad82f75f798c33a4ee63435ac2203002584d5256a2a780eda7feb5e19dc3b6351c2212e58b3f529e63d265a7cc79f7 + languageName: node + linkType: hard + +"file-uri-to-path@npm:1.0.0": + version: 1.0.0 + resolution: "file-uri-to-path@npm:1.0.0" + checksum: b648580bdd893a008c92c7ecc96c3ee57a5e7b6c4c18a9a09b44fb5d36d79146f8e442578bc0e173dc027adf3987e254ba1dfd6e3ec998b7c282873010502144 + languageName: node + linkType: hard + +"filesize@npm:^8.0.6": + version: 8.0.7 + resolution: "filesize@npm:8.0.7" + checksum: 8603d27c5287b984cb100733640645e078f5f5ad65c6d913173e01fb99e09b0747828498fd86647685ccecb69be31f3587b9739ab1e50732116b2374aff4cbf9 + languageName: node + linkType: hard + +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" + dependencies: + to-regex-range: ^5.0.1 + checksum: b4abfbca3839a3d55e4ae5ec62e131e2e356bf4859ce8480c64c4876100f4df292a63e5bb1618e1d7460282ca2b305653064f01654474aa35c68000980f17798 + languageName: node + linkType: hard + +"finalhandler@npm:1.3.1": + version: 1.3.1 + resolution: "finalhandler@npm:1.3.1" + dependencies: + debug: 2.6.9 + encodeurl: ~2.0.0 + escape-html: ~1.0.3 + on-finished: 2.4.1 + parseurl: ~1.3.3 + statuses: 2.0.1 + unpipe: ~1.0.0 + checksum: a8c58cd97c9cd47679a870f6833a7b417043f5a288cd6af6d0f49b476c874a506100303a128b6d3b654c3d74fa4ff2ffed68a48a27e8630cda5c918f2977dcf4 + languageName: node + linkType: hard + +"find-file-up@npm:^2.0.1": + version: 2.0.1 + resolution: "find-file-up@npm:2.0.1" + dependencies: + resolve-dir: ^1.0.1 + checksum: dfe820bfb80e75bed5dd5080057858c0ad2393e1438c48a3bb682663e9ecdcfbe3224ed4768bfedd00776800b4ae76bc8953d250d15ae3feabf381d2c6d04268 + languageName: node + linkType: hard + +"find-pkg@npm:2.0.0": + version: 2.0.0 + resolution: "find-pkg@npm:2.0.0" + dependencies: + find-file-up: ^2.0.1 + checksum: 44785204c8bbbdfeaece6b834ba81a35163421c30e20f531281d26e6b5890663d7ac884b82a9aebf6ce23e479336cd6f70ea5597da35495c16abdeba2fd4f845 + languageName: node + linkType: hard + +"find-root@npm:^1.1.0": + version: 1.1.0 + resolution: "find-root@npm:1.1.0" + checksum: b2a59fe4b6c932eef36c45a048ae8f93c85640212ebe8363164814990ee20f154197505965f3f4f102efc33bfb1cbc26fd17c4a2fc739ebc51b886b137cbefaf + languageName: node + linkType: hard + +"find-up@npm:^3.0.0": + version: 3.0.0 + resolution: "find-up@npm:3.0.0" + dependencies: + locate-path: ^3.0.0 + checksum: 38eba3fe7a66e4bc7f0f5a1366dc25508b7cfc349f852640e3678d26ad9a6d7e2c43eff0a472287de4a9753ef58f066a0ea892a256fa3636ad51b3fe1e17fae9 + languageName: node + linkType: hard + +"find-up@npm:^4.0.0, find-up@npm:^4.1.0": + version: 4.1.0 + resolution: "find-up@npm:4.1.0" + dependencies: + locate-path: ^5.0.0 + path-exists: ^4.0.0 + checksum: 4c172680e8f8c1f78839486e14a43ef82e9decd0e74145f40707cc42e7420506d5ec92d9a11c22bd2c48fb0c384ea05dd30e10dd152fefeec6f2f75282a8b844 + languageName: node + linkType: hard + +"find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" + dependencies: + locate-path: ^6.0.0 + path-exists: ^4.0.0 + checksum: 07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 + languageName: node + linkType: hard + +"flat-cache@npm:^3.0.4": + version: 3.2.0 + resolution: "flat-cache@npm:3.2.0" + dependencies: + flatted: ^3.2.9 + keyv: ^4.5.3 + rimraf: ^3.0.2 + checksum: e7e0f59801e288b54bee5cb9681e9ee21ee28ef309f886b312c9d08415b79fc0f24ac842f84356ce80f47d6a53de62197ce0e6e148dc42d5db005992e2a756ec + languageName: node + linkType: hard + +"flatted@npm:^3.2.7, flatted@npm:^3.2.9": + version: 3.3.1 + resolution: "flatted@npm:3.3.1" + checksum: 85ae7181650bb728c221e7644cbc9f4bf28bc556f2fc89bb21266962bdf0ce1029cc7acc44bb646cd469d9baac7c317f64e841c4c4c00516afa97320cdac7f94 + languageName: node + linkType: hard + +"fn.name@npm:1.x.x": + version: 1.1.0 + resolution: "fn.name@npm:1.1.0" + checksum: e357144f48cfc9a7f52a82bbc6c23df7c8de639fce049cac41d41d62cabb740cdb9f14eddc6485e29c933104455bdd7a69bb14a9012cef9cd4fa252a4d0cf293 + languageName: node + linkType: hard + +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.6": + version: 1.15.9 + resolution: "follow-redirects@npm:1.15.9" + peerDependenciesMeta: + debug: + optional: true + checksum: 859e2bacc7a54506f2bf9aacb10d165df78c8c1b0ceb8023f966621b233717dab56e8d08baadc3ad3b9db58af290413d585c999694b7c146aaf2616340c3d2a6 + languageName: node + linkType: hard + +"for-each@npm:^0.3.3": + version: 0.3.3 + resolution: "for-each@npm:0.3.3" + dependencies: + is-callable: ^1.1.3 + checksum: 6c48ff2bc63362319c65e2edca4a8e1e3483a2fabc72fbe7feaf8c73db94fc7861bd53bc02c8a66a0c1dd709da6b04eec42e0abdd6b40ce47305ae92a25e5d28 + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.3.0 + resolution: "foreground-child@npm:3.3.0" + dependencies: + cross-spawn: ^7.0.0 + signal-exit: ^4.0.1 + checksum: 1989698488f725b05b26bc9afc8a08f08ec41807cd7b92ad85d96004ddf8243fd3e79486b8348c64a3011ae5cc2c9f0936af989e1f28339805d8bc178a75b451 + languageName: node + linkType: hard + +"forever-agent@npm:~0.6.1": + version: 0.6.1 + resolution: "forever-agent@npm:0.6.1" + checksum: 766ae6e220f5fe23676bb4c6a99387cec5b7b62ceb99e10923376e27bfea72f3c3aeec2ba5f45f3f7ba65d6616965aa7c20b15002b6860833bb6e394dea546a8 + languageName: node + linkType: hard + +"fork-ts-checker-webpack-plugin@npm:^6.5.0": + version: 6.5.3 + resolution: "fork-ts-checker-webpack-plugin@npm:6.5.3" + dependencies: + "@babel/code-frame": ^7.8.3 + "@types/json-schema": ^7.0.5 + chalk: ^4.1.0 + chokidar: ^3.4.2 + cosmiconfig: ^6.0.0 + deepmerge: ^4.2.2 + fs-extra: ^9.0.0 + glob: ^7.1.6 + memfs: ^3.1.2 + minimatch: ^3.0.4 + schema-utils: 2.7.0 + semver: ^7.3.2 + tapable: ^1.0.0 + peerDependencies: + eslint: ">= 6" + typescript: ">= 2.7" + vue-template-compiler: "*" + webpack: ">= 4" + peerDependenciesMeta: + eslint: + optional: true + vue-template-compiler: + optional: true + checksum: 9732a49bfeed8fc23e6e8a59795fa7c238edeba91040a9b520db54b4d316dda27f9f1893d360e296fd0ad8930627d364417d28a8c7007fba60cc730ebfce4956 + languageName: node + linkType: hard + +"fork-ts-checker-webpack-plugin@npm:^9.0.0": + version: 9.0.2 + resolution: "fork-ts-checker-webpack-plugin@npm:9.0.2" + dependencies: + "@babel/code-frame": ^7.16.7 + chalk: ^4.1.2 + chokidar: ^3.5.3 + cosmiconfig: ^8.2.0 + deepmerge: ^4.2.2 + fs-extra: ^10.0.0 + memfs: ^3.4.1 + minimatch: ^3.0.4 + node-abort-controller: ^3.0.1 + schema-utils: ^3.1.1 + semver: ^7.3.5 + tapable: ^2.2.1 + peerDependencies: + typescript: ">3.6.0" + webpack: ^5.11.0 + checksum: 136a87bfa36cb6ca27d2ae0feb3c6cabe0de734c1c1ed38f95b71ddb3eb4b6c461829a2dbb04f18f0f717fc6341f544327598255758c269cec9774ccee035afc + languageName: node + linkType: hard + +"form-data@npm:^2.5.0": + version: 2.5.2 + resolution: "form-data@npm:2.5.2" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.6 + mime-types: ^2.1.12 + safe-buffer: ^5.2.1 + checksum: 89ed3d96238d6fa874d75435e20f1aad28a1c22a88ab4e726ac4f6b0d29bef33d7e5aca51248c1070eccbbf4df94020a53842e800b2f1fb63073881a268113b4 + languageName: node + linkType: hard + +"form-data@npm:^4.0.0": + version: 4.0.1 + resolution: "form-data@npm:4.0.1" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.8 + mime-types: ^2.1.12 + checksum: ccee458cd5baf234d6b57f349fe9cc5f9a2ea8fd1af5ecda501a18fd1572a6dd3bf08a49f00568afd995b6a65af34cb8dec083cf9d582c4e621836499498dd84 + languageName: node + linkType: hard + +"form-data@npm:~2.3.2": + version: 2.3.3 + resolution: "form-data@npm:2.3.3" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.6 + mime-types: ^2.1.12 + checksum: 10c1780fa13dbe1ff3100114c2ce1f9307f8be10b14bf16e103815356ff567b6be39d70fc4a40f8990b9660012dc24b0f5e1dde1b6426166eb23a445ba068ca3 + languageName: node + linkType: hard + +"format@npm:^0.2.0": + version: 0.2.2 + resolution: "format@npm:0.2.2" + checksum: 646a60e1336250d802509cf24fb801e43bd4a70a07510c816fa133aa42cdbc9c21e66e9cc0801bb183c5b031c9d68be62e7fbb6877756e52357850f92aa28799 + languageName: node + linkType: hard + +"formidable@npm:^2.1.2": + version: 2.1.2 + resolution: "formidable@npm:2.1.2" + dependencies: + dezalgo: ^1.0.4 + hexoid: ^1.0.0 + once: ^1.4.0 + qs: ^6.11.0 + checksum: 81c8e5d89f5eb873e992893468f0de22c01678ca3d315db62be0560f9de1c77d4faefc9b1f4575098eb2263b3c81ba1024833a9fc3206297ddbac88a4f69b7a8 + languageName: node + linkType: hard + +"formik@npm:^2.4.5": + version: 2.4.6 + resolution: "formik@npm:2.4.6" + dependencies: + "@types/hoist-non-react-statics": ^3.3.1 + deepmerge: ^2.1.1 + hoist-non-react-statics: ^3.3.0 + lodash: ^4.17.21 + lodash-es: ^4.17.21 + react-fast-compare: ^2.0.1 + tiny-warning: ^1.0.2 + tslib: ^2.0.0 + peerDependencies: + react: ">=16.8.0" + checksum: ed0ff8eca11b3c4b5564a6ae5650cb124820d239ece9a7cb3d526ff8ce3d8f9fb992e2f78ef0666e65b7c05c944a91cc98a736e806e424019d6aac8bee313a42 + languageName: node + linkType: hard + +"forwarded@npm:0.2.0": + version: 0.2.0 + resolution: "forwarded@npm:0.2.0" + checksum: fd27e2394d8887ebd16a66ffc889dc983fbbd797d5d3f01087c020283c0f019a7d05ee85669383d8e0d216b116d720fc0cef2f6e9b7eb9f4c90c6e0bc7fd28e6 + languageName: node + linkType: hard + +"fraction.js@npm:4.3.4": + version: 4.3.4 + resolution: "fraction.js@npm:4.3.4" + checksum: 26fdecf114e3b693c760d3b2d5447f8ba9e815991ca7c7cdb930156780793b87f10936979a890b389676d960d7cd026273da9a44a6e20c12e3c4fd282a026ed3 + languageName: node + linkType: hard + +"fresh@npm:0.5.2, fresh@npm:~0.5.2": + version: 0.5.2 + resolution: "fresh@npm:0.5.2" + checksum: 13ea8b08f91e669a64e3ba3a20eb79d7ca5379a81f1ff7f4310d54e2320645503cc0c78daedc93dfb6191287295f6479544a649c64d8e41a1c0fb0c221552346 + languageName: node + linkType: hard + +"from@npm:~0": + version: 0.1.7 + resolution: "from@npm:0.1.7" + checksum: b85125b7890489656eb2e4f208f7654a93ec26e3aefaf3bbbcc0d496fc1941e4405834fcc9fe7333192aa2187905510ace70417bbf9ac6f6f4784a731d986939 + languageName: node + linkType: hard + +"fromentries@npm:^1.3.1": + version: 1.3.2 + resolution: "fromentries@npm:1.3.2" + checksum: 33729c529ce19f5494f846f0dd4945078f4e37f4e8955f4ae8cc7385c218f600e9d93a7d225d17636c20d1889106fd87061f911550861b7072f53bf891e6b341 + languageName: node + linkType: hard + +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d + languageName: node + linkType: hard + +"fs-extra@npm:10.1.0, fs-extra@npm:^10.0.0": + version: 10.1.0 + resolution: "fs-extra@npm:10.1.0" + dependencies: + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: dc94ab37096f813cc3ca12f0f1b5ad6744dfed9ed21e953d72530d103cea193c2f81584a39e9dee1bea36de5ee66805678c0dddc048e8af1427ac19c00fffc50 + languageName: node + linkType: hard + +"fs-extra@npm:9.1.0, fs-extra@npm:^9.0.0": + version: 9.1.0 + resolution: "fs-extra@npm:9.1.0" + dependencies: + at-least-node: ^1.0.0 + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: ba71ba32e0faa74ab931b7a0031d1523c66a73e225de7426e275e238e312d07313d2da2d33e34a52aa406c8763ade5712eb3ec9ba4d9edce652bcacdc29e6b20 + languageName: node + linkType: hard + +"fs-extra@npm:^11.0.0, fs-extra@npm:^11.2.0": + version: 11.2.0 + resolution: "fs-extra@npm:11.2.0" + dependencies: + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: b12e42fa40ba47104202f57b8480dd098aa931c2724565e5e70779ab87605665594e76ee5fb00545f772ab9ace167fe06d2ab009c416dc8c842c5ae6df7aa7e8 + languageName: node + linkType: hard + +"fs-extra@npm:^7.0.1, fs-extra@npm:~7.0.1": + version: 7.0.1 + resolution: "fs-extra@npm:7.0.1" + dependencies: + graceful-fs: ^4.1.2 + jsonfile: ^4.0.0 + universalify: ^0.1.0 + checksum: 141b9dccb23b66a66cefdd81f4cda959ff89282b1d721b98cea19ba08db3dcbe6f862f28841f3cf24bb299e0b7e6c42303908f65093cb7e201708e86ea5a8dcf + languageName: node + linkType: hard + +"fs-extra@npm:^8.1.0": + version: 8.1.0 + resolution: "fs-extra@npm:8.1.0" + dependencies: + graceful-fs: ^4.2.0 + jsonfile: ^4.0.0 + universalify: ^0.1.0 + checksum: bf44f0e6cea59d5ce071bba4c43ca76d216f89e402dc6285c128abc0902e9b8525135aa808adad72c9d5d218e9f4bcc63962815529ff2f684ad532172a284880 + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: ^3.0.0 + checksum: 1b8d128dae2ac6cc94230cc5ead341ba3e0efaef82dab46a33d171c044caaa6ca001364178d42069b2809c35a1c3c35079a32107c770e9ffab3901b59af8c8b1 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: ^7.0.3 + checksum: 8722a41109130851d979222d3ec88aabaceeaaf8f57b2a8f744ef8bd2d1ce95453b04a61daa0078822bc5cd21e008814f06fe6586f56fef511e71b8d2394d802 + languageName: node + linkType: hard + +"fs-monkey@npm:^1.0.4": + version: 1.0.6 + resolution: "fs-monkey@npm:1.0.6" + checksum: 4e9986acf197581b10b79d3e63e74252681ca215ef82d4afbd98dcfe86b3f09189ac1d7e8064bc433e4e53cdb5c14fdb38773277d41bba18b1ff8bbdcab01a3a + languageName: node + linkType: hard + +"fs.realpath@npm:^1.0.0": + version: 1.0.0 + resolution: "fs.realpath@npm:1.0.0" + checksum: 99ddea01a7e75aa276c250a04eedeffe5662bce66c65c07164ad6264f9de18fb21be9433ead460e54cff20e31721c811f4fb5d70591799df5f85dce6d6746fd0 + languageName: node + linkType: hard + +"fsevents@npm:2.3.2": + version: 2.3.2 + resolution: "fsevents@npm:2.3.2" + dependencies: + node-gyp: latest + checksum: 97ade64e75091afee5265e6956cb72ba34db7819b4c3e94c431d4be2b19b8bb7a2d4116da417950c3425f17c8fe693d25e20212cac583ac1521ad066b77ae31f + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: latest + checksum: 11e6ea6fea15e42461fc55b4b0e4a0a3c654faa567f1877dbd353f39156f69def97a69936d1746619d656c4b93de2238bf731f6085a03a50cabf287c9d024317 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@2.3.2#~builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=18f3a7" + dependencies: + node-gyp: latest + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.2#~builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=18f3a7" + dependencies: + node-gyp: latest + conditions: os=darwin + languageName: node + linkType: hard + +"function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 2b0ff4ce708d99715ad14a6d1f894e2a83242e4a52ccfcefaee5e40050562e5f6dafc1adbb4ce2d4ab47279a45dc736ab91ea5042d843c3c092820dfe032efb1 + languageName: node + linkType: hard + +"function.prototype.name@npm:^1.1.6": + version: 1.1.6 + resolution: "function.prototype.name@npm:1.1.6" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.2.0 + es-abstract: ^1.22.1 + functions-have-names: ^1.2.3 + checksum: 7a3f9bd98adab09a07f6e1f03da03d3f7c26abbdeaeee15223f6c04a9fb5674792bdf5e689dac19b97ac71de6aad2027ba3048a9b883aa1b3173eed6ab07f479 + languageName: node + linkType: hard + +"functions-have-names@npm:^1.2.3": + version: 1.2.3 + resolution: "functions-have-names@npm:1.2.3" + checksum: c3f1f5ba20f4e962efb71344ce0a40722163e85bee2101ce25f88214e78182d2d2476aa85ef37950c579eb6cf6ee811c17b3101bb84004bb75655f3e33f3fdb5 + languageName: node + linkType: hard + +"gauge@npm:^3.0.0": + version: 3.0.2 + resolution: "gauge@npm:3.0.2" + dependencies: + aproba: ^1.0.3 || ^2.0.0 + color-support: ^1.1.2 + console-control-strings: ^1.0.0 + has-unicode: ^2.0.1 + object-assign: ^4.1.1 + signal-exit: ^3.0.0 + string-width: ^4.2.3 + strip-ansi: ^6.0.1 + wide-align: ^1.1.2 + checksum: 81296c00c7410cdd48f997800155fbead4f32e4f82109be0719c63edc8560e6579946cc8abd04205297640691ec26d21b578837fd13a4e96288ab4b40b1dc3e9 + languageName: node + linkType: hard + +"gauge@npm:^4.0.3": + version: 4.0.4 + resolution: "gauge@npm:4.0.4" + dependencies: + aproba: ^1.0.3 || ^2.0.0 + color-support: ^1.1.3 + console-control-strings: ^1.1.0 + has-unicode: ^2.0.1 + signal-exit: ^3.0.7 + string-width: ^4.2.3 + strip-ansi: ^6.0.1 + wide-align: ^1.1.5 + checksum: 788b6bfe52f1dd8e263cda800c26ac0ca2ff6de0b6eee2fe0d9e3abf15e149b651bd27bf5226be10e6e3edb5c4e5d5985a5a1a98137e7a892f75eff76467ad2d + languageName: node + linkType: hard + +"gaxios@npm:^6.0.0, gaxios@npm:^6.0.2, gaxios@npm:^6.1.1": + version: 6.7.1 + resolution: "gaxios@npm:6.7.1" + dependencies: + extend: ^3.0.2 + https-proxy-agent: ^7.0.1 + is-stream: ^2.0.0 + node-fetch: ^2.6.9 + uuid: ^9.0.1 + checksum: ed5952655339918e0868c6f4e079d6e9e55b20a242ddb1be25076cdfb0dd1ca5a2cb233da7352baa972c19f898a78fa6ba67e7d848717c9ca9877c269b5b02f7 + languageName: node + linkType: hard + +"gcp-metadata@npm:^6.1.0": + version: 6.1.0 + resolution: "gcp-metadata@npm:6.1.0" + dependencies: + gaxios: ^6.0.0 + json-bigint: ^1.0.0 + checksum: 55de8ae4a6b7664379a093abf7e758ae06e82f244d41bd58d881a470bf34db94c4067ce9e1b425d9455b7705636d5f8baad844e49bb73879c338753ba7785b2b + languageName: node + linkType: hard + +"generate-function@npm:^2.3.1": + version: 2.3.1 + resolution: "generate-function@npm:2.3.1" + dependencies: + is-property: ^1.0.2 + checksum: 652f083de206ead2bae4caf9c7eeb465e8d98c0b8ed2a29c6afc538cef0785b5c6eea10548f1e13cc586d3afd796c13c830c2cb3dc612ec2457b2aadda5f57c9 + languageName: node + linkType: hard + +"generic-names@npm:^4.0.0": + version: 4.0.0 + resolution: "generic-names@npm:4.0.0" + dependencies: + loader-utils: ^3.2.0 + checksum: 8dabd2505164191501b75f2861b5e1194458a344ae2a7c9776bdd72d1f50b248dff737bcdf118fff677275edb3632f2d10662e6ac122dd7b245c5baa8d303270 + languageName: node + linkType: hard + +"gensync@npm:^1.0.0-beta.2": + version: 1.0.0-beta.2 + resolution: "gensync@npm:1.0.0-beta.2" + checksum: a7437e58c6be12aa6c90f7730eac7fa9833dc78872b4ad2963d2031b00a3367a93f98aec75f9aaac7220848e4026d67a8655e870b24f20a543d103c0d65952ec + languageName: node + linkType: hard + +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2, get-intrinsic@npm:^1.2.3, get-intrinsic@npm:^1.2.4": + version: 1.2.4 + resolution: "get-intrinsic@npm:1.2.4" + dependencies: + es-errors: ^1.3.0 + function-bind: ^1.1.2 + has-proto: ^1.0.1 + has-symbols: ^1.0.3 + hasown: ^2.0.0 + checksum: 414e3cdf2c203d1b9d7d33111df746a4512a1aa622770b361dadddf8ed0b5aeb26c560f49ca077e24bfafb0acb55ca908d1f709216ccba33ffc548ec8a79a951 + languageName: node + linkType: hard + +"get-package-type@npm:^0.1.0": + version: 0.1.0 + resolution: "get-package-type@npm:0.1.0" + checksum: bba0811116d11e56d702682ddef7c73ba3481f114590e705fc549f4d868972263896af313c57a25c076e3c0d567e11d919a64ba1b30c879be985fc9d44f96148 + languageName: node + linkType: hard + +"get-port@npm:^5.1.1": + version: 5.1.1 + resolution: "get-port@npm:5.1.1" + checksum: 0162663ffe5c09e748cd79d97b74cd70e5a5c84b760a475ce5767b357fb2a57cb821cee412d646aa8a156ed39b78aab88974eddaa9e5ee926173c036c0713787 + languageName: node + linkType: hard + +"get-stream@npm:^6.0.0": + version: 6.0.1 + resolution: "get-stream@npm:6.0.1" + checksum: e04ecece32c92eebf5b8c940f51468cd53554dcbb0ea725b2748be583c9523d00128137966afce410b9b051eb2ef16d657cd2b120ca8edafcf5a65e81af63cad + languageName: node + linkType: hard + +"get-symbol-description@npm:^1.0.2": + version: 1.0.2 + resolution: "get-symbol-description@npm:1.0.2" + dependencies: + call-bind: ^1.0.5 + es-errors: ^1.3.0 + get-intrinsic: ^1.2.4 + checksum: e1cb53bc211f9dbe9691a4f97a46837a553c4e7caadd0488dc24ac694db8a390b93edd412b48dcdd0b4bbb4c595de1709effc75fc87c0839deedc6968f5bd973 + languageName: node + linkType: hard + +"get-tsconfig@npm:^4.7.0, get-tsconfig@npm:^4.7.2": + version: 4.8.1 + resolution: "get-tsconfig@npm:4.8.1" + dependencies: + resolve-pkg-maps: ^1.0.0 + checksum: 12df01672e691d2ff6db8cf7fed1ddfef90ed94a5f3d822c63c147a26742026d582acd86afcd6f65db67d809625d17dd7f9d34f4d3f38f69bc2f48e19b2bdd5b + languageName: node + linkType: hard + +"get-uri@npm:^6.0.1": + version: 6.0.3 + resolution: "get-uri@npm:6.0.3" + dependencies: + basic-ftp: ^5.0.2 + data-uri-to-buffer: ^6.0.2 + debug: ^4.3.4 + fs-extra: ^11.2.0 + checksum: 3eda448a59fa1ba82ad4f252e58490fec586b644f2dc9c98ba3ab20e801ecc8a1bc1784829c474c9d188edb633d4dfd81c33894ca6117a33a16e8e013b41b40f + languageName: node + linkType: hard + +"getopts@npm:2.3.0": + version: 2.3.0 + resolution: "getopts@npm:2.3.0" + checksum: bbb5fcef8d4a8582cf4499ea3fc492d95322df2184e65d550ddacede04871e7ba33194c7abd06a6c5d540de3b70112a16f988787e236e1c66b89521032b398ce + languageName: node + linkType: hard + +"getpass@npm:^0.1.1": + version: 0.1.7 + resolution: "getpass@npm:0.1.7" + dependencies: + assert-plus: ^1.0.0 + checksum: ab18d55661db264e3eac6012c2d3daeafaab7a501c035ae0ccb193c3c23e9849c6e29b6ac762b9c2adae460266f925d55a3a2a3a3c8b94be2f222df94d70c046 + languageName: node + linkType: hard + +"git-up@npm:^7.0.0": + version: 7.0.0 + resolution: "git-up@npm:7.0.0" + dependencies: + is-ssh: ^1.4.0 + parse-url: ^8.1.0 + checksum: 2faadbab51e94d2ffb220e426e950087cc02c15d664e673bd5d1f734cfa8196fed8b19493f7bf28fe216d087d10e22a7fd9b63687e0ba7d24f0ddcfb0a266d6e + languageName: node + linkType: hard + +"git-url-parse@npm:^14.0.0": + version: 14.1.0 + resolution: "git-url-parse@npm:14.1.0" + dependencies: + git-up: ^7.0.0 + checksum: 16bbf5ca423352ab1b0d704dc40b46123e0bfcc0ae2959ef6a93d43c509146151cd6a1d99690f3555324d2261b36443b7978abc379dc1a7bf8f564e52d676dee + languageName: node + linkType: hard + +"git-url-parse@npm:^15.0.0": + version: 15.0.0 + resolution: "git-url-parse@npm:15.0.0" + dependencies: + git-up: ^7.0.0 + checksum: 4379e9b0a1297b62d603630341a7ce1a6e17901114ee7bb70a611f62ea0fd3b3649ac754891d2746fd42031a21e97ddc0092287a04cc028cbf37df8f6be65a9e + languageName: node + linkType: hard + +"github-from-package@npm:0.0.0": + version: 0.0.0 + resolution: "github-from-package@npm:0.0.0" + checksum: 14e448192a35c1e42efee94c9d01a10f42fe790375891a24b25261246ce9336ab9df5d274585aedd4568f7922246c2a78b8a8cd2571bfe99c693a9718e7dd0e3 + languageName: node + linkType: hard + +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: ^4.0.1 + checksum: f4f2bfe2425296e8a47e36864e4f42be38a996db40420fe434565e4480e3322f18eb37589617a98640c5dc8fdec1a387007ee18dbb1f3f5553409c34d17f425e + languageName: node + linkType: hard + +"glob-parent@npm:^6.0.2": + version: 6.0.2 + resolution: "glob-parent@npm:6.0.2" + dependencies: + is-glob: ^4.0.3 + checksum: c13ee97978bef4f55106b71e66428eb1512e71a7466ba49025fc2aec59a5bfb0954d5abd58fc5ee6c9b076eef4e1f6d3375c2e964b88466ca390da4419a786a8 + languageName: node + linkType: hard + +"glob-to-regexp@npm:^0.4.1": + version: 0.4.1 + resolution: "glob-to-regexp@npm:0.4.1" + checksum: e795f4e8f06d2a15e86f76e4d92751cf8bbfcf0157cea5c2f0f35678a8195a750b34096b1256e436f0cebc1883b5ff0888c47348443e69546a5a87f9e1eb1167 + languageName: node + linkType: hard + +"glob@npm:9.3.5": + version: 9.3.5 + resolution: "glob@npm:9.3.5" + dependencies: + fs.realpath: ^1.0.0 + minimatch: ^8.0.2 + minipass: ^4.2.4 + path-scurry: ^1.6.1 + checksum: 94b093adbc591bc36b582f77927d1fb0dbf3ccc231828512b017601408be98d1fe798fc8c0b19c6f2d1a7660339c3502ce698de475e9d938ccbb69b47b647c84 + languageName: node + linkType: hard + +"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7, glob@npm:^10.4.1": + version: 10.4.5 + resolution: "glob@npm:10.4.5" + dependencies: + foreground-child: ^3.1.0 + jackspeak: ^3.1.2 + minimatch: ^9.0.4 + minipass: ^7.1.2 + package-json-from-dist: ^1.0.0 + path-scurry: ^1.11.1 + bin: + glob: dist/esm/bin.mjs + checksum: 0bc725de5e4862f9f387fd0f2b274baf16850dcd2714502ccf471ee401803997983e2c05590cb65f9675a3c6f2a58e7a53f9e365704108c6ad3cbf1d60934c4a + languageName: node + linkType: hard + +"glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7": + version: 7.2.3 + resolution: "glob@npm:7.2.3" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^3.1.1 + once: ^1.3.0 + path-is-absolute: ^1.0.0 + checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 + languageName: node + linkType: hard + +"glob@npm:^8.0.1, glob@npm:^8.0.3, glob@npm:^8.1.0": + version: 8.1.0 + resolution: "glob@npm:8.1.0" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^5.0.1 + once: ^1.3.0 + checksum: 92fbea3221a7d12075f26f0227abac435de868dd0736a17170663783296d0dd8d3d532a5672b4488a439bf5d7fb85cdd07c11185d6cd39184f0385cbdfb86a47 + languageName: node + linkType: hard + +"global-agent@npm:^3.0.0": + version: 3.0.0 + resolution: "global-agent@npm:3.0.0" + dependencies: + boolean: ^3.0.1 + es6-error: ^4.1.1 + matcher: ^3.0.0 + roarr: ^2.15.3 + semver: ^7.3.2 + serialize-error: ^7.0.1 + checksum: 75074d80733b4bd5386c47f5df028e798018025beac0ab310e9908c72bf5639e408203e7bca0130d5ee01b5f4abc6d34385d96a9f950ea5fe1979bb431c808f7 + languageName: node + linkType: hard + +"global-modules@npm:^1.0.0": + version: 1.0.0 + resolution: "global-modules@npm:1.0.0" + dependencies: + global-prefix: ^1.0.1 + is-windows: ^1.0.1 + resolve-dir: ^1.0.0 + checksum: 10be68796c1e1abc1e2ba87ec4ea507f5629873b119ab0cd29c07284ef2b930f1402d10df01beccb7391dedd9cd479611dd6a24311c71be58937beaf18edf85e + languageName: node + linkType: hard + +"global-modules@npm:^2.0.0": + version: 2.0.0 + resolution: "global-modules@npm:2.0.0" + dependencies: + global-prefix: ^3.0.0 + checksum: d6197f25856c878c2fb5f038899f2dca7cbb2f7b7cf8999660c0104972d5cfa5c68b5a0a77fa8206bb536c3903a4615665acb9709b4d80846e1bb47eaef65430 + languageName: node + linkType: hard + +"global-prefix@npm:^1.0.1": + version: 1.0.2 + resolution: "global-prefix@npm:1.0.2" + dependencies: + expand-tilde: ^2.0.2 + homedir-polyfill: ^1.0.1 + ini: ^1.3.4 + is-windows: ^1.0.1 + which: ^1.2.14 + checksum: 061b43470fe498271bcd514e7746e8a8535032b17ab9570517014ae27d700ff0dca749f76bbde13ba384d185be4310d8ba5712cb0e74f7d54d59390db63dd9a0 + languageName: node + linkType: hard + +"global-prefix@npm:^3.0.0": + version: 3.0.0 + resolution: "global-prefix@npm:3.0.0" + dependencies: + ini: ^1.3.5 + kind-of: ^6.0.2 + which: ^1.3.1 + checksum: 8a82fc1d6f22c45484a4e34656cc91bf021a03e03213b0035098d605bfc612d7141f1e14a21097e8a0413b4884afd5b260df0b6a25605ce9d722e11f1df2881d + languageName: node + linkType: hard + +"globals@npm:^11.1.0": + version: 11.12.0 + resolution: "globals@npm:11.12.0" + checksum: 67051a45eca3db904aee189dfc7cd53c20c7d881679c93f6146ddd4c9f4ab2268e68a919df740d39c71f4445d2b38ee360fc234428baea1dbdfe68bbcb46979e + languageName: node + linkType: hard + +"globals@npm:^13.19.0": + version: 13.24.0 + resolution: "globals@npm:13.24.0" + dependencies: + type-fest: ^0.20.2 + checksum: 56066ef058f6867c04ff203b8a44c15b038346a62efbc3060052a1016be9f56f4cf0b2cd45b74b22b81e521a889fc7786c73691b0549c2f3a6e825b3d394f43c + languageName: node + linkType: hard + +"globalthis@npm:^1.0.1, globalthis@npm:^1.0.3, globalthis@npm:^1.0.4": + version: 1.0.4 + resolution: "globalthis@npm:1.0.4" + dependencies: + define-properties: ^1.2.1 + gopd: ^1.0.1 + checksum: 39ad667ad9f01476474633a1834a70842041f70a55571e8dcef5fb957980a92da5022db5430fca8aecc5d47704ae30618c0bc877a579c70710c904e9ef06108a + languageName: node + linkType: hard + +"globby@npm:^11.0.0, globby@npm:^11.0.4, globby@npm:^11.1.0": + version: 11.1.0 + resolution: "globby@npm:11.1.0" + dependencies: + array-union: ^2.1.0 + dir-glob: ^3.0.1 + fast-glob: ^3.2.9 + ignore: ^5.2.0 + merge2: ^1.4.1 + slash: ^3.0.0 + checksum: b4be8885e0cfa018fc783792942d53926c35c50b3aefd3fdcfb9d22c627639dc26bd2327a40a0b74b074100ce95bb7187bfeae2f236856aa3de183af7a02aea6 + languageName: node + linkType: hard + +"google-auth-library@npm:^9.6.3": + version: 9.14.2 + resolution: "google-auth-library@npm:9.14.2" + dependencies: + base64-js: ^1.3.0 + ecdsa-sig-formatter: ^1.0.11 + gaxios: ^6.1.1 + gcp-metadata: ^6.1.0 + gtoken: ^7.0.0 + jws: ^4.0.0 + checksum: 64b3a6c1b1b14f1c891dbcfb850bc4db63dc8fae17e70197636244d00c83b539ac3da8688aae0bd1f09c884fc538d203945ae751edbabf666b41066385d86e30 + languageName: node + linkType: hard + +"gopd@npm:^1.0.1": + version: 1.0.1 + resolution: "gopd@npm:1.0.1" + dependencies: + get-intrinsic: ^1.1.3 + checksum: a5ccfb8806e0917a94e0b3de2af2ea4979c1da920bc381667c260e00e7cafdbe844e2cb9c5bcfef4e5412e8bf73bab837285bc35c7ba73aaaf0134d4583393a6 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 + languageName: node + linkType: hard + +"graphemer@npm:^1.4.0": + version: 1.4.0 + resolution: "graphemer@npm:1.4.0" + checksum: bab8f0be9b568857c7bec9fda95a89f87b783546d02951c40c33f84d05bb7da3fd10f863a9beb901463669b6583173a8c8cc6d6b306ea2b9b9d5d3d943c3a673 + languageName: node + linkType: hard + +"graphlib@npm:^2.1.8": + version: 2.1.8 + resolution: "graphlib@npm:2.1.8" + dependencies: + lodash: ^4.17.15 + checksum: 1e0db4dea1c8187d59103d5582ecf32008845ebe2103959a51d22cb6dae495e81fb9263e22c922bca3aaecb56064a45cd53424e15a4626cfb5a0c52d0aff61a8 + languageName: node + linkType: hard + +"graphql-tag@npm:^2.10.3": + version: 2.12.6 + resolution: "graphql-tag@npm:2.12.6" + dependencies: + tslib: ^2.1.0 + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: b15162a3d62f17b9b79302445b9ee330e041582f1c7faca74b9dec5daa74272c906ec1c34e1c50592bb6215e5c3eba80a309103f6ba9e4c1cddc350c46f010df + languageName: node + linkType: hard + +"graphql@npm:^16.0.0, graphql@npm:^16.8.1": + version: 16.9.0 + resolution: "graphql@npm:16.9.0" + checksum: 8cb3d54100e9227310383ce7f791ca48d12f15ed9f2021f23f8735f1121aafe4e5e611a853081dd935ce221724ea1ae4638faef5d2921fb1ad7c26b5f46611e9 + languageName: node + linkType: hard + +"gtoken@npm:^7.0.0": + version: 7.1.0 + resolution: "gtoken@npm:7.1.0" + dependencies: + gaxios: ^6.0.0 + jws: ^4.0.0 + checksum: 1f338dced78f9d895ea03cd507454eb5a7b77e841ecd1d45e44483b08c1e64d16a9b0342358d37586d87462ffc2d5f5bff5dfe77ed8d4f0aafc3b5b0347d5d16 + languageName: node + linkType: hard + +"gzip-size@npm:^6.0.0": + version: 6.0.0 + resolution: "gzip-size@npm:6.0.0" + dependencies: + duplexer: ^0.1.2 + checksum: 2df97f359696ad154fc171dcb55bc883fe6e833bca7a65e457b9358f3cb6312405ed70a8da24a77c1baac0639906cd52358dc0ce2ec1a937eaa631b934c94194 + languageName: node + linkType: hard + +"handle-thing@npm:^2.0.0": + version: 2.0.1 + resolution: "handle-thing@npm:2.0.1" + checksum: 68071f313062315cd9dce55710e9496873945f1dd425107007058fc1629f93002a7649fcc3e464281ce02c7e809a35f5925504ab8105d972cf649f1f47cb7d6c + languageName: node + linkType: hard + +"handlebars@npm:^4.7.3": + version: 4.7.8 + resolution: "handlebars@npm:4.7.8" + dependencies: + minimist: ^1.2.5 + neo-async: ^2.6.2 + source-map: ^0.6.1 + uglify-js: ^3.1.4 + wordwrap: ^1.0.0 + dependenciesMeta: + uglify-js: + optional: true + bin: + handlebars: bin/handlebars + checksum: 00e68bb5c183fd7b8b63322e6234b5ac8fbb960d712cb3f25587d559c2951d9642df83c04a1172c918c41bcfc81bfbd7a7718bbce93b893e0135fc99edea93ff + languageName: node + linkType: hard + +"har-schema@npm:^2.0.0": + version: 2.0.0 + resolution: "har-schema@npm:2.0.0" + checksum: d8946348f333fb09e2bf24cc4c67eabb47c8e1d1aa1c14184c7ffec1140a49ec8aa78aa93677ae452d71d5fc0fdeec20f0c8c1237291fc2bcb3f502a5d204f9b + languageName: node + linkType: hard + +"har-validator@npm:~5.1.3": + version: 5.1.5 + resolution: "har-validator@npm:5.1.5" + dependencies: + ajv: ^6.12.3 + har-schema: ^2.0.0 + checksum: b998a7269ca560d7f219eedc53e2c664cd87d487e428ae854a6af4573fc94f182fe9d2e3b92ab968249baec7ebaf9ead69cf975c931dc2ab282ec182ee988280 + languageName: node + linkType: hard + +"harmony-reflect@npm:^1.4.6": + version: 1.6.2 + resolution: "harmony-reflect@npm:1.6.2" + checksum: 2e5bae414cd2bfae5476147f9935dc69ee9b9a413206994dcb94c5b3208d4555da3d4313aff6fd14bd9991c1e3ef69cdda5c8fac1eb1d7afc064925839339b8c + languageName: node + linkType: hard + +"has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": + version: 1.0.2 + resolution: "has-bigints@npm:1.0.2" + checksum: 390e31e7be7e5c6fe68b81babb73dfc35d413604d7ee5f56da101417027a4b4ce6a27e46eff97ad040c835b5d228676eae99a9b5c3bc0e23c8e81a49241ff45b + languageName: node + linkType: hard + +"has-flag@npm:^3.0.0": + version: 3.0.0 + resolution: "has-flag@npm:3.0.0" + checksum: 4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b + languageName: node + linkType: hard + +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 261a1357037ead75e338156b1f9452c016a37dcd3283a972a30d9e4a87441ba372c8b81f818cd0fbcd9c0354b4ae7e18b9e1afa1971164aef6d18c2b6095a8ad + languageName: node + linkType: hard + +"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": + version: 1.0.2 + resolution: "has-property-descriptors@npm:1.0.2" + dependencies: + es-define-property: ^1.0.0 + checksum: fcbb246ea2838058be39887935231c6d5788babed499d0e9d0cc5737494c48aba4fe17ba1449e0d0fbbb1e36175442faa37f9c427ae357d6ccb1d895fbcd3de3 + languageName: node + linkType: hard + +"has-proto@npm:^1.0.1, has-proto@npm:^1.0.3": + version: 1.0.3 + resolution: "has-proto@npm:1.0.3" + checksum: fe7c3d50b33f50f3933a04413ed1f69441d21d2d2944f81036276d30635cad9279f6b43bc8f32036c31ebdfcf6e731150f46c1907ad90c669ffe9b066c3ba5c4 + languageName: node + linkType: hard + +"has-symbols@npm:^1.0.2, has-symbols@npm:^1.0.3": + version: 1.0.3 + resolution: "has-symbols@npm:1.0.3" + checksum: a054c40c631c0d5741a8285010a0777ea0c068f99ed43e5d6eb12972da223f8af553a455132fdb0801bdcfa0e0f443c0c03a68d8555aa529b3144b446c3f2410 + languageName: node + linkType: hard + +"has-tostringtag@npm:^1.0.0, has-tostringtag@npm:^1.0.2": + version: 1.0.2 + resolution: "has-tostringtag@npm:1.0.2" + dependencies: + has-symbols: ^1.0.3 + checksum: 999d60bb753ad714356b2c6c87b7fb74f32463b8426e159397da4bde5bca7e598ab1073f4d8d4deafac297f2eb311484cd177af242776bf05f0d11565680468d + languageName: node + linkType: hard + +"has-unicode@npm:^2.0.1": + version: 2.0.1 + resolution: "has-unicode@npm:2.0.1" + checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400 + languageName: node + linkType: hard + +"hash-base@npm:^3.0.0": + version: 3.1.0 + resolution: "hash-base@npm:3.1.0" + dependencies: + inherits: ^2.0.4 + readable-stream: ^3.6.0 + safe-buffer: ^5.2.0 + checksum: 26b7e97ac3de13cb23fc3145e7e3450b0530274a9562144fc2bf5c1e2983afd0e09ed7cc3b20974ba66039fad316db463da80eb452e7373e780cbee9a0d2f2dc + languageName: node + linkType: hard + +"hash-base@npm:~3.0": + version: 3.0.4 + resolution: "hash-base@npm:3.0.4" + dependencies: + inherits: ^2.0.1 + safe-buffer: ^5.0.1 + checksum: 878465a0dfcc33cce195c2804135352c590d6d10980adc91a9005fd377e77f2011256c2b7cfce472e3f2e92d561d1bf3228d2da06348a9017ce9a258b3b49764 + languageName: node + linkType: hard + +"hash.js@npm:^1.0.0, hash.js@npm:^1.0.3": + version: 1.1.7 + resolution: "hash.js@npm:1.1.7" + dependencies: + inherits: ^2.0.3 + minimalistic-assert: ^1.0.1 + checksum: e350096e659c62422b85fa508e4b3669017311aa4c49b74f19f8e1bc7f3a54a584fdfd45326d4964d6011f2b2d882e38bea775a96046f2a61b7779a979629d8f + languageName: node + linkType: hard + +"hasown@npm:^2.0.0, hasown@npm:^2.0.1, hasown@npm:^2.0.2": + version: 2.0.2 + resolution: "hasown@npm:2.0.2" + dependencies: + function-bind: ^1.1.2 + checksum: e8516f776a15149ca6c6ed2ae3110c417a00b62260e222590e54aa367cbcd6ed99122020b37b7fbdf05748df57b265e70095d7bf35a47660587619b15ffb93db + languageName: node + linkType: hard + +"hast-util-parse-selector@npm:^2.0.0": + version: 2.2.5 + resolution: "hast-util-parse-selector@npm:2.2.5" + checksum: 22ee4afbd11754562144cb3c4f3ec52524dafba4d90ee52512902d17cf11066d83b38f7bdf6ca571bbc2541f07ba30db0d234657b6ecb8ca4631587466459605 + languageName: node + linkType: hard + +"hast-util-whitespace@npm:^2.0.0": + version: 2.0.1 + resolution: "hast-util-whitespace@npm:2.0.1" + checksum: 431be6b2f35472f951615540d7a53f69f39461e5e080c0190268bdeb2be9ab9b1dddfd1f467dd26c1de7e7952df67beb1307b6ee940baf78b24a71b5e0663868 + languageName: node + linkType: hard + +"hastscript@npm:^6.0.0": + version: 6.0.0 + resolution: "hastscript@npm:6.0.0" + dependencies: + "@types/hast": ^2.0.0 + comma-separated-tokens: ^1.0.0 + hast-util-parse-selector: ^2.0.0 + property-information: ^5.0.0 + space-separated-tokens: ^1.0.0 + checksum: 5e50b85af0d2cb7c17979cb1ddca75d6b96b53019dd999b39e7833192c9004201c3cee6445065620ea05d0087d9ae147a4844e582d64868be5bc6b0232dfe52d + languageName: node + linkType: hard + +"he@npm:^1.2.0": + version: 1.2.0 + resolution: "he@npm:1.2.0" + bin: + he: bin/he + checksum: 3d4d6babccccd79c5c5a3f929a68af33360d6445587d628087f39a965079d84f18ce9c3d3f917ee1e3978916fc833bb8b29377c3b403f919426f91bc6965e7a7 + languageName: node + linkType: hard + +"headers-polyfill@npm:3.2.5": + version: 3.2.5 + resolution: "headers-polyfill@npm:3.2.5" + checksum: a3c4bdd661584fd39e40c0f91412abc514616edfbd20d29a75567e591f90ef5c445c8e209b7f3c2b2375d27e95e4690f33417368a168d4832484a93861ab6a3c + languageName: node + linkType: hard + +"helmet@npm:^6.0.0": + version: 6.2.0 + resolution: "helmet@npm:6.2.0" + checksum: cf01e024244205bd10d70fd2f3874244b72ba37a10a4604e4383bbd63fe1438ee24bae7672c4ee5c5e16e6cd88ac58003274034fab0ba199761471555a322b37 + languageName: node + linkType: hard + +"hexoid@npm:^1.0.0": + version: 1.0.0 + resolution: "hexoid@npm:1.0.0" + checksum: 27a148ca76a2358287f40445870116baaff4a0ed0acc99900bf167f0f708ffd82e044ff55e9949c71963852b580fc024146d3ac6d5d76b508b78d927fa48ae2d + languageName: node + linkType: hard + +"highlight.js@npm:^10.4.1, highlight.js@npm:^10.7.1, highlight.js@npm:~10.7.0": + version: 10.7.3 + resolution: "highlight.js@npm:10.7.3" + checksum: defeafcd546b535d710d8efb8e650af9e3b369ef53e28c3dc7893eacfe263200bba4c5fcf43524ae66d5c0c296b1af0870523ceae3e3104d24b7abf6374a4fea + languageName: node + linkType: hard + +"highlightjs-vue@npm:^1.0.0": + version: 1.0.0 + resolution: "highlightjs-vue@npm:1.0.0" + checksum: 895f2dd22c93a441aca7df8d21f18c00697537675af18832e50810a071715f79e45eda677e6244855f325234c6a06f7bd76f8f20bd602040fc350c80ac7725e4 + languageName: node + linkType: hard + +"history@npm:^5.0.0": + version: 5.3.0 + resolution: "history@npm:5.3.0" + dependencies: + "@babel/runtime": ^7.7.6 + checksum: d73c35df49d19ac172f9547d30a21a26793e83f16a78386d99583b5bf1429cc980799fcf1827eb215d31816a6600684fba9686ce78104e23bd89ec239e7c726f + languageName: node + linkType: hard + +"hmac-drbg@npm:^1.0.1": + version: 1.0.1 + resolution: "hmac-drbg@npm:1.0.1" + dependencies: + hash.js: ^1.0.3 + minimalistic-assert: ^1.0.0 + minimalistic-crypto-utils: ^1.0.1 + checksum: bd30b6a68d7f22d63f10e1888aee497d7c2c5c0bb469e66bbdac99f143904d1dfe95f8131f95b3e86c86dd239963c9d972fcbe147e7cffa00e55d18585c43fe0 + languageName: node + linkType: hard + +"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": + version: 3.3.2 + resolution: "hoist-non-react-statics@npm:3.3.2" + dependencies: + react-is: ^16.7.0 + checksum: b1538270429b13901ee586aa44f4cc3ecd8831c061d06cb8322e50ea17b3f5ce4d0e2e66394761e6c8e152cd8c34fb3b4b690116c6ce2bd45b18c746516cb9e8 + languageName: node + linkType: hard + +"homedir-polyfill@npm:^1.0.1": + version: 1.0.3 + resolution: "homedir-polyfill@npm:1.0.3" + dependencies: + parse-passwd: ^1.0.0 + checksum: 18dd4db87052c6a2179d1813adea0c4bfcfa4f9996f0e226fefb29eb3d548e564350fa28ec46b0bf1fbc0a1d2d6922ceceb80093115ea45ff8842a4990139250 + languageName: node + linkType: hard + +"hoopy@npm:^0.1.4": + version: 0.1.4 + resolution: "hoopy@npm:0.1.4" + checksum: cfa60c7684c5e1ee4efe26e167bc54b73f839ffb59d1d44a5c4bf891e26b4f5bcc666555219a98fec95508fea4eda3a79540c53c05cc79afc1f66f9a238f4d9e + languageName: node + linkType: hard + +"hpack.js@npm:^2.1.6": + version: 2.1.6 + resolution: "hpack.js@npm:2.1.6" + dependencies: + inherits: ^2.0.1 + obuf: ^1.0.0 + readable-stream: ^2.0.1 + wbuf: ^1.1.0 + checksum: 2de144115197967ad6eeee33faf41096c6ba87078703c5cb011632dcfbffeb45784569e0cf02c317bd79c48375597c8ec88c30fff5bb0b023e8f654fb6e9c06e + languageName: node + linkType: hard + +"html-encoding-sniffer@npm:^3.0.0": + version: 3.0.0 + resolution: "html-encoding-sniffer@npm:3.0.0" + dependencies: + whatwg-encoding: ^2.0.0 + checksum: 8d806aa00487e279e5ccb573366a951a9f68f65c90298eac9c3a2b440a7ffe46615aff2995a2f61c6746c639234e6179a97e18ca5ccbbf93d3725ef2099a4502 + languageName: node + linkType: hard + +"html-entities@npm:^2.1.0, html-entities@npm:^2.4.0, html-entities@npm:^2.5.2": + version: 2.5.2 + resolution: "html-entities@npm:2.5.2" + checksum: b23f4a07d33d49ade1994069af4e13d31650e3fb62621e92ae10ecdf01d1a98065c78fd20fdc92b4c7881612210b37c275f2c9fba9777650ab0d6f2ceb3b99b6 + languageName: node + linkType: hard + +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: d2df2da3ad40ca9ee3a39c5cc6475ef67c8f83c234475f24d8e9ce0dc80a2c82df8e1d6fa78ddd1e9022a586ea1bd247a615e80a5cd9273d90111ddda7d9e974 + languageName: node + linkType: hard + +"html-minifier-terser@npm:^6.0.2": + version: 6.1.0 + resolution: "html-minifier-terser@npm:6.1.0" + dependencies: + camel-case: ^4.1.2 + clean-css: ^5.2.2 + commander: ^8.3.0 + he: ^1.2.0 + param-case: ^3.0.4 + relateurl: ^0.2.7 + terser: ^5.10.0 + bin: + html-minifier-terser: cli.js + checksum: ac52c14006476f773204c198b64838477859dc2879490040efab8979c0207424da55d59df7348153f412efa45a0840a1ca3c757bf14767d23a15e3e389d37a93 + languageName: node + linkType: hard + +"html-webpack-plugin@npm:^5.3.1": + version: 5.6.2 + resolution: "html-webpack-plugin@npm:5.6.2" + dependencies: + "@types/html-minifier-terser": ^6.0.0 + html-minifier-terser: ^6.0.2 + lodash: ^4.17.21 + pretty-error: ^4.0.0 + tapable: ^2.0.0 + peerDependencies: + "@rspack/core": 0.x || 1.x + webpack: ^5.20.0 + peerDependenciesMeta: + "@rspack/core": + optional: true + webpack: + optional: true + checksum: c579ce8b34ef1cd903829402aa6a62a6c92fe1cdfcd81d17ebc87f39eaab1381438b9d805a63457b255238db8f2865d71f48cd382375aa28718881e3ab2d2f9a + languageName: node + linkType: hard + +"htmlparser2@npm:^6.1.0": + version: 6.1.0 + resolution: "htmlparser2@npm:6.1.0" + dependencies: + domelementtype: ^2.0.1 + domhandler: ^4.0.0 + domutils: ^2.5.2 + entities: ^2.0.0 + checksum: 81a7b3d9c3bb9acb568a02fc9b1b81ffbfa55eae7f1c41ae0bf840006d1dbf54cb3aa245b2553e2c94db674840a9f0fdad7027c9a9d01a062065314039058c4e + languageName: node + linkType: hard + +"http-assert@npm:^1.3.0": + version: 1.5.0 + resolution: "http-assert@npm:1.5.0" + dependencies: + deep-equal: ~1.0.1 + http-errors: ~1.8.0 + checksum: 69c9b3c14cf8b2822916360a365089ce936c883c49068f91c365eccba5c141a9964d19fdda589150a480013bf503bf37d8936c732e9635819339e730ab0e7527 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 + languageName: node + linkType: hard + +"http-deceiver@npm:^1.2.7": + version: 1.2.7 + resolution: "http-deceiver@npm:1.2.7" + checksum: 64d7d1ae3a6933eb0e9a94e6f27be4af45a53a96c3c34e84ff57113787105a89fff9d1c3df263ef63add823df019b0e8f52f7121e32393bb5ce9a713bf100b41 + languageName: node + linkType: hard + +"http-errors@npm:2.0.0": + version: 2.0.0 + resolution: "http-errors@npm:2.0.0" + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + checksum: 9b0a3782665c52ce9dc658a0d1560bcb0214ba5699e4ea15aefb2a496e2ca83db03ebc42e1cce4ac1f413e4e0d2d736a3fd755772c556a9a06853ba2a0b7d920 + languageName: node + linkType: hard + +"http-errors@npm:^1.6.3, http-errors@npm:~1.8.0": + version: 1.8.1 + resolution: "http-errors@npm:1.8.1" + dependencies: + depd: ~1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: ">= 1.5.0 < 2" + toidentifier: 1.0.1 + checksum: d3c7e7e776fd51c0a812baff570bdf06fe49a5dc448b700ab6171b1250e4cf7db8b8f4c0b133e4bfe2451022a5790c1ca6c2cae4094dedd6ac8304a1267f91d2 + languageName: node + linkType: hard + +"http-errors@npm:~1.6.2": + version: 1.6.3 + resolution: "http-errors@npm:1.6.3" + dependencies: + depd: ~1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: ">= 1.4.0 < 2" + checksum: a9654ee027e3d5de305a56db1d1461f25709ac23267c6dc28cdab8323e3f96caa58a9a6a5e93ac15d7285cee0c2f019378c3ada9026e7fe19c872d695f27de7c + languageName: node + linkType: hard + +"http-parser-js@npm:>=0.5.1": + version: 0.5.8 + resolution: "http-parser-js@npm:0.5.8" + checksum: 6bbdf2429858e8cf13c62375b0bfb6dc3955ca0f32e58237488bc86cd2378f31d31785fd3ac4ce93f1c74e0189cf8823c91f5cb061696214fd368d2452dc871d + languageName: node + linkType: hard + +"http-proxy-agent@npm:^5.0.0": + version: 5.0.0 + resolution: "http-proxy-agent@npm:5.0.0" + dependencies: + "@tootallnate/once": 2 + agent-base: 6 + debug: 4 + checksum: e2ee1ff1656a131953839b2a19cd1f3a52d97c25ba87bd2559af6ae87114abf60971e498021f9b73f9fd78aea8876d1fb0d4656aac8a03c6caa9fc175f22b786 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: ^7.1.0 + debug: ^4.3.4 + checksum: 670858c8f8f3146db5889e1fa117630910101db601fff7d5a8aa637da0abedf68c899f03d3451cac2f83bcc4c3d2dabf339b3aa00ff8080571cceb02c3ce02f3 + languageName: node + linkType: hard + +"http-proxy-middleware@npm:^2.0.3": + version: 2.0.7 + resolution: "http-proxy-middleware@npm:2.0.7" + dependencies: + "@types/http-proxy": ^1.17.8 + http-proxy: ^1.18.1 + is-glob: ^4.0.1 + is-plain-obj: ^3.0.0 + micromatch: ^4.0.2 + peerDependencies: + "@types/express": ^4.17.13 + peerDependenciesMeta: + "@types/express": + optional: true + checksum: 18caa21145917aa1054740353916e8f03f5a3a93bede9106f1f44d84f7b174df17af1c72bf5fade5cc440c2058ee813f47cbb2bdd6ae6874af1cf33e0ac575f3 + languageName: node + linkType: hard + +"http-proxy@npm:^1.18.1": + version: 1.18.1 + resolution: "http-proxy@npm:1.18.1" + dependencies: + eventemitter3: ^4.0.0 + follow-redirects: ^1.0.0 + requires-port: ^1.0.0 + checksum: f5bd96bf83e0b1e4226633dbb51f8b056c3e6321917df402deacec31dd7fe433914fc7a2c1831cf7ae21e69c90b3a669b8f434723e9e8b71fd68afe30737b6a5 + languageName: node + linkType: hard + +"http-signature@npm:~1.2.0": + version: 1.2.0 + resolution: "http-signature@npm:1.2.0" + dependencies: + assert-plus: ^1.0.0 + jsprim: ^1.2.2 + sshpk: ^1.7.0 + checksum: 3324598712266a9683585bb84a75dec4fd550567d5e0dd4a0fff6ff3f74348793404d3eeac4918fa0902c810eeee1a86419e4a2e92a164132dfe6b26743fb47c + languageName: node + linkType: hard + +"https-browserify@npm:^1.0.0": + version: 1.0.0 + resolution: "https-browserify@npm:1.0.0" + checksum: 09b35353e42069fde2435760d13f8a3fb7dd9105e358270e2e225b8a94f811b461edd17cb57594e5f36ec1218f121c160ddceeec6e8be2d55e01dcbbbed8cbae + languageName: node + linkType: hard + +"https-proxy-agent@npm:^5.0.0, https-proxy-agent@npm:^5.0.1": + version: 5.0.1 + resolution: "https-proxy-agent@npm:5.0.1" + dependencies: + agent-base: 6 + debug: 4 + checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.0, https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.3, https-proxy-agent@npm:^7.0.5": + version: 7.0.5 + resolution: "https-proxy-agent@npm:7.0.5" + dependencies: + agent-base: ^7.0.2 + debug: 4 + checksum: 2e1a28960f13b041a50702ee74f240add8e75146a5c37fc98f1960f0496710f6918b3a9fe1e5aba41e50f58e6df48d107edd9c405c5f0d73ac260dabf2210857 + languageName: node + linkType: hard + +"human-id@npm:^1.0.2": + version: 1.0.2 + resolution: "human-id@npm:1.0.2" + checksum: 95ee57ffae849f008e2ef3fe6e437be8c999861b4256f18c3b194c8928670a8a149e0576917105d5fd77e5edbb621c5a4736fade20bb7bf130113c1ebc95cb74 + languageName: node + linkType: hard + +"human-signals@npm:^2.1.0": + version: 2.1.0 + resolution: "human-signals@npm:2.1.0" + checksum: b87fd89fce72391625271454e70f67fe405277415b48bcc0117ca73d31fa23a4241787afdc8d67f5a116cf37258c052f59ea82daffa72364d61351423848e3b8 + languageName: node + linkType: hard + +"humanize-ms@npm:^1.2.1": + version: 1.2.1 + resolution: "humanize-ms@npm:1.2.1" + dependencies: + ms: ^2.0.0 + checksum: 9c7a74a2827f9294c009266c82031030eae811ca87b0da3dceb8d6071b9bde22c9f3daef0469c3c533cc67a97d8a167cd9fc0389350e5f415f61a79b171ded16 + languageName: node + linkType: hard + +"hyperdyperid@npm:^1.2.0": + version: 1.2.0 + resolution: "hyperdyperid@npm:1.2.0" + checksum: 210029d1c86926f09109f6317d143f8b056fc38e8dd11b0c3e3205fc6c6ff8429fb55b4b9c2bce065462719ed9d34366eced387aaa0035d93eb76b306a8547ef + languageName: node + linkType: hard + +"hyphenate-style-name@npm:^1.0.3": + version: 1.1.0 + resolution: "hyphenate-style-name@npm:1.1.0" + checksum: b9ed74e29181d96bd58a2d0e62fc4a19879db591dba268275829ff0ae595fcdf11faafaeaa63330a45c3004664d7db1f0fc7cdb372af8ee4615ed8260302c207 + languageName: node + linkType: hard + +"i18next@npm:^22.4.15": + version: 22.5.1 + resolution: "i18next@npm:22.5.1" + dependencies: + "@babel/runtime": ^7.20.6 + checksum: 175f8ab7fac2abcee147b00cc2d8e7d4fa9b05cdc227f02cac841fc2fd9545ed4a6d88774f594f8ad12dc944e4d34cc8e88aa00c8b9947baef9e859d93abd305 + languageName: node + linkType: hard + +"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24": + version: 0.4.24 + resolution: "iconv-lite@npm:0.4.24" + dependencies: + safer-buffer: ">= 2.1.2 < 3" + checksum: bd9f120f5a5b306f0bc0b9ae1edeb1577161503f5f8252a20f1a9e56ef8775c9959fd01c55f2d3a39d9a8abaf3e30c1abeb1895f367dcbbe0a8fd1c9ca01c4f6 + languageName: node + linkType: hard + +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: ">= 2.1.2 < 3.0.0" + checksum: 3f60d47a5c8fc3313317edfd29a00a692cc87a19cac0159e2ce711d0ebc9019064108323b5e493625e25594f11c6236647d8e256fbe7a58f4a3b33b89e6d30bf + languageName: node + linkType: hard + +"icss-replace-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "icss-replace-symbols@npm:1.1.0" + checksum: 24575b2c2f7e762bfc6f4beee31be9ba98a01cad521b5aa9954090a5de2b5e1bf67814c17e22f9e51b7d798238db8215a173d6c2b4726ce634ce06b68ece8045 + languageName: node + linkType: hard + +"icss-utils@npm:^5.0.0, icss-utils@npm:^5.1.0": + version: 5.1.0 + resolution: "icss-utils@npm:5.1.0" + peerDependencies: + postcss: ^8.1.0 + checksum: 5c324d283552b1269cfc13a503aaaa172a280f914e5b81544f3803bc6f06a3b585fb79f66f7c771a2c052db7982c18bf92d001e3b47282e3abbbb4c4cc488d68 + languageName: node + linkType: hard + +"identity-obj-proxy@npm:3.0.0": + version: 3.0.0 + resolution: "identity-obj-proxy@npm:3.0.0" + dependencies: + harmony-reflect: ^1.4.6 + checksum: 97559f8ea2aeaa1a880d279d8c49550dce01148321e00a2102cda5ddf9ce622fa1d7f3efc7bed63458af78889de888fdaebaf31c816312298bb3fdd0ef8aaf2c + languageName: node + linkType: hard + +"ieee754@npm:^1.1.13, ieee754@npm:^1.1.4, ieee754@npm:^1.2.1": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e + languageName: node + linkType: hard + +"ignore-walk@npm:^5.0.1": + version: 5.0.1 + resolution: "ignore-walk@npm:5.0.1" + dependencies: + minimatch: ^5.0.1 + checksum: 1a4ef35174653a1aa6faab3d9f8781269166536aee36a04946f6e2b319b2475c1903a75ed42f04219274128242f49d0a10e20c4354ee60d9548e97031451150b + languageName: node + linkType: hard + +"ignore@npm:^5.1.4, ignore@npm:^5.1.8, ignore@npm:^5.2.0, ignore@npm:^5.2.4": + version: 5.3.2 + resolution: "ignore@npm:5.3.2" + checksum: 2acfd32a573260ea522ea0bfeff880af426d68f6831f973129e2ba7363f422923cf53aab62f8369cbf4667c7b25b6f8a3761b34ecdb284ea18e87a5262a865be + languageName: node + linkType: hard + +"immer@npm:^9.0.6, immer@npm:^9.0.7": + version: 9.0.21 + resolution: "immer@npm:9.0.21" + checksum: 70e3c274165995352f6936695f0ef4723c52c92c92dd0e9afdfe008175af39fa28e76aafb3a2ca9d57d1fb8f796efc4dd1e1cc36f18d33fa5b74f3dfb0375432 + languageName: node + linkType: hard + +"import-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "import-cwd@npm:3.0.0" + dependencies: + import-from: ^3.0.0 + checksum: f2c4230e8389605154a390124381f9136811306ae4ba1c8017398c3c6926bc5cf75cf89350372b4938f79792ea373776b4efabd27506440ec301ce34c4e867eb + languageName: node + linkType: hard + +"import-fresh@npm:^3.1.0, import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": + version: 3.3.0 + resolution: "import-fresh@npm:3.3.0" + dependencies: + parent-module: ^1.0.0 + resolve-from: ^4.0.0 + checksum: 2cacfad06e652b1edc50be650f7ec3be08c5e5a6f6d12d035c440a42a8cc028e60a5b99ca08a77ab4d6b1346da7d971915828f33cdab730d3d42f08242d09baa + languageName: node + linkType: hard + +"import-from@npm:^3.0.0": + version: 3.0.0 + resolution: "import-from@npm:3.0.0" + dependencies: + resolve-from: ^5.0.0 + checksum: 5040a7400e77e41e2c3bb6b1b123b52a15a284de1ffc03d605879942c00e3a87428499d8d031d554646108a0f77652549411167f6a7788e4fc7027eefccf3356 + languageName: node + linkType: hard + +"import-lazy@npm:~4.0.0": + version: 4.0.0 + resolution: "import-lazy@npm:4.0.0" + checksum: 22f5e51702134aef78890156738454f620e5fe7044b204ebc057c614888a1dd6fdf2ede0fdcca44d5c173fd64f65c985f19a51775b06967ef58cc3d26898df07 + languageName: node + linkType: hard + +"import-local@npm:^3.0.2": + version: 3.2.0 + resolution: "import-local@npm:3.2.0" + dependencies: + pkg-dir: ^4.2.0 + resolve-cwd: ^3.0.0 + bin: + import-local-fixture: fixtures/cli.js + checksum: 0b0b0b412b2521739fbb85eeed834a3c34de9bc67e670b3d0b86248fc460d990a7b116ad056c084b87a693ef73d1f17268d6a5be626bb43c998a8b1c8a230004 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 7cae75c8cd9a50f57dadd77482359f659eaebac0319dd9368bcd1714f55e65badd6929ca58569da2b6494ef13fdd5598cd700b1eba23f8b79c5f19d195a3ecf7 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 824cfb9929d031dabf059bebfe08cf3137365e112019086ed3dcff6a0a7b698cb80cf67ccccde0e25b9e2d7527aa6cc1fed1ac490c752162496caba3e6699612 + languageName: node + linkType: hard + +"infer-owner@npm:^1.0.4": + version: 1.0.4 + resolution: "infer-owner@npm:1.0.4" + checksum: 181e732764e4a0611576466b4b87dac338972b839920b2a8cde43642e4ed6bd54dc1fb0b40874728f2a2df9a1b097b8ff83b56d5f8f8e3927f837fdcb47d8a89 + languageName: node + linkType: hard + +"inflight@npm:^1.0.4": + version: 1.0.6 + resolution: "inflight@npm:1.0.6" + dependencies: + once: ^1.3.0 + wrappy: 1 + checksum: f4f76aa072ce19fae87ce1ef7d221e709afb59d445e05d47fba710e85470923a75de35bfae47da6de1b18afc3ce83d70facf44cfb0aff89f0a3f45c0a0244dfd + languageName: node + linkType: hard + +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 + languageName: node + linkType: hard + +"inherits@npm:2.0.3": + version: 2.0.3 + resolution: "inherits@npm:2.0.3" + checksum: 78cb8d7d850d20a5e9a7f3620db31483aa00ad5f722ce03a55b110e5a723539b3716a3b463e2b96ce3fe286f33afc7c131fa2f91407528ba80cea98a7545d4c0 + languageName: node + linkType: hard + +"ini@npm:^1.3.4, ini@npm:^1.3.5, ini@npm:~1.3.0": + version: 1.3.8 + resolution: "ini@npm:1.3.8" + checksum: dfd98b0ca3a4fc1e323e38a6c8eb8936e31a97a918d3b377649ea15bdb15d481207a0dda1021efbd86b464cae29a0d33c1d7dcaf6c5672bee17fa849bc50a1b3 + languageName: node + linkType: hard + +"inline-style-parser@npm:0.1.1": + version: 0.1.1 + resolution: "inline-style-parser@npm:0.1.1" + checksum: 5d545056a3e1f2bf864c928a886a0e1656a3517127d36917b973de581bd54adc91b4bf1febcb0da054f204b4934763f1a4e09308b4d55002327cf1d48ac5d966 + languageName: node + linkType: hard + +"inline-style-prefixer@npm:^7.0.1": + version: 7.0.1 + resolution: "inline-style-prefixer@npm:7.0.1" + dependencies: + css-in-js-utils: ^3.1.0 + checksum: 07a72573dfdac5e08fa18f5ce71d922861716955e230175ac415db227d9ed49443c764356cb407a92f4c85b30ebf39604165260b4dfbf3196b7736d7332c5c06 + languageName: node + linkType: hard + +"inquirer@npm:8.2.6, inquirer@npm:^8.2.0": + version: 8.2.6 + resolution: "inquirer@npm:8.2.6" + dependencies: + ansi-escapes: ^4.2.1 + chalk: ^4.1.1 + cli-cursor: ^3.1.0 + cli-width: ^3.0.0 + external-editor: ^3.0.3 + figures: ^3.0.0 + lodash: ^4.17.21 + mute-stream: 0.0.8 + ora: ^5.4.1 + run-async: ^2.4.0 + rxjs: ^7.5.5 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + through: ^2.3.6 + wrap-ansi: ^6.0.1 + checksum: 387ffb0a513559cc7414eb42c57556a60e302f820d6960e89d376d092e257a919961cd485a1b4de693dbb5c0de8bc58320bfd6247dfd827a873aa82a4215a240 + languageName: node + linkType: hard + +"internal-slot@npm:^1.0.4, internal-slot@npm:^1.0.7": + version: 1.0.7 + resolution: "internal-slot@npm:1.0.7" + dependencies: + es-errors: ^1.3.0 + hasown: ^2.0.0 + side-channel: ^1.0.4 + checksum: cadc5eea5d7d9bc2342e93aae9f31f04c196afebb11bde97448327049f492cd7081e18623ae71388aac9cd237b692ca3a105be9c68ac39c1dec679d7409e33eb + languageName: node + linkType: hard + +"interpret@npm:^2.2.0": + version: 2.2.0 + resolution: "interpret@npm:2.2.0" + checksum: f51efef7cb8d02da16408ffa3504cd6053014c5aeb7bb8c223727e053e4235bf565e45d67028b0c8740d917c603807aa3c27d7bd2f21bf20b6417e2bb3e5fd6e + languageName: node + linkType: hard + +"ioredis@npm:^5.4.1": + version: 5.4.1 + resolution: "ioredis@npm:5.4.1" + dependencies: + "@ioredis/commands": ^1.1.1 + cluster-key-slot: ^1.1.0 + debug: ^4.3.4 + denque: ^2.1.0 + lodash.defaults: ^4.2.0 + lodash.isarguments: ^3.1.0 + redis-errors: ^1.2.0 + redis-parser: ^3.0.0 + standard-as-callback: ^2.1.0 + checksum: 92210294f75800febe7544c27b07e4892480172363b11971aa575be5b68f023bfed4bc858abc9792230c153aa80409047a358f174062c14d17536aa4499fe10b + languageName: node + linkType: hard + +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: 1.1.0 + sprintf-js: ^1.1.3 + checksum: aa15f12cfd0ef5e38349744e3654bae649a34c3b10c77a674a167e99925d1549486c5b14730eebce9fea26f6db9d5e42097b00aa4f9f612e68c79121c71652dc + languageName: node + linkType: hard + +"ip-regex@npm:^4.1.0": + version: 4.3.0 + resolution: "ip-regex@npm:4.3.0" + checksum: 7ff904b891221b1847f3fdf3dbb3e6a8660dc39bc283f79eb7ed88f5338e1a3d1104b779bc83759159be266249c59c2160e779ee39446d79d4ed0890dfd06f08 + languageName: node + linkType: hard + +"ipaddr.js@npm:1.9.1": + version: 1.9.1 + resolution: "ipaddr.js@npm:1.9.1" + checksum: f88d3825981486f5a1942414c8d77dd6674dd71c065adcfa46f578d677edcb99fda25af42675cb59db492fdf427b34a5abfcde3982da11a8fd83a500b41cfe77 + languageName: node + linkType: hard + +"ipaddr.js@npm:^2.1.0": + version: 2.2.0 + resolution: "ipaddr.js@npm:2.2.0" + checksum: 770ba8451fd9bf78015e8edac0d5abd7a708cbf75f9429ca9147a9d2f3a2d60767cd5de2aab2b1e13ca6e4445bdeff42bf12ef6f151c07a5c6cf8a44328e2859 + languageName: node + linkType: hard + +"is-alphabetical@npm:^1.0.0": + version: 1.0.4 + resolution: "is-alphabetical@npm:1.0.4" + checksum: 6508cce44fd348f06705d377b260974f4ce68c74000e7da4045f0d919e568226dc3ce9685c5a2af272195384df6930f748ce9213fc9f399b5d31b362c66312cb + languageName: node + linkType: hard + +"is-alphanumerical@npm:^1.0.0": + version: 1.0.4 + resolution: "is-alphanumerical@npm:1.0.4" + dependencies: + is-alphabetical: ^1.0.0 + is-decimal: ^1.0.0 + checksum: e2e491acc16fcf5b363f7c726f666a9538dba0a043665740feb45bba1652457a73441e7c5179c6768a638ed396db3437e9905f403644ec7c468fb41f4813d03f + languageName: node + linkType: hard + +"is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1": + version: 1.1.1 + resolution: "is-arguments@npm:1.1.1" + dependencies: + call-bind: ^1.0.2 + has-tostringtag: ^1.0.0 + checksum: 7f02700ec2171b691ef3e4d0e3e6c0ba408e8434368504bb593d0d7c891c0dbfda6d19d30808b904a6cb1929bca648c061ba438c39f296c2a8ca083229c49f27 + languageName: node + linkType: hard + +"is-array-buffer@npm:^3.0.2, is-array-buffer@npm:^3.0.4": + version: 3.0.4 + resolution: "is-array-buffer@npm:3.0.4" + dependencies: + call-bind: ^1.0.2 + get-intrinsic: ^1.2.1 + checksum: e4e3e6ef0ff2239e75371d221f74bc3c26a03564a22efb39f6bb02609b598917ddeecef4e8c877df2a25888f247a98198959842a5e73236bc7f22cabdf6351a7 + languageName: node + linkType: hard + +"is-arrayish@npm:^0.2.1": + version: 0.2.1 + resolution: "is-arrayish@npm:0.2.1" + checksum: eef4417e3c10e60e2c810b6084942b3ead455af16c4509959a27e490e7aee87cfb3f38e01bbde92220b528a0ee1a18d52b787e1458ee86174d8c7f0e58cd488f + languageName: node + linkType: hard + +"is-arrayish@npm:^0.3.1": + version: 0.3.2 + resolution: "is-arrayish@npm:0.3.2" + checksum: 977e64f54d91c8f169b59afcd80ff19227e9f5c791fa28fa2e5bce355cbaf6c2c356711b734656e80c9dd4a854dd7efcf7894402f1031dfc5de5d620775b4d5f + languageName: node + linkType: hard + +"is-async-function@npm:^2.0.0": + version: 2.0.0 + resolution: "is-async-function@npm:2.0.0" + dependencies: + has-tostringtag: ^1.0.0 + checksum: e3471d95e6c014bf37cad8a93f2f4b6aac962178e0a5041e8903147166964fdc1c5c1d2ef87e86d77322c370ca18f2ea004fa7420581fa747bcaf7c223069dbd + languageName: node + linkType: hard + +"is-bigint@npm:^1.0.1": + version: 1.0.4 + resolution: "is-bigint@npm:1.0.4" + dependencies: + has-bigints: ^1.0.1 + checksum: c56edfe09b1154f8668e53ebe8252b6f185ee852a50f9b41e8d921cb2bed425652049fbe438723f6cb48a63ca1aa051e948e7e401e093477c99c84eba244f666 + languageName: node + linkType: hard + +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: ^2.0.0 + checksum: 84192eb88cff70d320426f35ecd63c3d6d495da9d805b19bc65b518984b7c0760280e57dbf119b7e9be6b161784a5a673ab2c6abe83abb5198a432232ad5b35c + languageName: node + linkType: hard + +"is-boolean-object@npm:^1.1.0": + version: 1.1.2 + resolution: "is-boolean-object@npm:1.1.2" + dependencies: + call-bind: ^1.0.2 + has-tostringtag: ^1.0.0 + checksum: c03b23dbaacadc18940defb12c1c0e3aaece7553ef58b162a0f6bba0c2a7e1551b59f365b91e00d2dbac0522392d576ef322628cb1d036a0fe51eb466db67222 + languageName: node + linkType: hard + +"is-buffer@npm:^2.0.0": + version: 2.0.5 + resolution: "is-buffer@npm:2.0.5" + checksum: 764c9ad8b523a9f5a32af29bdf772b08eb48c04d2ad0a7240916ac2688c983bf5f8504bf25b35e66240edeb9d9085461f9b5dae1f3d2861c6b06a65fe983de42 + languageName: node + linkType: hard + +"is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": + version: 1.2.7 + resolution: "is-callable@npm:1.2.7" + checksum: 61fd57d03b0d984e2ed3720fb1c7a897827ea174bd44402878e059542ea8c4aeedee0ea0985998aa5cc2736b2fa6e271c08587addb5b3959ac52cf665173d1ac + languageName: node + linkType: hard + +"is-cidr@npm:^4.0.0": + version: 4.0.2 + resolution: "is-cidr@npm:4.0.2" + dependencies: + cidr-regex: ^3.1.1 + checksum: ee6e670e655a835710a7fa15268b428adbf80267114a494ce1c2ca2b09e1ca0b629fe1375aae621d4c093b32930d5ff7c4ee6da97eae14e3836bc7b3a07b171f + languageName: node + linkType: hard + +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1": + version: 2.15.1 + resolution: "is-core-module@npm:2.15.1" + dependencies: + hasown: ^2.0.2 + checksum: df134c168115690724b62018c37b2f5bba0d5745fa16960b329c5a00883a8bea6a5632fdb1e3efcce237c201826ba09f93197b7cd95577ea56b0df335be23633 + languageName: node + linkType: hard + +"is-data-view@npm:^1.0.1": + version: 1.0.1 + resolution: "is-data-view@npm:1.0.1" + dependencies: + is-typed-array: ^1.1.13 + checksum: 4ba4562ac2b2ec005fefe48269d6bd0152785458cd253c746154ffb8a8ab506a29d0cfb3b74af87513843776a88e4981ae25c89457bf640a33748eab1a7216b5 + languageName: node + linkType: hard + +"is-date-object@npm:^1.0.1, is-date-object@npm:^1.0.5": + version: 1.0.5 + resolution: "is-date-object@npm:1.0.5" + dependencies: + has-tostringtag: ^1.0.0 + checksum: baa9077cdf15eb7b58c79398604ca57379b2fc4cf9aa7a9b9e295278648f628c9b201400c01c5e0f7afae56507d741185730307cbe7cad3b9f90a77e5ee342fc + languageName: node + linkType: hard + +"is-decimal@npm:^1.0.0": + version: 1.0.4 + resolution: "is-decimal@npm:1.0.4" + checksum: ed483a387517856dc395c68403a10201fddcc1b63dc56513fbe2fe86ab38766120090ecdbfed89223d84ca8b1cd28b0641b93cb6597b6e8f4c097a7c24e3fb96 + languageName: node + linkType: hard + +"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1": + version: 2.2.1 + resolution: "is-docker@npm:2.2.1" + bin: + is-docker: cli.js + checksum: 3fef7ddbf0be25958e8991ad941901bf5922ab2753c46980b60b05c1bf9c9c2402d35e6dc32e4380b980ef5e1970a5d9d5e5aa2e02d77727c3b6b5e918474c56 + languageName: node + linkType: hard + +"is-docker@npm:^3.0.0": + version: 3.0.0 + resolution: "is-docker@npm:3.0.0" + bin: + is-docker: cli.js + checksum: b698118f04feb7eaf3338922bd79cba064ea54a1c3db6ec8c0c8d8ee7613e7e5854d802d3ef646812a8a3ace81182a085dfa0a71cc68b06f3fa794b9783b3c90 + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: df033653d06d0eb567461e58a7a8c9f940bd8c22274b94bf7671ab36df5719791aae15eef6d83bbb5e23283967f2f984b8914559d4449efda578c775c4be6f85 + languageName: node + linkType: hard + +"is-finalizationregistry@npm:^1.0.2": + version: 1.0.2 + resolution: "is-finalizationregistry@npm:1.0.2" + dependencies: + call-bind: ^1.0.2 + checksum: 4f243a8e06228cd45bdab8608d2cb7abfc20f6f0189c8ac21ea8d603f1f196eabd531ce0bb8e08cbab047e9845ef2c191a3761c9a17ad5cabf8b35499c4ad35d + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 + languageName: node + linkType: hard + +"is-generator-fn@npm:^2.0.0": + version: 2.1.0 + resolution: "is-generator-fn@npm:2.1.0" + checksum: a6ad5492cf9d1746f73b6744e0c43c0020510b59d56ddcb78a91cbc173f09b5e6beff53d75c9c5a29feb618bfef2bf458e025ecf3a57ad2268e2fb2569f56215 + languageName: node + linkType: hard + +"is-generator-function@npm:^1.0.10, is-generator-function@npm:^1.0.7": + version: 1.0.10 + resolution: "is-generator-function@npm:1.0.10" + dependencies: + has-tostringtag: ^1.0.0 + checksum: d54644e7dbaccef15ceb1e5d91d680eb5068c9ee9f9eb0a9e04173eb5542c9b51b5ab52c5537f5703e48d5fddfd376817c1ca07a84a407b7115b769d4bdde72b + languageName: node + linkType: hard + +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: ^2.1.1 + checksum: d381c1319fcb69d341cc6e6c7cd588e17cd94722d9a32dbd60660b993c4fb7d0f19438674e68dfec686d09b7c73139c9166b47597f846af387450224a8101ab4 + languageName: node + linkType: hard + +"is-hexadecimal@npm:^1.0.0": + version: 1.0.4 + resolution: "is-hexadecimal@npm:1.0.4" + checksum: a452e047587b6069332d83130f54d30da4faf2f2ebaa2ce6d073c27b5703d030d58ed9e0b729c8e4e5b52c6f1dab26781bb77b7bc6c7805f14f320e328ff8cd5 + languageName: node + linkType: hard + +"is-in-browser@npm:^1.0.2, is-in-browser@npm:^1.1.3": + version: 1.1.3 + resolution: "is-in-browser@npm:1.1.3" + checksum: 178491f97f6663c0574565701b76f41633dbe065e4bd8d518ce017a8fa25e5109ecb6a3bd8bd55c0aba11b208f86b9f0f9c91f3664e148ebf618b74a74fcaf09 + languageName: node + linkType: hard + +"is-inside-container@npm:^1.0.0": + version: 1.0.0 + resolution: "is-inside-container@npm:1.0.0" + dependencies: + is-docker: ^3.0.0 + bin: + is-inside-container: cli.js + checksum: c50b75a2ab66ab3e8b92b3bc534e1ea72ca25766832c0623ac22d134116a98bcf012197d1caabe1d1c4bd5f84363d4aa5c36bb4b585fbcaf57be172cd10a1a03 + languageName: node + linkType: hard + +"is-interactive@npm:^1.0.0": + version: 1.0.0 + resolution: "is-interactive@npm:1.0.0" + checksum: 824808776e2d468b2916cdd6c16acacebce060d844c35ca6d82267da692e92c3a16fdba624c50b54a63f38bdc4016055b6f443ce57d7147240de4f8cdabaf6f9 + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 93a32f01940220532e5948538699ad610d5924ac86093fcee83022252b363eb0cc99ba53ab084a04e4fb62bf7b5731f55496257a4c38adf87af9c4d352c71c35 + languageName: node + linkType: hard + +"is-map@npm:^2.0.2, is-map@npm:^2.0.3": + version: 2.0.3 + resolution: "is-map@npm:2.0.3" + checksum: e6ce5f6380f32b141b3153e6ba9074892bbbbd655e92e7ba5ff195239777e767a976dcd4e22f864accaf30e53ebf961ab1995424aef91af68788f0591b7396cc + languageName: node + linkType: hard + +"is-module@npm:^1.0.0": + version: 1.0.0 + resolution: "is-module@npm:1.0.0" + checksum: 8cd5390730c7976fb4e8546dd0b38865ee6f7bacfa08dfbb2cc07219606755f0b01709d9361e01f13009bbbd8099fa2927a8ed665118a6105d66e40f1b838c3f + languageName: node + linkType: hard + +"is-negative-zero@npm:^2.0.3": + version: 2.0.3 + resolution: "is-negative-zero@npm:2.0.3" + checksum: c1e6b23d2070c0539d7b36022d5a94407132411d01aba39ec549af824231f3804b1aea90b5e4e58e807a65d23ceb538ed6e355ce76b267bdd86edb757ffcbdcd + languageName: node + linkType: hard + +"is-network-error@npm:^1.0.0": + version: 1.1.0 + resolution: "is-network-error@npm:1.1.0" + checksum: b2fe6aac07f814a9de275efd05934c832c129e7ba292d27614e9e8eec9e043b7a0bbeaeca5d0916b0f462edbec2aa2eaee974ee0a12ac095040e9515c222c251 + languageName: node + linkType: hard + +"is-node-process@npm:^1.2.0": + version: 1.2.0 + resolution: "is-node-process@npm:1.2.0" + checksum: 930765cdc6d81ab8f1bbecbea4a8d35c7c6d88a3ff61f3630e0fc7f22d624d7661c1df05c58547d0eb6a639dfa9304682c8e342c4113a6ed51472b704cee2928 + languageName: node + linkType: hard + +"is-number-object@npm:^1.0.4": + version: 1.0.7 + resolution: "is-number-object@npm:1.0.7" + dependencies: + has-tostringtag: ^1.0.0 + checksum: d1e8d01bb0a7134c74649c4e62da0c6118a0bfc6771ea3c560914d52a627873e6920dd0fd0ebc0e12ad2ff4687eac4c308f7e80320b973b2c8a2c8f97a7524f7 + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 456ac6f8e0f3111ed34668a624e45315201dff921e5ac181f8ec24923b99e9f32ca1a194912dc79d539c97d33dba17dc635202ff0b2cf98326f608323276d27a + languageName: node + linkType: hard + +"is-path-inside@npm:^3.0.3": + version: 3.0.3 + resolution: "is-path-inside@npm:3.0.3" + checksum: abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 + languageName: node + linkType: hard + +"is-plain-obj@npm:^3.0.0": + version: 3.0.0 + resolution: "is-plain-obj@npm:3.0.0" + checksum: a6ebdf8e12ab73f33530641972a72a4b8aed6df04f762070d823808303e4f76d87d5ea5bd76f96a7bbe83d93f04ac7764429c29413bd9049853a69cb630fb21c + languageName: node + linkType: hard + +"is-plain-obj@npm:^4.0.0": + version: 4.1.0 + resolution: "is-plain-obj@npm:4.1.0" + checksum: 6dc45da70d04a81f35c9310971e78a6a3c7a63547ef782e3a07ee3674695081b6ca4e977fbb8efc48dae3375e0b34558d2bcd722aec9bddfa2d7db5b041be8ce + languageName: node + linkType: hard + +"is-plain-object@npm:^5.0.0": + version: 5.0.0 + resolution: "is-plain-object@npm:5.0.0" + checksum: e32d27061eef62c0847d303125440a38660517e586f2f3db7c9d179ae5b6674ab0f469d519b2e25c147a1a3bc87156d0d5f4d8821e0ce4a9ee7fe1fcf11ce45c + languageName: node + linkType: hard + +"is-potential-custom-element-name@npm:^1.0.1": + version: 1.0.1 + resolution: "is-potential-custom-element-name@npm:1.0.1" + checksum: ced7bbbb6433a5b684af581872afe0e1767e2d1146b2207ca0068a648fb5cab9d898495d1ac0583524faaf24ca98176a7d9876363097c2d14fee6dd324f3a1ab + languageName: node + linkType: hard + +"is-promise@npm:^4.0.0": + version: 4.0.0 + resolution: "is-promise@npm:4.0.0" + checksum: 0b46517ad47b00b6358fd6553c83ec1f6ba9acd7ffb3d30a0bf519c5c69e7147c132430452351b8a9fc198f8dd6c4f76f8e6f5a7f100f8c77d57d9e0f4261a8a + languageName: node + linkType: hard + +"is-property@npm:^1.0.2": + version: 1.0.2 + resolution: "is-property@npm:1.0.2" + checksum: 33b661a3690bcc88f7e47bb0a21b9e3187e76a317541ea7ec5e8096d954f441b77a46d8930c785f7fbf4ef8dfd624c25495221e026e50f74c9048fe501773be5 + languageName: node + linkType: hard + +"is-reference@npm:1.2.1": + version: 1.2.1 + resolution: "is-reference@npm:1.2.1" + dependencies: + "@types/estree": "*" + checksum: e7b48149f8abda2c10849ea51965904d6a714193d68942ad74e30522231045acf06cbfae5a4be2702fede5d232e61bf50b3183acdc056e6e3afe07fcf4f4b2bc + languageName: node + linkType: hard + +"is-regex@npm:^1.1.4": + version: 1.1.4 + resolution: "is-regex@npm:1.1.4" + dependencies: + call-bind: ^1.0.2 + has-tostringtag: ^1.0.0 + checksum: 362399b33535bc8f386d96c45c9feb04cf7f8b41c182f54174c1a45c9abbbe5e31290bbad09a458583ff6bf3b2048672cdb1881b13289569a7c548370856a652 + languageName: node + linkType: hard + +"is-root@npm:^2.1.0": + version: 2.1.0 + resolution: "is-root@npm:2.1.0" + checksum: 37eea0822a2a9123feb58a9d101558ba276771a6d830f87005683349a9acff15958a9ca590a44e778c6b335660b83e85c744789080d734f6081a935a4880aee2 + languageName: node + linkType: hard + +"is-set@npm:^2.0.2, is-set@npm:^2.0.3": + version: 2.0.3 + resolution: "is-set@npm:2.0.3" + checksum: 36e3f8c44bdbe9496c9689762cc4110f6a6a12b767c5d74c0398176aa2678d4467e3bf07595556f2dba897751bde1422480212b97d973c7b08a343100b0c0dfe + languageName: node + linkType: hard + +"is-shared-array-buffer@npm:^1.0.2, is-shared-array-buffer@npm:^1.0.3": + version: 1.0.3 + resolution: "is-shared-array-buffer@npm:1.0.3" + dependencies: + call-bind: ^1.0.7 + checksum: a4fff602c309e64ccaa83b859255a43bb011145a42d3f56f67d9268b55bc7e6d98a5981a1d834186ad3105d6739d21547083fe7259c76c0468483fc538e716d8 + languageName: node + linkType: hard + +"is-ssh@npm:^1.4.0": + version: 1.4.0 + resolution: "is-ssh@npm:1.4.0" + dependencies: + protocols: ^2.0.1 + checksum: 75eaa17b538bee24b661fbeb0f140226ac77e904a6039f787bea418431e2162f1f9c4c4ccad3bd169e036cd701cc631406e8c505d9fa7e20164e74b47f86f40f + languageName: node + linkType: hard + +"is-stream@npm:^2.0.0, is-stream@npm:^2.0.1": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 + languageName: node + linkType: hard + +"is-string@npm:^1.0.5, is-string@npm:^1.0.7": + version: 1.0.7 + resolution: "is-string@npm:1.0.7" + dependencies: + has-tostringtag: ^1.0.0 + checksum: 323b3d04622f78d45077cf89aab783b2f49d24dc641aa89b5ad1a72114cfeff2585efc8c12ef42466dff32bde93d839ad321b26884cf75e5a7892a938b089989 + languageName: node + linkType: hard + +"is-subdir@npm:^1.1.1": + version: 1.2.0 + resolution: "is-subdir@npm:1.2.0" + dependencies: + better-path-resolve: 1.0.0 + checksum: 31029a383972bff4cc4f1bd1463fd04dde017e0a04ae3a6f6e08124a90c6c4656312d593101b0f38805fa3f3c8f6bc4583524bbf72c50784fa5ca0d3e5a76279 + languageName: node + linkType: hard + +"is-symbol@npm:^1.0.2, is-symbol@npm:^1.0.3": + version: 1.0.4 + resolution: "is-symbol@npm:1.0.4" + dependencies: + has-symbols: ^1.0.2 + checksum: 92805812ef590738d9de49d677cd17dfd486794773fb6fa0032d16452af46e9b91bb43ffe82c983570f015b37136f4b53b28b8523bfb10b0ece7a66c31a54510 + languageName: node + linkType: hard + +"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.3": + version: 1.1.13 + resolution: "is-typed-array@npm:1.1.13" + dependencies: + which-typed-array: ^1.1.14 + checksum: 150f9ada183a61554c91e1c4290086d2c100b0dff45f60b028519be72a8db964da403c48760723bf5253979b8dffe7b544246e0e5351dcd05c5fdb1dcc1dc0f0 + languageName: node + linkType: hard + +"is-typedarray@npm:~1.0.0": + version: 1.0.0 + resolution: "is-typedarray@npm:1.0.0" + checksum: 3508c6cd0a9ee2e0df2fa2e9baabcdc89e911c7bd5cf64604586697212feec525aa21050e48affb5ffc3df20f0f5d2e2cf79b08caa64e1ccc9578e251763aef7 + languageName: node + linkType: hard + +"is-unicode-supported@npm:^0.1.0": + version: 0.1.0 + resolution: "is-unicode-supported@npm:0.1.0" + checksum: a2aab86ee7712f5c2f999180daaba5f361bdad1efadc9610ff5b8ab5495b86e4f627839d085c6530363c6d6d4ecbde340fb8e54bdb83da4ba8e0865ed5513c52 + languageName: node + linkType: hard + +"is-url@npm:^1.2.4": + version: 1.2.4 + resolution: "is-url@npm:1.2.4" + checksum: 100e74b3b1feab87a43ef7653736e88d997eb7bd32e71fd3ebc413e58c1cbe56269699c776aaea84244b0567f2a7d68dfaa512a062293ed2f9fdecb394148432 + languageName: node + linkType: hard + +"is-weakmap@npm:^2.0.2": + version: 2.0.2 + resolution: "is-weakmap@npm:2.0.2" + checksum: f36aef758b46990e0d3c37269619c0a08c5b29428c0bb11ecba7f75203442d6c7801239c2f31314bc79199217ef08263787f3837d9e22610ad1da62970d6616d + languageName: node + linkType: hard + +"is-weakref@npm:^1.0.2": + version: 1.0.2 + resolution: "is-weakref@npm:1.0.2" + dependencies: + call-bind: ^1.0.2 + checksum: 95bd9a57cdcb58c63b1c401c60a474b0f45b94719c30f548c891860f051bc2231575c290a6b420c6bc6e7ed99459d424c652bd5bf9a1d5259505dc35b4bf83de + languageName: node + linkType: hard + +"is-weakset@npm:^2.0.3": + version: 2.0.3 + resolution: "is-weakset@npm:2.0.3" + dependencies: + call-bind: ^1.0.7 + get-intrinsic: ^1.2.4 + checksum: 8b6a20ee9f844613ff8f10962cfee49d981d584525f2357fee0a04dfbcde9fd607ed60cb6dab626dbcc470018ae6392e1ff74c0c1aced2d487271411ad9d85ae + languageName: node + linkType: hard + +"is-windows@npm:^1.0.0, is-windows@npm:^1.0.1": + version: 1.0.2 + resolution: "is-windows@npm:1.0.2" + checksum: 438b7e52656fe3b9b293b180defb4e448088e7023a523ec21a91a80b9ff8cdb3377ddb5b6e60f7c7de4fa8b63ab56e121b6705fe081b3cf1b828b0a380009ad7 + languageName: node + linkType: hard + +"is-wsl@npm:^2.2.0": + version: 2.2.0 + resolution: "is-wsl@npm:2.2.0" + dependencies: + is-docker: ^2.0.0 + checksum: 20849846ae414997d290b75e16868e5261e86ff5047f104027026fd61d8b5a9b0b3ade16239f35e1a067b3c7cc02f70183cb661010ed16f4b6c7c93dad1b19d8 + languageName: node + linkType: hard + +"is-wsl@npm:^3.1.0": + version: 3.1.0 + resolution: "is-wsl@npm:3.1.0" + dependencies: + is-inside-container: ^1.0.0 + checksum: f9734c81f2f9cf9877c5db8356bfe1ff61680f1f4c1011e91278a9c0564b395ae796addb4bf33956871041476ec82c3e5260ed57b22ac91794d4ae70a1d2f0a9 + languageName: node + linkType: hard + +"isarray@npm:^1.0.0, isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab + languageName: node + linkType: hard + +"isarray@npm:^2.0.5": + version: 2.0.5 + resolution: "isarray@npm:2.0.5" + checksum: bd5bbe4104438c4196ba58a54650116007fa0262eccef13a4c55b2e09a5b36b59f1e75b9fcc49883dd9d4953892e6fc007eef9e9155648ceea036e184b0f930a + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e + languageName: node + linkType: hard + +"isomorphic-git@npm:^1.23.0": + version: 1.27.1 + resolution: "isomorphic-git@npm:1.27.1" + dependencies: + async-lock: ^1.4.1 + clean-git-ref: ^2.0.1 + crc-32: ^1.2.0 + diff3: 0.0.3 + ignore: ^5.1.4 + minimisted: ^2.0.0 + pako: ^1.0.10 + pify: ^4.0.1 + readable-stream: ^3.4.0 + sha.js: ^2.4.9 + simple-get: ^4.0.1 + bin: + isogit: cli.cjs + checksum: ba6f3c10b3160dac74185881f1da1c5a9b6cbd32d5f273ebce7291055566e5c58f466f89be9039e9c83ededd86a69e367bc4050262bbfbc6b785eea211a7f923 + languageName: node + linkType: hard + +"isomorphic-ws@npm:5.0.0, isomorphic-ws@npm:^5.0.0": + version: 5.0.0 + resolution: "isomorphic-ws@npm:5.0.0" + peerDependencies: + ws: "*" + checksum: e20eb2aee09ba96247465fda40c6d22c1153394c0144fa34fe6609f341af4c8c564f60ea3ba762335a7a9c306809349f9b863c8beedf2beea09b299834ad5398 + languageName: node + linkType: hard + +"isstream@npm:~0.1.2": + version: 0.1.2 + resolution: "isstream@npm:0.1.2" + checksum: 1eb2fe63a729f7bdd8a559ab552c69055f4f48eb5c2f03724430587c6f450783c8f1cd936c1c952d0a927925180fcc892ebd5b174236cf1065d4bd5bdb37e963 + languageName: node + linkType: hard + +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 2367407a8d13982d8f7a859a35e7f8dd5d8f75aae4bb5484ede3a9ea1b426dc245aff28b976a2af48ee759fdd9be374ce2bd2669b644f31e76c5f46a2e29a831 + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^5.0.4": + version: 5.2.1 + resolution: "istanbul-lib-instrument@npm:5.2.1" + dependencies: + "@babel/core": ^7.12.3 + "@babel/parser": ^7.14.7 + "@istanbuljs/schema": ^0.1.2 + istanbul-lib-coverage: ^3.2.0 + semver: ^6.3.0 + checksum: bf16f1803ba5e51b28bbd49ed955a736488381e09375d830e42ddeb403855b2006f850711d95ad726f2ba3f1ae8e7366de7e51d2b9ac67dc4d80191ef7ddf272 + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^6.0.0": + version: 6.0.3 + resolution: "istanbul-lib-instrument@npm:6.0.3" + dependencies: + "@babel/core": ^7.23.9 + "@babel/parser": ^7.23.9 + "@istanbuljs/schema": ^0.1.3 + istanbul-lib-coverage: ^3.2.0 + semver: ^7.5.4 + checksum: 74104c60c65c4fa0e97cc76f039226c356123893929f067bfad5f86fe839e08f5d680354a68fead3bc9c1e2f3fa6f3f53cded70778e821d911e851d349f3545a + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: ^3.0.0 + make-dir: ^4.0.0 + supports-color: ^7.1.0 + checksum: fd17a1b879e7faf9bb1dc8f80b2a16e9f5b7b8498fe6ed580a618c34df0bfe53d2abd35bf8a0a00e628fb7405462576427c7df20bbe4148d19c14b431c974b21 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^4.0.0": + version: 4.0.1 + resolution: "istanbul-lib-source-maps@npm:4.0.1" + dependencies: + debug: ^4.1.1 + istanbul-lib-coverage: ^3.0.0 + source-map: ^0.6.1 + checksum: 21ad3df45db4b81852b662b8d4161f6446cd250c1ddc70ef96a585e2e85c26ed7cd9c2a396a71533cfb981d1a645508bc9618cae431e55d01a0628e7dec62ef2 + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.3": + version: 3.1.7 + resolution: "istanbul-reports@npm:3.1.7" + dependencies: + html-escaper: ^2.0.0 + istanbul-lib-report: ^3.0.0 + checksum: 2072db6e07bfbb4d0eb30e2700250636182398c1af811aea5032acb219d2080f7586923c09fa194029efd6b92361afb3dcbe1ebcc3ee6651d13340f7c6c4ed95 + languageName: node + linkType: hard + +"iterare@npm:1.2.1": + version: 1.2.1 + resolution: "iterare@npm:1.2.1" + checksum: 70bc80038e3718aa9072bc63b3a0135166d7120bde46bfcaf80a88d11005dcef1b2d69cd353849f87a3f58ba8f546a8c6e6983408236ff01fa50b52339ee5223 + languageName: node + linkType: hard + +"iterator.prototype@npm:^1.1.3": + version: 1.1.3 + resolution: "iterator.prototype@npm:1.1.3" + dependencies: + define-properties: ^1.2.1 + get-intrinsic: ^1.2.1 + has-symbols: ^1.0.3 + reflect.getprototypeof: ^1.0.4 + set-function-name: ^2.0.1 + checksum: 7d2a1f8bcbba7b76f72e956faaf7b25405f4de54430c9d099992e6fb9d571717c3044604e8cdfb8e624cb881337d648030ee8b1541d544af8b338835e3f47ebe + languageName: node + linkType: hard + +"jackspeak@npm:^3.1.2": + version: 3.4.3 + resolution: "jackspeak@npm:3.4.3" + dependencies: + "@isaacs/cliui": ^8.0.2 + "@pkgjs/parseargs": ^0.11.0 + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: be31027fc72e7cc726206b9f560395604b82e0fddb46c4cbf9f97d049bcef607491a5afc0699612eaa4213ca5be8fd3e1e7cd187b3040988b65c9489838a7c00 + languageName: node + linkType: hard + +"javascript-natural-sort@npm:^0.7.1": + version: 0.7.1 + resolution: "javascript-natural-sort@npm:0.7.1" + checksum: 161e2c512cc7884bc055a582c6645d9032cab88497a76123d73cb23bfb03d97a04cf7772ecdb8bd3366fc07192c2f996366f479f725c23ef073fffe03d6a586a + languageName: node + linkType: hard + +"jest-changed-files@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-changed-files@npm:29.7.0" + dependencies: + execa: ^5.0.0 + jest-util: ^29.7.0 + p-limit: ^3.1.0 + checksum: 963e203893c396c5dfc75e00a49426688efea7361b0f0e040035809cecd2d46b3c01c02be2d9e8d38b1138357d2de7719ea5b5be21f66c10f2e9685a5a73bb99 + languageName: node + linkType: hard + +"jest-circus@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-circus@npm:29.7.0" + dependencies: + "@jest/environment": ^29.7.0 + "@jest/expect": ^29.7.0 + "@jest/test-result": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + co: ^4.6.0 + dedent: ^1.0.0 + is-generator-fn: ^2.0.0 + jest-each: ^29.7.0 + jest-matcher-utils: ^29.7.0 + jest-message-util: ^29.7.0 + jest-runtime: ^29.7.0 + jest-snapshot: ^29.7.0 + jest-util: ^29.7.0 + p-limit: ^3.1.0 + pretty-format: ^29.7.0 + pure-rand: ^6.0.0 + slash: ^3.0.0 + stack-utils: ^2.0.3 + checksum: 349437148924a5a109c9b8aad6d393a9591b4dac1918fc97d81b7fc515bc905af9918495055071404af1fab4e48e4b04ac3593477b1d5dcf48c4e71b527c70a7 + languageName: node + linkType: hard + +"jest-cli@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-cli@npm:29.7.0" + dependencies: + "@jest/core": ^29.7.0 + "@jest/test-result": ^29.7.0 + "@jest/types": ^29.6.3 + chalk: ^4.0.0 + create-jest: ^29.7.0 + exit: ^0.1.2 + import-local: ^3.0.2 + jest-config: ^29.7.0 + jest-util: ^29.7.0 + jest-validate: ^29.7.0 + yargs: ^17.3.1 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 664901277a3f5007ea4870632ed6e7889db9da35b2434e7cb488443e6bf5513889b344b7fddf15112135495b9875892b156faeb2d7391ddb9e2a849dcb7b6c36 + languageName: node + linkType: hard + +"jest-config@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-config@npm:29.7.0" + dependencies: + "@babel/core": ^7.11.6 + "@jest/test-sequencer": ^29.7.0 + "@jest/types": ^29.6.3 + babel-jest: ^29.7.0 + chalk: ^4.0.0 + ci-info: ^3.2.0 + deepmerge: ^4.2.2 + glob: ^7.1.3 + graceful-fs: ^4.2.9 + jest-circus: ^29.7.0 + jest-environment-node: ^29.7.0 + jest-get-type: ^29.6.3 + jest-regex-util: ^29.6.3 + jest-resolve: ^29.7.0 + jest-runner: ^29.7.0 + jest-util: ^29.7.0 + jest-validate: ^29.7.0 + micromatch: ^4.0.4 + parse-json: ^5.2.0 + pretty-format: ^29.7.0 + slash: ^3.0.0 + strip-json-comments: ^3.1.1 + peerDependencies: + "@types/node": "*" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + ts-node: + optional: true + checksum: 4cabf8f894c180cac80b7df1038912a3fc88f96f2622de33832f4b3314f83e22b08fb751da570c0ab2b7988f21604bdabade95e3c0c041068ac578c085cf7dff + languageName: node + linkType: hard + +"jest-css-modules@npm:^2.1.0": + version: 2.1.0 + resolution: "jest-css-modules@npm:2.1.0" + dependencies: + identity-obj-proxy: 3.0.0 + checksum: ddf01a327379f0186fc506b0c2a6cecad59acf3a7c947113f75530d1ea87e4f09aa98c9894283c0ead29688ef9fbc3c91ce1b158756034872fa097e491ee9f8c + languageName: node + linkType: hard + +"jest-diff@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-diff@npm:29.7.0" + dependencies: + chalk: ^4.0.0 + diff-sequences: ^29.6.3 + jest-get-type: ^29.6.3 + pretty-format: ^29.7.0 + checksum: 08e24a9dd43bfba1ef07a6374e5af138f53137b79ec3d5cc71a2303515335898888fa5409959172e1e05de966c9e714368d15e8994b0af7441f0721ee8e1bb77 + languageName: node + linkType: hard + +"jest-docblock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-docblock@npm:29.7.0" + dependencies: + detect-newline: ^3.0.0 + checksum: 66390c3e9451f8d96c5da62f577a1dad701180cfa9b071c5025acab2f94d7a3efc2515cfa1654ebe707213241541ce9c5530232cdc8017c91ed64eea1bd3b192 + languageName: node + linkType: hard + +"jest-each@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-each@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + chalk: ^4.0.0 + jest-get-type: ^29.6.3 + jest-util: ^29.7.0 + pretty-format: ^29.7.0 + checksum: e88f99f0184000fc8813f2a0aa79e29deeb63700a3b9b7928b8a418d7d93cd24933608591dbbdea732b473eb2021c72991b5cc51a17966842841c6e28e6f691c + languageName: node + linkType: hard + +"jest-environment-jsdom@npm:^29.0.2": + version: 29.7.0 + resolution: "jest-environment-jsdom@npm:29.7.0" + dependencies: + "@jest/environment": ^29.7.0 + "@jest/fake-timers": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/jsdom": ^20.0.0 + "@types/node": "*" + jest-mock: ^29.7.0 + jest-util: ^29.7.0 + jsdom: ^20.0.0 + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: 559aac134c196fccc1dfc794d8fc87377e9f78e894bb13012b0831d88dec0abd7ece99abec69da564b8073803be4f04a9eb4f4d1bb80e29eec0cb252c254deb8 + languageName: node + linkType: hard + +"jest-environment-node@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-environment-node@npm:29.7.0" + dependencies: + "@jest/environment": ^29.7.0 + "@jest/fake-timers": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + jest-mock: ^29.7.0 + jest-util: ^29.7.0 + checksum: 501a9966292cbe0ca3f40057a37587cb6def25e1e0c5e39ac6c650fe78d3c70a2428304341d084ac0cced5041483acef41c477abac47e9a290d5545fd2f15646 + languageName: node + linkType: hard + +"jest-get-type@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-get-type@npm:29.6.3" + checksum: 88ac9102d4679d768accae29f1e75f592b760b44277df288ad76ce5bf038c3f5ce3719dea8aa0f035dac30e9eb034b848ce716b9183ad7cc222d029f03e92205 + languageName: node + linkType: hard + +"jest-haste-map@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-haste-map@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + "@types/graceful-fs": ^4.1.3 + "@types/node": "*" + anymatch: ^3.0.3 + fb-watchman: ^2.0.0 + fsevents: ^2.3.2 + graceful-fs: ^4.2.9 + jest-regex-util: ^29.6.3 + jest-util: ^29.7.0 + jest-worker: ^29.7.0 + micromatch: ^4.0.4 + walker: ^1.0.8 + dependenciesMeta: + fsevents: + optional: true + checksum: c2c8f2d3e792a963940fbdfa563ce14ef9e14d4d86da645b96d3cd346b8d35c5ce0b992ee08593939b5f718cf0a1f5a90011a056548a1dbf58397d4356786f01 + languageName: node + linkType: hard + +"jest-leak-detector@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-leak-detector@npm:29.7.0" + dependencies: + jest-get-type: ^29.6.3 + pretty-format: ^29.7.0 + checksum: e3950e3ddd71e1d0c22924c51a300a1c2db6cf69ec1e51f95ccf424bcc070f78664813bef7aed4b16b96dfbdeea53fe358f8aeaaea84346ae15c3735758f1605 + languageName: node + linkType: hard + +"jest-matcher-utils@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-matcher-utils@npm:29.7.0" + dependencies: + chalk: ^4.0.0 + jest-diff: ^29.7.0 + jest-get-type: ^29.6.3 + pretty-format: ^29.7.0 + checksum: d7259e5f995d915e8a37a8fd494cb7d6af24cd2a287b200f831717ba0d015190375f9f5dc35393b8ba2aae9b2ebd60984635269c7f8cff7d85b077543b7744cd + languageName: node + linkType: hard + +"jest-message-util@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-message-util@npm:29.7.0" + dependencies: + "@babel/code-frame": ^7.12.13 + "@jest/types": ^29.6.3 + "@types/stack-utils": ^2.0.0 + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + micromatch: ^4.0.4 + pretty-format: ^29.7.0 + slash: ^3.0.0 + stack-utils: ^2.0.3 + checksum: a9d025b1c6726a2ff17d54cc694de088b0489456c69106be6b615db7a51b7beb66788bea7a59991a019d924fbf20f67d085a445aedb9a4d6760363f4d7d09930 + languageName: node + linkType: hard + +"jest-mock@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-mock@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + "@types/node": "*" + jest-util: ^29.7.0 + checksum: 81ba9b68689a60be1482212878973700347cb72833c5e5af09895882b9eb5c4e02843a1bbdf23f94c52d42708bab53a30c45a3482952c9eec173d1eaac5b86c5 + languageName: node + linkType: hard + +"jest-pnp-resolver@npm:^1.2.2": + version: 1.2.3 + resolution: "jest-pnp-resolver@npm:1.2.3" + peerDependencies: + jest-resolve: "*" + peerDependenciesMeta: + jest-resolve: + optional: true + checksum: db1a8ab2cb97ca19c01b1cfa9a9c8c69a143fde833c14df1fab0766f411b1148ff0df878adea09007ac6a2085ec116ba9a996a6ad104b1e58c20adbf88eed9b2 + languageName: node + linkType: hard + +"jest-regex-util@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-regex-util@npm:29.6.3" + checksum: 0518beeb9bf1228261695e54f0feaad3606df26a19764bc19541e0fc6e2a3737191904607fb72f3f2ce85d9c16b28df79b7b1ec9443aa08c3ef0e9efda6f8f2a + languageName: node + linkType: hard + +"jest-resolve-dependencies@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve-dependencies@npm:29.7.0" + dependencies: + jest-regex-util: ^29.6.3 + jest-snapshot: ^29.7.0 + checksum: aeb75d8150aaae60ca2bb345a0d198f23496494677cd6aefa26fc005faf354061f073982175daaf32b4b9d86b26ca928586344516e3e6969aa614cb13b883984 + languageName: node + linkType: hard + +"jest-resolve@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-resolve@npm:29.7.0" + dependencies: + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.7.0 + jest-pnp-resolver: ^1.2.2 + jest-util: ^29.7.0 + jest-validate: ^29.7.0 + resolve: ^1.20.0 + resolve.exports: ^2.0.0 + slash: ^3.0.0 + checksum: 0ca218e10731aa17920526ec39deaec59ab9b966237905ffc4545444481112cd422f01581230eceb7e82d86f44a543d520a71391ec66e1b4ef1a578bd5c73487 + languageName: node + linkType: hard + +"jest-runner@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runner@npm:29.7.0" + dependencies: + "@jest/console": ^29.7.0 + "@jest/environment": ^29.7.0 + "@jest/test-result": ^29.7.0 + "@jest/transform": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + emittery: ^0.13.1 + graceful-fs: ^4.2.9 + jest-docblock: ^29.7.0 + jest-environment-node: ^29.7.0 + jest-haste-map: ^29.7.0 + jest-leak-detector: ^29.7.0 + jest-message-util: ^29.7.0 + jest-resolve: ^29.7.0 + jest-runtime: ^29.7.0 + jest-util: ^29.7.0 + jest-watcher: ^29.7.0 + jest-worker: ^29.7.0 + p-limit: ^3.1.0 + source-map-support: 0.5.13 + checksum: f0405778ea64812bf9b5c50b598850d94ccf95d7ba21f090c64827b41decd680ee19fcbb494007cdd7f5d0d8906bfc9eceddd8fa583e753e736ecd462d4682fb + languageName: node + linkType: hard + +"jest-runtime@npm:^29.0.2, jest-runtime@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-runtime@npm:29.7.0" + dependencies: + "@jest/environment": ^29.7.0 + "@jest/fake-timers": ^29.7.0 + "@jest/globals": ^29.7.0 + "@jest/source-map": ^29.6.3 + "@jest/test-result": ^29.7.0 + "@jest/transform": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + cjs-module-lexer: ^1.0.0 + collect-v8-coverage: ^1.0.0 + glob: ^7.1.3 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.7.0 + jest-message-util: ^29.7.0 + jest-mock: ^29.7.0 + jest-regex-util: ^29.6.3 + jest-resolve: ^29.7.0 + jest-snapshot: ^29.7.0 + jest-util: ^29.7.0 + slash: ^3.0.0 + strip-bom: ^4.0.0 + checksum: d19f113d013e80691e07047f68e1e3448ef024ff2c6b586ce4f90cd7d4c62a2cd1d460110491019719f3c59bfebe16f0e201ed005ef9f80e2cf798c374eed54e + languageName: node + linkType: hard + +"jest-snapshot@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-snapshot@npm:29.7.0" + dependencies: + "@babel/core": ^7.11.6 + "@babel/generator": ^7.7.2 + "@babel/plugin-syntax-jsx": ^7.7.2 + "@babel/plugin-syntax-typescript": ^7.7.2 + "@babel/types": ^7.3.3 + "@jest/expect-utils": ^29.7.0 + "@jest/transform": ^29.7.0 + "@jest/types": ^29.6.3 + babel-preset-current-node-syntax: ^1.0.0 + chalk: ^4.0.0 + expect: ^29.7.0 + graceful-fs: ^4.2.9 + jest-diff: ^29.7.0 + jest-get-type: ^29.6.3 + jest-matcher-utils: ^29.7.0 + jest-message-util: ^29.7.0 + jest-util: ^29.7.0 + natural-compare: ^1.4.0 + pretty-format: ^29.7.0 + semver: ^7.5.3 + checksum: 86821c3ad0b6899521ce75ee1ae7b01b17e6dfeff9166f2cf17f012e0c5d8c798f30f9e4f8f7f5bed01ea7b55a6bc159f5eda778311162cbfa48785447c237ad + languageName: node + linkType: hard + +"jest-util@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-util@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + ci-info: ^3.2.0 + graceful-fs: ^4.2.9 + picomatch: ^2.2.3 + checksum: 042ab4980f4ccd4d50226e01e5c7376a8556b472442ca6091a8f102488c0f22e6e8b89ea874111d2328a2080083bf3225c86f3788c52af0bd0345a00eb57a3ca + languageName: node + linkType: hard + +"jest-validate@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-validate@npm:29.7.0" + dependencies: + "@jest/types": ^29.6.3 + camelcase: ^6.2.0 + chalk: ^4.0.0 + jest-get-type: ^29.6.3 + leven: ^3.1.0 + pretty-format: ^29.7.0 + checksum: 191fcdc980f8a0de4dbdd879fa276435d00eb157a48683af7b3b1b98b0f7d9de7ffe12689b617779097ff1ed77601b9f7126b0871bba4f776e222c40f62e9dae + languageName: node + linkType: hard + +"jest-watcher@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-watcher@npm:29.7.0" + dependencies: + "@jest/test-result": ^29.7.0 + "@jest/types": ^29.6.3 + "@types/node": "*" + ansi-escapes: ^4.2.1 + chalk: ^4.0.0 + emittery: ^0.13.1 + jest-util: ^29.7.0 + string-length: ^4.0.1 + checksum: 67e6e7fe695416deff96b93a14a561a6db69389a0667e9489f24485bb85e5b54e12f3b2ba511ec0b777eca1e727235b073e3ebcdd473d68888650489f88df92f + languageName: node + linkType: hard + +"jest-worker@npm:^27.4.5": + version: 27.5.1 + resolution: "jest-worker@npm:27.5.1" + dependencies: + "@types/node": "*" + merge-stream: ^2.0.0 + supports-color: ^8.0.0 + checksum: 98cd68b696781caed61c983a3ee30bf880b5bd021c01d98f47b143d4362b85d0737f8523761e2713d45e18b4f9a2b98af1eaee77afade4111bb65c77d6f7c980 + languageName: node + linkType: hard + +"jest-worker@npm:^29.7.0": + version: 29.7.0 + resolution: "jest-worker@npm:29.7.0" + dependencies: + "@types/node": "*" + jest-util: ^29.7.0 + merge-stream: ^2.0.0 + supports-color: ^8.0.0 + checksum: 30fff60af49675273644d408b650fc2eb4b5dcafc5a0a455f238322a8f9d8a98d847baca9d51ff197b6747f54c7901daa2287799230b856a0f48287d131f8c13 + languageName: node + linkType: hard + +"jest@npm:^29.7.0": + version: 29.7.0 + resolution: "jest@npm:29.7.0" + dependencies: + "@jest/core": ^29.7.0 + "@jest/types": ^29.6.3 + import-local: ^3.0.2 + jest-cli: ^29.7.0 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 17ca8d67504a7dbb1998cf3c3077ec9031ba3eb512da8d71cb91bcabb2b8995c4e4b292b740cb9bf1cbff5ce3e110b3f7c777b0cefb6f41ab05445f248d0ee0b + languageName: node + linkType: hard + +"jiti@npm:^2.3.3": + version: 2.4.0 + resolution: "jiti@npm:2.4.0" + bin: + jiti: lib/jiti-cli.mjs + checksum: b7d8c441214e48f6c1be2952a83f40e2b1eb6e94fe81b1fd89370d11a7e322c61eb3fbd9a8d47029e14338414091ebbb575e1a92c645ab30fea6240c5c4957c7 + languageName: node + linkType: hard + +"jju@npm:~1.4.0": + version: 1.4.0 + resolution: "jju@npm:1.4.0" + checksum: 3790481bd2b7827dd6336e6e3dc2dcc6d425679ba7ebde7b679f61dceb4457ea0cda330972494de608571f4973c6dfb5f70fab6f3c5037dbab19ac449a60424f + languageName: node + linkType: hard + +"joi@npm:^17.13.3": + version: 17.13.3 + resolution: "joi@npm:17.13.3" + dependencies: + "@hapi/hoek": ^9.3.0 + "@hapi/topo": ^5.1.0 + "@sideway/address": ^4.1.5 + "@sideway/formula": ^3.0.1 + "@sideway/pinpoint": ^2.0.0 + checksum: 66ed454fee3d8e8da1ce21657fd2c7d565d98f3e539d2c5c028767e5f38cbd6297ce54df8312d1d094e62eb38f9452ebb43da4ce87321df66cf5e3f128cbc400 + languageName: node + linkType: hard + +"jose@npm:^4.15.9": + version: 4.15.9 + resolution: "jose@npm:4.15.9" + checksum: 41abe1c99baa3cf8a78ebbf93da8f8e50e417b7a26754c4afa21865d87527b8ac2baf66de2c5f6accc3f7d7158658dae7364043677236ea1d07895b040097f15 + languageName: node + linkType: hard + +"jose@npm:^5.0.0": + version: 5.9.4 + resolution: "jose@npm:5.9.4" + checksum: d1c3dd0fb6dbfb6c16e0904f4930b9df1abde784d137b0987777b7722798b39b4ab15e6aff7b768195018f1656618a368d04b758c318436ad482a2a7ae92db82 + languageName: node + linkType: hard + +"js-cookie@npm:^2.2.1": + version: 2.2.1 + resolution: "js-cookie@npm:2.2.1" + checksum: 9b1fb980a1c5e624fd4b28ea4867bb30c71e04c4484bb3a42766344c533faa684de9498e443425479ec68609e96e27b60614bfe354877c449c631529b6d932f2 + languageName: node + linkType: hard + +"js-levenshtein@npm:^1.1.6": + version: 1.1.6 + resolution: "js-levenshtein@npm:1.1.6" + checksum: 409f052a7f1141be4058d97da7860e08efd97fc588b7a4c5cfa0548bc04f6d576644dae65ab630266dff685d56fb90d494e03d4d79cb484c287746b4f1bf0694 + languageName: node + linkType: hard + +"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": + version: 4.0.0 + resolution: "js-tokens@npm:4.0.0" + checksum: 8a95213a5a77deb6cbe94d86340e8d9ace2b93bc367790b260101d2f36a2eaf4e4e22d9fa9cf459b38af3a32fb4190e638024cf82ec95ef708680e405ea7cc78 + languageName: node + linkType: hard + +"js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.6.1, js-yaml@npm:^3.8.3": + version: 3.14.1 + resolution: "js-yaml@npm:3.14.1" + dependencies: + argparse: ^1.0.7 + esprima: ^4.0.0 + bin: + js-yaml: bin/js-yaml.js + checksum: bef146085f472d44dee30ec34e5cf36bf89164f5d585435a3d3da89e52622dff0b188a580e4ad091c3341889e14cb88cac6e4deb16dc5b1e9623bb0601fc255c + languageName: node + linkType: hard + +"js-yaml@npm:^4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" + dependencies: + argparse: ^2.0.1 + bin: + js-yaml: bin/js-yaml.js + checksum: c7830dfd456c3ef2c6e355cc5a92e6700ceafa1d14bba54497b34a99f0376cecbb3e9ac14d3e5849b426d5a5140709a66237a8c991c675431271c4ce5504151a + languageName: node + linkType: hard + +"js-yaml@npm:~3.13.1": + version: 3.13.1 + resolution: "js-yaml@npm:3.13.1" + dependencies: + argparse: ^1.0.7 + esprima: ^4.0.0 + bin: + js-yaml: bin/js-yaml.js + checksum: 7511b764abb66d8aa963379f7d2a404f078457d106552d05a7b556d204f7932384e8477513c124749fa2de52eb328961834562bd09924902c6432e40daa408bc + languageName: node + linkType: hard + +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 944f924f2bd67ad533b3850eee47603eed0f6ae425fd1ee8c760f477e8c34a05f144c1bd4f5a5dd1963141dc79a2c55f89ccc5ab77d039e7077f3ad196b64965 + languageName: node + linkType: hard + +"jsbn@npm:~0.1.0": + version: 0.1.1 + resolution: "jsbn@npm:0.1.1" + checksum: e5ff29c1b8d965017ef3f9c219dacd6e40ad355c664e277d31246c90545a02e6047018c16c60a00f36d561b3647215c41894f5d869ada6908a2e0ce4200c88f2 + languageName: node + linkType: hard + +"jsdom@npm:^20.0.0": + version: 20.0.3 + resolution: "jsdom@npm:20.0.3" + dependencies: + abab: ^2.0.6 + acorn: ^8.8.1 + acorn-globals: ^7.0.0 + cssom: ^0.5.0 + cssstyle: ^2.3.0 + data-urls: ^3.0.2 + decimal.js: ^10.4.2 + domexception: ^4.0.0 + escodegen: ^2.0.0 + form-data: ^4.0.0 + html-encoding-sniffer: ^3.0.0 + http-proxy-agent: ^5.0.0 + https-proxy-agent: ^5.0.1 + is-potential-custom-element-name: ^1.0.1 + nwsapi: ^2.2.2 + parse5: ^7.1.1 + saxes: ^6.0.0 + symbol-tree: ^3.2.4 + tough-cookie: ^4.1.2 + w3c-xmlserializer: ^4.0.0 + webidl-conversions: ^7.0.0 + whatwg-encoding: ^2.0.0 + whatwg-mimetype: ^3.0.0 + whatwg-url: ^11.0.0 + ws: ^8.11.0 + xml-name-validator: ^4.0.0 + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: 6e2ae21db397133a061b270c26d2dbc0b9051733ea3b896a7ece78d79f475ff0974f766a413c1198a79c793159119169f2335ddb23150348fbfdcfa6f3105536 + languageName: node + linkType: hard + +"jsep@npm:^0.3.0": + version: 0.3.5 + resolution: "jsep@npm:0.3.5" + checksum: fb89b4ca9d85d3f4ec9edb079c042ffad1c548e00297d7c08e90194e7c8688e09e52a5781af2e028d51fc86fd6a9089a5e5fc0cd2a6c778e4e4531a1a3ff7015 + languageName: node + linkType: hard + +"jsep@npm:^1.1.2, jsep@npm:^1.2.0, jsep@npm:^1.3.9": + version: 1.3.9 + resolution: "jsep@npm:1.3.9" + checksum: d1f3e2cc00209f67a989b73c2a89d2ccbea908d950ec959e2448c6449b134c6367b47eef4e1292767cb490f0b5b72e7309080b93ee4c7398684df2514dbd33a3 + languageName: node + linkType: hard + +"jsesc@npm:^3.0.2, jsesc@npm:~3.0.2": + version: 3.0.2 + resolution: "jsesc@npm:3.0.2" + bin: + jsesc: bin/jsesc + checksum: a36d3ca40574a974d9c2063bf68c2b6141c20da8f2a36bd3279fc802563f35f0527a6c828801295bdfb2803952cf2cf387786c2c90ed564f88d5782475abfe3c + languageName: node + linkType: hard + +"json-bigint@npm:^1.0.0": + version: 1.0.0 + resolution: "json-bigint@npm:1.0.0" + dependencies: + bignumber.js: ^9.0.0 + checksum: c67bb93ccb3c291e60eb4b62931403e378906aab113ec1c2a8dd0f9a7f065ad6fd9713d627b732abefae2e244ac9ce1721c7a3142b2979532f12b258634ce6f6 + languageName: node + linkType: hard + +"json-buffer@npm:3.0.1, json-buffer@npm:^3.0.1": + version: 3.0.1 + resolution: "json-buffer@npm:3.0.1" + checksum: 9026b03edc2847eefa2e37646c579300a1f3a4586cfb62bf857832b60c852042d0d6ae55d1afb8926163fa54c2b01d83ae24705f34990348bdac6273a29d4581 + languageName: node + linkType: hard + +"json-parse-even-better-errors@npm:^2.3.0, json-parse-even-better-errors@npm:^2.3.1": + version: 2.3.1 + resolution: "json-parse-even-better-errors@npm:2.3.1" + checksum: 798ed4cf3354a2d9ccd78e86d2169515a0097a5c133337807cdf7f1fc32e1391d207ccfc276518cc1d7d8d4db93288b8a50ba4293d212ad1336e52a8ec0a941f + languageName: node + linkType: hard + +"json-schema-compare@npm:^0.2.2": + version: 0.2.2 + resolution: "json-schema-compare@npm:0.2.2" + dependencies: + lodash: ^4.17.4 + checksum: dd6f2173857c8e3b77d6ebdfa05bd505bba5b08709ab46b532722f5d1c33b5fee1fc8f3c97d0c0d011db25f9f3b0baf7ab783bb5f55c32abd9f1201760e43c2c + languageName: node + linkType: hard + +"json-schema-merge-allof@npm:^0.8.1": + version: 0.8.1 + resolution: "json-schema-merge-allof@npm:0.8.1" + dependencies: + compute-lcm: ^1.1.2 + json-schema-compare: ^0.2.2 + lodash: ^4.17.20 + checksum: 82700f6ac77351959138d6b153d77375a8c29cf48d907241b85c8292dd77aabd8cb816400f2b0d17062c4ccc8893832ec4f664ab9c814927ef502e7a595ea873 + languageName: node + linkType: hard + +"json-schema-traverse@npm:^0.4.1": + version: 0.4.1 + resolution: "json-schema-traverse@npm:0.4.1" + checksum: 7486074d3ba247769fda17d5181b345c9fb7d12e0da98b22d1d71a5db9698d8b4bd900a3ec1a4ffdd60846fc2556274a5c894d0c48795f14cb03aeae7b55260b + languageName: node + linkType: hard + +"json-schema-traverse@npm:^1.0.0": + version: 1.0.0 + resolution: "json-schema-traverse@npm:1.0.0" + checksum: 02f2f466cdb0362558b2f1fd5e15cce82ef55d60cd7f8fa828cf35ba74330f8d767fcae5c5c2adb7851fa811766c694b9405810879bc4e1ddd78a7c0e03658ad + languageName: node + linkType: hard + +"json-schema@npm:0.4.0, json-schema@npm:^0.4.0": + version: 0.4.0 + resolution: "json-schema@npm:0.4.0" + checksum: 66389434c3469e698da0df2e7ac5a3281bcff75e797a5c127db7c5b56270e01ae13d9afa3c03344f76e32e81678337a8c912bdbb75101c62e487dc3778461d72 + languageName: node + linkType: hard + +"json-stable-stringify-without-jsonify@npm:^1.0.1": + version: 1.0.1 + resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" + checksum: cff44156ddce9c67c44386ad5cddf91925fe06b1d217f2da9c4910d01f358c6e3989c4d5a02683c7a5667f9727ff05831f7aa8ae66c8ff691c556f0884d49215 + languageName: node + linkType: hard + +"json-stable-stringify@npm:^1.0.1": + version: 1.1.1 + resolution: "json-stable-stringify@npm:1.1.1" + dependencies: + call-bind: ^1.0.5 + isarray: ^2.0.5 + jsonify: ^0.0.1 + object-keys: ^1.1.1 + checksum: e1ba06600fd278767eeff53f28e408e29c867e79abf564e7aadc3ce8f31f667258f8db278ef28831e45884dd687388fa1910f46e599fc19fb94c9afbbe3a4de8 + languageName: node + linkType: hard + +"json-stringify-safe@npm:^5.0.1, json-stringify-safe@npm:~5.0.1": + version: 5.0.1 + resolution: "json-stringify-safe@npm:5.0.1" + checksum: 48ec0adad5280b8a96bb93f4563aa1667fd7a36334f79149abd42446d0989f2ddc58274b479f4819f1f00617957e6344c886c55d05a4e15ebb4ab931e4a6a8ee + languageName: node + linkType: hard + +"json5@npm:^1.0.1, json5@npm:^1.0.2": + version: 1.0.2 + resolution: "json5@npm:1.0.2" + dependencies: + minimist: ^1.2.0 + bin: + json5: lib/cli.js + checksum: 866458a8c58a95a49bef3adba929c625e82532bcff1fe93f01d29cb02cac7c3fe1f4b79951b7792c2da9de0b32871a8401a6e3c5b36778ad852bf5b8a61165d7 + languageName: node + linkType: hard + +"json5@npm:^2.1.2, json5@npm:^2.2.3": + version: 2.2.3 + resolution: "json5@npm:2.2.3" + bin: + json5: lib/cli.js + checksum: 2a7436a93393830bce797d4626275152e37e877b265e94ca69c99e3d20c2b9dab021279146a39cdb700e71b2dd32a4cebd1514cd57cee102b1af906ce5040349 + languageName: node + linkType: hard + +"jsonc-parser@npm:^3.2.0": + version: 3.3.1 + resolution: "jsonc-parser@npm:3.3.1" + checksum: 81ef19d98d9c6bd6e4a37a95e2753c51c21705cbeffd895e177f4b542cca9cda5fda12fb942a71a2e824a9132cf119dc2e642e9286386055e1365b5478f49a47 + languageName: node + linkType: hard + +"jsonc-parser@npm:~2.2.1": + version: 2.2.1 + resolution: "jsonc-parser@npm:2.2.1" + checksum: c113878b5edd4232ba0742c7e0ddefb22a2a8ef1aafa1674c0eb4c5df0be11ed02bc8288f52ebe44b1696de336e1bc06e7bbc1458d0f910540d72b57ee7c8084 + languageName: node + linkType: hard + +"jsonfile@npm:^4.0.0": + version: 4.0.0 + resolution: "jsonfile@npm:4.0.0" + dependencies: + graceful-fs: ^4.1.6 + dependenciesMeta: + graceful-fs: + optional: true + checksum: 6447d6224f0d31623eef9b51185af03ac328a7553efcee30fa423d98a9e276ca08db87d71e17f2310b0263fd3ffa6c2a90a6308367f661dc21580f9469897c9e + languageName: node + linkType: hard + +"jsonfile@npm:^6.0.1": + version: 6.1.0 + resolution: "jsonfile@npm:6.1.0" + dependencies: + graceful-fs: ^4.1.6 + universalify: ^2.0.0 + dependenciesMeta: + graceful-fs: + optional: true + checksum: 7af3b8e1ac8fe7f1eccc6263c6ca14e1966fcbc74b618d3c78a0a2075579487547b94f72b7a1114e844a1e15bb00d440e5d1720bfc4612d790a6f285d5ea8354 + languageName: node + linkType: hard + +"jsonify@npm:^0.0.1": + version: 0.0.1 + resolution: "jsonify@npm:0.0.1" + checksum: 027287e1c0294fce15f18c0ff990cfc2318e7f01fb76515f784d5cd0784abfec6fc5c2355c3a2f2cb0ad7f4aa2f5b74ebbfe4e80476c35b2d13cabdb572e1134 + languageName: node + linkType: hard + +"jsonpath-plus@npm:7.1.0": + version: 7.1.0 + resolution: "jsonpath-plus@npm:7.1.0" + checksum: a4005dc860c6b7e339229842537ceb6eb839d87a3447f989792b9c64f2564bbbd40663515f9481fb5a1b6cb0f988afba5b0b150e0285c463b794a45ed1aaf555 + languageName: node + linkType: hard + +"jsonpath-plus@npm:^10.0.0": + version: 10.1.0 + resolution: "jsonpath-plus@npm:10.1.0" + dependencies: + "@jsep-plugin/assignment": ^1.2.1 + "@jsep-plugin/regex": ^1.0.3 + jsep: ^1.3.9 + bin: + jsonpath: bin/jsonpath-cli.js + jsonpath-plus: bin/jsonpath-cli.js + checksum: 9369a9466e3bb3eca064debe3fd87d9cf791b9c8a23be7c00902497cd8f9dd632290d471e3c7bb09da0aa92626204f9c9a27c429940802bc5046cc8c87f3c596 + languageName: node + linkType: hard + +"jsonpath-plus@npm:^6.0.1": + version: 6.0.1 + resolution: "jsonpath-plus@npm:6.0.1" + checksum: bddec34b742249c5b38077dfcd8eb479fab4e077943253017326503ce4f527ef66938288c728712fd923907493d6eaba69a43015dc3dd9fdf48d89028ae7f466 + languageName: node + linkType: hard + +"jsonpath-plus@npm:^7.2.0": + version: 7.2.0 + resolution: "jsonpath-plus@npm:7.2.0" + checksum: 05f447339d29be861e307d6e812aec1b9b88a3ba6bba286966a4e8bed3e752bee3d715eabfc21dce968be85ccb48bf79d2c1af78da7b9b74cd1b446d4d5d02f5 + languageName: node + linkType: hard + +"jsonpath@npm:^1.1.1": + version: 1.1.1 + resolution: "jsonpath@npm:1.1.1" + dependencies: + esprima: 1.2.2 + static-eval: 2.0.2 + underscore: 1.12.1 + checksum: 5480d8e9e424fe2ed4ade6860b6e2cefddb21adb3a99abe0254cd9428e8ef9b0c9fb5729d6a5a514e90df50d645ccea9f3be48d627570e6222dd5dadc28eba7b + languageName: node + linkType: hard + +"jsonpointer@npm:^5.0.0, jsonpointer@npm:^5.0.1": + version: 5.0.1 + resolution: "jsonpointer@npm:5.0.1" + checksum: 0b40f712900ad0c846681ea2db23b6684b9d5eedf55807b4708c656f5894b63507d0e28ae10aa1bddbea551241035afe62b6df0800fc94c2e2806a7f3adecd7c + languageName: node + linkType: hard + +"jsonwebtoken@npm:^9.0.0, jsonwebtoken@npm:^9.0.2": + version: 9.0.2 + resolution: "jsonwebtoken@npm:9.0.2" + dependencies: + jws: ^3.2.2 + lodash.includes: ^4.3.0 + lodash.isboolean: ^3.0.3 + lodash.isinteger: ^4.0.4 + lodash.isnumber: ^3.0.3 + lodash.isplainobject: ^4.0.6 + lodash.isstring: ^4.0.1 + lodash.once: ^4.0.0 + ms: ^2.1.1 + semver: ^7.5.4 + checksum: fc739a6a8b33f1974f9772dca7f8493ca8df4cc31c5a09dcfdb7cff77447dcf22f4236fb2774ef3fe50df0abeb8e1c6f4c41eba82f500a804ab101e2fbc9d61a + languageName: node + linkType: hard + +"jsprim@npm:^1.2.2": + version: 1.4.2 + resolution: "jsprim@npm:1.4.2" + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + checksum: 2ad1b9fdcccae8b3d580fa6ced25de930eaa1ad154db21bbf8478a4d30bbbec7925b5f5ff29b933fba9412b16a17bd484a8da4fdb3663b5e27af95dd693bab2a + languageName: node + linkType: hard + +"jss-plugin-camel-case@npm:^10.5.1": + version: 10.10.0 + resolution: "jss-plugin-camel-case@npm:10.10.0" + dependencies: + "@babel/runtime": ^7.3.1 + hyphenate-style-name: ^1.0.3 + jss: 10.10.0 + checksum: 693485b86f7a0e0bd0c16b8ddd057ca02a993fc088558c96501f9131e7e6261cc9f4b08047879a68441c688c40dceeb5219b1f15ade9043935aade4f37f5ca85 + languageName: node + linkType: hard + +"jss-plugin-default-unit@npm:^10.5.1": + version: 10.10.0 + resolution: "jss-plugin-default-unit@npm:10.10.0" + dependencies: + "@babel/runtime": ^7.3.1 + jss: 10.10.0 + checksum: 6e56213830753ad80bca3824973a667106defaef698d5996d45d03a0e2a3e035b33cd257aa8015040c41bd6669e7598dce72c36099d7ae69db758a7b2ca453fa + languageName: node + linkType: hard + +"jss-plugin-global@npm:^10.5.1": + version: 10.10.0 + resolution: "jss-plugin-global@npm:10.10.0" + dependencies: + "@babel/runtime": ^7.3.1 + jss: 10.10.0 + checksum: f3af4f40358e96cf89e0c7c84b6e441dc9b4d543cd6109fdf9314a9818fd780d252035f46cc526c3d3fb4393bc29effc6993cc22e04f4e67ec3c889ab760d580 + languageName: node + linkType: hard + +"jss-plugin-nested@npm:^10.5.1": + version: 10.10.0 + resolution: "jss-plugin-nested@npm:10.10.0" + dependencies: + "@babel/runtime": ^7.3.1 + jss: 10.10.0 + tiny-warning: ^1.0.2 + checksum: 190094375972b68eb8f683387c74e97dc8347e7cc4f2fbfd40b3baf077dfde83d70e57be56744690d22537c0390e0a398714d86736df820c64e498df95f937de + languageName: node + linkType: hard + +"jss-plugin-props-sort@npm:^10.5.1": + version: 10.10.0 + resolution: "jss-plugin-props-sort@npm:10.10.0" + dependencies: + "@babel/runtime": ^7.3.1 + jss: 10.10.0 + checksum: 274483444b6733bd58d229ebdcdb32b3c24172bc83cb2f6f8364926de19acd872758bcf06c7b3af11cf75504a67a7d67abba62b25081d144585a56b4df9512ba + languageName: node + linkType: hard + +"jss-plugin-rule-value-function@npm:^10.5.1": + version: 10.10.0 + resolution: "jss-plugin-rule-value-function@npm:10.10.0" + dependencies: + "@babel/runtime": ^7.3.1 + jss: 10.10.0 + tiny-warning: ^1.0.2 + checksum: 009c9593b9be8b9f1030b797e58e3c233d90e034e5c68b0cabd25bffc7da965c69dc1ccb1bb6a542d72bb824df89036b2264fe564e8538320ef99febaf2882ee + languageName: node + linkType: hard + +"jss-plugin-vendor-prefixer@npm:^10.5.1": + version: 10.10.0 + resolution: "jss-plugin-vendor-prefixer@npm:10.10.0" + dependencies: + "@babel/runtime": ^7.3.1 + css-vendor: ^2.0.8 + jss: 10.10.0 + checksum: 879b7233f9b0b571074dc2b88d97a05dbb949012ba2405f1481bbedd521167dc835133632adb3f2d8ffceddd337c8c13e3e8b1931590516c0664039598752dff + languageName: node + linkType: hard + +"jss@npm:10.10.0, jss@npm:^10.5.1": + version: 10.10.0 + resolution: "jss@npm:10.10.0" + dependencies: + "@babel/runtime": ^7.3.1 + csstype: ^3.0.2 + is-in-browser: ^1.1.3 + tiny-warning: ^1.0.2 + checksum: ecf71971df42729668c283e432e841349b7fdbe52e520f7704991cf4a738fd2451ec0feeb25c12cdc5addf7facecf838e74e62936fd461fb4c99f23d54a4792d + languageName: node + linkType: hard + +"jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.3.5": + version: 3.3.5 + resolution: "jsx-ast-utils@npm:3.3.5" + dependencies: + array-includes: ^3.1.6 + array.prototype.flat: ^1.3.1 + object.assign: ^4.1.4 + object.values: ^1.1.6 + checksum: f4b05fa4d7b5234230c905cfa88d36dc8a58a6666975a3891429b1a8cdc8a140bca76c297225cb7a499fad25a2c052ac93934449a2c31a44fc9edd06c773780a + languageName: node + linkType: hard + +"jwa@npm:^1.4.1": + version: 1.4.1 + resolution: "jwa@npm:1.4.1" + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: ^5.0.1 + checksum: ff30ea7c2dcc61f3ed2098d868bf89d43701605090c5b21b5544b512843ec6fd9e028381a4dda466cbcdb885c2d1150f7c62e7168394ee07941b4098e1035e2f + languageName: node + linkType: hard + +"jwa@npm:^2.0.0": + version: 2.0.0 + resolution: "jwa@npm:2.0.0" + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: ^5.0.1 + checksum: 8f00b71ad5fe94cb55006d0d19202f8f56889109caada2f7eeb63ca81755769ce87f4f48101967f398462e3b8ae4faebfbd5a0269cb755dead5d63c77ba4d2f1 + languageName: node + linkType: hard + +"jws@npm:^3.2.2": + version: 3.2.2 + resolution: "jws@npm:3.2.2" + dependencies: + jwa: ^1.4.1 + safe-buffer: ^5.0.1 + checksum: f0213fe5b79344c56cd443428d8f65c16bf842dc8cb8f5aed693e1e91d79c20741663ad6eff07a6d2c433d1831acc9814e8d7bada6a0471fbb91d09ceb2bf5c2 + languageName: node + linkType: hard + +"jws@npm:^4.0.0": + version: 4.0.0 + resolution: "jws@npm:4.0.0" + dependencies: + jwa: ^2.0.0 + safe-buffer: ^5.0.1 + checksum: d68d07aa6d1b8cb35c363a9bd2b48f15064d342a5d9dc18a250dbbce8dc06bd7e4792516c50baa16b8d14f61167c19e851fd7f66b59ecc68b7f6a013759765f7 + languageName: node + linkType: hard + +"keygrip@npm:~1.1.0": + version: 1.1.0 + resolution: "keygrip@npm:1.1.0" + dependencies: + tsscmp: 1.0.6 + checksum: 078cd16a463d187121f0a27c1c9c95c52ad392b620f823431689f345a0501132cee60f6e96914b07d570105af470b96960402accd6c48a0b1f3cd8fac4fa2cae + languageName: node + linkType: hard + +"keyv@npm:*": + version: 5.1.0 + resolution: "keyv@npm:5.1.0" + dependencies: + "@keyv/serialize": "*" + checksum: 02a0c795c199c9d94f6781216d762faadac11da088814c88eb242db24f0317ff3c9147779c40c474b6d856b32a6793fc8ff6eedca0ec597ff1fe02e3452f8bd2 + languageName: node + linkType: hard + +"keyv@npm:^4.5.2, keyv@npm:^4.5.3": + version: 4.5.4 + resolution: "keyv@npm:4.5.4" + dependencies: + json-buffer: 3.0.1 + checksum: 74a24395b1c34bd44ad5cb2b49140d087553e170625240b86755a6604cd65aa16efdbdeae5cdb17ba1284a0fbb25ad06263755dbc71b8d8b06f74232ce3cdd72 + languageName: node + linkType: hard + +"kind-of@npm:^6.0.2": + version: 6.0.3 + resolution: "kind-of@npm:6.0.3" + checksum: 3ab01e7b1d440b22fe4c31f23d8d38b4d9b91d9f291df683476576493d5dfd2e03848a8b05813dd0c3f0e835bc63f433007ddeceb71f05cb25c45ae1b19c6d3b + languageName: node + linkType: hard + +"kleur@npm:^3.0.3": + version: 3.0.3 + resolution: "kleur@npm:3.0.3" + checksum: df82cd1e172f957bae9c536286265a5cdbd5eeca487cb0a3b2a7b41ef959fc61f8e7c0e9aeea9c114ccf2c166b6a8dd45a46fd619c1c569d210ecd2765ad5169 + languageName: node + linkType: hard + +"kleur@npm:^4.0.3": + version: 4.1.5 + resolution: "kleur@npm:4.1.5" + checksum: 1dc476e32741acf0b1b5b0627ffd0d722e342c1b0da14de3e8ae97821327ca08f9fb944542fb3c126d90ac5f27f9d804edbe7c585bf7d12ef495d115e0f22c12 + languageName: node + linkType: hard + +"knex-mock-client@npm:2.0.1": + version: 2.0.1 + resolution: "knex-mock-client@npm:2.0.1" + dependencies: + lodash.clonedeep: ^4.5.0 + peerDependencies: + knex: ">=2.0.0" + checksum: 18d152f58cbadc908f38091b642d9f816df67e75dfc4b98d7d8dfdc25a707be474d931180dfc05edc9b1434a2fe44b732892c89d58bdcccdb8452e8095bd33a6 + languageName: node + linkType: hard + +"knex@npm:^3.0.0": + version: 3.1.0 + resolution: "knex@npm:3.1.0" + dependencies: + colorette: 2.0.19 + commander: ^10.0.0 + debug: 4.3.4 + escalade: ^3.1.1 + esm: ^3.2.25 + get-package-type: ^0.1.0 + getopts: 2.3.0 + interpret: ^2.2.0 + lodash: ^4.17.21 + pg-connection-string: 2.6.2 + rechoir: ^0.8.0 + resolve-from: ^5.0.0 + tarn: ^3.0.2 + tildify: 2.0.0 + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + sqlite3: + optional: true + tedious: + optional: true + bin: + knex: bin/cli.js + checksum: 3905f8d27960975f7f57f3f488d1ef3ccf47784acc8eb627e8a28cbbe1f296c6879c8ef0cbd9e17e867be80117d305cd948545f3fbd4c74b24c90d2413bbc021 + languageName: node + linkType: hard + +"knip@npm:^5.27.4": + version: 5.36.0 + resolution: "knip@npm:5.36.0" + dependencies: + "@nodelib/fs.walk": 1.2.8 + "@snyk/github-codeowners": 1.1.0 + easy-table: 1.2.0 + enhanced-resolve: ^5.17.1 + fast-glob: ^3.3.2 + jiti: ^2.3.3 + js-yaml: ^4.1.0 + minimist: ^1.2.8 + picocolors: ^1.1.0 + picomatch: ^4.0.1 + pretty-ms: ^9.0.0 + smol-toml: ^1.3.0 + strip-json-comments: 5.0.1 + summary: 2.1.0 + zod: ^3.22.4 + zod-validation-error: ^3.0.3 + peerDependencies: + "@types/node": ">=18" + typescript: ">=5.0.4" + bin: + knip: bin/knip.js + knip-bun: bin/knip-bun.js + checksum: 5c3cbfbdb61dfcbc0ae8df7a3e6e85ecc4de4aa359633e70066319219fa0fa31fb9fc4ce1133a9875d149ec776570d301983967693d148a3a1a670ba25ecb7a1 + languageName: node + linkType: hard + +"koa-compose@npm:^4.1.0": + version: 4.1.0 + resolution: "koa-compose@npm:4.1.0" + checksum: 46cb16792d96425e977c2ae4e5cb04930280740e907242ec9c25e3fb8b4a1d7b54451d7432bc24f40ec62255edea71894d2ceeb8238501842b4e48014f2e83db + languageName: node + linkType: hard + +"koa-convert@npm:^2.0.0": + version: 2.0.0 + resolution: "koa-convert@npm:2.0.0" + dependencies: + co: ^4.6.0 + koa-compose: ^4.1.0 + checksum: 7385b3391995f59c1312142e110d5dff677f9850dbfbcf387cd36a7b0af03b5d26e82b811eb9bb008b4f3e661cdab1f8817596e46b1929da2cf6e97a2f7456ed + languageName: node + linkType: hard + +"koa@npm:2.15.3": + version: 2.15.3 + resolution: "koa@npm:2.15.3" + dependencies: + accepts: ^1.3.5 + cache-content-type: ^1.0.0 + content-disposition: ~0.5.2 + content-type: ^1.0.4 + cookies: ~0.9.0 + debug: ^4.3.2 + delegates: ^1.0.0 + depd: ^2.0.0 + destroy: ^1.0.4 + encodeurl: ^1.0.2 + escape-html: ^1.0.3 + fresh: ~0.5.2 + http-assert: ^1.3.0 + http-errors: ^1.6.3 + is-generator-function: ^1.0.7 + koa-compose: ^4.1.0 + koa-convert: ^2.0.0 + on-finished: ^2.3.0 + only: ~0.0.2 + parseurl: ^1.3.2 + statuses: ^1.5.0 + type-is: ^1.6.16 + vary: ^1.1.2 + checksum: 7c3537443b1a588cf5c3e5554b914ff2bad510323d22b41861d5e0c97d47e9c5997965f303ede8be8bd83d309a4eea1f82cd45d35d6838bc21bb1bb6a90d5d25 + languageName: node + linkType: hard + +"kubernetes-models@npm:^4.3.1": + version: 4.4.0 + resolution: "kubernetes-models@npm:4.4.0" + dependencies: + "@kubernetes-models/apimachinery": ^2.0.0 + "@kubernetes-models/base": ^5.0.0 + "@kubernetes-models/validate": ^4.0.0 + "@swc/helpers": ^0.5.8 + checksum: 9884c56c35415fe4b44a676a8b628e91fab17eb9a5ef5de08b7b9652d74afeb843d4a139d52bb19e1aaaeba44545542e92cef737b845a2cff5139aa62f820ced + languageName: node + linkType: hard + +"kuler@npm:^2.0.0": + version: 2.0.0 + resolution: "kuler@npm:2.0.0" + checksum: 9e10b5a1659f9ed8761d38df3c35effabffbd19fc6107324095238e4ef0ff044392cae9ac64a1c2dda26e532426485342226b93806bd97504b174b0dcf04ed81 + languageName: node + linkType: hard + +"language-subtag-registry@npm:^0.3.20": + version: 0.3.23 + resolution: "language-subtag-registry@npm:0.3.23" + checksum: 0b64c1a6c5431c8df648a6d25594ff280613c886f4a1a542d9b864e5472fb93e5c7856b9c41595c38fac31370328fc79fcc521712e89ea6d6866cbb8e0995d81 + languageName: node + linkType: hard + +"language-tags@npm:^1.0.9": + version: 1.0.9 + resolution: "language-tags@npm:1.0.9" + dependencies: + language-subtag-registry: ^0.3.20 + checksum: 57c530796dc7179914dee71bc94f3747fd694612480241d0453a063777265dfe3a951037f7acb48f456bf167d6eb419d4c00263745326b3ba1cdcf4657070e78 + languageName: node + linkType: hard + +"launch-editor@npm:^2.6.1": + version: 2.9.1 + resolution: "launch-editor@npm:2.9.1" + dependencies: + picocolors: ^1.0.0 + shell-quote: ^1.8.1 + checksum: bed887085a9729cc2ad050329d92a99f4c69bacccf96d1ed8c84670608a3a128a828ba8e9a8a41101c5aea5aea6f79984658e2fd11f6ba85e32e6e1ed16dbb1c + languageName: node + linkType: hard + +"lazy-ass@npm:1.6.0": + version: 1.6.0 + resolution: "lazy-ass@npm:1.6.0" + checksum: 5a3ebb17915b03452320804466345382a6c25ac782ec4874fecdb2385793896cd459be2f187dc7def8899180c32ee0ab9a1aa7fe52193ac3ff3fe29bb0591729 + languageName: node + linkType: hard + +"lazystream@npm:^1.0.0": + version: 1.0.1 + resolution: "lazystream@npm:1.0.1" + dependencies: + readable-stream: ^2.0.5 + checksum: 822c54c6b87701a6491c70d4fabc4cafcf0f87d6b656af168ee7bb3c45de9128a801cb612e6eeeefc64d298a7524a698dd49b13b0121ae50c2ae305f0dcc5310 + languageName: node + linkType: hard + +"leven@npm:3.1.0, leven@npm:^3.1.0": + version: 3.1.0 + resolution: "leven@npm:3.1.0" + checksum: 638401d534585261b6003db9d99afd244dfe82d75ddb6db5c0df412842d5ab30b2ef18de471aaec70fe69a46f17b4ae3c7f01d8a4e6580ef7adb9f4273ad1e55 + languageName: node + linkType: hard + +"levn@npm:^0.4.1": + version: 0.4.1 + resolution: "levn@npm:0.4.1" + dependencies: + prelude-ls: ^1.2.1 + type-check: ~0.4.0 + checksum: 12c5021c859bd0f5248561bf139121f0358285ec545ebf48bb3d346820d5c61a4309535c7f387ed7d84361cf821e124ce346c6b7cef8ee09a67c1473b46d0fc4 + languageName: node + linkType: hard + +"levn@npm:~0.3.0": + version: 0.3.0 + resolution: "levn@npm:0.3.0" + dependencies: + prelude-ls: ~1.1.2 + type-check: ~0.3.2 + checksum: 0d084a524231a8246bb10fec48cdbb35282099f6954838604f3c7fc66f2e16fa66fd9cc2f3f20a541a113c4dafdf181e822c887c8a319c9195444e6c64ac395e + languageName: node + linkType: hard + +"lilconfig@npm:^2.0.3, lilconfig@npm:^2.0.5": + version: 2.1.0 + resolution: "lilconfig@npm:2.1.0" + checksum: 8549bb352b8192375fed4a74694cd61ad293904eee33f9d4866c2192865c44c4eb35d10782966242634e0cbc1e91fe62b1247f148dc5514918e3a966da7ea117 + languageName: node + linkType: hard + +"lines-and-columns@npm:^1.1.6": + version: 1.2.4 + resolution: "lines-and-columns@npm:1.2.4" + checksum: 0c37f9f7fa212b38912b7145e1cd16a5f3cd34d782441c3e6ca653485d326f58b3caccda66efce1c5812bde4961bbde3374fae4b0d11bf1226152337f3894aa5 + languageName: node + linkType: hard + +"linkify-react@npm:4.1.3": + version: 4.1.3 + resolution: "linkify-react@npm:4.1.3" + peerDependencies: + linkifyjs: ^4.0.0 + react: ">= 15.0.0" + checksum: 1c28ab02774d5427fad9f4a5ad1c7b852b83aece983fd143fdb4ec95dedf7edc77da59883aaf6fb1a2c2060e8b5e72fdfad4d704d544fabc2b173a1b1eb6473d + languageName: node + linkType: hard + +"linkifyjs@npm:4.1.3": + version: 4.1.3 + resolution: "linkifyjs@npm:4.1.3" + checksum: 023d467499a717a49ebbfa256a80cb2811a3b038ff2593e5be0fb8a4715b0a63bf80c571838e19e120833d5b9874464f3a1448965c8eebbde8c19458b3a6c6e4 + languageName: node + linkType: hard + +"loader-runner@npm:^4.2.0": + version: 4.3.0 + resolution: "loader-runner@npm:4.3.0" + checksum: a90e00dee9a16be118ea43fec3192d0b491fe03a32ed48a4132eb61d498f5536a03a1315531c19d284392a8726a4ecad71d82044c28d7f22ef62e029bf761569 + languageName: node + linkType: hard + +"loader-utils@npm:^1.1.0": + version: 1.4.2 + resolution: "loader-utils@npm:1.4.2" + dependencies: + big.js: ^5.2.2 + emojis-list: ^3.0.0 + json5: ^1.0.1 + checksum: eb6fb622efc0ffd1abdf68a2022f9eac62bef8ec599cf8adb75e94d1d338381780be6278534170e99edc03380a6d29bc7eb1563c89ce17c5fed3a0b17f1ad804 + languageName: node + linkType: hard + +"loader-utils@npm:^2.0.0, loader-utils@npm:^2.0.4": + version: 2.0.4 + resolution: "loader-utils@npm:2.0.4" + dependencies: + big.js: ^5.2.2 + emojis-list: ^3.0.0 + json5: ^2.1.2 + checksum: a5281f5fff1eaa310ad5e1164095689443630f3411e927f95031ab4fb83b4a98f388185bb1fe949e8ab8d4247004336a625e9255c22122b815bb9a4c5d8fc3b7 + languageName: node + linkType: hard + +"loader-utils@npm:^3.2.0": + version: 3.3.1 + resolution: "loader-utils@npm:3.3.1" + checksum: d35808e081635e5bc50228a52ed79f83e2c82bd8f7578818c12b1b4cf0b7f409d72d9b93a683ec36b9eaa93346693d3f3c8380183ba2ff81599b0829d685de39 + languageName: node + linkType: hard + +"locate-path@npm:^3.0.0": + version: 3.0.0 + resolution: "locate-path@npm:3.0.0" + dependencies: + p-locate: ^3.0.0 + path-exists: ^3.0.0 + checksum: 53db3996672f21f8b0bf2a2c645ae2c13ffdae1eeecfcd399a583bce8516c0b88dcb4222ca6efbbbeb6949df7e46860895be2c02e8d3219abd373ace3bfb4e11 + languageName: node + linkType: hard + +"locate-path@npm:^5.0.0": + version: 5.0.0 + resolution: "locate-path@npm:5.0.0" + dependencies: + p-locate: ^4.1.0 + checksum: 83e51725e67517287d73e1ded92b28602e3ae5580b301fe54bfb76c0c723e3f285b19252e375712316774cf52006cb236aed5704692c32db0d5d089b69696e30 + languageName: node + linkType: hard + +"locate-path@npm:^6.0.0": + version: 6.0.0 + resolution: "locate-path@npm:6.0.0" + dependencies: + p-locate: ^5.0.0 + checksum: 72eb661788a0368c099a184c59d2fee760b3831c9c1c33955e8a19ae4a21b4116e53fa736dc086cdeb9fce9f7cc508f2f92d2d3aae516f133e16a2bb59a39f5a + languageName: node + linkType: hard + +"lodash-es@npm:^4.17.21": + version: 4.17.21 + resolution: "lodash-es@npm:4.17.21" + checksum: 05cbffad6e2adbb331a4e16fbd826e7faee403a1a04873b82b42c0f22090f280839f85b95393f487c1303c8a3d2a010048bf06151a6cbe03eee4d388fb0a12d2 + languageName: node + linkType: hard + +"lodash.camelcase@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.camelcase@npm:4.3.0" + checksum: cb9227612f71b83e42de93eccf1232feeb25e705bdb19ba26c04f91e885bfd3dd5c517c4a97137658190581d3493ea3973072ca010aab7e301046d90740393d1 + languageName: node + linkType: hard + +"lodash.clonedeep@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.clonedeep@npm:4.5.0" + checksum: 92c46f094b064e876a23c97f57f81fbffd5d760bf2d8a1c61d85db6d1e488c66b0384c943abee4f6af7debf5ad4e4282e74ff83177c9e63d8ff081a4837c3489 + languageName: node + linkType: hard + +"lodash.clonedeepwith@npm:4.5.0": + version: 4.5.0 + resolution: "lodash.clonedeepwith@npm:4.5.0" + checksum: 9fbf4ebfa04b381df226a2298eba680327bea3d0d5d19c5118de7ae218fd219186e30e9fd0d33b13729f34ffbc83c1cf09cb27aff265ba94cb602b8a2b1e71c9 + languageName: node + linkType: hard + +"lodash.debounce@npm:^4.0.8": + version: 4.0.8 + resolution: "lodash.debounce@npm:4.0.8" + checksum: a3f527d22c548f43ae31c861ada88b2637eb48ac6aa3eb56e82d44917971b8aa96fbb37aa60efea674dc4ee8c42074f90f7b1f772e9db375435f6c83a19b3bc6 + languageName: node + linkType: hard + +"lodash.defaults@npm:^4.2.0": + version: 4.2.0 + resolution: "lodash.defaults@npm:4.2.0" + checksum: 84923258235592c8886e29de5491946ff8c2ae5c82a7ac5cddd2e3cb697e6fbdfbbb6efcca015795c86eec2bb953a5a2ee4016e3735a3f02720428a40efbb8f1 + languageName: node + linkType: hard + +"lodash.flattendeep@npm:^4.0.0": + version: 4.4.0 + resolution: "lodash.flattendeep@npm:4.4.0" + checksum: 8521c919acac3d4bcf0aaf040c1ca9cb35d6c617e2d72e9b4d51c9a58b4366622cd6077441a18be626c3f7b28227502b3bf042903d447b056ee7e0b11d45c722 + languageName: node + linkType: hard + +"lodash.groupby@npm:^4.6.0": + version: 4.6.0 + resolution: "lodash.groupby@npm:4.6.0" + checksum: e2d4d13d12790a1cacab3f5f120b7c072a792224e83b2f403218866d18efde76024b2579996dfebb230a61ce06469332e16639103669a35a605287e19ced6b9b + languageName: node + linkType: hard + +"lodash.includes@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.includes@npm:4.3.0" + checksum: 71092c130515a67ab3bd928f57f6018434797c94def7f46aafa417771e455ce3a4834889f4267b17887d7f75297dfabd96231bf704fd2b8c5096dc4a913568b6 + languageName: node + linkType: hard + +"lodash.isarguments@npm:^3.1.0": + version: 3.1.0 + resolution: "lodash.isarguments@npm:3.1.0" + checksum: ae1526f3eb5c61c77944b101b1f655f846ecbedcb9e6b073526eba6890dc0f13f09f72e11ffbf6540b602caee319af9ac363d6cdd6be41f4ee453436f04f13b5 + languageName: node + linkType: hard + +"lodash.isboolean@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isboolean@npm:3.0.3" + checksum: b70068b4a8b8837912b54052557b21fc4774174e3512ed3c5b94621e5aff5eb6c68089d0a386b7e801d679cd105d2e35417978a5e99071750aa2ed90bffd0250 + languageName: node + linkType: hard + +"lodash.isequal@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.isequal@npm:4.5.0" + checksum: da27515dc5230eb1140ba65ff8de3613649620e8656b19a6270afe4866b7bd461d9ba2ac8a48dcc57f7adac4ee80e1de9f965d89d4d81a0ad52bb3eec2609644 + languageName: node + linkType: hard + +"lodash.isinteger@npm:^4.0.4": + version: 4.0.4 + resolution: "lodash.isinteger@npm:4.0.4" + checksum: 6034821b3fc61a2ffc34e7d5644bb50c5fd8f1c0121c554c21ac271911ee0c0502274852845005f8651d51e199ee2e0cfebfe40aaa49c7fe617f603a8a0b1691 + languageName: node + linkType: hard + +"lodash.isnumber@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isnumber@npm:3.0.3" + checksum: 913784275b565346255e6ae6a6e30b760a0da70abc29f3e1f409081585875105138cda4a429ff02577e1bc0a7ae2a90e0a3079a37f3a04c3d6c5aaa532f4cab2 + languageName: node + linkType: hard + +"lodash.isplainobject@npm:^4.0.6": + version: 4.0.6 + resolution: "lodash.isplainobject@npm:4.0.6" + checksum: 29c6351f281e0d9a1d58f1a4c8f4400924b4c79f18dfc4613624d7d54784df07efaff97c1ff2659f3e085ecf4fff493300adc4837553104cef2634110b0d5337 + languageName: node + linkType: hard + +"lodash.isstring@npm:^4.0.1": + version: 4.0.1 + resolution: "lodash.isstring@npm:4.0.1" + checksum: eaac87ae9636848af08021083d796e2eea3d02e80082ab8a9955309569cb3a463ce97fd281d7dc119e402b2e7d8c54a23914b15d2fc7fff56461511dc8937ba0 + languageName: node + linkType: hard + +"lodash.memoize@npm:^4.1.2": + version: 4.1.2 + resolution: "lodash.memoize@npm:4.1.2" + checksum: 9ff3942feeccffa4f1fafa88d32f0d24fdc62fd15ded5a74a5f950ff5f0c6f61916157246744c620173dddf38d37095a92327d5fd3861e2063e736a5c207d089 + languageName: node + linkType: hard + +"lodash.merge@npm:^4.6.2": + version: 4.6.2 + resolution: "lodash.merge@npm:4.6.2" + checksum: ad580b4bdbb7ca1f7abf7e1bce63a9a0b98e370cf40194b03380a46b4ed799c9573029599caebc1b14e3f24b111aef72b96674a56cfa105e0f5ac70546cdc005 + languageName: node + linkType: hard + +"lodash.omit@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.omit@npm:4.5.0" + checksum: 434645e49fe84ab315719bd5a9a3a585a0f624aa4160bc09157dd041a414bcc287c15840365c1379476a3f3eda41fbe838976c3f7bdecbbf4c5478e86c471a30 + languageName: node + linkType: hard + +"lodash.once@npm:^4.0.0": + version: 4.1.1 + resolution: "lodash.once@npm:4.1.1" + checksum: d768fa9f9b4e1dc6453be99b753906f58990e0c45e7b2ca5a3b40a33111e5d17f6edf2f768786e2716af90a8e78f8f91431ab8435f761fef00f9b0c256f6d245 + languageName: node + linkType: hard + +"lodash.startcase@npm:^4.4.0": + version: 4.4.0 + resolution: "lodash.startcase@npm:4.4.0" + checksum: c03a4a784aca653845fe09d0ef67c902b6e49288dc45f542a4ab345a9c406a6dc194c774423fa313ee7b06283950301c1221dd2a1d8ecb2dac8dfbb9ed5606b5 + languageName: node + linkType: hard + +"lodash.topath@npm:^4.5.2": + version: 4.5.2 + resolution: "lodash.topath@npm:4.5.2" + checksum: 04583e220f4bb1c4ac0008ff8f46d9cb4ddce0ea1090085790da30a41f4cb1b904d885cb73257fca619fa825cd96f9bb97c67d039635cb76056e18f5e08bfdee + languageName: node + linkType: hard + +"lodash.uniq@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.uniq@npm:4.5.0" + checksum: a4779b57a8d0f3c441af13d9afe7ecff22dd1b8ce1129849f71d9bbc8e8ee4e46dfb4b7c28f7ad3d67481edd6e51126e4e2a6ee276e25906d10f7140187c392d + languageName: node + linkType: hard + +"lodash@npm:4.17.21, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:~4.17.15, lodash@npm:~4.17.21": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 + languageName: node + linkType: hard + +"log-symbols@npm:^4.1.0": + version: 4.1.0 + resolution: "log-symbols@npm:4.1.0" + dependencies: + chalk: ^4.1.0 + is-unicode-supported: ^0.1.0 + checksum: fce1497b3135a0198803f9f07464165e9eb83ed02ceb2273930a6f8a508951178d8cf4f0378e9d28300a2ed2bc49050995d2bd5f53ab716bb15ac84d58c6ef74 + languageName: node + linkType: hard + +"log4js@npm:6.9.1": + version: 6.9.1 + resolution: "log4js@npm:6.9.1" + dependencies: + date-format: ^4.0.14 + debug: ^4.3.4 + flatted: ^3.2.7 + rfdc: ^1.3.0 + streamroller: ^3.1.5 + checksum: 59d98c37d4163138dab5d9b06ae26965d1353106fece143973d57b1003b3a482791aa21374fd2cca81a953b8837b2f9756ac225404e60cbfa4dd3ab59f082e2e + languageName: node + linkType: hard + +"logform@npm:^2.3.2, logform@npm:^2.6.0, logform@npm:^2.6.1": + version: 2.6.1 + resolution: "logform@npm:2.6.1" + dependencies: + "@colors/colors": 1.6.0 + "@types/triple-beam": ^1.3.2 + fecha: ^4.2.0 + ms: ^2.1.1 + safe-stable-stringify: ^2.3.1 + triple-beam: ^1.3.0 + checksum: 0c6b95fa8350ccc33c7c33d77de2a9920205399706fc1b125151c857b61eb90873f4670d9e0e58e58c165b68a363206ae670d6da8b714527c838da3c84449605 + languageName: node + linkType: hard + +"long-timeout@npm:0.1.1": + version: 0.1.1 + resolution: "long-timeout@npm:0.1.1" + checksum: 48668e5362cb74c4b77a6b833d59f149b9bb9e99c5a5097609807e2597cd0920613b2a42b89bd0870848298be3691064d95599a04ae010023d07dba39932afa7 + languageName: node + linkType: hard + +"long@npm:^5.2.1": + version: 5.2.3 + resolution: "long@npm:5.2.3" + checksum: 885ede7c3de4facccbd2cacc6168bae3a02c3e836159ea4252c87b6e34d40af819824b2d4edce330bfb5c4d6e8ce3ec5864bdcf9473fa1f53a4f8225860e5897 + languageName: node + linkType: hard + +"longest-streak@npm:^3.0.0": + version: 3.1.0 + resolution: "longest-streak@npm:3.1.0" + checksum: d7f952ed004cbdb5c8bcfc4f7f5c3d65449e6c5a9e9be4505a656e3df5a57ee125f284286b4bf8ecea0c21a7b3bf2b8f9001ad506c319b9815ad6a63a47d0fd0 + languageName: node + linkType: hard + +"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": + version: 1.4.0 + resolution: "loose-envify@npm:1.4.0" + dependencies: + js-tokens: ^3.0.0 || ^4.0.0 + bin: + loose-envify: cli.js + checksum: 6517e24e0cad87ec9888f500c5b5947032cdfe6ef65e1c1936a0c48a524b81e65542c9c3edc91c97d5bddc806ee2a985dbc79be89215d613b1de5db6d1cfe6f4 + languageName: node + linkType: hard + +"lower-case@npm:^2.0.2": + version: 2.0.2 + resolution: "lower-case@npm:2.0.2" + dependencies: + tslib: ^2.0.3 + checksum: 83a0a5f159ad7614bee8bf976b96275f3954335a84fad2696927f609ddae902802c4f3312d86668722e668bef41400254807e1d3a7f2e8c3eede79691aa1f010 + languageName: node + linkType: hard + +"lowlight@npm:^1.17.0": + version: 1.20.0 + resolution: "lowlight@npm:1.20.0" + dependencies: + fault: ^1.0.0 + highlight.js: ~10.7.0 + checksum: 14a1815d6bae202ddee313fc60f06d46e5235c02fa483a77950b401d85b4c1e12290145ccd17a716b07f9328bd5864aa2d402b6a819ff3be7c833d9748ff8ba7 + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.4.3 + resolution: "lru-cache@npm:10.4.3" + checksum: 6476138d2125387a6d20f100608c2583d415a4f64a0fecf30c9e2dda976614f09cad4baa0842447bd37dd459a7bd27f57d9d8f8ce558805abd487c583f3d774a + languageName: node + linkType: hard + +"lru-cache@npm:^4.0.1": + version: 4.1.5 + resolution: "lru-cache@npm:4.1.5" + dependencies: + pseudomap: ^1.0.2 + yallist: ^2.1.2 + checksum: 4bb4b58a36cd7dc4dcec74cbe6a8f766a38b7426f1ff59d4cf7d82a2aa9b9565cd1cb98f6ff60ce5cd174524868d7bc9b7b1c294371851356066ca9ac4cf135a + languageName: node + linkType: hard + +"lru-cache@npm:^5.1.1": + version: 5.1.1 + resolution: "lru-cache@npm:5.1.1" + dependencies: + yallist: ^3.0.2 + checksum: c154ae1cbb0c2206d1501a0e94df349653c92c8cbb25236d7e85190bcaf4567a03ac6eb43166fabfa36fd35623694da7233e88d9601fbf411a9a481d85dbd2cb + languageName: node + linkType: hard + +"lru-cache@npm:^6.0.0": + version: 6.0.0 + resolution: "lru-cache@npm:6.0.0" + dependencies: + yallist: ^4.0.0 + checksum: f97f499f898f23e4585742138a22f22526254fdba6d75d41a1c2526b3b6cc5747ef59c5612ba7375f42aca4f8461950e925ba08c991ead0651b4918b7c978297 + languageName: node + linkType: hard + +"lru-cache@npm:^7.14.1, lru-cache@npm:^7.7.1": + version: 7.18.3 + resolution: "lru-cache@npm:7.18.3" + checksum: e550d772384709deea3f141af34b6d4fa392e2e418c1498c078de0ee63670f1f46f5eee746e8ef7e69e1c895af0d4224e62ee33e66a543a14763b0f2e74c1356 + languageName: node + linkType: hard + +"lru-cache@npm:^9.0.0": + version: 9.1.2 + resolution: "lru-cache@npm:9.1.2" + checksum: d3415634be3908909081fc4c56371a8d562d9081eba70543d86871b978702fffd0e9e362b83921b27a29ae2b37b90f55675aad770a54ac83bb3e4de5049d4b15 + languageName: node + linkType: hard + +"lru.min@npm:^1.0.0": + version: 1.1.1 + resolution: "lru.min@npm:1.1.1" + checksum: 26ec06c656220a240427f29c3528871b9cfb3214bd5d1bf4c5f2b2cb69402f7558c560e30055fc09bd61e4bf651c1eda2f9b1ab1b16336616fca381b7d42ecba + languageName: node + linkType: hard + +"luxon@npm:^3.0.0, luxon@npm:^3.2.1": + version: 3.5.0 + resolution: "luxon@npm:3.5.0" + checksum: f290fe5788c8e51e748744f05092160d4be12150dca70f9fadc0d233e53d60ce86acd82e7d909a114730a136a77e56f0d3ebac6141bbb82fd310969a4704825b + languageName: node + linkType: hard + +"luxon@npm:~3.4.0": + version: 3.4.4 + resolution: "luxon@npm:3.4.4" + checksum: 36c1f99c4796ee4bfddf7dc94fa87815add43ebc44c8934c924946260a58512f0fd2743a629302885df7f35ccbd2d13f178c15df046d0e3b6eb71db178f1c60c + languageName: node + linkType: hard + +"lz-string@npm:^1.5.0": + version: 1.5.0 + resolution: "lz-string@npm:1.5.0" + bin: + lz-string: bin/bin.js + checksum: 1ee98b4580246fd90dd54da6e346fb1caefcf05f677c686d9af237a157fdea3fd7c83a4bc58f858cd5b10a34d27afe0fdcbd0505a47e0590726a873dc8b8f65d + languageName: node + linkType: hard + +"magic-string@npm:^0.30.10, magic-string@npm:^0.30.3": + version: 0.30.12 + resolution: "magic-string@npm:0.30.12" + dependencies: + "@jridgewell/sourcemap-codec": ^1.5.0 + checksum: 3f0d23b74371765f0e6cad4284eebba0ac029c7a55e39292de5aa92281afb827138cb2323d24d2924f6b31f138c3783596c5ccaa98653fe9cf122e1f81325b59 + languageName: node + linkType: hard + +"make-dir@npm:^3.1.0": + version: 3.1.0 + resolution: "make-dir@npm:3.1.0" + dependencies: + semver: ^6.0.0 + checksum: 484200020ab5a1fdf12f393fe5f385fc8e4378824c940fba1729dcd198ae4ff24867bc7a5646331e50cead8abff5d9270c456314386e629acec6dff4b8016b78 + languageName: node + linkType: hard + +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: ^7.5.3 + checksum: bf0731a2dd3aab4db6f3de1585cea0b746bb73eb5a02e3d8d72757e376e64e6ada190b1eddcde5b2f24a81b688a9897efd5018737d05e02e2a671dda9cff8a8a + languageName: node + linkType: hard + +"make-error@npm:^1.1.1": + version: 1.3.6 + resolution: "make-error@npm:1.3.6" + checksum: b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^10.0.3": + version: 10.2.1 + resolution: "make-fetch-happen@npm:10.2.1" + dependencies: + agentkeepalive: ^4.2.1 + cacache: ^16.1.0 + http-cache-semantics: ^4.1.0 + http-proxy-agent: ^5.0.0 + https-proxy-agent: ^5.0.0 + is-lambda: ^1.0.1 + lru-cache: ^7.7.1 + minipass: ^3.1.6 + minipass-collect: ^1.0.2 + minipass-fetch: ^2.0.3 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + negotiator: ^0.6.3 + promise-retry: ^2.0.1 + socks-proxy-agent: ^7.0.0 + ssri: ^9.0.0 + checksum: 2332eb9a8ec96f1ffeeea56ccefabcb4193693597b132cd110734d50f2928842e22b84cfa1508e921b8385cdfd06dda9ad68645fed62b50fff629a580f5fb72c + languageName: node + linkType: hard + +"make-fetch-happen@npm:^13.0.0": + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" + dependencies: + "@npmcli/agent": ^2.0.0 + cacache: ^18.0.0 + http-cache-semantics: ^4.1.1 + is-lambda: ^1.0.1 + minipass: ^7.0.2 + minipass-fetch: ^3.0.0 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + negotiator: ^0.6.3 + proc-log: ^4.2.0 + promise-retry: ^2.0.1 + ssri: ^10.0.0 + checksum: 5c9fad695579b79488fa100da05777213dd9365222f85e4757630f8dd2a21a79ddd3206c78cfd6f9b37346819681782b67900ac847a57cf04190f52dda5343fd + languageName: node + linkType: hard + +"makeerror@npm:1.0.12": + version: 1.0.12 + resolution: "makeerror@npm:1.0.12" + dependencies: + tmpl: 1.0.5 + checksum: b38a025a12c8146d6eeea5a7f2bf27d51d8ad6064da8ca9405fcf7bf9b54acd43e3b30ddd7abb9b1bfa4ddb266019133313482570ddb207de568f71ecfcf6060 + languageName: node + linkType: hard + +"map-age-cleaner@npm:^0.2.0": + version: 0.2.0 + resolution: "map-age-cleaner@npm:0.2.0" + dependencies: + p-defer: ^1.0.0 + checksum: 13a6810b76b0067efa7f4b0f3dc58b58b4a4b5faa4cae5a0e8d5d59eda04d7074724eee426c9b5890a1d7e14d1e2902a090587acc8e2430198e79ab1556a2dad + languageName: node + linkType: hard + +"map-stream@npm:~0.1.0": + version: 0.1.0 + resolution: "map-stream@npm:0.1.0" + checksum: 38abbe4eb883888031e6b2fc0630bc583c99396be16b8ace5794b937b682a8a081f03e8b15bfd4914d1bc88318f0e9ac73ba3512ae65955cd449f63256ddb31d + languageName: node + linkType: hard + +"markdown-escape@npm:^2.0.0": + version: 2.0.0 + resolution: "markdown-escape@npm:2.0.0" + checksum: 74c66d817636ac5f6a275fdc79ecb1e208d907ca85289d660b515256fbc3e380eb18d29b6bbbd6a77968ee4fb5872d40ecf31e52bc9f17855bb01bb723569fa0 + languageName: node + linkType: hard + +"markdown-table@npm:^3.0.0": + version: 3.0.3 + resolution: "markdown-table@npm:3.0.3" + checksum: 8fcd3d9018311120fbb97115987f8b1665a603f3134c93fbecc5d1463380c8036f789e2a62c19432058829e594fff8db9ff81c88f83690b2f8ed6c074f8d9e10 + languageName: node + linkType: hard + +"markdown-to-jsx@npm:^7.4.1": + version: 7.5.0 + resolution: "markdown-to-jsx@npm:7.5.0" + peerDependencies: + react: ">= 0.14.0" + checksum: c9c6f1bfad5f2d9b1d3476eb0313ae3dffd0a9f14011c74efdd7c664fd32ee1842ef48abb16a496046f90361af49aa80a827e9d9c0bc04824a1986fdeb4d1852 + languageName: node + linkType: hard + +"matcher@npm:^3.0.0": + version: 3.0.0 + resolution: "matcher@npm:3.0.0" + dependencies: + escape-string-regexp: ^4.0.0 + checksum: 8bee1a7ab7609c2c21d9c9254b6785fa708eadf289032b556d57a34e98fcd4c537659a004dafee6ce80ab157099e645c199dc52678dff1e7fb0a6684e0da4dbe + languageName: node + linkType: hard + +"material-ui-popup-state@npm:^1.9.3": + version: 1.9.3 + resolution: "material-ui-popup-state@npm:1.9.3" + dependencies: + "@babel/runtime": ^7.12.5 + "@material-ui/types": ^6.0.1 + classnames: ^2.2.6 + prop-types: ^15.7.2 + peerDependencies: + "@material-ui/core": ^4.0.0 || ^5.0.0-beta + react: ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 0acd73b54afec02072e9b401738eb1c8832fd90771efe9894220778cc6f6d89f60f3902fdeb109a4c037b19a26bcf5b77a60a79fcaa024ddf67224bbee466530 + languageName: node + linkType: hard + +"mathjs@npm:^11.11.2": + version: 11.12.0 + resolution: "mathjs@npm:11.12.0" + dependencies: + "@babel/runtime": ^7.23.2 + complex.js: ^2.1.1 + decimal.js: ^10.4.3 + escape-latex: ^1.2.0 + fraction.js: 4.3.4 + javascript-natural-sort: ^0.7.1 + seedrandom: ^3.0.5 + tiny-emitter: ^2.1.0 + typed-function: ^4.1.1 + bin: + mathjs: bin/cli.js + checksum: 69d9ba52435bfebf50d0169939d6c6a0293f4b0e63683c4b95004574863a312503baab826680aa3963e21dc409628493ee333ed9d6a3e45cd4003d430a1ef0ef + languageName: node + linkType: hard + +"md5.js@npm:^1.3.4": + version: 1.3.5 + resolution: "md5.js@npm:1.3.5" + dependencies: + hash-base: ^3.0.0 + inherits: ^2.0.1 + safe-buffer: ^5.1.2 + checksum: 098494d885684bcc4f92294b18ba61b7bd353c23147fbc4688c75b45cb8590f5a95fd4584d742415dcc52487f7a1ef6ea611cfa1543b0dc4492fe026357f3f0c + languageName: node + linkType: hard + +"mdast-util-definitions@npm:^5.0.0": + version: 5.1.2 + resolution: "mdast-util-definitions@npm:5.1.2" + dependencies: + "@types/mdast": ^3.0.0 + "@types/unist": ^2.0.0 + unist-util-visit: ^4.0.0 + checksum: 2544daccab744ea1ede76045c2577ae4f1cc1b9eb1ea51ab273fe1dca8db5a8d6f50f87759c0ce6484975914b144b7f40316f805cb9c86223a78db8de0b77bae + languageName: node + linkType: hard + +"mdast-util-find-and-replace@npm:^2.0.0": + version: 2.2.2 + resolution: "mdast-util-find-and-replace@npm:2.2.2" + dependencies: + "@types/mdast": ^3.0.0 + escape-string-regexp: ^5.0.0 + unist-util-is: ^5.0.0 + unist-util-visit-parents: ^5.0.0 + checksum: b4ce463c43fe6e1c38a53a89703f755c84ab5437f49bff9a0ac751279733332ca11c85ed0262aa6c17481f77b555d26ca6d64e70d6814f5b8d12d34a3e53a60b + languageName: node + linkType: hard + +"mdast-util-from-markdown@npm:^1.0.0": + version: 1.3.1 + resolution: "mdast-util-from-markdown@npm:1.3.1" + dependencies: + "@types/mdast": ^3.0.0 + "@types/unist": ^2.0.0 + decode-named-character-reference: ^1.0.0 + mdast-util-to-string: ^3.1.0 + micromark: ^3.0.0 + micromark-util-decode-numeric-character-reference: ^1.0.0 + micromark-util-decode-string: ^1.0.0 + micromark-util-normalize-identifier: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + unist-util-stringify-position: ^3.0.0 + uvu: ^0.5.0 + checksum: c2fac225167e248d394332a4ea39596e04cbde07d8cdb3889e91e48972c4c3462a02b39fda3855345d90231eb17a90ac6e082fb4f012a77c1d0ddfb9c7446940 + languageName: node + linkType: hard + +"mdast-util-gfm-autolink-literal@npm:^1.0.0": + version: 1.0.3 + resolution: "mdast-util-gfm-autolink-literal@npm:1.0.3" + dependencies: + "@types/mdast": ^3.0.0 + ccount: ^2.0.0 + mdast-util-find-and-replace: ^2.0.0 + micromark-util-character: ^1.0.0 + checksum: 1748a8727cfc533bac0c287d6e72d571d165bfa77ae0418be4828177a3ec73c02c3f2ee534d87eb75cbaffa00c0866853bbcc60ae2255babb8210f7636ec2ce2 + languageName: node + linkType: hard + +"mdast-util-gfm-footnote@npm:^1.0.0": + version: 1.0.2 + resolution: "mdast-util-gfm-footnote@npm:1.0.2" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-to-markdown: ^1.3.0 + micromark-util-normalize-identifier: ^1.0.0 + checksum: 2d77505f9377ed7e14472ef5e6b8366c3fec2cf5f936bb36f9fbe5b97ccb7cce0464d9313c236fa86fb844206fd585db05707e4fcfb755e4fc1864194845f1f6 + languageName: node + linkType: hard + +"mdast-util-gfm-strikethrough@npm:^1.0.0": + version: 1.0.3 + resolution: "mdast-util-gfm-strikethrough@npm:1.0.3" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-to-markdown: ^1.3.0 + checksum: 17003340ff1bba643ec4a59fd4370fc6a32885cab2d9750a508afa7225ea71449fb05acaef60faa89c6378b8bcfbd86a9d94b05f3c6651ff27a60e3ddefc2549 + languageName: node + linkType: hard + +"mdast-util-gfm-table@npm:^1.0.0": + version: 1.0.7 + resolution: "mdast-util-gfm-table@npm:1.0.7" + dependencies: + "@types/mdast": ^3.0.0 + markdown-table: ^3.0.0 + mdast-util-from-markdown: ^1.0.0 + mdast-util-to-markdown: ^1.3.0 + checksum: 8b8c401bb4162e53f072a2dff8efbca880fd78d55af30601c791315ab6722cb2918176e8585792469a0c530cebb9df9b4e7fede75fdc4d83df2839e238836692 + languageName: node + linkType: hard + +"mdast-util-gfm-task-list-item@npm:^1.0.0": + version: 1.0.2 + resolution: "mdast-util-gfm-task-list-item@npm:1.0.2" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-to-markdown: ^1.3.0 + checksum: c9b86037d6953b84f11fb2fc3aa23d5b8e14ca0dfcb0eb2fb289200e172bb9d5647bfceb4f86606dc6d935e8d58f6a458c04d3e55e87ff8513c7d4ade976200b + languageName: node + linkType: hard + +"mdast-util-gfm@npm:^2.0.0": + version: 2.0.2 + resolution: "mdast-util-gfm@npm:2.0.2" + dependencies: + mdast-util-from-markdown: ^1.0.0 + mdast-util-gfm-autolink-literal: ^1.0.0 + mdast-util-gfm-footnote: ^1.0.0 + mdast-util-gfm-strikethrough: ^1.0.0 + mdast-util-gfm-table: ^1.0.0 + mdast-util-gfm-task-list-item: ^1.0.0 + mdast-util-to-markdown: ^1.0.0 + checksum: 7078cb985255208bcbce94a121906417d38353c6b1a9acbe56ee8888010d3500608b5d51c16b0999ac63ca58848fb13012d55f26930ff6c6f3450f053d56514e + languageName: node + linkType: hard + +"mdast-util-phrasing@npm:^3.0.0": + version: 3.0.1 + resolution: "mdast-util-phrasing@npm:3.0.1" + dependencies: + "@types/mdast": ^3.0.0 + unist-util-is: ^5.0.0 + checksum: c5b616d9b1eb76a6b351d195d94318494722525a12a89d9c8a3b091af7db3dd1fc55d294f9d29266d8159a8267b0df4a7a133bda8a3909d5331c383e1e1ff328 + languageName: node + linkType: hard + +"mdast-util-to-hast@npm:^12.1.0": + version: 12.3.0 + resolution: "mdast-util-to-hast@npm:12.3.0" + dependencies: + "@types/hast": ^2.0.0 + "@types/mdast": ^3.0.0 + mdast-util-definitions: ^5.0.0 + micromark-util-sanitize-uri: ^1.1.0 + trim-lines: ^3.0.0 + unist-util-generated: ^2.0.0 + unist-util-position: ^4.0.0 + unist-util-visit: ^4.0.0 + checksum: ea40c9f07dd0b731754434e81c913590c611b1fd753fa02550a1492aadfc30fb3adecaf62345ebb03cea2ddd250c15ab6e578fffde69c19955c9b87b10f2a9bb + languageName: node + linkType: hard + +"mdast-util-to-markdown@npm:^1.0.0, mdast-util-to-markdown@npm:^1.3.0": + version: 1.5.0 + resolution: "mdast-util-to-markdown@npm:1.5.0" + dependencies: + "@types/mdast": ^3.0.0 + "@types/unist": ^2.0.0 + longest-streak: ^3.0.0 + mdast-util-phrasing: ^3.0.0 + mdast-util-to-string: ^3.0.0 + micromark-util-decode-string: ^1.0.0 + unist-util-visit: ^4.0.0 + zwitch: ^2.0.0 + checksum: 64338eb33e49bb0aea417591fd986f72fdd39205052563bb7ce9eb9ecc160824509bfacd740086a05af355c6d5c36353aafe95cab9e6927d674478757cee6259 + languageName: node + linkType: hard + +"mdast-util-to-string@npm:^3.0.0, mdast-util-to-string@npm:^3.1.0": + version: 3.2.0 + resolution: "mdast-util-to-string@npm:3.2.0" + dependencies: + "@types/mdast": ^3.0.0 + checksum: dc40b544d54339878ae2c9f2b3198c029e1e07291d2126bd00ca28272ee6616d0d2194eb1c9828a7c34d412a79a7e73b26512a734698d891c710a1e73db1e848 + languageName: node + linkType: hard + +"mdn-data@npm:2.0.14": + version: 2.0.14 + resolution: "mdn-data@npm:2.0.14" + checksum: 9d0128ed425a89f4cba8f787dca27ad9408b5cb1b220af2d938e2a0629d17d879a34d2cb19318bdb26c3f14c77dd5dfbae67211f5caaf07b61b1f2c5c8c7dc16 + languageName: node + linkType: hard + +"media-typer@npm:0.3.0": + version: 0.3.0 + resolution: "media-typer@npm:0.3.0" + checksum: af1b38516c28ec95d6b0826f6c8f276c58aec391f76be42aa07646b4e39d317723e869700933ca6995b056db4b09a78c92d5440dc23657e6764be5d28874bba1 + languageName: node + linkType: hard + +"memfs@npm:^3.1.2, memfs@npm:^3.4.1": + version: 3.5.3 + resolution: "memfs@npm:3.5.3" + dependencies: + fs-monkey: ^1.0.4 + checksum: 18dfdeacad7c8047b976a6ccd58bc98ba76e122ad3ca0e50a21837fe2075fc0d9aafc58ab9cf2576c2b6889da1dd2503083f2364191b695273f40969db2ecc44 + languageName: node + linkType: hard + +"memfs@npm:^4.6.0": + version: 4.14.0 + resolution: "memfs@npm:4.14.0" + dependencies: + "@jsonjoy.com/json-pack": ^1.0.3 + "@jsonjoy.com/util": ^1.3.0 + tree-dump: ^1.0.1 + tslib: ^2.0.0 + checksum: 162e61510983488b0c524bd24191ab8be0f8a95636906ee305c10f2027e6a9f90831f42c072657aaf5fa859bb5132c5e97aa97aa96dfd959810327aec5b067c3 + languageName: node + linkType: hard + +"memjs@npm:^1.3.2": + version: 1.3.2 + resolution: "memjs@npm:1.3.2" + checksum: f92c2a43725b70af69832f807d02b87a07609a1c1f2c8c37670dff5bae6ac5f0d767cc8b3a6a59626703538f96c0bd4f03f9d00ea3b28aeb33270d24e8782233 + languageName: node + linkType: hard + +"memoize-one@npm:>=3.1.1 <6, memoize-one@npm:^5.1.1": + version: 5.2.1 + resolution: "memoize-one@npm:5.2.1" + checksum: a3cba7b824ebcf24cdfcd234aa7f86f3ad6394b8d9be4c96ff756dafb8b51c7f71320785fbc2304f1af48a0467cbbd2a409efc9333025700ed523f254cb52e3d + languageName: node + linkType: hard + +"merge-descriptors@npm:1.0.3": + version: 1.0.3 + resolution: "merge-descriptors@npm:1.0.3" + checksum: 52117adbe0313d5defa771c9993fe081e2d2df9b840597e966aadafde04ae8d0e3da46bac7ca4efc37d4d2b839436582659cd49c6a43eacb3fe3050896a105d1 + languageName: node + linkType: hard + +"merge-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-stream@npm:2.0.0" + checksum: 6fa4dcc8d86629705cea944a4b88ef4cb0e07656ebf223fa287443256414283dd25d91c1cd84c77987f2aec5927af1a9db6085757cb43d90eb170ebf4b47f4f4 + languageName: node + linkType: hard + +"merge2@npm:^1.3.0, merge2@npm:^1.4.1": + version: 1.4.1 + resolution: "merge2@npm:1.4.1" + checksum: 7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 + languageName: node + linkType: hard + +"methods@npm:^1.0.0, methods@npm:^1.1.2, methods@npm:~1.1.2": + version: 1.1.2 + resolution: "methods@npm:1.1.2" + checksum: 0917ff4041fa8e2f2fda5425a955fe16ca411591fbd123c0d722fcf02b73971ed6f764d85f0a6f547ce49ee0221ce2c19a5fa692157931cecb422984f1dcd13a + languageName: node + linkType: hard + +"micromark-core-commonmark@npm:^1.0.0, micromark-core-commonmark@npm:^1.0.1": + version: 1.1.0 + resolution: "micromark-core-commonmark@npm:1.1.0" + dependencies: + decode-named-character-reference: ^1.0.0 + micromark-factory-destination: ^1.0.0 + micromark-factory-label: ^1.0.0 + micromark-factory-space: ^1.0.0 + micromark-factory-title: ^1.0.0 + micromark-factory-whitespace: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-chunked: ^1.0.0 + micromark-util-classify-character: ^1.0.0 + micromark-util-html-tag-name: ^1.0.0 + micromark-util-normalize-identifier: ^1.0.0 + micromark-util-resolve-all: ^1.0.0 + micromark-util-subtokenize: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.1 + uvu: ^0.5.0 + checksum: c6dfedc95889cc73411cb222fc2330b9eda6d849c09c9fd9eb3cd3398af246167e9d3cdb0ae3ce9ae59dd34a14624c8330e380255d41279ad7350cf6c6be6c5b + languageName: node + linkType: hard + +"micromark-extension-gfm-autolink-literal@npm:^1.0.0": + version: 1.0.5 + resolution: "micromark-extension-gfm-autolink-literal@npm:1.0.5" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-sanitize-uri: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: ec2f6bc4a3eb238c1b8be9744454ffbc2957e3d8a248697af5a26bb21479862300c0e40e0a92baf17c299ddf70d4bc4470d4eee112cd92322f87d81e45c2e83d + languageName: node + linkType: hard + +"micromark-extension-gfm-footnote@npm:^1.0.0": + version: 1.1.2 + resolution: "micromark-extension-gfm-footnote@npm:1.1.2" + dependencies: + micromark-core-commonmark: ^1.0.0 + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-normalize-identifier: ^1.0.0 + micromark-util-sanitize-uri: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: c151a629ee1cd92363c018a50f926a002c944ac481ca72b3720b9529e9c20f1cbef98b0fefdcd2d594af37d0d9743673409cac488af0d2b194210fd16375dcb7 + languageName: node + linkType: hard + +"micromark-extension-gfm-strikethrough@npm:^1.0.0": + version: 1.0.7 + resolution: "micromark-extension-gfm-strikethrough@npm:1.0.7" + dependencies: + micromark-util-chunked: ^1.0.0 + micromark-util-classify-character: ^1.0.0 + micromark-util-resolve-all: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: 169e310a4408feade0df80180f60d48c5cc5b7070e5e75e0bbd914e9100273508162c4bb20b72d53081dc37f1ff5834b3afa137862576f763878552c03389811 + languageName: node + linkType: hard + +"micromark-extension-gfm-table@npm:^1.0.0": + version: 1.0.7 + resolution: "micromark-extension-gfm-table@npm:1.0.7" + dependencies: + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: 4853731285224e409d7e2c94c6ec849165093bff819e701221701aa7b7b34c17702c44f2f831e96b49dc27bb07e445b02b025561b68e62f5c3254415197e7af6 + languageName: node + linkType: hard + +"micromark-extension-gfm-tagfilter@npm:^1.0.0": + version: 1.0.2 + resolution: "micromark-extension-gfm-tagfilter@npm:1.0.2" + dependencies: + micromark-util-types: ^1.0.0 + checksum: 7d2441df51f890c86f8e7cf7d331a570b69c8105fa1c2fc5b737cb739502c16c8ee01cf35550a8a78f89497c5dfacc97cf82d55de6274e8320f3aec25e2b0dd2 + languageName: node + linkType: hard + +"micromark-extension-gfm-task-list-item@npm:^1.0.0": + version: 1.0.5 + resolution: "micromark-extension-gfm-task-list-item@npm:1.0.5" + dependencies: + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: 929f05343d272cffb8008899289f4cffe986ef98fc622ebbd1aa4ff11470e6b32ed3e1f18cd294adb69cabb961a400650078f6c12b322cc515b82b5068b31960 + languageName: node + linkType: hard + +"micromark-extension-gfm@npm:^2.0.0": + version: 2.0.3 + resolution: "micromark-extension-gfm@npm:2.0.3" + dependencies: + micromark-extension-gfm-autolink-literal: ^1.0.0 + micromark-extension-gfm-footnote: ^1.0.0 + micromark-extension-gfm-strikethrough: ^1.0.0 + micromark-extension-gfm-table: ^1.0.0 + micromark-extension-gfm-tagfilter: ^1.0.0 + micromark-extension-gfm-task-list-item: ^1.0.0 + micromark-util-combine-extensions: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: c4a917c16d7aa5d00d1767b5ce5f3b1a78c0de11dbd5c8f69d2545083568aa6bb13bd9d8e4c7fec5f4da10e7ed8344b15acffc843b33a615c17396a118bc2bc1 + languageName: node + linkType: hard + +"micromark-factory-destination@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-factory-destination@npm:1.1.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 9e2b5fb5fedbf622b687e20d51eb3d56ae90c0e7ecc19b37bd5285ec392c1e56f6e21aa7cfcb3c01eda88df88fe528f3acb91a5f57d7f4cba310bc3cd7f824fa + languageName: node + linkType: hard + +"micromark-factory-label@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-factory-label@npm:1.1.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: fcda48f1287d9b148c562c627418a2ab759cdeae9c8e017910a0cba94bb759a96611e1fc6df33182e97d28fbf191475237298983bb89ef07d5b02464b1ad28d5 + languageName: node + linkType: hard + +"micromark-factory-space@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-factory-space@npm:1.1.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: b58435076b998a7e244259a4694eb83c78915581206b6e7fc07b34c6abd36a1726ade63df8972fbf6c8fa38eecb9074f4e17be8d53f942e3b3d23d1a0ecaa941 + languageName: node + linkType: hard + +"micromark-factory-title@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-factory-title@npm:1.1.0" + dependencies: + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 4432d3dbc828c81f483c5901b0c6591a85d65a9e33f7d96ba7c3ae821617a0b3237ff5faf53a9152d00aaf9afb3a9f185b205590f40ed754f1d9232e0e9157b1 + languageName: node + linkType: hard + +"micromark-factory-whitespace@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-factory-whitespace@npm:1.1.0" + dependencies: + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: ef0fa682c7d593d85a514ee329809dee27d10bc2a2b65217d8ef81173e33b8e83c549049764b1ad851adfe0a204dec5450d9d20a4ca8598f6c94533a73f73fcd + languageName: node + linkType: hard + +"micromark-util-character@npm:^1.0.0": + version: 1.2.0 + resolution: "micromark-util-character@npm:1.2.0" + dependencies: + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 089e79162a19b4a28731736246579ab7e9482ac93cd681c2bfca9983dcff659212ef158a66a5957e9d4b1dba957d1b87b565d85418a5b009f0294f1f07f2aaac + languageName: node + linkType: hard + +"micromark-util-chunked@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-chunked@npm:1.1.0" + dependencies: + micromark-util-symbol: ^1.0.0 + checksum: c435bde9110cb595e3c61b7f54c2dc28ee03e6a57fa0fc1e67e498ad8bac61ee5a7457a2b6a73022ddc585676ede4b912d28dcf57eb3bd6951e54015e14dc20b + languageName: node + linkType: hard + +"micromark-util-classify-character@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-classify-character@npm:1.1.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: 8499cb0bb1f7fb946f5896285fcca65cd742f66cd3e79ba7744792bd413ec46834f932a286de650349914d02e822946df3b55d03e6a8e1d245d1ddbd5102e5b0 + languageName: node + linkType: hard + +"micromark-util-combine-extensions@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-combine-extensions@npm:1.1.0" + dependencies: + micromark-util-chunked: ^1.0.0 + micromark-util-types: ^1.0.0 + checksum: ee78464f5d4b61ccb437850cd2d7da4d690b260bca4ca7a79c4bb70291b84f83988159e373b167181b6716cb197e309bc6e6c96a68cc3ba9d50c13652774aba9 + languageName: node + linkType: hard + +"micromark-util-decode-numeric-character-reference@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-decode-numeric-character-reference@npm:1.1.0" + dependencies: + micromark-util-symbol: ^1.0.0 + checksum: 4733fe75146e37611243f055fc6847137b66f0cde74d080e33bd26d0408c1d6f44cabc984063eee5968b133cb46855e729d555b9ff8d744652262b7b51feec73 + languageName: node + linkType: hard + +"micromark-util-decode-string@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-decode-string@npm:1.1.0" + dependencies: + decode-named-character-reference: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-decode-numeric-character-reference: ^1.0.0 + micromark-util-symbol: ^1.0.0 + checksum: f1625155db452f15aa472918499689ba086b9c49d1322a08b22bfbcabe918c61b230a3002c8bc3ea9b1f52ca7a9bb1c3dd43ccb548c7f5f8b16c24a1ae77a813 + languageName: node + linkType: hard + +"micromark-util-encode@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-encode@npm:1.1.0" + checksum: 4ef29d02b12336918cea6782fa87c8c578c67463925221d4e42183a706bde07f4b8b5f9a5e1c7ce8c73bb5a98b261acd3238fecd152e6dd1cdfa2d1ae11b60a0 + languageName: node + linkType: hard + +"micromark-util-html-tag-name@npm:^1.0.0": + version: 1.2.0 + resolution: "micromark-util-html-tag-name@npm:1.2.0" + checksum: ccf0fa99b5c58676dc5192c74665a3bfd1b536fafaf94723bd7f31f96979d589992df6fcf2862eba290ef18e6a8efb30ec8e1e910d9f3fc74f208871e9f84750 + languageName: node + linkType: hard + +"micromark-util-normalize-identifier@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-normalize-identifier@npm:1.1.0" + dependencies: + micromark-util-symbol: ^1.0.0 + checksum: 8655bea41ffa4333e03fc22462cb42d631bbef9c3c07b625fd852b7eb442a110f9d2e5902a42e65188d85498279569502bf92f3434a1180fc06f7c37edfbaee2 + languageName: node + linkType: hard + +"micromark-util-resolve-all@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-resolve-all@npm:1.1.0" + dependencies: + micromark-util-types: ^1.0.0 + checksum: 1ce6c0237cd3ca061e76fae6602cf95014e764a91be1b9f10d36cb0f21ca88f9a07de8d49ab8101efd0b140a4fbfda6a1efb72027ab3f4d5b54c9543271dc52c + languageName: node + linkType: hard + +"micromark-util-sanitize-uri@npm:^1.0.0, micromark-util-sanitize-uri@npm:^1.1.0": + version: 1.2.0 + resolution: "micromark-util-sanitize-uri@npm:1.2.0" + dependencies: + micromark-util-character: ^1.0.0 + micromark-util-encode: ^1.0.0 + micromark-util-symbol: ^1.0.0 + checksum: 6663f365c4fe3961d622a580f4a61e34867450697f6806f027f21cf63c92989494895fcebe2345d52e249fe58a35be56e223a9776d084c9287818b40c779acc1 + languageName: node + linkType: hard + +"micromark-util-subtokenize@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-subtokenize@npm:1.1.0" + dependencies: + micromark-util-chunked: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.0 + uvu: ^0.5.0 + checksum: 4a9d780c4d62910e196ea4fd886dc4079d8e424e5d625c0820016da0ed399a281daff39c50f9288045cc4bcd90ab47647e5396aba500f0853105d70dc8b1fc45 + languageName: node + linkType: hard + +"micromark-util-symbol@npm:^1.0.0": + version: 1.1.0 + resolution: "micromark-util-symbol@npm:1.1.0" + checksum: 02414a753b79f67ff3276b517eeac87913aea6c028f3e668a19ea0fc09d98aea9f93d6222a76ca783d20299af9e4b8e7c797fe516b766185dcc6e93290f11f88 + languageName: node + linkType: hard + +"micromark-util-types@npm:^1.0.0, micromark-util-types@npm:^1.0.1": + version: 1.1.0 + resolution: "micromark-util-types@npm:1.1.0" + checksum: b0ef2b4b9589f15aec2666690477a6a185536927ceb7aa55a0f46475852e012d75a1ab945187e5c7841969a842892164b15d58ff8316b8e0d6cc920cabd5ede7 + languageName: node + linkType: hard + +"micromark@npm:^3.0.0": + version: 3.2.0 + resolution: "micromark@npm:3.2.0" + dependencies: + "@types/debug": ^4.0.0 + debug: ^4.0.0 + decode-named-character-reference: ^1.0.0 + micromark-core-commonmark: ^1.0.1 + micromark-factory-space: ^1.0.0 + micromark-util-character: ^1.0.0 + micromark-util-chunked: ^1.0.0 + micromark-util-combine-extensions: ^1.0.0 + micromark-util-decode-numeric-character-reference: ^1.0.0 + micromark-util-encode: ^1.0.0 + micromark-util-normalize-identifier: ^1.0.0 + micromark-util-resolve-all: ^1.0.0 + micromark-util-sanitize-uri: ^1.0.0 + micromark-util-subtokenize: ^1.0.0 + micromark-util-symbol: ^1.0.0 + micromark-util-types: ^1.0.1 + uvu: ^0.5.0 + checksum: 56c15851ad3eb8301aede65603473443e50c92a54849cac1dadd57e4ec33ab03a0a77f3df03de47133e6e8f695dae83b759b514586193269e98c0bf319ecd5e4 + languageName: node + linkType: hard + +"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": + version: 4.0.8 + resolution: "micromatch@npm:4.0.8" + dependencies: + braces: ^3.0.3 + picomatch: ^2.3.1 + checksum: 79920eb634e6f400b464a954fcfa589c4e7c7143209488e44baf627f9affc8b1e306f41f4f0deedde97e69cb725920879462d3e750ab3bd3c1aed675bb3a8966 + languageName: node + linkType: hard + +"miller-rabin@npm:^4.0.0": + version: 4.0.1 + resolution: "miller-rabin@npm:4.0.1" + dependencies: + bn.js: ^4.0.0 + brorand: ^1.0.1 + bin: + miller-rabin: bin/miller-rabin + checksum: 00cd1ab838ac49b03f236cc32a14d29d7d28637a53096bf5c6246a032a37749c9bd9ce7360cbf55b41b89b7d649824949ff12bc8eee29ac77c6b38eada619ece + languageName: node + linkType: hard + +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 0d99a03585f8b39d68182803b12ac601d9c01abfa28ec56204fa330bc9f3d1c5e14beb049bafadb3dbdf646dfb94b87e24d4ec7b31b7279ef906a8ea9b6a513f + languageName: node + linkType: hard + +"mime-db@npm:>= 1.43.0 < 2": + version: 1.53.0 + resolution: "mime-db@npm:1.53.0" + checksum: 3fd9380bdc0b085d0b56b580e4f89ca4fc3b823722310d795c248f0806b9a80afd5d8f4347f015ad943b9ecfa7cc0b71dffa0db96fa776d01a13474821a2c7fb + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: 1.52.0 + checksum: 89a5b7f1def9f3af5dad6496c5ed50191ae4331cc5389d7c521c8ad28d5fdad2d06fd81baf38fed813dc4e46bb55c8145bb0ff406330818c9cf712fb2e9b3836 + languageName: node + linkType: hard + +"mime@npm:1.6.0": + version: 1.6.0 + resolution: "mime@npm:1.6.0" + bin: + mime: cli.js + checksum: fef25e39263e6d207580bdc629f8872a3f9772c923c7f8c7e793175cee22777bbe8bba95e5d509a40aaa292d8974514ce634ae35769faa45f22d17edda5e8557 + languageName: node + linkType: hard + +"mime@npm:2.6.0": + version: 2.6.0 + resolution: "mime@npm:2.6.0" + bin: + mime: cli.js + checksum: 1497ba7b9f6960694268a557eae24b743fd2923da46ec392b042469f4b901721ba0adcf8b0d3c2677839d0e243b209d76e5edcbd09cfdeffa2dfb6bb4df4b862 + languageName: node + linkType: hard + +"mime@npm:^3.0.0": + version: 3.0.0 + resolution: "mime@npm:3.0.0" + bin: + mime: cli.js + checksum: f43f9b7bfa64534e6b05bd6062961681aeb406a5b53673b53b683f27fcc4e739989941836a355eef831f4478923651ecc739f4a5f6e20a76487b432bfd4db928 + languageName: node + linkType: hard + +"mimic-fn@npm:^2.1.0": + version: 2.1.0 + resolution: "mimic-fn@npm:2.1.0" + checksum: d2421a3444848ce7f84bd49115ddacff29c15745db73f54041edc906c14b131a38d05298dae3081667627a59b2eb1ca4b436ff2e1b80f69679522410418b478a + languageName: node + linkType: hard + +"mimic-response@npm:^2.0.0": + version: 2.1.0 + resolution: "mimic-response@npm:2.1.0" + checksum: 014fad6ab936657e5f2f48bd87af62a8e928ebe84472aaf9e14fec4fcb31257a5edff77324d8ac13ddc6685ba5135cf16e381efac324e5f174fb4ddbf902bf07 + languageName: node + linkType: hard + +"mimic-response@npm:^3.1.0": + version: 3.1.0 + resolution: "mimic-response@npm:3.1.0" + checksum: 25739fee32c17f433626bf19f016df9036b75b3d84a3046c7d156e72ec963dd29d7fc8a302f55a3d6c5a4ff24259676b15d915aad6480815a969ff2ec0836867 + languageName: node + linkType: hard + +"min-indent@npm:^1.0.0": + version: 1.0.1 + resolution: "min-indent@npm:1.0.1" + checksum: bfc6dd03c5eaf623a4963ebd94d087f6f4bbbfd8c41329a7f09706b0cb66969c4ddd336abeb587bc44bc6f08e13bf90f0b374f9d71f9f01e04adc2cd6f083ef1 + languageName: node + linkType: hard + +"mini-css-extract-plugin@npm:^2.4.2": + version: 2.9.1 + resolution: "mini-css-extract-plugin@npm:2.9.1" + dependencies: + schema-utils: ^4.0.0 + tapable: ^2.2.1 + peerDependencies: + webpack: ^5.0.0 + checksum: 036b0fbb207cf9a56e2f5f5dce5e35100cbd255e5b5a920a5357ec99215af16a77136020729b2d004a041d04ebb0a544b2f442535cbb982704dcd50297014c9e + languageName: node + linkType: hard + +"minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-assert@npm:1.0.1" + checksum: cc7974a9268fbf130fb055aff76700d7e2d8be5f761fb5c60318d0ed010d839ab3661a533ad29a5d37653133385204c503bfac995aaa4236f4e847461ea32ba7 + languageName: node + linkType: hard + +"minimalistic-crypto-utils@npm:^1.0.1": + version: 1.0.1 + resolution: "minimalistic-crypto-utils@npm:1.0.1" + checksum: 6e8a0422b30039406efd4c440829ea8f988845db02a3299f372fceba56ffa94994a9c0f2fd70c17f9969eedfbd72f34b5070ead9656a34d3f71c0bd72583a0ed + languageName: node + linkType: hard + +"minimatch@npm:3.1.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: ^1.1.7 + checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a + languageName: node + linkType: hard + +"minimatch@npm:9.0.3": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: ^2.0.1 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + languageName: node + linkType: hard + +"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" + dependencies: + brace-expansion: ^2.0.1 + checksum: 7564208ef81d7065a370f788d337cd80a689e981042cb9a1d0e6580b6c6a8c9279eba80010516e258835a988363f99f54a6f711a315089b8b42694f5da9d0d77 + languageName: node + linkType: hard + +"minimatch@npm:^7.4.2": + version: 7.4.6 + resolution: "minimatch@npm:7.4.6" + dependencies: + brace-expansion: ^2.0.1 + checksum: 1a6c8d22618df9d2a88aabeef1de5622eb7b558e9f8010be791cb6b0fa6e102d39b11c28d75b855a1e377b12edc7db8ff12a99c20353441caa6a05e78deb5da9 + languageName: node + linkType: hard + +"minimatch@npm:^8.0.2": + version: 8.0.4 + resolution: "minimatch@npm:8.0.4" + dependencies: + brace-expansion: ^2.0.1 + checksum: 2e46cffb86bacbc524ad45a6426f338920c529dd13f3a732cc2cf7618988ee1aae88df4ca28983285aca9e0f45222019ac2d14ebd17c1edadd2ee12221ab801a + languageName: node + linkType: hard + +"minimatch@npm:^9.0.0, minimatch@npm:^9.0.4": + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" + dependencies: + brace-expansion: ^2.0.1 + checksum: 2c035575eda1e50623c731ec6c14f65a85296268f749b9337005210bb2b34e2705f8ef1a358b188f69892286ab99dc42c8fb98a57bde55c8d81b3023c19cea28 + languageName: node + linkType: hard + +"minimatch@npm:~3.0.3": + version: 3.0.8 + resolution: "minimatch@npm:3.0.8" + dependencies: + brace-expansion: ^1.1.7 + checksum: 850cca179cad715133132693e6963b0db64ab0988c4d211415b087fc23a3e46321e2c5376a01bf5623d8782aba8bdf43c571e2e902e51fdce7175c7215c29f8b + languageName: node + linkType: hard + +"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": + version: 1.2.8 + resolution: "minimist@npm:1.2.8" + checksum: 75a6d645fb122dad29c06a7597bddea977258957ed88d7a6df59b5cd3fe4a527e253e9bbf2e783e4b73657f9098b96a5fe96ab8a113655d4109108577ecf85b0 + languageName: node + linkType: hard + +"minimisted@npm:^2.0.0": + version: 2.0.1 + resolution: "minimisted@npm:2.0.1" + dependencies: + minimist: ^1.2.5 + checksum: 6bc3df14558481c96764cfd6bf77a59f5838dec715c38c1e338193c1e56f536ba792ccbae84ff6632d13a7dd37ac888141c091d23733229b8d100148eec930aa + languageName: node + linkType: hard + +"minipass-collect@npm:^1.0.2": + version: 1.0.2 + resolution: "minipass-collect@npm:1.0.2" + dependencies: + minipass: ^3.0.0 + checksum: 14df761028f3e47293aee72888f2657695ec66bd7d09cae7ad558da30415fdc4752bbfee66287dcc6fd5e6a2fa3466d6c484dc1cbd986525d9393b9523d97f10 + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: ^7.0.3 + checksum: b251bceea62090f67a6cced7a446a36f4cd61ee2d5cea9aee7fff79ba8030e416327a1c5aa2908dc22629d06214b46d88fdab8c51ac76bacbf5703851b5ad342 + languageName: node + linkType: hard + +"minipass-fetch@npm:^2.0.3": + version: 2.1.2 + resolution: "minipass-fetch@npm:2.1.2" + dependencies: + encoding: ^0.1.13 + minipass: ^3.1.6 + minipass-sized: ^1.0.3 + minizlib: ^2.1.2 + dependenciesMeta: + encoding: + optional: true + checksum: 3f216be79164e915fc91210cea1850e488793c740534985da017a4cbc7a5ff50506956d0f73bb0cb60e4fe91be08b6b61ef35101706d3ef5da2c8709b5f08f91 + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" + dependencies: + encoding: ^0.1.13 + minipass: ^7.0.3 + minipass-sized: ^1.0.3 + minizlib: ^2.1.2 + dependenciesMeta: + encoding: + optional: true + checksum: 8047d273236157aab27ab7cd8eab7ea79e6ecd63e8f80c3366ec076cb9a0fed550a6935bab51764369027c414647fd8256c2a20c5445fb250c483de43350de83 + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: ^3.0.0 + checksum: 56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: ^3.0.0 + checksum: b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: ^3.0.0 + checksum: 79076749fcacf21b5d16dd596d32c3b6bf4d6e62abb43868fac21674078505c8b15eaca4e47ed844985a4514854f917d78f588fcd029693709417d8f98b2bd60 + languageName: node + linkType: hard + +"minipass@npm:^3.0.0, minipass@npm:^3.1.1, minipass@npm:^3.1.6": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: ^4.0.0 + checksum: a30d083c8054cee83cdcdc97f97e4641a3f58ae743970457b1489ce38ee1167b3aaf7d815cd39ec7a99b9c40397fd4f686e83750e73e652b21cb516f6d845e48 + languageName: node + linkType: hard + +"minipass@npm:^4.2.4": + version: 4.2.8 + resolution: "minipass@npm:4.2.8" + checksum: 7f4914d5295a9a30807cae5227a37a926e6d910c03f315930fde52332cf0575dfbc20295318f91f0baf0e6bb11a6f668e30cde8027dea7a11b9d159867a3c830 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": + version: 7.1.2 + resolution: "minipass@npm:7.1.2" + checksum: 2bfd325b95c555f2b4d2814d49325691c7bee937d753814861b0b49d5edcda55cbbf22b6b6a60bb91eddac8668771f03c5ff647dcd9d0f798e9548b9cdc46ee3 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: ^3.0.0 + yallist: ^4.0.0 + checksum: f1fdeac0b07cf8f30fcf12f4b586795b97be856edea22b5e9072707be51fc95d41487faec3f265b42973a304fe3a64acd91a44a3826a963e37b37bafde0212c3 + languageName: node + linkType: hard + +"minizlib@npm:^3.0.1": + version: 3.0.1 + resolution: "minizlib@npm:3.0.1" + dependencies: + minipass: ^7.0.4 + rimraf: ^5.0.5 + checksum: da0a53899252380475240c587e52c824f8998d9720982ba5c4693c68e89230718884a209858c156c6e08d51aad35700a3589987e540593c36f6713fe30cd7338 + languageName: node + linkType: hard + +"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac + languageName: node + linkType: hard + +"mkdirp@npm:^0.5.6": + version: 0.5.6 + resolution: "mkdirp@npm:0.5.6" + dependencies: + minimist: ^1.2.6 + bin: + mkdirp: bin/cmd.js + checksum: 0c91b721bb12c3f9af4b77ebf73604baf350e64d80df91754dc509491ae93bf238581e59c7188360cec7cb62fc4100959245a42cfe01834efedc5e9d068376c2 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: a96865108c6c3b1b8e1d5e9f11843de1e077e57737602de1b82030815f311be11f96f09cce59bd5b903d0b29834733e5313f9301e3ed6d6f6fba2eae0df4298f + languageName: node + linkType: hard + +"mkdirp@npm:^2.1.3": + version: 2.1.6 + resolution: "mkdirp@npm:2.1.6" + bin: + mkdirp: dist/cjs/src/bin.js + checksum: 8a1d09ffac585e55f41c54f445051f5bc33a7de99b952bb04c576cafdf1a67bb4bae8cb93736f7da6838771fbf75bc630430a3a59e1252047d2278690bd150ee + languageName: node + linkType: hard + +"mkdirp@npm:^3.0.1": + version: 3.0.1 + resolution: "mkdirp@npm:3.0.1" + bin: + mkdirp: dist/cjs/src/bin.js + checksum: 972deb188e8fb55547f1e58d66bd6b4a3623bf0c7137802582602d73e6480c1c2268dcbafbfb1be466e00cc7e56ac514d7fd9334b7cf33e3e2ab547c16f83a8d + languageName: node + linkType: hard + +"moo@npm:^0.5.0": + version: 0.5.2 + resolution: "moo@npm:0.5.2" + checksum: 5a41ddf1059fd0feb674d917c4774e41c877f1ca980253be4d3aae1a37f4bc513f88815041243f36f5cf67a62fb39324f3f997cf7fb17b6cb00767c165e7c499 + languageName: node + linkType: hard + +"morgan@npm:^1.10.0": + version: 1.10.0 + resolution: "morgan@npm:1.10.0" + dependencies: + basic-auth: ~2.0.1 + debug: 2.6.9 + depd: ~2.0.0 + on-finished: ~2.3.0 + on-headers: ~1.0.2 + checksum: fb41e226ab5a1abf7e8909e486b387076534716d60207e361acfb5df78b84d703a7b7ea58f3046a9fd0b83d3c94bfabde32323341a1f1b26ce50680abd2ea5dd + languageName: node + linkType: hard + +"mri@npm:^1.1.0, mri@npm:^1.2.0": + version: 1.2.0 + resolution: "mri@npm:1.2.0" + checksum: 83f515abbcff60150873e424894a2f65d68037e5a7fcde8a9e2b285ee9c13ac581b63cfc1e6826c4732de3aeb84902f7c1e16b7aff46cd3f897a0f757a894e85 + languageName: node + linkType: hard + +"ms@npm:2.0.0": + version: 2.0.0 + resolution: "ms@npm:2.0.0" + checksum: 0e6a22b8b746d2e0b65a430519934fefd41b6db0682e3477c10f60c76e947c4c0ad06f63ffdf1d78d335f83edee8c0aa928aa66a36c7cd95b69b26f468d527f4 + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: 673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f + languageName: node + linkType: hard + +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d + languageName: node + linkType: hard + +"msw@npm:1.3.3": + version: 1.3.3 + resolution: "msw@npm:1.3.3" + dependencies: + "@mswjs/cookies": ^0.2.2 + "@mswjs/interceptors": ^0.17.10 + "@open-draft/until": ^1.0.3 + "@types/cookie": ^0.4.1 + "@types/js-levenshtein": ^1.1.1 + chalk: ^4.1.1 + chokidar: ^3.4.2 + cookie: ^0.4.2 + graphql: ^16.8.1 + headers-polyfill: 3.2.5 + inquirer: ^8.2.0 + is-node-process: ^1.2.0 + js-levenshtein: ^1.1.6 + node-fetch: ^2.6.7 + outvariant: ^1.4.0 + path-to-regexp: ^6.2.0 + strict-event-emitter: ^0.4.3 + type-fest: ^2.19.0 + yargs: ^17.3.1 + peerDependencies: + typescript: ">= 4.4.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: cb3fda1519485f219d36c4e5ac1e1190ffe77dab66121c88cb9db0bace1ecb5a45c83db49e68e7c688b330ce43eed17d00939e09812dc710c0d4b3e59925730c + languageName: node + linkType: hard + +"msw@npm:^1.0.0": + version: 1.3.4 + resolution: "msw@npm:1.3.4" + dependencies: + "@mswjs/cookies": ^0.2.2 + "@mswjs/interceptors": ^0.17.10 + "@open-draft/until": ^1.0.3 + "@types/cookie": ^0.4.1 + "@types/js-levenshtein": ^1.1.1 + chalk: ^4.1.1 + chokidar: ^3.4.2 + cookie: ^0.4.2 + graphql: ^16.8.1 + headers-polyfill: 3.2.5 + inquirer: ^8.2.0 + is-node-process: ^1.2.0 + js-levenshtein: ^1.1.6 + node-fetch: ^2.6.7 + outvariant: ^1.4.0 + path-to-regexp: ^6.2.0 + strict-event-emitter: ^0.4.3 + type-fest: ^2.19.0 + yargs: ^17.3.1 + peerDependencies: + typescript: ">= 4.4.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 57646ecb831e98f00387e60bad4d535e426d406ae2645340e59500c219059be225f1f02a5ff21aee9daeb7a8bdde922a00fb82930781d27e3f3fdaf6b292c25f + languageName: node + linkType: hard + +"multicast-dns@npm:^7.2.5": + version: 7.2.5 + resolution: "multicast-dns@npm:7.2.5" + dependencies: + dns-packet: ^5.2.2 + thunky: ^1.0.2 + bin: + multicast-dns: cli.js + checksum: 00b8a57df152d4cd0297946320a94b7c3cdf75a46a2247f32f958a8927dea42958177f9b7fdae69fab2e4e033fb3416881af1f5e9055a3e1542888767139e2fb + languageName: node + linkType: hard + +"mute-stream@npm:0.0.8": + version: 0.0.8 + resolution: "mute-stream@npm:0.0.8" + checksum: ff48d251fc3f827e5b1206cda0ffdaec885e56057ee86a3155e1951bc940fd5f33531774b1cc8414d7668c10a8907f863f6561875ee6e8768931a62121a531a1 + languageName: node + linkType: hard + +"mysql2@npm:^3.0.0": + version: 3.11.3 + resolution: "mysql2@npm:3.11.3" + dependencies: + aws-ssl-profiles: ^1.1.1 + denque: ^2.1.0 + generate-function: ^2.3.1 + iconv-lite: ^0.6.3 + long: ^5.2.1 + lru.min: ^1.0.0 + named-placeholders: ^1.1.3 + seq-queue: ^0.0.5 + sqlstring: ^2.3.2 + checksum: ae62b5b997da429a33f0762158db965d22daece0030e75ac8e822b0b342ad082fa5f1ead87d922d0ba93595e9d5491036916ef91457c762517f25f41afa6e2d9 + languageName: node + linkType: hard + +"mz@npm:^2.4.0, mz@npm:^2.7.0": + version: 2.7.0 + resolution: "mz@npm:2.7.0" + dependencies: + any-promise: ^1.0.0 + object-assign: ^4.0.1 + thenify-all: ^1.0.0 + checksum: 8427de0ece99a07e9faed3c0c6778820d7543e3776f9a84d22cf0ec0a8eb65f6e9aee9c9d353ff9a105ff62d33a9463c6ca638974cc652ee8140cd1e35951c87 + languageName: node + linkType: hard + +"named-placeholders@npm:^1.1.3": + version: 1.1.3 + resolution: "named-placeholders@npm:1.1.3" + dependencies: + lru-cache: ^7.14.1 + checksum: 7834adc91e92ae1b9c4413384e3ccd297de5168bb44017ff0536705ddc4db421723bd964607849265feb3f6ded390f84cf138e5925f22f7c13324f87a803dc73 + languageName: node + linkType: hard + +"nan@npm:^2.17.0, nan@npm:^2.19.0, nan@npm:^2.20.0": + version: 2.22.0 + resolution: "nan@npm:2.22.0" + dependencies: + node-gyp: latest + checksum: 222e3a090e326c72f6782d948f44ee9b81cfb2161d5fe53216f04426a273fd094deee9dcc6813096dd2397689a2b10c1a92d3885d2e73fd2488a51547beb2929 + languageName: node + linkType: hard + +"nano-css@npm:^5.6.2": + version: 5.6.2 + resolution: "nano-css@npm:5.6.2" + dependencies: + "@jridgewell/sourcemap-codec": ^1.4.15 + css-tree: ^1.1.2 + csstype: ^3.1.2 + fastest-stable-stringify: ^2.0.2 + inline-style-prefixer: ^7.0.1 + rtl-css-js: ^1.16.1 + stacktrace-js: ^2.0.2 + stylis: ^4.3.0 + peerDependencies: + react: "*" + react-dom: "*" + checksum: 85d5e730798387bee3090e9943801489ec4269bd376a848b75515cf0f44dc7ce53d4a9fec575081a7dff53a8a5d4b00eebdc1bbf217d75fae7195819f917aba1 + languageName: node + linkType: hard + +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" + bin: + nanoid: bin/nanoid.cjs + checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2 + languageName: node + linkType: hard + +"napi-build-utils@npm:^1.0.1": + version: 1.0.2 + resolution: "napi-build-utils@npm:1.0.2" + checksum: 06c14271ee966e108d55ae109f340976a9556c8603e888037145d6522726aebe89dd0c861b4b83947feaf6d39e79e08817559e8693deedc2c94e82c5cbd090c7 + languageName: node + linkType: hard + +"natural-compare@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare@npm:1.4.0" + checksum: 23ad088b08f898fc9b53011d7bb78ec48e79de7627e01ab5518e806033861bef68d5b0cd0e2205c2f36690ac9571ff6bcb05eb777ced2eeda8d4ac5b44592c3d + languageName: node + linkType: hard + +"nearley@npm:^2.20.1": + version: 2.20.1 + resolution: "nearley@npm:2.20.1" + dependencies: + commander: ^2.19.0 + moo: ^0.5.0 + railroad-diagrams: ^1.0.0 + randexp: 0.4.6 + bin: + nearley-railroad: bin/nearley-railroad.js + nearley-test: bin/nearley-test.js + nearley-unparse: bin/nearley-unparse.js + nearleyc: bin/nearleyc.js + checksum: 42c2c330c13c7991b48221c5df00f4352c2f8851636ae4d1f8ca3c8e193fc1b7668c78011d1cad88cca4c1c4dc087425420629c19cc286d7598ec15533aaef26 + languageName: node + linkType: hard + +"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 + languageName: node + linkType: hard + +"neo-async@npm:^2.6.2": + version: 2.6.2 + resolution: "neo-async@npm:2.6.2" + checksum: deac9f8d00eda7b2e5cd1b2549e26e10a0faa70adaa6fdadca701cc55f49ee9018e427f424bac0c790b7c7e2d3068db97f3093f1093975f2acb8f8818b936ed9 + languageName: node + linkType: hard + +"netmask@npm:^2.0.2": + version: 2.0.2 + resolution: "netmask@npm:2.0.2" + checksum: c65cb8d3f7ea5669edddb3217e4c96910a60d0d9a4b52d9847ff6b28b2d0277cd8464eee0ef85133cdee32605c57940cacdd04a9a019079b091b6bba4cb0ec22 + languageName: node + linkType: hard + +"nimma@npm:0.2.2": + version: 0.2.2 + resolution: "nimma@npm:0.2.2" + dependencies: + "@jsep-plugin/regex": ^1.0.1 + "@jsep-plugin/ternary": ^1.0.2 + astring: ^1.8.1 + jsep: ^1.2.0 + jsonpath-plus: ^6.0.1 + lodash.topath: ^4.5.2 + dependenciesMeta: + jsonpath-plus: + optional: true + lodash.topath: + optional: true + checksum: 09369253a962e6cdddd37c4994d414a5fa00abc955c4d91946140b45b57465749a9f05663a64812ad5ac70caacb7ca22a8fc7c8db002032d0768c83dbba7b3ad + languageName: node + linkType: hard + +"no-case@npm:^3.0.4": + version: 3.0.4 + resolution: "no-case@npm:3.0.4" + dependencies: + lower-case: ^2.0.2 + tslib: ^2.0.3 + checksum: 0b2ebc113dfcf737d48dde49cfebf3ad2d82a8c3188e7100c6f375e30eafbef9e9124aadc3becef237b042fd5eb0aad2fd78669c20972d045bbe7fea8ba0be5c + languageName: node + linkType: hard + +"node-abi@npm:^3.3.0": + version: 3.71.0 + resolution: "node-abi@npm:3.71.0" + dependencies: + semver: ^7.3.5 + checksum: d7f34c294c0351b636688a792e41493840cc195f64a76ecdc35eb0c1682d86e633a932b03e924395b0d2f52ca1db5046898839d57bcfb5819226e64e922b0617 + languageName: node + linkType: hard + +"node-abort-controller@npm:^3.0.1": + version: 3.1.1 + resolution: "node-abort-controller@npm:3.1.1" + checksum: 2c340916af9710328b11c0828223fc65ba320e0d082214a211311bf64c2891028e42ef276b9799188c4ada9e6e1c54cf7a0b7c05dd9d59fcdc8cd633304c8047 + languageName: node + linkType: hard + +"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9, node-fetch@npm:^2.7.0": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: ^5.0.0 + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5 + languageName: node + linkType: hard + +"node-forge@npm:^1, node-forge@npm:^1.3.1": + version: 1.3.1 + resolution: "node-forge@npm:1.3.1" + checksum: 08fb072d3d670599c89a1704b3e9c649ff1b998256737f0e06fbd1a5bf41cae4457ccaee32d95052d80bbafd9ffe01284e078c8071f0267dc9744e51c5ed42a9 + languageName: node + linkType: hard + +"node-gyp@npm:^9.0.0": + version: 9.4.1 + resolution: "node-gyp@npm:9.4.1" + dependencies: + env-paths: ^2.2.0 + exponential-backoff: ^3.1.1 + glob: ^7.1.4 + graceful-fs: ^4.2.6 + make-fetch-happen: ^10.0.3 + nopt: ^6.0.0 + npmlog: ^6.0.0 + rimraf: ^3.0.2 + semver: ^7.3.5 + tar: ^6.1.2 + which: ^2.0.2 + bin: + node-gyp: bin/node-gyp.js + checksum: 8576c439e9e925ab50679f87b7dfa7aa6739e42822e2ad4e26c36341c0ba7163fdf5a946f0a67a476d2f24662bc40d6c97bd9e79ced4321506738e6b760a1577 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 10.2.0 + resolution: "node-gyp@npm:10.2.0" + dependencies: + env-paths: ^2.2.0 + exponential-backoff: ^3.1.1 + glob: ^10.3.10 + graceful-fs: ^4.2.6 + make-fetch-happen: ^13.0.0 + nopt: ^7.0.0 + proc-log: ^4.1.0 + semver: ^7.3.5 + tar: ^6.2.1 + which: ^4.0.0 + bin: + node-gyp: bin/node-gyp.js + checksum: 0233759d8c19765f7fdc259a35eb046ad86c3d09e22f7384613ae2b89647dd27fcf833fdf5293d9335041e91f9b1c539494225959cdb312a5c8080b7534b926f + languageName: node + linkType: hard + +"node-int64@npm:^0.4.0": + version: 0.4.0 + resolution: "node-int64@npm:0.4.0" + checksum: d0b30b1ee6d961851c60d5eaa745d30b5c95d94bc0e74b81e5292f7c42a49e3af87f1eb9e89f59456f80645d679202537de751b7d72e9e40ceea40c5e449057e + languageName: node + linkType: hard + +"node-libs-browser@npm:^2.2.1": + version: 2.2.1 + resolution: "node-libs-browser@npm:2.2.1" + dependencies: + assert: ^1.1.1 + browserify-zlib: ^0.2.0 + buffer: ^4.3.0 + console-browserify: ^1.1.0 + constants-browserify: ^1.0.0 + crypto-browserify: ^3.11.0 + domain-browser: ^1.1.1 + events: ^3.0.0 + https-browserify: ^1.0.0 + os-browserify: ^0.3.0 + path-browserify: 0.0.1 + process: ^0.11.10 + punycode: ^1.2.4 + querystring-es3: ^0.2.0 + readable-stream: ^2.3.3 + stream-browserify: ^2.0.1 + stream-http: ^2.7.2 + string_decoder: ^1.0.0 + timers-browserify: ^2.0.4 + tty-browserify: 0.0.0 + url: ^0.11.0 + util: ^0.11.0 + vm-browserify: ^1.0.1 + checksum: 41fa7927378edc0cb98a8cc784d3f4a47e43378d3b42ec57a23f81125baa7287c4b54d6d26d062072226160a3ce4d8b7a62e873d2fb637aceaddf71f5a26eca0 + languageName: node + linkType: hard + +"node-machine-id@npm:^1.1.12": + version: 1.1.12 + resolution: "node-machine-id@npm:1.1.12" + checksum: e23088a0fb4a77a1d6484b7f09a22992fd3e0054d4f2e427692b4c7081e6cf30118ba07b6113b6c89f1ce46fd26ec5ab1d76dcaf6c10317717889124511283a5 + languageName: node + linkType: hard + +"node-releases@npm:^2.0.18": + version: 2.0.18 + resolution: "node-releases@npm:2.0.18" + checksum: ef55a3d853e1269a6d6279b7692cd6ff3e40bc74947945101138745bfdc9a5edabfe72cb19a31a8e45752e1910c4c65c77d931866af6357f242b172b7283f5b3 + languageName: node + linkType: hard + +"node-sarif-builder@npm:^2.0.3": + version: 2.0.3 + resolution: "node-sarif-builder@npm:2.0.3" + dependencies: + "@types/sarif": ^2.1.4 + fs-extra: ^10.0.0 + checksum: 397dd9bfb0780c6753fb47d1fd0465f3c8a935082cb1bbd7ad6232d18b6343d9d499c6bc572ad0415db282efd6058fe8b7a6657020434adef4fbf93a8b95306e + languageName: node + linkType: hard + +"node-schedule@npm:2.1.1": + version: 2.1.1 + resolution: "node-schedule@npm:2.1.1" + dependencies: + cron-parser: ^4.2.0 + long-timeout: 0.1.1 + sorted-array-functions: ^1.3.0 + checksum: 6a8822b16fb024277c42efe710bdb35b6f1f6ab3a2f826283640511247d693f34ebd5ddf2863cd91609e7f323574e36c81cd2084dc204fa521f931380f0f963f + languageName: node + linkType: hard + +"nopt@npm:^5.0.0": + version: 5.0.0 + resolution: "nopt@npm:5.0.0" + dependencies: + abbrev: 1 + bin: + nopt: bin/nopt.js + checksum: d35fdec187269503843924e0114c0c6533fb54bbf1620d0f28b4b60ba01712d6687f62565c55cc20a504eff0fbe5c63e22340c3fad549ad40469ffb611b04f2f + languageName: node + linkType: hard + +"nopt@npm:^6.0.0": + version: 6.0.0 + resolution: "nopt@npm:6.0.0" + dependencies: + abbrev: ^1.0.0 + bin: + nopt: bin/nopt.js + checksum: 82149371f8be0c4b9ec2f863cc6509a7fd0fa729929c009f3a58e4eb0c9e4cae9920e8f1f8eb46e7d032fec8fb01bede7f0f41a67eb3553b7b8e14fa53de1dac + languageName: node + linkType: hard + +"nopt@npm:^7.0.0": + version: 7.2.1 + resolution: "nopt@npm:7.2.1" + dependencies: + abbrev: ^2.0.0 + bin: + nopt: bin/nopt.js + checksum: 6fa729cc77ce4162cfad8abbc9ba31d4a0ff6850c3af61d59b505653bef4781ec059f8890ecfe93ee8aa0c511093369cca88bfc998101616a2904e715bbbb7c9 + languageName: node + linkType: hard + +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: 88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20 + languageName: node + linkType: hard + +"normalize-url@npm:^6.0.1": + version: 6.1.0 + resolution: "normalize-url@npm:6.1.0" + checksum: 4a4944631173e7d521d6b80e4c85ccaeceb2870f315584fa30121f505a6dfd86439c5e3fdd8cd9e0e291290c41d0c3599f0cb12ab356722ed242584c30348e50 + languageName: node + linkType: hard + +"npm-bundled@npm:^2.0.0": + version: 2.0.1 + resolution: "npm-bundled@npm:2.0.1" + dependencies: + npm-normalize-package-bin: ^2.0.0 + checksum: 7747293985c48c5268871efe691545b03731cb80029692000cbdb0b3344b9617be5187aa36281cabbe6b938e3651b4e87236d1c31f9e645eef391a1a779413e6 + languageName: node + linkType: hard + +"npm-normalize-package-bin@npm:^2.0.0": + version: 2.0.0 + resolution: "npm-normalize-package-bin@npm:2.0.0" + checksum: 7c5379f9b188b564c4332c97bdd9a5d6b7b15f02b5823b00989d6a0e6fb31eb0280f02b0a924f930e1fcaf00e60fae333aec8923d2a4c7747613c7d629d8aa25 + languageName: node + linkType: hard + +"npm-packlist@npm:^5.0.0": + version: 5.1.3 + resolution: "npm-packlist@npm:5.1.3" + dependencies: + glob: ^8.0.1 + ignore-walk: ^5.0.1 + npm-bundled: ^2.0.0 + npm-normalize-package-bin: ^2.0.0 + bin: + npm-packlist: bin/index.js + checksum: 94cc9c66740e8f80243301de85eb0a2cec5bbd570c3f26b6ad7af1a3eca155f7e810580dc7ea4448f12a8fd82f6db307e7132a5fe69e157eb45b325acadeb22a + languageName: node + linkType: hard + +"npm-run-path@npm:^4.0.1": + version: 4.0.1 + resolution: "npm-run-path@npm:4.0.1" + dependencies: + path-key: ^3.0.0 + checksum: 5374c0cea4b0bbfdfae62da7bbdf1e1558d338335f4cacf2515c282ff358ff27b2ecb91ffa5330a8b14390ac66a1e146e10700440c1ab868208430f56b5f4d23 + languageName: node + linkType: hard + +"npmlog@npm:^5.0.1": + version: 5.0.1 + resolution: "npmlog@npm:5.0.1" + dependencies: + are-we-there-yet: ^2.0.0 + console-control-strings: ^1.1.0 + gauge: ^3.0.0 + set-blocking: ^2.0.0 + checksum: 516b2663028761f062d13e8beb3f00069c5664925871a9b57989642ebe09f23ab02145bf3ab88da7866c4e112cafff72401f61a672c7c8a20edc585a7016ef5f + languageName: node + linkType: hard + +"npmlog@npm:^6.0.0": + version: 6.0.2 + resolution: "npmlog@npm:6.0.2" + dependencies: + are-we-there-yet: ^3.0.0 + console-control-strings: ^1.1.0 + gauge: ^4.0.3 + set-blocking: ^2.0.0 + checksum: ae238cd264a1c3f22091cdd9e2b106f684297d3c184f1146984ecbe18aaa86343953f26b9520dedd1b1372bc0316905b736c1932d778dbeb1fcf5a1001390e2a + languageName: node + linkType: hard + +"nth-check@npm:^2.0.1": + version: 2.1.1 + resolution: "nth-check@npm:2.1.1" + dependencies: + boolbase: ^1.0.0 + checksum: 5afc3dafcd1573b08877ca8e6148c52abd565f1d06b1eb08caf982e3fa289a82f2cae697ffb55b5021e146d60443f1590a5d6b944844e944714a5b549675bcd3 + languageName: node + linkType: hard + +"nwsapi@npm:^2.2.2": + version: 2.2.13 + resolution: "nwsapi@npm:2.2.13" + checksum: d34fb7838517c3c7e8cc824e443275b08b57f6a025a860693d18c56ddcfd176e32df9bf0ae7f5a95c7a32981501caa1f9fda31b59f28aa72a4b9d01f573a8e6b + languageName: node + linkType: hard + +"oauth-sign@npm:~0.9.0": + version: 0.9.0 + resolution: "oauth-sign@npm:0.9.0" + checksum: 8f5497a127967866a3c67094c21efd295e46013a94e6e828573c62220e9af568cc1d2d04b16865ba583e430510fa168baf821ea78f355146d8ed7e350fc44c64 + languageName: node + linkType: hard + +"object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.1": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f + languageName: node + linkType: hard + +"object-hash@npm:^2.2.0": + version: 2.2.0 + resolution: "object-hash@npm:2.2.0" + checksum: 55ba841e3adce9c4f1b9b46b41983eda40f854e0d01af2802d3ae18a7085a17168d6b81731d43fdf1d6bcbb3c9f9c56d22c8fea992203ad90a38d7d919bc28f1 + languageName: node + linkType: hard + +"object-inspect@npm:^1.13.1": + version: 1.13.2 + resolution: "object-inspect@npm:1.13.2" + checksum: 9f850b3c045db60e0e97746e809ee4090d6ce62195af17dd1e9438ac761394a7d8ec4f7906559aea5424eaf61e35d3e53feded2ccd5f62fcc7d9670d3c8eb353 + languageName: node + linkType: hard + +"object-is@npm:^1.1.5": + version: 1.1.6 + resolution: "object-is@npm:1.1.6" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + checksum: 3ea22759967e6f2380a2cbbd0f737b42dc9ddb2dfefdb159a1b927fea57335e1b058b564bfa94417db8ad58cddab33621a035de6f5e5ad56d89f2dd03e66c6a1 + languageName: node + linkType: hard + +"object-keys@npm:^1.1.1": + version: 1.1.1 + resolution: "object-keys@npm:1.1.1" + checksum: b363c5e7644b1e1b04aa507e88dcb8e3a2f52b6ffd0ea801e4c7a62d5aa559affe21c55a07fd4b1fd55fc03a33c610d73426664b20032405d7b92a1414c34d6a + languageName: node + linkType: hard + +"object.assign@npm:^4.1.4, object.assign@npm:^4.1.5": + version: 4.1.5 + resolution: "object.assign@npm:4.1.5" + dependencies: + call-bind: ^1.0.5 + define-properties: ^1.2.1 + has-symbols: ^1.0.3 + object-keys: ^1.1.1 + checksum: f9aeac0541661370a1fc86e6a8065eb1668d3e771f7dbb33ee54578201336c057b21ee61207a186dd42db0c62201d91aac703d20d12a79fc79c353eed44d4e25 + languageName: node + linkType: hard + +"object.entries@npm:^1.1.8": + version: 1.1.8 + resolution: "object.entries@npm:1.1.8" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-object-atoms: ^1.0.0 + checksum: 5314877cb637ef3437a30bba61d9bacdb3ce74bf73ac101518be0633c37840c8cc67407edb341f766e8093b3d7516d5c3358f25adfee4a2c697c0ec4c8491907 + languageName: node + linkType: hard + +"object.fromentries@npm:^2.0.8": + version: 2.0.8 + resolution: "object.fromentries@npm:2.0.8" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-abstract: ^1.23.2 + es-object-atoms: ^1.0.0 + checksum: 29b2207a2db2782d7ced83f93b3ff5d425f901945f3665ffda1821e30a7253cd1fd6b891a64279976098137ddfa883d748787a6fea53ecdb51f8df8b8cec0ae1 + languageName: node + linkType: hard + +"object.groupby@npm:^1.0.3": + version: 1.0.3 + resolution: "object.groupby@npm:1.0.3" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-abstract: ^1.23.2 + checksum: 0d30693ca3ace29720bffd20b3130451dca7a56c612e1926c0a1a15e4306061d84410bdb1456be2656c5aca53c81b7a3661eceaa362db1bba6669c2c9b6d1982 + languageName: node + linkType: hard + +"object.values@npm:^1.1.6, object.values@npm:^1.2.0": + version: 1.2.0 + resolution: "object.values@npm:1.2.0" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-object-atoms: ^1.0.0 + checksum: 51fef456c2a544275cb1766897f34ded968b22adfc13ba13b5e4815fdaf4304a90d42a3aee114b1f1ede048a4890381d47a5594d84296f2767c6a0364b9da8fa + languageName: node + linkType: hard + +"obuf@npm:^1.0.0, obuf@npm:^1.1.2": + version: 1.1.2 + resolution: "obuf@npm:1.1.2" + checksum: 41a2ba310e7b6f6c3b905af82c275bf8854896e2e4c5752966d64cbcd2f599cfffd5932006bcf3b8b419dfdacebb3a3912d5d94e10f1d0acab59876c8757f27f + languageName: node + linkType: hard + +"oidc-token-hash@npm:^5.0.3": + version: 5.0.3 + resolution: "oidc-token-hash@npm:5.0.3" + checksum: 35fa19aea9ff2c509029ec569d74b778c8a215b92bd5e6e9bc4ebbd7ab035f44304ff02430a6397c3fb7c1d15ebfa467807ca0bcd31d06ba610b47798287d303 + languageName: node + linkType: hard + +"on-finished@npm:2.4.1, on-finished@npm:^2.3.0, on-finished@npm:^2.4.1": + version: 2.4.1 + resolution: "on-finished@npm:2.4.1" + dependencies: + ee-first: 1.1.1 + checksum: d20929a25e7f0bb62f937a425b5edeb4e4cde0540d77ba146ec9357f00b0d497cdb3b9b05b9c8e46222407d1548d08166bff69cc56dfa55ba0e4469228920ff0 + languageName: node + linkType: hard + +"on-finished@npm:~2.3.0": + version: 2.3.0 + resolution: "on-finished@npm:2.3.0" + dependencies: + ee-first: 1.1.1 + checksum: 1db595bd963b0124d6fa261d18320422407b8f01dc65863840f3ddaaf7bcad5b28ff6847286703ca53f4ec19595bd67a2f1253db79fc4094911ec6aa8df1671b + languageName: node + linkType: hard + +"on-headers@npm:~1.0.2": + version: 1.0.2 + resolution: "on-headers@npm:1.0.2" + checksum: 2bf13467215d1e540a62a75021e8b318a6cfc5d4fc53af8e8f84ad98dbcea02d506c6d24180cd62e1d769c44721ba542f3154effc1f7579a8288c9f7873ed8e5 + languageName: node + linkType: hard + +"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: 1 + checksum: cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 + languageName: node + linkType: hard + +"one-time@npm:^1.0.0": + version: 1.0.0 + resolution: "one-time@npm:1.0.0" + dependencies: + fn.name: 1.x.x + checksum: fd008d7e992bdec1c67f53a2f9b46381ee12a9b8c309f88b21f0223546003fb47e8ad7c1fd5843751920a8d276c63bd4b45670ef80c61fb3e07dbccc962b5c7d + languageName: node + linkType: hard + +"onetime@npm:^5.1.0, onetime@npm:^5.1.2": + version: 5.1.2 + resolution: "onetime@npm:5.1.2" + dependencies: + mimic-fn: ^2.1.0 + checksum: 2478859ef817fc5d4e9c2f9e5728512ddd1dbc9fb7829ad263765bb6d3b91ce699d6e2332eef6b7dff183c2f490bd3349f1666427eaba4469fba0ac38dfd0d34 + languageName: node + linkType: hard + +"only@npm:~0.0.2": + version: 0.0.2 + resolution: "only@npm:0.0.2" + checksum: d399710db867a1ef436dd3ce74499c87ece794aa81ab0370b5d153968766ee4aed2f98d3f92fc87c963e45b7a74d400d6f463ef651a5e7cfb861b15e88e9efe6 + languageName: node + linkType: hard + +"open@npm:^10.0.3": + version: 10.1.0 + resolution: "open@npm:10.1.0" + dependencies: + default-browser: ^5.2.1 + define-lazy-prop: ^3.0.0 + is-inside-container: ^1.0.0 + is-wsl: ^3.1.0 + checksum: 079b0771616bac13b08129b0300032dc9328d72f345e460dd0416b8a8196a5bdf5e0251fefec8aa2a6a97c736734ac65dd8f1d29ab3fc9a13e85624aa5bc4470 + languageName: node + linkType: hard + +"open@npm:^8.0.0, open@npm:^8.4.0": + version: 8.4.2 + resolution: "open@npm:8.4.2" + dependencies: + define-lazy-prop: ^2.0.0 + is-docker: ^2.1.1 + is-wsl: ^2.2.0 + checksum: 6388bfff21b40cb9bd8f913f9130d107f2ed4724ea81a8fd29798ee322b361ca31fa2cdfb491a5c31e43a3996cfe9566741238c7a741ada8d7af1cb78d85cf26 + languageName: node + linkType: hard + +"openapi-types@npm:^12.0.2": + version: 12.1.3 + resolution: "openapi-types@npm:12.1.3" + checksum: 7fa5547f87a58d2aa0eba6e91d396f42d7d31bc3ae140e61b5d60b47d2fd068b48776f42407d5a8da7280cf31195aa128c2fc285e8bb871d1105edee5647a0bb + languageName: node + linkType: hard + +"openid-client@npm:^5.3.0": + version: 5.7.0 + resolution: "openid-client@npm:5.7.0" + dependencies: + jose: ^4.15.9 + lru-cache: ^6.0.0 + object-hash: ^2.2.0 + oidc-token-hash: ^5.0.3 + checksum: 63fc76918fc12f3d6e1456a0b170f417defccf6820acb4581ffc226cb8c9a18d50f76f0982d7a00cce2896c732eb2a6361ad6ea04b127b2603e56408b680ef9c + languageName: node + linkType: hard + +"oppa@npm:^0.4.0": + version: 0.4.0 + resolution: "oppa@npm:0.4.0" + dependencies: + chalk: ^4.1.1 + checksum: ecc43e63ede05c3ccb10e0f2c3f3020a6d72e1a3b318f3e37b8cc8a1a279e300991c043e5385d560c1eebb54a56c7f9b69bf0db0d1933acf350bcd2980c96055 + languageName: node + linkType: hard + +"optionator@npm:^0.8.1": + version: 0.8.3 + resolution: "optionator@npm:0.8.3" + dependencies: + deep-is: ~0.1.3 + fast-levenshtein: ~2.0.6 + levn: ~0.3.0 + prelude-ls: ~1.1.2 + type-check: ~0.3.2 + word-wrap: ~1.2.3 + checksum: b8695ddf3d593203e25ab0900e265d860038486c943ff8b774f596a310f8ceebdb30c6832407a8198ba3ec9debe1abe1f51d4aad94843612db3b76d690c61d34 + languageName: node + linkType: hard + +"optionator@npm:^0.9.3": + version: 0.9.4 + resolution: "optionator@npm:0.9.4" + dependencies: + deep-is: ^0.1.3 + fast-levenshtein: ^2.0.6 + levn: ^0.4.1 + prelude-ls: ^1.2.1 + type-check: ^0.4.0 + word-wrap: ^1.2.5 + checksum: ecbd010e3dc73e05d239976422d9ef54a82a13f37c11ca5911dff41c98a6c7f0f163b27f922c37e7f8340af9d36febd3b6e9cef508f3339d4c393d7276d716bb + languageName: node + linkType: hard + +"ora@npm:^5.3.0, ora@npm:^5.4.1": + version: 5.4.1 + resolution: "ora@npm:5.4.1" + dependencies: + bl: ^4.1.0 + chalk: ^4.1.0 + cli-cursor: ^3.1.0 + cli-spinners: ^2.5.0 + is-interactive: ^1.0.0 + is-unicode-supported: ^0.1.0 + log-symbols: ^4.1.0 + strip-ansi: ^6.0.0 + wcwidth: ^1.0.1 + checksum: 28d476ee6c1049d68368c0dc922e7225e3b5600c3ede88fade8052837f9ed342625fdaa84a6209302587c8ddd9b664f71f0759833cbdb3a4cf81344057e63c63 + languageName: node + linkType: hard + +"os-browserify@npm:^0.3.0": + version: 0.3.0 + resolution: "os-browserify@npm:0.3.0" + checksum: 16e37ba3c0e6a4c63443c7b55799ce4066d59104143cb637ecb9fce586d5da319cdca786ba1c867abbe3890d2cbf37953f2d51eea85e20dd6c4570d6c54bfebf + languageName: node + linkType: hard + +"os-tmpdir@npm:~1.0.2": + version: 1.0.2 + resolution: "os-tmpdir@npm:1.0.2" + checksum: 5666560f7b9f10182548bf7013883265be33620b1c1b4a4d405c25be2636f970c5488ff3e6c48de75b55d02bde037249fe5dbfbb4c0fb7714953d56aed062e6d + languageName: node + linkType: hard + +"outdent@npm:^0.5.0": + version: 0.5.0 + resolution: "outdent@npm:0.5.0" + checksum: 6e6c63dd09e9890e67ef9a0b4d35df0b0b850b2059ce3f7e19e4cc1a146b26dc5d8c45df238dbf187dfffc8bd82cd07d37c697544015680bcb9f07f29a36c678 + languageName: node + linkType: hard + +"outvariant@npm:^1.2.1, outvariant@npm:^1.4.0": + version: 1.4.3 + resolution: "outvariant@npm:1.4.3" + checksum: 4a3551fb2b45309e585eebf88bad094dbe56ac6d3a28d59dd2e4050b431aa2beb6097a0763fce3cd82ca0f077026f380a9b60fffc306aaf430141421e7a7b6ed + languageName: node + linkType: hard + +"p-defer@npm:^1.0.0": + version: 1.0.0 + resolution: "p-defer@npm:1.0.0" + checksum: 4271b935c27987e7b6f229e5de4cdd335d808465604644cb7b4c4c95bef266735859a93b16415af8a41fd663ee9e3b97a1a2023ca9def613dba1bad2a0da0c7b + languageName: node + linkType: hard + +"p-filter@npm:^2.1.0": + version: 2.1.0 + resolution: "p-filter@npm:2.1.0" + dependencies: + p-map: ^2.0.0 + checksum: 76e552ca624ce2233448d68b19eec9de42b695208121998f7e011edce71d1079a83096ee6a2078fb2a59cfa8a5c999f046edf00ebf16a8e780022010b4693234 + languageName: node + linkType: hard + +"p-finally@npm:^1.0.0": + version: 1.0.0 + resolution: "p-finally@npm:1.0.0" + checksum: 93a654c53dc805dd5b5891bab16eb0ea46db8f66c4bfd99336ae929323b1af2b70a8b0654f8f1eae924b2b73d037031366d645f1fd18b3d30cbd15950cc4b1d4 + languageName: node + linkType: hard + +"p-limit@npm:^2.0.0, p-limit@npm:^2.2.0": + version: 2.3.0 + resolution: "p-limit@npm:2.3.0" + dependencies: + p-try: ^2.0.0 + checksum: 84ff17f1a38126c3314e91ecfe56aecbf36430940e2873dadaa773ffe072dc23b7af8e46d4b6485d302a11673fe94c6b67ca2cfbb60c989848b02100d0594ac1 + languageName: node + linkType: hard + +"p-limit@npm:^3.0.1, p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": + version: 3.1.0 + resolution: "p-limit@npm:3.1.0" + dependencies: + yocto-queue: ^0.1.0 + checksum: 7c3690c4dbf62ef625671e20b7bdf1cbc9534e83352a2780f165b0d3ceba21907e77ad63401708145ca4e25bfc51636588d89a8c0aeb715e6c37d1c066430360 + languageName: node + linkType: hard + +"p-locate@npm:^3.0.0": + version: 3.0.0 + resolution: "p-locate@npm:3.0.0" + dependencies: + p-limit: ^2.0.0 + checksum: 83991734a9854a05fe9dbb29f707ea8a0599391f52daac32b86f08e21415e857ffa60f0e120bfe7ce0cc4faf9274a50239c7895fc0d0579d08411e513b83a4ae + languageName: node + linkType: hard + +"p-locate@npm:^4.1.0": + version: 4.1.0 + resolution: "p-locate@npm:4.1.0" + dependencies: + p-limit: ^2.2.0 + checksum: 513bd14a455f5da4ebfcb819ef706c54adb09097703de6aeaa5d26fe5ea16df92b48d1ac45e01e3944ce1e6aa2a66f7f8894742b8c9d6e276e16cd2049a2b870 + languageName: node + linkType: hard + +"p-locate@npm:^5.0.0": + version: 5.0.0 + resolution: "p-locate@npm:5.0.0" + dependencies: + p-limit: ^3.0.2 + checksum: 1623088f36cf1cbca58e9b61c4e62bf0c60a07af5ae1ca99a720837356b5b6c5ba3eb1b2127e47a06865fee59dd0453cad7cc844cda9d5a62ac1a5a51b7c86d3 + languageName: node + linkType: hard + +"p-map@npm:^2.0.0": + version: 2.1.0 + resolution: "p-map@npm:2.1.0" + checksum: 9e3ad3c9f6d75a5b5661bcad78c91f3a63849189737cd75e4f1225bf9ac205194e5c44aac2ef6f09562b1facdb9bd1425584d7ac375bfaa17b3f1a142dab936d + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: ^3.0.0 + checksum: cb0ab21ec0f32ddffd31dfc250e3afa61e103ef43d957cc45497afe37513634589316de4eb88abdfd969fe6410c22c0b93ab24328833b8eb1ccc087fc0442a1c + languageName: node + linkType: hard + +"p-queue@npm:^6.6.2": + version: 6.6.2 + resolution: "p-queue@npm:6.6.2" + dependencies: + eventemitter3: ^4.0.4 + p-timeout: ^3.2.0 + checksum: 832642fcc4ab6477b43e6d7c30209ab10952969ed211c6d6f2931be8a4f9935e3578c72e8cce053dc34f2eb6941a408a2c516a54904e989851a1a209cf19761c + languageName: node + linkType: hard + +"p-retry@npm:^6.2.0": + version: 6.2.0 + resolution: "p-retry@npm:6.2.0" + dependencies: + "@types/retry": 0.12.2 + is-network-error: ^1.0.0 + retry: ^0.13.1 + checksum: 6003573c559ee812329c9c3ede7ba12a783fdc8dd70602116646e850c920b4597dc502fe001c3f9526fca4e93275045db7a27341c458e51db179c1374a01ac44 + languageName: node + linkType: hard + +"p-timeout@npm:^3.2.0": + version: 3.2.0 + resolution: "p-timeout@npm:3.2.0" + dependencies: + p-finally: ^1.0.0 + checksum: 3dd0eaa048780a6f23e5855df3dd45c7beacff1f820476c1d0d1bcd6648e3298752ba2c877aa1c92f6453c7dd23faaf13d9f5149fc14c0598a142e2c5e8d649c + languageName: node + linkType: hard + +"p-try@npm:^2.0.0": + version: 2.2.0 + resolution: "p-try@npm:2.2.0" + checksum: f8a8e9a7693659383f06aec604ad5ead237c7a261c18048a6e1b5b85a5f8a067e469aa24f5bc009b991ea3b058a87f5065ef4176793a200d4917349881216cae + languageName: node + linkType: hard + +"pac-proxy-agent@npm:^7.0.1": + version: 7.0.2 + resolution: "pac-proxy-agent@npm:7.0.2" + dependencies: + "@tootallnate/quickjs-emscripten": ^0.23.0 + agent-base: ^7.0.2 + debug: ^4.3.4 + get-uri: ^6.0.1 + http-proxy-agent: ^7.0.0 + https-proxy-agent: ^7.0.5 + pac-resolver: ^7.0.1 + socks-proxy-agent: ^8.0.4 + checksum: 82772aaa489a4ad6f598b75d56daf609e7ba294a05a91cfe3101b004e2df494f0a269c98452cb47aaa4a513428e248308a156e26fee67eb78a76a58e9346921e + languageName: node + linkType: hard + +"pac-resolver@npm:^7.0.1": + version: 7.0.1 + resolution: "pac-resolver@npm:7.0.1" + dependencies: + degenerator: ^5.0.0 + netmask: ^2.0.2 + checksum: 839134328781b80d49f9684eae1f5c74f50a1d4482076d44c84fc2f3ca93da66fa11245a4725a057231e06b311c20c989fd0681e662a0792d17f644d8fe62a5e + languageName: node + linkType: hard + +"package-json-from-dist@npm:^1.0.0": + version: 1.0.1 + resolution: "package-json-from-dist@npm:1.0.1" + checksum: 58ee9538f2f762988433da00e26acc788036914d57c71c246bf0be1b60cdbd77dd60b6a3e1a30465f0b248aeb80079e0b34cb6050b1dfa18c06953bb1cbc7602 + languageName: node + linkType: hard + +"package-manager-detector@npm:^0.2.0": + version: 0.2.2 + resolution: "package-manager-detector@npm:0.2.2" + checksum: acc0d5a8b6b2a265474c1bac2b3569b6e57fe13db4d764b75cf5fcd11463a44f0ce00bb5dc439a78a1999993780385f431d36ceea51b51a35ce40d512b7388c6 + languageName: node + linkType: hard + +"pako@npm:^1.0.10, pako@npm:~1.0.5": + version: 1.0.11 + resolution: "pako@npm:1.0.11" + checksum: 1be2bfa1f807608c7538afa15d6f25baa523c30ec870a3228a89579e474a4d992f4293859524e46d5d87fd30fa17c5edf34dbef0671251d9749820b488660b16 + languageName: node + linkType: hard + +"param-case@npm:^3.0.4": + version: 3.0.4 + resolution: "param-case@npm:3.0.4" + dependencies: + dot-case: ^3.0.4 + tslib: ^2.0.3 + checksum: b34227fd0f794e078776eb3aa6247442056cb47761e9cd2c4c881c86d84c64205f6a56ef0d70b41ee7d77da02c3f4ed2f88e3896a8fefe08bdfb4deca037c687 + languageName: node + linkType: hard + +"parent-module@npm:^1.0.0": + version: 1.0.1 + resolution: "parent-module@npm:1.0.1" + dependencies: + callsites: ^3.0.0 + checksum: 6ba8b255145cae9470cf5551eb74be2d22281587af787a2626683a6c20fbb464978784661478dd2a3f1dad74d1e802d403e1b03c1a31fab310259eec8ac560ff + languageName: node + linkType: hard + +"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.7": + version: 5.1.7 + resolution: "parse-asn1@npm:5.1.7" + dependencies: + asn1.js: ^4.10.1 + browserify-aes: ^1.2.0 + evp_bytestokey: ^1.0.3 + hash-base: ~3.0 + pbkdf2: ^3.1.2 + safe-buffer: ^5.2.1 + checksum: 93c7194c1ed63a13e0b212d854b5213ad1aca0ace41c66b311e97cca0519cf9240f79435a0306a3b412c257f0ea3f1953fd0d9549419a0952c9e995ab361fd6c + languageName: node + linkType: hard + +"parse-entities@npm:^2.0.0": + version: 2.0.0 + resolution: "parse-entities@npm:2.0.0" + dependencies: + character-entities: ^1.0.0 + character-entities-legacy: ^1.0.0 + character-reference-invalid: ^1.0.0 + is-alphanumerical: ^1.0.0 + is-decimal: ^1.0.0 + is-hexadecimal: ^1.0.0 + checksum: 7addfd3e7d747521afac33c8121a5f23043c6973809756920d37e806639b4898385d386fcf4b3c8e2ecf1bc28aac5ae97df0b112d5042034efbe80f44081ebce + languageName: node + linkType: hard + +"parse-json@npm:^5.0.0, parse-json@npm:^5.2.0": + version: 5.2.0 + resolution: "parse-json@npm:5.2.0" + dependencies: + "@babel/code-frame": ^7.0.0 + error-ex: ^1.3.1 + json-parse-even-better-errors: ^2.3.0 + lines-and-columns: ^1.1.6 + checksum: 62085b17d64da57f40f6afc2ac1f4d95def18c4323577e1eced571db75d9ab59b297d1d10582920f84b15985cbfc6b6d450ccbf317644cfa176f3ed982ad87e2 + languageName: node + linkType: hard + +"parse-ms@npm:^4.0.0": + version: 4.0.0 + resolution: "parse-ms@npm:4.0.0" + checksum: 673c801d9f957ff79962d71ed5a24850163f4181a90dd30c4e3666b3a804f53b77f1f0556792e8b2adbb5d58757907d1aa51d7d7dc75997c2a56d72937cbc8b7 + languageName: node + linkType: hard + +"parse-passwd@npm:^1.0.0": + version: 1.0.0 + resolution: "parse-passwd@npm:1.0.0" + checksum: 4e55e0231d58f828a41d0f1da2bf2ff7bcef8f4cb6146e69d16ce499190de58b06199e6bd9b17fbf0d4d8aef9052099cdf8c4f13a6294b1a522e8e958073066e + languageName: node + linkType: hard + +"parse-path@npm:^7.0.0": + version: 7.0.0 + resolution: "parse-path@npm:7.0.0" + dependencies: + protocols: ^2.0.0 + checksum: 244b46523a58181d251dda9b888efde35d8afb957436598d948852f416d8c76ddb4f2010f9fc94218b4be3e5c0f716aa0d2026194a781e3b8981924142009302 + languageName: node + linkType: hard + +"parse-url@npm:^8.1.0": + version: 8.1.0 + resolution: "parse-url@npm:8.1.0" + dependencies: + parse-path: ^7.0.0 + checksum: b93e21ab4c93c7d7317df23507b41be7697694d4c94f49ed5c8d6288b01cba328fcef5ba388e147948eac20453dee0df9a67ab2012415189fff85973bdffe8d9 + languageName: node + linkType: hard + +"parse5-htmlparser2-tree-adapter@npm:^6.0.0": + version: 6.0.1 + resolution: "parse5-htmlparser2-tree-adapter@npm:6.0.1" + dependencies: + parse5: ^6.0.1 + checksum: 1848378b355d027915645c13f13f982e60502d201f53bc2067a508bf2dba4aac08219fc781dcd160167f5f50f0c73f58d20fa4fb3d90ee46762c20234fa90a6d + languageName: node + linkType: hard + +"parse5@npm:^5.1.1": + version: 5.1.1 + resolution: "parse5@npm:5.1.1" + checksum: 613a714af4c1101d1cb9f7cece2558e35b9ae8a0c03518223a4a1e35494624d9a9ad5fad4c13eab66a0e0adccd9aa3d522fc8f5f9cc19789e0579f3fa0bdfc65 + languageName: node + linkType: hard + +"parse5@npm:^6.0.1": + version: 6.0.1 + resolution: "parse5@npm:6.0.1" + checksum: 7d569a176c5460897f7c8f3377eff640d54132b9be51ae8a8fa4979af940830b2b0c296ce75e5bd8f4041520aadde13170dbdec44889975f906098ea0002f4bd + languageName: node + linkType: hard + +"parse5@npm:^7.0.0, parse5@npm:^7.1.1": + version: 7.2.0 + resolution: "parse5@npm:7.2.0" + dependencies: + entities: ^4.5.0 + checksum: 78a3286521d5ae09837ed3112a3c817cc718ee444951aced617c46a229b9872b10b7b20941d4d0ca7176c7f37f13dbf013206abe2e5e533563d635d36a9a3dc6 + languageName: node + linkType: hard + +"parseurl@npm:^1.3.2, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": + version: 1.3.3 + resolution: "parseurl@npm:1.3.3" + checksum: 407cee8e0a3a4c5cd472559bca8b6a45b82c124e9a4703302326e9ab60fc1081442ada4e02628efef1eb16197ddc7f8822f5a91fd7d7c86b51f530aedb17dfa2 + languageName: node + linkType: hard + +"pascal-case@npm:^3.1.2": + version: 3.1.2 + resolution: "pascal-case@npm:3.1.2" + dependencies: + no-case: ^3.0.4 + tslib: ^2.0.3 + checksum: ba98bfd595fc91ef3d30f4243b1aee2f6ec41c53b4546bfa3039487c367abaa182471dcfc830a1f9e1a0df00c14a370514fa2b3a1aacc68b15a460c31116873e + languageName: node + linkType: hard + +"passport-strategy@npm:1.x.x": + version: 1.0.0 + resolution: "passport-strategy@npm:1.0.0" + checksum: 5086693f2508e538dffa55a338c89fe8192fb5f4478c71f80cd5890b8573419a098f4fec88b505374f60bbe9049f6f24b9f3992678612528a3370b4dc73354a2 + languageName: node + linkType: hard + +"passport@npm:^0.7.0": + version: 0.7.0 + resolution: "passport@npm:0.7.0" + dependencies: + passport-strategy: 1.x.x + pause: 0.0.1 + utils-merge: ^1.0.1 + checksum: 5080b46df2df7a84f7ba4a8a20437ce71a1346fd27ab47b62df3251a666af9f3430d6c8a1beda3174f6a9d91edc823b57b88050d423a6cff9831848a2d97725c + languageName: node + linkType: hard + +"path-browserify@npm:0.0.1": + version: 0.0.1 + resolution: "path-browserify@npm:0.0.1" + checksum: ae8dcd45d0d3cfbaf595af4f206bf3ed82d77f72b4877ae7e77328079e1468c84f9386754bb417d994d5a19bf47882fd253565c18441cd5c5c90ae5187599e35 + languageName: node + linkType: hard + +"path-browserify@npm:^1.0.1": + version: 1.0.1 + resolution: "path-browserify@npm:1.0.1" + checksum: c6d7fa376423fe35b95b2d67990060c3ee304fc815ff0a2dc1c6c3cfaff2bd0d572ee67e18f19d0ea3bbe32e8add2a05021132ac40509416459fffee35200699 + languageName: node + linkType: hard + +"path-equal@npm:^1.2.5": + version: 1.2.5 + resolution: "path-equal@npm:1.2.5" + checksum: 2bef7bcb98c7ae371c52c1562b2fc515bfd03bc1a5571df9a8591038db8d742ba2d1ff39aa5130853e6afb69e773ccba5095f54d2e6d17422ca03ef9047992d7 + languageName: node + linkType: hard + +"path-exists@npm:^3.0.0": + version: 3.0.0 + resolution: "path-exists@npm:3.0.0" + checksum: 96e92643aa34b4b28d0de1cd2eba52a1c5313a90c6542d03f62750d82480e20bfa62bc865d5cfc6165f5fcd5aeb0851043c40a39be5989646f223300021bae0a + languageName: node + linkType: hard + +"path-exists@npm:^4.0.0": + version: 4.0.0 + resolution: "path-exists@npm:4.0.0" + checksum: 505807199dfb7c50737b057dd8d351b82c033029ab94cb10a657609e00c1bc53b951cfdbccab8de04c5584d5eff31128ce6afd3db79281874a5ef2adbba55ed1 + languageName: node + linkType: hard + +"path-is-absolute@npm:^1.0.0": + version: 1.0.1 + resolution: "path-is-absolute@npm:1.0.1" + checksum: 060840f92cf8effa293bcc1bea81281bd7d363731d214cbe5c227df207c34cd727430f70c6037b5159c8a870b9157cba65e775446b0ab06fd5ecc7e54615a3b8 + languageName: node + linkType: hard + +"path-key@npm:^3.0.0, path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 + languageName: node + linkType: hard + +"path-parse@npm:^1.0.7": + version: 1.0.7 + resolution: "path-parse@npm:1.0.7" + checksum: 49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a + languageName: node + linkType: hard + +"path-scurry@npm:^1.11.1, path-scurry@npm:^1.6.1": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" + dependencies: + lru-cache: ^10.2.0 + minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 + checksum: 890d5abcd593a7912dcce7cf7c6bf7a0b5648e3dee6caf0712c126ca0a65c7f3d7b9d769072a4d1baf370f61ce493ab5b038d59988688e0c5f3f646ee3c69023 + languageName: node + linkType: hard + +"path-to-regexp@npm:0.1.10": + version: 0.1.10 + resolution: "path-to-regexp@npm:0.1.10" + checksum: ab7a3b7a0b914476d44030340b0a65d69851af2a0f33427df1476100ccb87d409c39e2182837a96b98fb38c4ef2ba6b87bdad62bb70a2c153876b8061760583c + languageName: node + linkType: hard + +"path-to-regexp@npm:3.3.0": + version: 3.3.0 + resolution: "path-to-regexp@npm:3.3.0" + checksum: bb249d08804f7961dd44fb175466c900b893c56e909db8e2a66ec12b9d9a964af269eb7a50892c933f52b47315953dfdb4279639fbce20977c3625a9ef3055fe + languageName: node + linkType: hard + +"path-to-regexp@npm:^6.2.0": + version: 6.3.0 + resolution: "path-to-regexp@npm:6.3.0" + checksum: eca78602e6434a1b6799d511d375ec044e8d7e28f5a48aa5c28d57d8152fb52f3fc62fb1cfc5dfa2198e1f041c2a82ed14043d75740a2fe60e91b5089a153250 + languageName: node + linkType: hard + +"path-to-regexp@npm:^8.0.0": + version: 8.2.0 + resolution: "path-to-regexp@npm:8.2.0" + checksum: 56e13e45962e776e9e7cd72e87a441cfe41f33fd539d097237ceb16adc922281136ca12f5a742962e33d8dda9569f630ba594de56d8b7b6e49adf31803c5e771 + languageName: node + linkType: hard + +"path-type@npm:^4.0.0": + version: 4.0.0 + resolution: "path-type@npm:4.0.0" + checksum: 5b1e2daa247062061325b8fdbfd1fb56dde0a448fb1455453276ea18c60685bdad23a445dc148cf87bc216be1573357509b7d4060494a6fd768c7efad833ee45 + languageName: node + linkType: hard + +"pause-stream@npm:0.0.11": + version: 0.0.11 + resolution: "pause-stream@npm:0.0.11" + dependencies: + through: ~2.3 + checksum: 3c4a14052a638b92e0c96eb00c0d7977df7f79ea28395250c525d197f1fc02d34ce1165d5362e2e6ebbb251524b94a76f3f0d4abc39ab8b016d97449fe15583c + languageName: node + linkType: hard + +"pause@npm:0.0.1": + version: 0.0.1 + resolution: "pause@npm:0.0.1" + checksum: e96ee581b68085e6f2ba5adbcb4d4a41fe88e5b514061e76df2fe1905f0f65f4fe5a843b538e9551122c6b9184ff4be266c2ee0ea4614702f9a3d04466d9f462 + languageName: node + linkType: hard + +"pbkdf2@npm:^3.0.3, pbkdf2@npm:^3.1.2": + version: 3.1.2 + resolution: "pbkdf2@npm:3.1.2" + dependencies: + create-hash: ^1.1.2 + create-hmac: ^1.1.4 + ripemd160: ^2.0.1 + safe-buffer: ^5.0.1 + sha.js: ^2.4.8 + checksum: 2c950a100b1da72123449208e231afc188d980177d021d7121e96a2de7f2abbc96ead2b87d03d8fe5c318face097f203270d7e27908af9f471c165a4e8e69c92 + languageName: node + linkType: hard + +"pct-encode@npm:~1.0.0": + version: 1.0.3 + resolution: "pct-encode@npm:1.0.3" + checksum: 04344233107a40590dd2d6fff3463040288d68ec66b6026cbb90a6ab1b29afdb5f196ff35b6ab5f86d4799a0dfea6117ab19fe836e0d5ffb49695c6ba60d05d8 + languageName: node + linkType: hard + +"pend@npm:~1.2.0": + version: 1.2.0 + resolution: "pend@npm:1.2.0" + checksum: 6c72f5243303d9c60bd98e6446ba7d30ae29e3d56fdb6fae8767e8ba6386f33ee284c97efe3230a0d0217e2b1723b8ab490b1bbf34fcbb2180dbc8a9de47850d + languageName: node + linkType: hard + +"performance-now@npm:^2.1.0": + version: 2.1.0 + resolution: "performance-now@npm:2.1.0" + checksum: 534e641aa8f7cba160f0afec0599b6cecefbb516a2e837b512be0adbe6c1da5550e89c78059c7fabc5c9ffdf6627edabe23eb7c518c4500067a898fa65c2b550 + languageName: node + linkType: hard + +"pg-cloudflare@npm:^1.1.1": + version: 1.1.1 + resolution: "pg-cloudflare@npm:1.1.1" + checksum: 32aac06b5dc4588bbf78801b6267781bc7e13be672009df949d08e9627ba9fdc26924916665d4de99d47f9b0495301930547488dad889d826856976c7b3f3731 + languageName: node + linkType: hard + +"pg-connection-string@npm:2.6.2": + version: 2.6.2 + resolution: "pg-connection-string@npm:2.6.2" + checksum: 22265882c3b6f2320785378d0760b051294a684989163d5a1cde4009e64e84448d7bf67d9a7b9e7f69440c3ee9e2212f9aa10dd17ad6773f6143c6020cebbcb5 + languageName: node + linkType: hard + +"pg-connection-string@npm:^2.3.0, pg-connection-string@npm:^2.7.0": + version: 2.7.0 + resolution: "pg-connection-string@npm:2.7.0" + checksum: 68015a8874b7ca5dad456445e4114af3d2602bac2fdb8069315ecad0ff9660ec93259b9af7186606529ac4f6f72a06831e6f20897a689b16cc7fda7ca0e247fd + languageName: node + linkType: hard + +"pg-format@npm:^1.0.4": + version: 1.0.4 + resolution: "pg-format@npm:1.0.4" + checksum: 159b43ad57d2f963f1072def86080dd2a6dd42c1a86046e388d47b491e00afe795139520eb01c8dffc43ac0243c77b3c4c5882d0ec5f488bb3281f17458b1b3d + languageName: node + linkType: hard + +"pg-int8@npm:1.0.1": + version: 1.0.1 + resolution: "pg-int8@npm:1.0.1" + checksum: a1e3a05a69005ddb73e5f324b6b4e689868a447c5fa280b44cd4d04e6916a344ac289e0b8d2695d66e8e89a7fba023affb9e0e94778770ada5df43f003d664c9 + languageName: node + linkType: hard + +"pg-pool@npm:^3.7.0": + version: 3.7.0 + resolution: "pg-pool@npm:3.7.0" + peerDependencies: + pg: ">=8.0" + checksum: 66fc1a5ad0e17b72671b9a2cd4c7a856fb08d3cb82da7af0b322590ada23127ac591111e855740405fde4f06c9de888abe9f3aa685ed6038c3232578e1fce8cf + languageName: node + linkType: hard + +"pg-protocol@npm:^1.7.0": + version: 1.7.0 + resolution: "pg-protocol@npm:1.7.0" + checksum: 2dba740f6fc4b7f9761682c4c42d183b444292cdc7638b373f5247ec995c8199c369953343479281da3c41611fe34130a80c8668348d49a399c164f802f76be2 + languageName: node + linkType: hard + +"pg-types@npm:^2.1.0": + version: 2.2.0 + resolution: "pg-types@npm:2.2.0" + dependencies: + pg-int8: 1.0.1 + postgres-array: ~2.0.0 + postgres-bytea: ~1.0.0 + postgres-date: ~1.0.4 + postgres-interval: ^1.1.0 + checksum: bf4ec3f594743442857fb3a8dfe5d2478a04c98f96a0a47365014557cbc0b4b0cee01462c79adca863b93befbf88f876299b75b72c665b5fb84a2c94fbd10316 + languageName: node + linkType: hard + +"pg@npm:^8.11.3": + version: 8.13.0 + resolution: "pg@npm:8.13.0" + dependencies: + pg-cloudflare: ^1.1.1 + pg-connection-string: ^2.7.0 + pg-pool: ^3.7.0 + pg-protocol: ^1.7.0 + pg-types: ^2.1.0 + pgpass: 1.x + peerDependencies: + pg-native: ">=3.0.1" + dependenciesMeta: + pg-cloudflare: + optional: true + peerDependenciesMeta: + pg-native: + optional: true + checksum: 81560755ff4ee62b71bf1204dd696f66451574d1db56cbd5aa514ce91c6474030ee8078461b3cb85cce8d2f185be5846e0a7a707a818f5e2e3fb198a7ea795ea + languageName: node + linkType: hard + +"pgpass@npm:1.x": + version: 1.0.5 + resolution: "pgpass@npm:1.0.5" + dependencies: + split2: ^4.1.0 + checksum: 947ac096c031eebdf08d989de2e9f6f156b8133d6858c7c2c06c041e1e71dda6f5f3bad3c0ec1e96a09497bbc6ef89e762eefe703b5ef9cb2804392ec52ec400 + languageName: node + linkType: hard + +"picocolors@npm:^1.0.0, picocolors@npm:^1.1.0": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 + languageName: node + linkType: hard + +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.2, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf + languageName: node + linkType: hard + +"picomatch@npm:^4.0.1": + version: 4.0.2 + resolution: "picomatch@npm:4.0.2" + checksum: a7a5188c954f82c6585720e9143297ccd0e35ad8072231608086ca950bee672d51b0ef676254af0788205e59bd4e4deb4e7708769226bed725bf13370a7d1464 + languageName: node + linkType: hard + +"pify@npm:^4.0.1": + version: 4.0.1 + resolution: "pify@npm:4.0.1" + checksum: 9c4e34278cb09987685fa5ef81499c82546c033713518f6441778fbec623fc708777fe8ac633097c72d88470d5963094076c7305cafc7ad340aae27cfacd856b + languageName: node + linkType: hard + +"pify@npm:^5.0.0": + version: 5.0.0 + resolution: "pify@npm:5.0.0" + checksum: 443e3e198ad6bfa8c0c533764cf75c9d5bc976387a163792fb553ffe6ce923887cf14eebf5aea9b7caa8eab930da8c33612990ae85bd8c2bc18bedb9eae94ecb + languageName: node + linkType: hard + +"pirates@npm:^4.0.1, pirates@npm:^4.0.4, pirates@npm:^4.0.6": + version: 4.0.6 + resolution: "pirates@npm:4.0.6" + checksum: 46a65fefaf19c6f57460388a5af9ab81e3d7fd0e7bc44ca59d753cb5c4d0df97c6c6e583674869762101836d68675f027d60f841c105d72734df9dfca97cbcc6 + languageName: node + linkType: hard + +"pkg-dir@npm:^4.2.0": + version: 4.2.0 + resolution: "pkg-dir@npm:4.2.0" + dependencies: + find-up: ^4.0.0 + checksum: 9863e3f35132bf99ae1636d31ff1e1e3501251d480336edb1c211133c8d58906bed80f154a1d723652df1fda91e01c7442c2eeaf9dc83157c7ae89087e43c8d6 + languageName: node + linkType: hard + +"pkg-up@npm:^3.1.0": + version: 3.1.0 + resolution: "pkg-up@npm:3.1.0" + dependencies: + find-up: ^3.0.0 + checksum: 5bac346b7c7c903613c057ae3ab722f320716199d753f4a7d053d38f2b5955460f3e6ab73b4762c62fd3e947f58e04f1343e92089e7bb6091c90877406fcd8c8 + languageName: node + linkType: hard + +"playwright-core@npm:1.45.3": + version: 1.45.3 + resolution: "playwright-core@npm:1.45.3" + bin: + playwright-core: cli.js + checksum: cecb58877b2c643403d7a72c24a7aa0fdd087a3c7f9a5ea5403851336ea831d8e304b1f159aacbbabd12e5c47eaac054333746c9e5431ec07b13d64dbf3b50ec + languageName: node + linkType: hard + +"playwright@npm:1.45.3": + version: 1.45.3 + resolution: "playwright@npm:1.45.3" + dependencies: + fsevents: 2.3.2 + playwright-core: 1.45.3 + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: d9d23b155ccd001553214f710561b01e48eb409676102f8ab94c0b4aa5ac5f398becc1a96528b0554944e07e34189503d891913e0e0a4aa58ad36b9c08747983 + languageName: node + linkType: hard + +"pluralize@npm:^8.0.0": + version: 8.0.0 + resolution: "pluralize@npm:8.0.0" + checksum: 08931d4a6a4a5561a7f94f67a31c17e6632cb21e459ab3ff4f6f629d9a822984cf8afef2311d2005fbea5d7ef26016ebb090db008e2d8bce39d0a9a9d218736e + languageName: node + linkType: hard + +"pony-cause@npm:^1.0.0": + version: 1.1.1 + resolution: "pony-cause@npm:1.1.1" + checksum: 5ff8878b808be48db801d52246a99d7e4789e52d20575ba504ede30c818fd85d38a033915e02c15fa9b6dce72448836dc1a47094acf8f1c21c4f04a4603b0cfb + languageName: node + linkType: hard + +"popper.js@npm:1.16.1-lts": + version: 1.16.1-lts + resolution: "popper.js@npm:1.16.1-lts" + checksum: 27c00b5b07afa91a5e9f9db78a9a61b50f44ca156d09c851cd29d79cd359e54cfde4288ae555b88801438227e452e56cb4b56bd79fd45ab17dac780a70a7e9ac + languageName: node + linkType: hard + +"portfinder@npm:^1.0.32": + version: 1.0.32 + resolution: "portfinder@npm:1.0.32" + dependencies: + async: ^2.6.4 + debug: ^3.2.7 + mkdirp: ^0.5.6 + checksum: 116b4aed1b9e16f6d5503823d966d9ffd41b1c2339e27f54c06cd2f3015a9d8ef53e2a53b57bc0a25af0885977b692007353aa28f9a0a98a44335cb50487240d + languageName: node + linkType: hard + +"possible-typed-array-names@npm:^1.0.0": + version: 1.0.0 + resolution: "possible-typed-array-names@npm:1.0.0" + checksum: b32d403ece71e042385cc7856385cecf1cd8e144fa74d2f1de40d1e16035dba097bc189715925e79b67bdd1472796ff168d3a90d296356c9c94d272d5b95f3ae + languageName: node + linkType: hard + +"postcss-calc@npm:^8.2.3": + version: 8.2.4 + resolution: "postcss-calc@npm:8.2.4" + dependencies: + postcss-selector-parser: ^6.0.9 + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.2 + checksum: 314b4cebb0c4ed0cf8356b4bce71eca78f5a7842e6a3942a3bba49db168d5296b2bd93c3f735ae1c616f2651d94719ade33becc03c73d2d79c7394fb7f73eabb + languageName: node + linkType: hard + +"postcss-colormin@npm:^5.3.1": + version: 5.3.1 + resolution: "postcss-colormin@npm:5.3.1" + dependencies: + browserslist: ^4.21.4 + caniuse-api: ^3.0.0 + colord: ^2.9.1 + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: e5778baab30877cd1f51e7dc9d2242a162aeca6360a52956acd7f668c5bc235c2ccb7e4df0370a804d65ebe00c5642366f061db53aa823f9ed99972cebd16024 + languageName: node + linkType: hard + +"postcss-convert-values@npm:^5.1.3": + version: 5.1.3 + resolution: "postcss-convert-values@npm:5.1.3" + dependencies: + browserslist: ^4.21.4 + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: df48cdaffabf9737f9cfdc58a3dc2841cf282506a7a944f6c70236cff295d3a69f63de6e0935eeb8a9d3f504324e5b4e240abc29e21df9e35a02585d3060aeb5 + languageName: node + linkType: hard + +"postcss-discard-comments@npm:^5.1.2": + version: 5.1.2 + resolution: "postcss-discard-comments@npm:5.1.2" + peerDependencies: + postcss: ^8.2.15 + checksum: abfd064ebc27aeaf5037643dd51ffaff74d1fa4db56b0523d073ace4248cbb64ffd9787bd6924b0983a9d0bd0e9bf9f10d73b120e50391dc236e0d26c812fa2a + languageName: node + linkType: hard + +"postcss-discard-duplicates@npm:^5.1.0": + version: 5.1.0 + resolution: "postcss-discard-duplicates@npm:5.1.0" + peerDependencies: + postcss: ^8.2.15 + checksum: 88d6964201b1f4ed6bf7a32cefe68e86258bb6e42316ca01d9b32bdb18e7887d02594f89f4a2711d01b51ea6e3fcca8c54be18a59770fe5f4521c61d3eb6ca35 + languageName: node + linkType: hard + +"postcss-discard-empty@npm:^5.1.1": + version: 5.1.1 + resolution: "postcss-discard-empty@npm:5.1.1" + peerDependencies: + postcss: ^8.2.15 + checksum: 970adb12fae5c214c0768236ad9a821552626e77dedbf24a8213d19cc2c4a531a757cd3b8cdd3fc22fb1742471b8692a1db5efe436a71236dec12b1318ee8ff4 + languageName: node + linkType: hard + +"postcss-discard-overridden@npm:^5.1.0": + version: 5.1.0 + resolution: "postcss-discard-overridden@npm:5.1.0" + peerDependencies: + postcss: ^8.2.15 + checksum: d64d4a545aa2c81b22542895cfcddc787d24119f294d35d29b0599a1c818b3cc51f4ee80b80f5a0a09db282453dd5ac49f104c2117cc09112d0ac9b40b499a41 + languageName: node + linkType: hard + +"postcss-load-config@npm:^3.0.0": + version: 3.1.4 + resolution: "postcss-load-config@npm:3.1.4" + dependencies: + lilconfig: ^2.0.5 + yaml: ^1.10.2 + peerDependencies: + postcss: ">=8.0.9" + ts-node: ">=9.0.0" + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + checksum: 1c589504c2d90b1568aecae8238ab993c17dba2c44f848a8f13619ba556d26a1c09644d5e6361b5784e721e94af37b604992f9f3dc0483e687a0cc1cc5029a34 + languageName: node + linkType: hard + +"postcss-merge-longhand@npm:^5.1.7": + version: 5.1.7 + resolution: "postcss-merge-longhand@npm:5.1.7" + dependencies: + postcss-value-parser: ^4.2.0 + stylehacks: ^5.1.1 + peerDependencies: + postcss: ^8.2.15 + checksum: 81c3fc809f001b9b71a940148e242bdd6e2d77713d1bfffa15eb25c1f06f6648d5e57cb21645746d020a2a55ff31e1740d2b27900442913a9d53d8a01fb37e1b + languageName: node + linkType: hard + +"postcss-merge-rules@npm:^5.1.4": + version: 5.1.4 + resolution: "postcss-merge-rules@npm:5.1.4" + dependencies: + browserslist: ^4.21.4 + caniuse-api: ^3.0.0 + cssnano-utils: ^3.1.0 + postcss-selector-parser: ^6.0.5 + peerDependencies: + postcss: ^8.2.15 + checksum: 8ab6a569babe6cb412d6612adee74f053cea7edb91fa013398515ab36754b1fec830d68782ed8cdfb44cffdc6b78c79eab157bff650f428aa4460d3f3857447e + languageName: node + linkType: hard + +"postcss-minify-font-values@npm:^5.1.0": + version: 5.1.0 + resolution: "postcss-minify-font-values@npm:5.1.0" + dependencies: + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: 35e858fa41efa05acdeb28f1c76579c409fdc7eabb1744c3bd76e895bb9fea341a016746362a67609688ab2471f587202b9a3e14ea28ad677754d663a2777ece + languageName: node + linkType: hard + +"postcss-minify-gradients@npm:^5.1.1": + version: 5.1.1 + resolution: "postcss-minify-gradients@npm:5.1.1" + dependencies: + colord: ^2.9.1 + cssnano-utils: ^3.1.0 + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: 27354072a07c5e6dab36731103b94ca2354d4ed3c5bc6aacfdf2ede5a55fa324679d8fee5450800bc50888dbb5e9ed67569c0012040c2be128143d0cebb36d67 + languageName: node + linkType: hard + +"postcss-minify-params@npm:^5.1.4": + version: 5.1.4 + resolution: "postcss-minify-params@npm:5.1.4" + dependencies: + browserslist: ^4.21.4 + cssnano-utils: ^3.1.0 + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: bd63e2cc89edcf357bb5c2a16035f6d02ef676b8cede4213b2bddd42626b3d428403849188f95576fc9f03e43ebd73a29bf61d33a581be9a510b13b7f7f100d5 + languageName: node + linkType: hard + +"postcss-minify-selectors@npm:^5.2.1": + version: 5.2.1 + resolution: "postcss-minify-selectors@npm:5.2.1" + dependencies: + postcss-selector-parser: ^6.0.5 + peerDependencies: + postcss: ^8.2.15 + checksum: 6fdbc84f99a60d56b43df8930707da397775e4c36062a106aea2fd2ac81b5e24e584a1892f4baa4469fa495cb87d1422560eaa8f6c9d500f9f0b691a5f95bab5 + languageName: node + linkType: hard + +"postcss-modules-extract-imports@npm:^3.0.0, postcss-modules-extract-imports@npm:^3.1.0": + version: 3.1.0 + resolution: "postcss-modules-extract-imports@npm:3.1.0" + peerDependencies: + postcss: ^8.1.0 + checksum: b9192e0f4fb3d19431558be6f8af7ca45fc92baaad9b2778d1732a5880cd25c3df2074ce5484ae491e224f0d21345ffc2d419bd51c25b019af76d7a7af88c17f + languageName: node + linkType: hard + +"postcss-modules-local-by-default@npm:^4.0.0, postcss-modules-local-by-default@npm:^4.0.5": + version: 4.0.5 + resolution: "postcss-modules-local-by-default@npm:4.0.5" + dependencies: + icss-utils: ^5.0.0 + postcss-selector-parser: ^6.0.2 + postcss-value-parser: ^4.1.0 + peerDependencies: + postcss: ^8.1.0 + checksum: ca9b01f4a0a3dfb33e016299e2dfb7e85c3123292f7aec2efc0c6771b9955648598bfb4c1561f7ee9732fb27fb073681233661b32eef98baab43743f96735452 + languageName: node + linkType: hard + +"postcss-modules-scope@npm:^3.0.0, postcss-modules-scope@npm:^3.2.0": + version: 3.2.0 + resolution: "postcss-modules-scope@npm:3.2.0" + dependencies: + postcss-selector-parser: ^6.0.4 + peerDependencies: + postcss: ^8.1.0 + checksum: 2ffe7e98c1fa993192a39c8dd8ade93fc4f59fbd1336ce34fcedaee0ee3bafb29e2e23fb49189256895b30e4f21af661c6a6a16ef7b17ae2c859301e4a4459ae + languageName: node + linkType: hard + +"postcss-modules-values@npm:^4.0.0": + version: 4.0.0 + resolution: "postcss-modules-values@npm:4.0.0" + dependencies: + icss-utils: ^5.0.0 + peerDependencies: + postcss: ^8.1.0 + checksum: f7f2cdf14a575b60e919ad5ea52fed48da46fe80db2733318d71d523fc87db66c835814940d7d05b5746b0426e44661c707f09bdb83592c16aea06e859409db6 + languageName: node + linkType: hard + +"postcss-modules@npm:^4.0.0": + version: 4.3.1 + resolution: "postcss-modules@npm:4.3.1" + dependencies: + generic-names: ^4.0.0 + icss-replace-symbols: ^1.1.0 + lodash.camelcase: ^4.3.0 + postcss-modules-extract-imports: ^3.0.0 + postcss-modules-local-by-default: ^4.0.0 + postcss-modules-scope: ^3.0.0 + postcss-modules-values: ^4.0.0 + string-hash: ^1.1.1 + peerDependencies: + postcss: ^8.0.0 + checksum: fa592183bb3d96c4aaf535e3b9b3bcfc54274cbb5b337616543c24ec68cd56675e9fd8aabf994e627513af628d090e43d2f1f4928ff6cdd4b9d3b1ba3fce4d42 + languageName: node + linkType: hard + +"postcss-normalize-charset@npm:^5.1.0": + version: 5.1.0 + resolution: "postcss-normalize-charset@npm:5.1.0" + peerDependencies: + postcss: ^8.2.15 + checksum: e79d92971fc05b8b3c9b72f3535a574e077d13c69bef68156a0965f397fdf157de670da72b797f57b0e3bac8f38155b5dd1735ecab143b9cc4032d72138193b4 + languageName: node + linkType: hard + +"postcss-normalize-display-values@npm:^5.1.0": + version: 5.1.0 + resolution: "postcss-normalize-display-values@npm:5.1.0" + dependencies: + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: b6eb7b9b02c3bdd62bbc54e01e2b59733d73a1c156905d238e178762962efe0c6f5104544da39f32cade8a4fb40f10ff54b63a8ebfbdff51e8780afb9fbdcf86 + languageName: node + linkType: hard + +"postcss-normalize-positions@npm:^5.1.1": + version: 5.1.1 + resolution: "postcss-normalize-positions@npm:5.1.1" + dependencies: + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: d9afc233729c496463c7b1cdd06732469f401deb387484c3a2422125b46ec10b4af794c101f8c023af56f01970b72b535e88373b9058ecccbbf88db81662b3c4 + languageName: node + linkType: hard + +"postcss-normalize-repeat-style@npm:^5.1.1": + version: 5.1.1 + resolution: "postcss-normalize-repeat-style@npm:5.1.1" + dependencies: + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: 2c6ad2b0ae10a1fda156b948c34f78c8f1e185513593de4d7e2480973586675520edfec427645fa168c337b0a6b3ceca26f92b96149741ca98a9806dad30d534 + languageName: node + linkType: hard + +"postcss-normalize-string@npm:^5.1.0": + version: 5.1.0 + resolution: "postcss-normalize-string@npm:5.1.0" + dependencies: + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: 6e549c6e5b2831e34c7bdd46d8419e2278f6af1d5eef6d26884a37c162844e60339340c57e5e06058cdbe32f27fc6258eef233e811ed2f71168ef2229c236ada + languageName: node + linkType: hard + +"postcss-normalize-timing-functions@npm:^5.1.0": + version: 5.1.0 + resolution: "postcss-normalize-timing-functions@npm:5.1.0" + dependencies: + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: da550f50e90b0b23e17b67449a7d1efd1aa68288e66d4aa7614ca6f5cc012896be1972b7168eee673d27da36504faccf7b9f835c0f7e81243f966a42c8c030aa + languageName: node + linkType: hard + +"postcss-normalize-unicode@npm:^5.1.1": + version: 5.1.1 + resolution: "postcss-normalize-unicode@npm:5.1.1" + dependencies: + browserslist: ^4.21.4 + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: 4c24d26cc9f4b19a9397db4e71dd600dab690f1de8e14a3809e2aa1452dbc3791c208c38a6316bbc142f29e934fdf02858e68c94038c06174d78a4937e0f273c + languageName: node + linkType: hard + +"postcss-normalize-url@npm:^5.1.0": + version: 5.1.0 + resolution: "postcss-normalize-url@npm:5.1.0" + dependencies: + normalize-url: ^6.0.1 + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: 3bd4b3246d6600230bc827d1760b24cb3101827ec97570e3016cbe04dc0dd28f4dbe763245d1b9d476e182c843008fbea80823061f1d2219b96f0d5c724a24c0 + languageName: node + linkType: hard + +"postcss-normalize-whitespace@npm:^5.1.1": + version: 5.1.1 + resolution: "postcss-normalize-whitespace@npm:5.1.1" + dependencies: + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: 12d8fb6d1c1cba208cc08c1830959b7d7ad447c3f5581873f7e185f99a9a4230c43d3af21ca12c818e4690a5085a95b01635b762ad4a7bef69d642609b4c0e19 + languageName: node + linkType: hard + +"postcss-ordered-values@npm:^5.1.3": + version: 5.1.3 + resolution: "postcss-ordered-values@npm:5.1.3" + dependencies: + cssnano-utils: ^3.1.0 + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: 6f3ca85b6ceffc68aadaf319d9ee4c5ac16d93195bf8cba2d1559b631555ad61941461cda6d3909faab86e52389846b2b36345cff8f0c3f4eb345b1b8efadcf9 + languageName: node + linkType: hard + +"postcss-reduce-initial@npm:^5.1.2": + version: 5.1.2 + resolution: "postcss-reduce-initial@npm:5.1.2" + dependencies: + browserslist: ^4.21.4 + caniuse-api: ^3.0.0 + peerDependencies: + postcss: ^8.2.15 + checksum: 55db697f85231a81f1969d54c894e4773912d9ddb914f9b03d2e73abc4030f2e3bef4d7465756d0c1acfcc2c2d69974bfb50a972ab27546a7d68b5a4fc90282b + languageName: node + linkType: hard + +"postcss-reduce-transforms@npm:^5.1.0": + version: 5.1.0 + resolution: "postcss-reduce-transforms@npm:5.1.0" + dependencies: + postcss-value-parser: ^4.2.0 + peerDependencies: + postcss: ^8.2.15 + checksum: 0c6af2cba20e3ff63eb9ad045e634ddfb9c3e5c0e614c020db2a02f3aa20632318c4ede9e0c995f9225d9a101e673de91c0a6e10bb2fa5da6d6c75d15a55882f + languageName: node + linkType: hard + +"postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4, postcss-selector-parser@npm:^6.0.5, postcss-selector-parser@npm:^6.0.9": + version: 6.1.2 + resolution: "postcss-selector-parser@npm:6.1.2" + dependencies: + cssesc: ^3.0.0 + util-deprecate: ^1.0.2 + checksum: ce9440fc42a5419d103f4c7c1847cb75488f3ac9cbe81093b408ee9701193a509f664b4d10a2b4d82c694ee7495e022f8f482d254f92b7ffd9ed9dea696c6f84 + languageName: node + linkType: hard + +"postcss-svgo@npm:^5.1.0": + version: 5.1.0 + resolution: "postcss-svgo@npm:5.1.0" + dependencies: + postcss-value-parser: ^4.2.0 + svgo: ^2.7.0 + peerDependencies: + postcss: ^8.2.15 + checksum: d86eb5213d9f700cf5efe3073799b485fb7cacae0c731db3d7749c9c2b1c9bc85e95e0baeca439d699ff32ea24815fc916c4071b08f67ed8219df229ce1129bd + languageName: node + linkType: hard + +"postcss-unique-selectors@npm:^5.1.1": + version: 5.1.1 + resolution: "postcss-unique-selectors@npm:5.1.1" + dependencies: + postcss-selector-parser: ^6.0.5 + peerDependencies: + postcss: ^8.2.15 + checksum: 637e7b786e8558265775c30400c54b6b3b24d4748923f4a39f16a65fd0e394f564ccc9f0a1d3c0e770618a7637a7502ea1d0d79f731d429cb202255253c23278 + languageName: node + linkType: hard + +"postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0": + version: 4.2.0 + resolution: "postcss-value-parser@npm:4.2.0" + checksum: 819ffab0c9d51cf0acbabf8996dffbfafbafa57afc0e4c98db88b67f2094cb44488758f06e5da95d7036f19556a4a732525e84289a425f4f6fd8e412a9d7442f + languageName: node + linkType: hard + +"postcss@npm:^8.1.0, postcss@npm:^8.4.33": + version: 8.4.47 + resolution: "postcss@npm:8.4.47" + dependencies: + nanoid: ^3.3.7 + picocolors: ^1.1.0 + source-map-js: ^1.2.1 + checksum: f78440a9d8f97431dd2ab1ab8e1de64f12f3eff38a3d8d4a33919b96c381046a314658d2de213a5fa5eb296b656de76a3ec269fdea27f16d5ab465b916a0f52c + languageName: node + linkType: hard + +"postgres-array@npm:~2.0.0": + version: 2.0.0 + resolution: "postgres-array@npm:2.0.0" + checksum: 0e1e659888147c5de579d229a2d95c0d83ebdbffc2b9396d890a123557708c3b758a0a97ed305ce7f58edfa961fa9f0bbcd1ea9f08b6e5df73322e683883c464 + languageName: node + linkType: hard + +"postgres-bytea@npm:~1.0.0": + version: 1.0.0 + resolution: "postgres-bytea@npm:1.0.0" + checksum: d844ae4ca7a941b70e45cac1261a73ee8ed39d72d3d74ab1d645248185a1b7f0ac91a3c63d6159441020f4e1f7fe64689ac56536a307b31cef361e5187335090 + languageName: node + linkType: hard + +"postgres-date@npm:~1.0.4": + version: 1.0.7 + resolution: "postgres-date@npm:1.0.7" + checksum: 5745001d47e51cd767e46bcb1710649cd705d91a24d42fa661c454b6dcbb7353c066a5047983c90a626cd3bbfea9e626cc6fa84a35ec57e5bbb28b49f78e13ed + languageName: node + linkType: hard + +"postgres-interval@npm:^1.1.0": + version: 1.2.0 + resolution: "postgres-interval@npm:1.2.0" + dependencies: + xtend: ^4.0.0 + checksum: 746b71f93805ae33b03528e429dc624706d1f9b20ee81bf743263efb6a0cd79ae02a642a8a480dbc0f09547b4315ab7df6ce5ec0be77ed700bac42730f5c76b2 + languageName: node + linkType: hard + +"prebuild-install@npm:^7.1.1": + version: 7.1.2 + resolution: "prebuild-install@npm:7.1.2" + dependencies: + detect-libc: ^2.0.0 + expand-template: ^2.0.3 + github-from-package: 0.0.0 + minimist: ^1.2.3 + mkdirp-classic: ^0.5.3 + napi-build-utils: ^1.0.1 + node-abi: ^3.3.0 + pump: ^3.0.0 + rc: ^1.2.7 + simple-get: ^4.0.0 + tar-fs: ^2.0.0 + tunnel-agent: ^0.6.0 + bin: + prebuild-install: bin.js + checksum: 543dadf8c60e004ae9529e6013ca0cbeac8ef38b5f5ba5518cb0b622fe7f8758b34e4b5cb1a791db3cdc9d2281766302df6088bd1a225f206925d6fee17d6c5c + languageName: node + linkType: hard + +"prelude-ls@npm:^1.2.1": + version: 1.2.1 + resolution: "prelude-ls@npm:1.2.1" + checksum: cd192ec0d0a8e4c6da3bb80e4f62afe336df3f76271ac6deb0e6a36187133b6073a19e9727a1ff108cd8b9982e4768850d413baa71214dd80c7979617dca827a + languageName: node + linkType: hard + +"prelude-ls@npm:~1.1.2": + version: 1.1.2 + resolution: "prelude-ls@npm:1.1.2" + checksum: c4867c87488e4a0c233e158e4d0d5565b609b105d75e4c05dc760840475f06b731332eb93cc8c9cecb840aa8ec323ca3c9a56ad7820ad2e63f0261dadcb154e4 + languageName: node + linkType: hard + +"prettier@npm:3.3.3": + version: 3.3.3 + resolution: "prettier@npm:3.3.3" + bin: + prettier: bin/prettier.cjs + checksum: bc8604354805acfdde6106852d14b045bb20827ad76a5ffc2455b71a8257f94de93f17f14e463fe844808d2ccc87248364a5691488a3304f1031326e62d9276e + languageName: node + linkType: hard + +"prettier@npm:^2.3.2, prettier@npm:^2.7.1": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" + bin: + prettier: bin-prettier.js + checksum: b49e409431bf129dd89238d64299ba80717b57ff5a6d1c1a8b1a28b590d998a34e083fa13573bc732bb8d2305becb4c9a4407f8486c81fa7d55100eb08263cf8 + languageName: node + linkType: hard + +"pretty-error@npm:^4.0.0": + version: 4.0.0 + resolution: "pretty-error@npm:4.0.0" + dependencies: + lodash: ^4.17.20 + renderkid: ^3.0.0 + checksum: a5b9137365690104ded6947dca2e33360bf55e62a4acd91b1b0d7baa3970e43754c628cc9e16eafbdd4e8f8bcb260a5865475d4fc17c3106ff2d61db4e72cdf3 + languageName: node + linkType: hard + +"pretty-format@npm:^27.0.2": + version: 27.5.1 + resolution: "pretty-format@npm:27.5.1" + dependencies: + ansi-regex: ^5.0.1 + ansi-styles: ^5.0.0 + react-is: ^17.0.1 + checksum: cf610cffcb793885d16f184a62162f2dd0df31642d9a18edf4ca298e909a8fe80bdbf556d5c9573992c102ce8bf948691da91bf9739bee0ffb6e79c8a8a6e088 + languageName: node + linkType: hard + +"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": + version: 29.7.0 + resolution: "pretty-format@npm:29.7.0" + dependencies: + "@jest/schemas": ^29.6.3 + ansi-styles: ^5.0.0 + react-is: ^18.0.0 + checksum: 032c1602383e71e9c0c02a01bbd25d6759d60e9c7cf21937dde8357aa753da348fcec5def5d1002c9678a8524d5fe099ad98861286550ef44de8808cc61e43b6 + languageName: node + linkType: hard + +"pretty-ms@npm:^9.0.0": + version: 9.1.0 + resolution: "pretty-ms@npm:9.1.0" + dependencies: + parse-ms: ^4.0.0 + checksum: 0f66507467f2005040cccdcb36f35b82674d7809f41c4432009235ed6c920787afa17f621c25b7ccb8ccd80b0840c7b71f7f4a3addb8f0eeef3a033ff1e5cf71 + languageName: node + linkType: hard + +"prismjs@npm:^1.27.0": + version: 1.29.0 + resolution: "prismjs@npm:1.29.0" + checksum: 007a8869d4456ff8049dc59404e32d5666a07d99c3b0e30a18bd3b7676dfa07d1daae9d0f407f20983865fd8da56de91d09cb08e6aa61f5bc420a27c0beeaf93 + languageName: node + linkType: hard + +"prismjs@npm:~1.27.0": + version: 1.27.0 + resolution: "prismjs@npm:1.27.0" + checksum: 85c7f4a3e999073502cc9e1882af01e3709706369ec254b60bff1149eda701f40d02512acab956012dc7e61cfd61743a3a34c1bd0737e8dbacd79141e5698bbc + languageName: node + linkType: hard + +"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 98f6cd012d54b5334144c5255ecb941ee171744f45fca8b43b58ae5a0c1af07352475f481cadd9848e7f0250376ee584f6aa0951a856ff8f021bdfbff4eb33fc + languageName: node + linkType: hard + +"process-nextick-args@npm:~2.0.0": + version: 2.0.1 + resolution: "process-nextick-args@npm:2.0.1" + checksum: 1d38588e520dab7cea67cbbe2efdd86a10cc7a074c09657635e34f035277b59fbb57d09d8638346bf7090f8e8ebc070c96fa5fd183b777fff4f5edff5e9466cf + languageName: node + linkType: hard + +"process@npm:^0.11.10": + version: 0.11.10 + resolution: "process@npm:0.11.10" + checksum: bfcce49814f7d172a6e6a14d5fa3ac92cc3d0c3b9feb1279774708a719e19acd673995226351a082a9ae99978254e320ccda4240ddc474ba31a76c79491ca7c3 + languageName: node + linkType: hard + +"promise-inflight@npm:^1.0.1": + version: 1.0.1 + resolution: "promise-inflight@npm:1.0.1" + checksum: 22749483091d2c594261517f4f80e05226d4d5ecc1fc917e1886929da56e22b5718b7f2a75f3807e7a7d471bc3be2907fe92e6e8f373ddf5c64bae35b5af3981 + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: ^2.0.2 + retry: ^0.12.0 + checksum: f96a3f6d90b92b568a26f71e966cbbc0f63ab85ea6ff6c81284dc869b41510e6cdef99b6b65f9030f0db422bf7c96652a3fff9f2e8fb4a0f069d8f4430359429 + languageName: node + linkType: hard + +"promise.series@npm:^0.2.0": + version: 0.2.0 + resolution: "promise.series@npm:0.2.0" + checksum: 26b5956b5463d032b43d39fd8d34fdacf453ed3352462eed9626494a11d44beb385f86d6544dd12e51482a6ca8f303e0dfdee8653db4703213ba27dd2234754a + languageName: node + linkType: hard + +"prompts@npm:^2.0.1, prompts@npm:^2.4.2": + version: 2.4.2 + resolution: "prompts@npm:2.4.2" + dependencies: + kleur: ^3.0.3 + sisteransi: ^1.0.5 + checksum: d8fd1fe63820be2412c13bfc5d0a01909acc1f0367e32396962e737cb2fc52d004f3302475d5ce7d18a1e8a79985f93ff04ee03007d091029c3f9104bffc007d + languageName: node + linkType: hard + +"prop-types@npm:^15.0.0, prop-types@npm:^15.5.10, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": + version: 15.8.1 + resolution: "prop-types@npm:15.8.1" + dependencies: + loose-envify: ^1.4.0 + object-assign: ^4.1.1 + react-is: ^16.13.1 + checksum: c056d3f1c057cb7ff8344c645450e14f088a915d078dcda795041765047fa080d38e5d626560ccaac94a4e16e3aa15f3557c1a9a8d1174530955e992c675e459 + languageName: node + linkType: hard + +"proper-lockfile@npm:^4.1.2": + version: 4.1.2 + resolution: "proper-lockfile@npm:4.1.2" + dependencies: + graceful-fs: ^4.2.4 + retry: ^0.12.0 + signal-exit: ^3.0.2 + checksum: 00078ee6a61c216a56a6140c7d2a98c6c733b3678503002dc073ab8beca5d50ca271de4c85fca13b9b8ee2ff546c36674d1850509b84a04a5d0363bcb8638939 + languageName: node + linkType: hard + +"properties-reader@npm:^2.3.0": + version: 2.3.0 + resolution: "properties-reader@npm:2.3.0" + dependencies: + mkdirp: ^1.0.4 + checksum: cbf59e862dc507f8ce1f8d7641ed9737119f16a1d4dad8e79f17b303aaca1c6af7d36ddfef0f649cab4d200ba4334ac159af0b238f6978a085f5b1b5126b6cc3 + languageName: node + linkType: hard + +"property-expr@npm:^2.0.5": + version: 2.0.6 + resolution: "property-expr@npm:2.0.6" + checksum: 89977f4bb230736c1876f460dd7ca9328034502fd92e738deb40516d16564b850c0bbc4e052c3df88b5b8cd58e51c93b46a94bea049a3f23f4a022c038864cab + languageName: node + linkType: hard + +"property-information@npm:^5.0.0": + version: 5.6.0 + resolution: "property-information@npm:5.6.0" + dependencies: + xtend: ^4.0.0 + checksum: fcf87c6542e59a8bbe31ca0b3255a4a63ac1059b01b04469680288998bcfa97f341ca989566adbb63975f4d85339030b82320c324a511532d390910d1c583893 + languageName: node + linkType: hard + +"property-information@npm:^6.0.0": + version: 6.5.0 + resolution: "property-information@npm:6.5.0" + checksum: 6e55664e2f64083b715011e5bafaa1e694faf36986c235b0907e95d09259cc37c38382e3cc94a4c3f56366e05336443db12c8a0f0968a8c0a1b1416eebfc8f53 + languageName: node + linkType: hard + +"protocols@npm:^2.0.0, protocols@npm:^2.0.1": + version: 2.0.1 + resolution: "protocols@npm:2.0.1" + checksum: 4a9bef6aa0449a0245ded319ac3cbfd032c3e76ebb562777037a3a832c99253d0e8bc2847f7be350236df620a11f7d4fe683ea7f59a2cc14c69f746b6259eda4 + languageName: node + linkType: hard + +"proxy-addr@npm:~2.0.7": + version: 2.0.7 + resolution: "proxy-addr@npm:2.0.7" + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + checksum: 29c6990ce9364648255454842f06f8c46fcd124d3e6d7c5066df44662de63cdc0bad032e9bf5a3d653ff72141cc7b6019873d685708ac8210c30458ad99f2b74 + languageName: node + linkType: hard + +"proxy-agent@npm:6.4.0": + version: 6.4.0 + resolution: "proxy-agent@npm:6.4.0" + dependencies: + agent-base: ^7.0.2 + debug: ^4.3.4 + http-proxy-agent: ^7.0.1 + https-proxy-agent: ^7.0.3 + lru-cache: ^7.14.1 + pac-proxy-agent: ^7.0.1 + proxy-from-env: ^1.1.0 + socks-proxy-agent: ^8.0.2 + checksum: 4d3794ad5e07486298902f0a7f250d0f869fa0e92d790767ca3f793a81374ce0ab6c605f8ab8e791c4d754da96656b48d1c24cb7094bfd310a15867e4a0841d7 + languageName: node + linkType: hard + +"proxy-from-env@npm:^1.1.0": + version: 1.1.0 + resolution: "proxy-from-env@npm:1.1.0" + checksum: ed7fcc2ba0a33404958e34d95d18638249a68c430e30fcb6c478497d72739ba64ce9810a24f53a7d921d0c065e5b78e3822759800698167256b04659366ca4d4 + languageName: node + linkType: hard + +"ps-tree@npm:1.2.0": + version: 1.2.0 + resolution: "ps-tree@npm:1.2.0" + dependencies: + event-stream: =3.3.4 + bin: + ps-tree: ./bin/ps-tree.js + checksum: e635dd00f53d30d31696cf5f95b3a8dbdf9b1aeb36d4391578ce8e8cd22949b7c5536c73b0dc18c78615ea3ddd4be96101166be59ca2e3e3cb1e2f79ba3c7f98 + languageName: node + linkType: hard + +"pseudomap@npm:^1.0.2": + version: 1.0.2 + resolution: "pseudomap@npm:1.0.2" + checksum: 856c0aae0ff2ad60881168334448e898ad7a0e45fe7386d114b150084254c01e200c957cf378378025df4e052c7890c5bd933939b0e0d2ecfcc1dc2f0b2991f5 + languageName: node + linkType: hard + +"psl@npm:^1.1.28, psl@npm:^1.1.33": + version: 1.9.0 + resolution: "psl@npm:1.9.0" + checksum: 20c4277f640c93d393130673f392618e9a8044c6c7bf61c53917a0fddb4952790f5f362c6c730a9c32b124813e173733f9895add8d26f566ed0ea0654b2e711d + languageName: node + linkType: hard + +"public-encrypt@npm:^4.0.0": + version: 4.0.3 + resolution: "public-encrypt@npm:4.0.3" + dependencies: + bn.js: ^4.1.0 + browserify-rsa: ^4.0.0 + create-hash: ^1.1.0 + parse-asn1: ^5.0.0 + randombytes: ^2.0.1 + safe-buffer: ^5.1.2 + checksum: 215d446e43cef021a20b67c1df455e5eea134af0b1f9b8a35f9e850abf32991b0c307327bc5b9bc07162c288d5cdb3d4a783ea6c6640979ed7b5017e3e0c9935 + languageName: node + linkType: hard + +"pump@npm:^3.0.0": + version: 3.0.2 + resolution: "pump@npm:3.0.2" + dependencies: + end-of-stream: ^1.1.0 + once: ^1.3.1 + checksum: e0c4216874b96bd25ddf31a0b61a5613e26cc7afa32379217cf39d3915b0509def3565f5f6968fafdad2894c8bbdbd67d340e84f3634b2a29b950cffb6442d9f + languageName: node + linkType: hard + +"punycode@npm:^1.2.4, punycode@npm:^1.4.1": + version: 1.4.1 + resolution: "punycode@npm:1.4.1" + checksum: fa6e698cb53db45e4628559e557ddaf554103d2a96a1d62892c8f4032cd3bc8871796cae9eabc1bc700e2b6677611521ce5bb1d9a27700086039965d0cf34518 + languageName: node + linkType: hard + +"punycode@npm:^2.1.0, punycode@npm:^2.1.1": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: bb0a0ceedca4c3c57a9b981b90601579058903c62be23c5e8e843d2c2d4148a3ecf029d5133486fb0e1822b098ba8bba09e89d6b21742d02fa26bda6441a6fb2 + languageName: node + linkType: hard + +"pure-rand@npm:^6.0.0": + version: 6.1.0 + resolution: "pure-rand@npm:6.1.0" + checksum: 8d53bc02bed99eca0b65b505090152ee7e9bd67dd74f8ff32ba1c883b87234067c5bf68d2614759fb217d82594d7a92919e6df80f97885e7b12b42af4bd3316a + languageName: node + linkType: hard + +"qs@npm:6.11.2": + version: 6.11.2 + resolution: "qs@npm:6.11.2" + dependencies: + side-channel: ^1.0.4 + checksum: e812f3c590b2262548647d62f1637b6989cc56656dc960b893fe2098d96e1bd633f36576f4cd7564dfbff9db42e17775884db96d846bebe4f37420d073ecdc0b + languageName: node + linkType: hard + +"qs@npm:6.13.0, qs@npm:^6.11.0, qs@npm:^6.12.3, qs@npm:^6.9.4": + version: 6.13.0 + resolution: "qs@npm:6.13.0" + dependencies: + side-channel: ^1.0.6 + checksum: e9404dc0fc2849245107108ce9ec2766cde3be1b271de0bf1021d049dc5b98d1a2901e67b431ac5509f865420a7ed80b7acb3980099fe1c118a1c5d2e1432ad8 + languageName: node + linkType: hard + +"qs@npm:~6.5.2": + version: 6.5.3 + resolution: "qs@npm:6.5.3" + checksum: 6f20bf08cabd90c458e50855559539a28d00b2f2e7dddcb66082b16a43188418cb3cb77cbd09268bcef6022935650f0534357b8af9eeb29bf0f27ccb17655692 + languageName: node + linkType: hard + +"querystring-es3@npm:^0.2.0": + version: 0.2.1 + resolution: "querystring-es3@npm:0.2.1" + checksum: 691e8d6b8b157e7cd49ae8e83fcf86de39ab3ba948c25abaa94fba84c0986c641aa2f597770848c64abce290ed17a39c9df6df737dfa7e87c3b63acc7d225d61 + languageName: node + linkType: hard + +"querystringify@npm:^2.1.1": + version: 2.2.0 + resolution: "querystringify@npm:2.2.0" + checksum: 5641ea231bad7ef6d64d9998faca95611ed4b11c2591a8cae741e178a974f6a8e0ebde008475259abe1621cb15e692404e6b6626e927f7b849d5c09392604b15 + languageName: node + linkType: hard + +"queue-microtask@npm:^1.2.2": + version: 1.2.3 + resolution: "queue-microtask@npm:1.2.3" + checksum: b676f8c040cdc5b12723ad2f91414d267605b26419d5c821ff03befa817ddd10e238d22b25d604920340fd73efd8ba795465a0377c4adf45a4a41e4234e42dc4 + languageName: node + linkType: hard + +"queue-tick@npm:^1.0.1": + version: 1.0.1 + resolution: "queue-tick@npm:1.0.1" + checksum: 57c3292814b297f87f792fbeb99ce982813e4e54d7a8bdff65cf53d5c084113913289d4a48ec8bbc964927a74b847554f9f4579df43c969a6c8e0f026457ad01 + languageName: node + linkType: hard + +"raf-schd@npm:^4.0.2": + version: 4.0.3 + resolution: "raf-schd@npm:4.0.3" + checksum: 45514041c5ad31fa96aef3bb3c572a843b92da2f2cd1cb4a47c9ad58e48761d3a4126e18daa32b2bfa0bc2551a42d8f324a0e40e536cb656969929602b4e8b58 + languageName: node + linkType: hard + +"railroad-diagrams@npm:^1.0.0": + version: 1.0.0 + resolution: "railroad-diagrams@npm:1.0.0" + checksum: 9e312af352b5ed89c2118edc0c06cef2cc039681817f65266719606e4e91ff6ae5374c707cc9033fe29a82c2703edf3c63471664f97f0167c85daf6f93496319 + languageName: node + linkType: hard + +"rambda@npm:^9.1.0": + version: 9.3.0 + resolution: "rambda@npm:9.3.0" + checksum: 9ab615c7f00dd8f4165887a92c34e752244b7c197ffd283255e3cd4f78c57a3832fef63ec9deda5bbeb66199f822add7d124acd8d85edb173839481ee809bd30 + languageName: node + linkType: hard + +"randexp@npm:0.4.6": + version: 0.4.6 + resolution: "randexp@npm:0.4.6" + dependencies: + discontinuous-range: 1.0.0 + ret: ~0.1.10 + checksum: 3c0d440a3f89d6d36844aa4dd57b5cdb0cab938a41956a16da743d3a3578ab32538fc41c16cc0984b6938f2ae4cbc0216967e9829e52191f70e32690d8e3445d + languageName: node + linkType: hard + +"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0": + version: 2.1.0 + resolution: "randombytes@npm:2.1.0" + dependencies: + safe-buffer: ^5.1.0 + checksum: d779499376bd4cbb435ef3ab9a957006c8682f343f14089ed5f27764e4645114196e75b7f6abf1cbd84fd247c0cb0651698444df8c9bf30e62120fbbc52269d6 + languageName: node + linkType: hard + +"randomfill@npm:^1.0.3": + version: 1.0.4 + resolution: "randomfill@npm:1.0.4" + dependencies: + randombytes: ^2.0.5 + safe-buffer: ^5.1.0 + checksum: 33734bb578a868d29ee1b8555e21a36711db084065d94e019a6d03caa67debef8d6a1bfd06a2b597e32901ddc761ab483a85393f0d9a75838f1912461d4dbfc7 + languageName: node + linkType: hard + +"range-parser@npm:^1.2.1, range-parser@npm:~1.2.1": + version: 1.2.1 + resolution: "range-parser@npm:1.2.1" + checksum: 0a268d4fea508661cf5743dfe3d5f47ce214fd6b7dec1de0da4d669dd4ef3d2144468ebe4179049eff253d9d27e719c88dae55be64f954e80135a0cada804ec9 + languageName: node + linkType: hard + +"raw-body@npm:2.5.2, raw-body@npm:^2.4.1": + version: 2.5.2 + resolution: "raw-body@npm:2.5.2" + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + checksum: ba1583c8d8a48e8fbb7a873fdbb2df66ea4ff83775421bfe21ee120140949ab048200668c47d9ae3880012f6e217052690628cf679ddfbd82c9fc9358d574676 + languageName: node + linkType: hard + +"raw-loader@npm:^4.0.2": + version: 4.0.2 + resolution: "raw-loader@npm:4.0.2" + dependencies: + loader-utils: ^2.0.0 + schema-utils: ^3.0.0 + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + checksum: 51cc1b0d0e8c37c4336b5318f3b2c9c51d6998ad6f56ea09612afcfefc9c1f596341309e934a744ae907177f28efc9f1654eacd62151e82853fcc6d37450e795 + languageName: node + linkType: hard + +"rc-progress@npm:3.5.1": + version: 3.5.1 + resolution: "rc-progress@npm:3.5.1" + dependencies: + "@babel/runtime": ^7.10.1 + classnames: ^2.2.6 + rc-util: ^5.16.1 + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: b0722a696396f985267e35e26f49c1c1bd6a17b4918eb93318fc36a7a5ffae9806932d4982a7da0d83349648ca85325b792003ec40240820fd6e00e0bc6f3c1d + languageName: node + linkType: hard + +"rc-util@npm:^5.16.1": + version: 5.43.0 + resolution: "rc-util@npm:5.43.0" + dependencies: + "@babel/runtime": ^7.18.3 + react-is: ^18.2.0 + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: 48c10afb5886aed86d1f5241883f972b2b16235b0cc4867a05d061324f107aa113260c34eeb13ad18f4b66d1264dbcb3baf725c8ea34fbdaa504410d4e71b3ce + languageName: node + linkType: hard + +"rc@npm:^1.2.7": + version: 1.2.8 + resolution: "rc@npm:1.2.8" + dependencies: + deep-extend: ^0.6.0 + ini: ~1.3.0 + minimist: ^1.2.0 + strip-json-comments: ~2.0.1 + bin: + rc: ./cli.js + checksum: 2e26e052f8be2abd64e6d1dabfbd7be03f80ec18ccbc49562d31f617d0015fbdbcf0f9eed30346ea6ab789e0fdfe4337f033f8016efdbee0df5354751842080e + languageName: node + linkType: hard + +"re2-wasm@npm:^1.0.2": + version: 1.0.2 + resolution: "re2-wasm@npm:1.0.2" + checksum: cd47ad62db1e2f01dec40129f0c994f86bebbade1bd85920fa32229bec0a64b0ebbf550fefbba68a1f8268b73d811f223f79264d5ed9a208efda3fb832e9f0a9 + languageName: node + linkType: hard + +"react-beautiful-dnd@npm:^13.0.0": + version: 13.1.1 + resolution: "react-beautiful-dnd@npm:13.1.1" + dependencies: + "@babel/runtime": ^7.9.2 + css-box-model: ^1.2.0 + memoize-one: ^5.1.1 + raf-schd: ^4.0.2 + react-redux: ^7.2.0 + redux: ^4.0.4 + use-memo-one: ^1.1.1 + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + checksum: 5f90f7c0ab77a14dfcd496cbd94bbde457612f380c6fc815f3bba7b52effd75132948fcaa661a902a184bb1e6ae5896dcf5b0c77c4ddf809a2c65288f3eed5a7 + languageName: node + linkType: hard + +"react-dev-utils@npm:^12.0.0-next.60": + version: 12.0.1 + resolution: "react-dev-utils@npm:12.0.1" + dependencies: + "@babel/code-frame": ^7.16.0 + address: ^1.1.2 + browserslist: ^4.18.1 + chalk: ^4.1.2 + cross-spawn: ^7.0.3 + detect-port-alt: ^1.1.6 + escape-string-regexp: ^4.0.0 + filesize: ^8.0.6 + find-up: ^5.0.0 + fork-ts-checker-webpack-plugin: ^6.5.0 + global-modules: ^2.0.0 + globby: ^11.0.4 + gzip-size: ^6.0.0 + immer: ^9.0.7 + is-root: ^2.1.0 + loader-utils: ^3.2.0 + open: ^8.4.0 + pkg-up: ^3.1.0 + prompts: ^2.4.2 + react-error-overlay: ^6.0.11 + recursive-readdir: ^2.2.2 + shell-quote: ^1.7.3 + strip-ansi: ^6.0.1 + text-table: ^0.2.0 + checksum: 2c6917e47f03d9595044770b0f883a61c6b660fcaa97b8ba459a1d57c9cca9aa374cd51296b22d461ff5e432105dbe6f04732dab128e52729c79239e1c23ab56 + languageName: node + linkType: hard + +"react-dom@npm:^16.13.1 || ^17.0.0 || ^18.0.0": + version: 18.3.1 + resolution: "react-dom@npm:18.3.1" + dependencies: + loose-envify: ^1.1.0 + scheduler: ^0.23.2 + peerDependencies: + react: ^18.3.1 + checksum: 298954ecd8f78288dcaece05e88b570014d8f6dce5db6f66e6ee91448debeb59dcd31561dddb354eee47e6c1bb234669459060deb238ed0213497146e555a0b9 + languageName: node + linkType: hard + +"react-double-scrollbar@npm:0.0.15": + version: 0.0.15 + resolution: "react-double-scrollbar@npm:0.0.15" + peerDependencies: + react: ">= 0.14.7" + checksum: f81c13bdf698d6f699178b6597cb43fff3ec7d2b47f489ee306499a814151822e21b2daed995840832a11261f24dbd56573fe9225d43df22c14af5c564041bc0 + languageName: node + linkType: hard + +"react-error-boundary@npm:^3.1.0": + version: 3.1.4 + resolution: "react-error-boundary@npm:3.1.4" + dependencies: + "@babel/runtime": ^7.12.5 + peerDependencies: + react: ">=16.13.1" + checksum: f36270a5d775a25c8920f854c0d91649ceea417b15b5bc51e270a959b0476647bb79abb4da3be7dd9a4597b029214e8fe43ea914a7f16fa7543c91f784977f1b + languageName: node + linkType: hard + +"react-error-overlay@npm:^6.0.11": + version: 6.0.11 + resolution: "react-error-overlay@npm:6.0.11" + checksum: ce7b44c38fadba9cedd7c095cf39192e632daeccf1d0747292ed524f17dcb056d16bc197ddee5723f9dd888f0b9b19c3b486c430319e30504289b9296f2d2c42 + languageName: node + linkType: hard + +"react-fast-compare@npm:^2.0.1": + version: 2.0.4 + resolution: "react-fast-compare@npm:2.0.4" + checksum: 06046595f90a4e3e3a56f40a8078c00aa71bdb064ddb98343f577f546aa22e888831fd45f009c93b34707cc842b4c637737e956fd13d6f80607ee92fb9cf9a1c + languageName: node + linkType: hard + +"react-fast-compare@npm:^3.1.1": + version: 3.2.2 + resolution: "react-fast-compare@npm:3.2.2" + checksum: 2071415b4f76a3e6b55c84611c4d24dcb12ffc85811a2840b5a3f1ff2d1a99be1020d9437ee7c6e024c9f4cbb84ceb35e48cf84f28fcb00265ad2dfdd3947704 + languageName: node + linkType: hard + +"react-helmet@npm:6.1.0": + version: 6.1.0 + resolution: "react-helmet@npm:6.1.0" + dependencies: + object-assign: ^4.1.1 + prop-types: ^15.7.2 + react-fast-compare: ^3.1.1 + react-side-effect: ^2.1.0 + peerDependencies: + react: ">=16.3.0" + checksum: a4998479dab7fc1c2799eddefb1870a9d881b5f71cfdf97979a9882e42f4bb50402d55335f308f461e735e01a06f46b16cc7b4e6bcb22c7a4a6f85a753c5c106 + languageName: node + linkType: hard + +"react-hook-form@npm:^7.12.2": + version: 7.53.0 + resolution: "react-hook-form@npm:7.53.0" + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + checksum: 84d67fb79bad03d0aa809b5e411d97fb081fc13cd2b6d063a988f81f6fbef8545463e05360afa9d8d58fff19f08fa919930dcdc98a9e68bf74048c9f63e10ad5 + languageName: node + linkType: hard + +"react-idle-timer@npm:5.7.2": + version: 5.7.2 + resolution: "react-idle-timer@npm:5.7.2" + peerDependencies: + react: ">=16" + react-dom: ">=16" + checksum: 6faf3cfa87c9d65ae7a87078a2d82db5b821936a45565a98d69e7341e4b4acd5610b1f26cf1a6809b5551e4c30357f2ab5ce729c4c33751f66cb9ce6072dfb02 + languageName: node + linkType: hard + +"react-is@npm:^16.13.1, react-is@npm:^16.7.0": + version: 16.13.1 + resolution: "react-is@npm:16.13.1" + checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f + languageName: node + linkType: hard + +"react-is@npm:^16.8.0 || ^17.0.0, react-is@npm:^17.0.1, react-is@npm:^17.0.2": + version: 17.0.2 + resolution: "react-is@npm:17.0.2" + checksum: 9d6d111d8990dc98bc5402c1266a808b0459b5d54830bbea24c12d908b536df7883f268a7868cfaedde3dd9d4e0d574db456f84d2e6df9c4526f99bb4b5344d8 + languageName: node + linkType: hard + +"react-is@npm:^18.0.0, react-is@npm:^18.2.0, react-is@npm:^18.3.1": + version: 18.3.1 + resolution: "react-is@npm:18.3.1" + checksum: e20fe84c86ff172fc8d898251b7cc2c43645d108bf96d0b8edf39b98f9a2cae97b40520ee7ed8ee0085ccc94736c4886294456033304151c3f94978cec03df21 + languageName: node + linkType: hard + +"react-markdown@npm:^8.0.0": + version: 8.0.7 + resolution: "react-markdown@npm:8.0.7" + dependencies: + "@types/hast": ^2.0.0 + "@types/prop-types": ^15.0.0 + "@types/unist": ^2.0.0 + comma-separated-tokens: ^2.0.0 + hast-util-whitespace: ^2.0.0 + prop-types: ^15.0.0 + property-information: ^6.0.0 + react-is: ^18.0.0 + remark-parse: ^10.0.0 + remark-rehype: ^10.0.0 + space-separated-tokens: ^2.0.0 + style-to-object: ^0.4.0 + unified: ^10.0.0 + unist-util-visit: ^4.0.0 + vfile: ^5.0.0 + peerDependencies: + "@types/react": ">=16" + react: ">=16" + checksum: 0f3e570975134a3382c3fe5189e04e742ae154941463bdfaab2293319da1f1585cb9b75b6f07d99f514c4d728d69cc1af3c96ab37df90003b3bcc210dd0001ba + languageName: node + linkType: hard + +"react-redux@npm:^7.2.0": + version: 7.2.9 + resolution: "react-redux@npm:7.2.9" + dependencies: + "@babel/runtime": ^7.15.4 + "@types/react-redux": ^7.1.20 + hoist-non-react-statics: ^3.3.2 + loose-envify: ^1.4.0 + prop-types: ^15.7.2 + react-is: ^17.0.2 + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: 369a2bdcf87915659af9e5c55abfd9f52a84e43e0d12dcc108ed17dbe6933558b7b7fc12caa9c10c1a10a8be7df89454b6c96989d8573fedec1a772c94a1f145 + languageName: node + linkType: hard + +"react-refresh@npm:^0.14.0": + version: 0.14.2 + resolution: "react-refresh@npm:0.14.2" + checksum: d80db4bd40a36dab79010dc8aa317a5b931f960c0d83c4f3b81f0552cbcf7f29e115b84bb7908ec6a1eb67720fff7023084eff73ece8a7ddc694882478464382 + languageName: node + linkType: hard + +"react-router-dom@npm:^6.0.0": + version: 6.27.0 + resolution: "react-router-dom@npm:6.27.0" + dependencies: + "@remix-run/router": 1.20.0 + react-router: 6.27.0 + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: de3dcc56297a2879a0e3997fa34ba0f3e1b9986a2ad3ef7991f913902ecf38da0282c98f7834f344ce2d881dbab0a382201a57e9f9ef5e9816febdb26dc038b7 + languageName: node + linkType: hard + +"react-router@npm:6.27.0": + version: 6.27.0 + resolution: "react-router@npm:6.27.0" + dependencies: + "@remix-run/router": 1.20.0 + peerDependencies: + react: ">=16.8" + checksum: d22eedc33bcb11891b431655f90eed2d52c2fb3165ad11ca625f62970caf59c4859e6b1a3f92e78902b31ff1a8b2482ebf97ddebb82e9687d1f98730c14e04e6 + languageName: node + linkType: hard + +"react-side-effect@npm:^2.1.0": + version: 2.1.2 + resolution: "react-side-effect@npm:2.1.2" + peerDependencies: + react: ^16.3.0 || ^17.0.0 || ^18.0.0 + checksum: c5eb1f42b464fb093bca59aaae0f1b2060373a2aaff95275b8781493628cdbbb6acdd6014e7883782c65c361f35a30f28cc515d68a1263ddb39cbbc47110be53 + languageName: node + linkType: hard + +"react-sparklines@npm:^1.7.0": + version: 1.7.0 + resolution: "react-sparklines@npm:1.7.0" + dependencies: + prop-types: ^15.5.10 + peerDependencies: + react: "*" + react-dom: "*" + checksum: 9d2f701031e56e0c7b49e3b56479cd7bc1b651c029c2d525d2b480cf6ebcecbdb4dfe83053e7bcdecee1c490f3e5b4cecfa8b48301860b679778d6df7758e480 + languageName: node + linkType: hard + +"react-syntax-highlighter@npm:^15.4.5": + version: 15.6.1 + resolution: "react-syntax-highlighter@npm:15.6.1" + dependencies: + "@babel/runtime": ^7.3.1 + highlight.js: ^10.4.1 + highlightjs-vue: ^1.0.0 + lowlight: ^1.17.0 + prismjs: ^1.27.0 + refractor: ^3.6.0 + peerDependencies: + react: ">= 0.14.0" + checksum: 417b6f1f2e0c1e00dcc12d34da457b94c7419345306a951d0a8d2d031a0c964179d6b700137870ad1397572cbc3a4454e94de7bbef914a81674edae2098f02dc + languageName: node + linkType: hard + +"react-transition-group@npm:^4.0.0, react-transition-group@npm:^4.4.0, react-transition-group@npm:^4.4.5": + version: 4.4.5 + resolution: "react-transition-group@npm:4.4.5" + dependencies: + "@babel/runtime": ^7.5.5 + dom-helpers: ^5.0.1 + loose-envify: ^1.4.0 + prop-types: ^15.6.2 + peerDependencies: + react: ">=16.6.0" + react-dom: ">=16.6.0" + checksum: 75602840106aa9c6545149d6d7ae1502fb7b7abadcce70a6954c4b64a438ff1cd16fc77a0a1e5197cdd72da398f39eb929ea06f9005c45b132ed34e056ebdeb1 + languageName: node + linkType: hard + +"react-universal-interface@npm:^0.6.2": + version: 0.6.2 + resolution: "react-universal-interface@npm:0.6.2" + peerDependencies: + react: "*" + tslib: "*" + checksum: 070a7e9e3cdd8b0ec91a2ac9ac0a8df6bcb3fd183d2775bf0f439b9870fc1faf5b4fa9fe9741abd5187f0a35be645cb4004e1c9ebda9ada7e5d0a624f94910cb + languageName: node + linkType: hard + +"react-use@npm:^17.2.4, react-use@npm:^17.3.2, react-use@npm:^17.4.0, react-use@npm:^17.5.0": + version: 17.5.1 + resolution: "react-use@npm:17.5.1" + dependencies: + "@types/js-cookie": ^2.2.6 + "@xobotyi/scrollbar-width": ^1.9.5 + copy-to-clipboard: ^3.3.1 + fast-deep-equal: ^3.1.3 + fast-shallow-equal: ^1.0.0 + js-cookie: ^2.2.1 + nano-css: ^5.6.2 + react-universal-interface: ^0.6.2 + resize-observer-polyfill: ^1.5.1 + screenfull: ^5.1.0 + set-harmonic-interval: ^1.0.1 + throttle-debounce: ^3.0.1 + ts-easing: ^0.2.0 + tslib: ^2.1.0 + peerDependencies: + react: "*" + react-dom: "*" + checksum: 68f4333d986161038308a844d4ab99103484b69a0599a03c345eeb7cb5a0eabd0c55994fefc471ef11d4d2799a8e063d7f11fe0c48d56b54516333025fc7d726 + languageName: node + linkType: hard + +"react-virtualized-auto-sizer@npm:^1.0.11": + version: 1.0.24 + resolution: "react-virtualized-auto-sizer@npm:1.0.24" + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + checksum: e7d98563735dabbd1c58727c9d3e9f08f6a60a9964d25507cf4ef08f8964b6e421491c892ee0a99e47630118fdca42f1c60cef15ebda3659face58025dba3e98 + languageName: node + linkType: hard + +"react-window@npm:^1.8.6": + version: 1.8.10 + resolution: "react-window@npm:1.8.10" + dependencies: + "@babel/runtime": ^7.0.0 + memoize-one: ">=3.1.1 <6" + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + checksum: e8830f32e3ad4bf91af9cdc5cead84148c7694ce6abd9fdb447fb609da6cd4bbd0bbc75ff985f78828f4bbbd3ba4cbc98235cc9c056b5e5787578518f7fafbb9 + languageName: node + linkType: hard + +"react@npm:^16.13.1 || ^17.0.0 || ^18.0.0": + version: 18.3.1 + resolution: "react@npm:18.3.1" + dependencies: + loose-envify: ^1.1.0 + checksum: a27bcfa8ff7c15a1e50244ad0d0c1cb2ad4375eeffefd266a64889beea6f6b64c4966c9b37d14ee32d6c9fcd5aa6ba183b6988167ab4d127d13e7cb5b386a376 + languageName: node + linkType: hard + +"read-yaml-file@npm:^1.1.0": + version: 1.1.0 + resolution: "read-yaml-file@npm:1.1.0" + dependencies: + graceful-fs: ^4.1.5 + js-yaml: ^3.6.1 + pify: ^4.0.1 + strip-bom: ^3.0.0 + checksum: 41ee5f075507ef0403328dd54e225a61c3149f915675ce7fd0fd791ddcce2e6c30a9fe0f76ffa7a465c1c157b9b4ad8ded1dcf47dc3b396103eeb013490bbc2e + languageName: node + linkType: hard + +"readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.6, readable-stream@npm:^2.3.8": + version: 2.3.8 + resolution: "readable-stream@npm:2.3.8" + dependencies: + core-util-is: ~1.0.0 + inherits: ~2.0.3 + isarray: ~1.0.0 + process-nextick-args: ~2.0.0 + safe-buffer: ~5.1.1 + string_decoder: ~1.1.1 + util-deprecate: ~1.0.1 + checksum: 65645467038704f0c8aaf026a72fbb588a9e2ef7a75cd57a01702ee9db1c4a1e4b03aaad36861a6a0926546a74d174149c8c207527963e0c2d3eee2f37678a42 + languageName: node + linkType: hard + +"readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: ^2.0.3 + string_decoder: ^1.1.1 + util-deprecate: ^1.0.1 + checksum: bdcbe6c22e846b6af075e32cf8f4751c2576238c5043169a1c221c92ee2878458a816a4ea33f4c67623c0b6827c8a400409bfb3cf0bf3381392d0b1dfb52ac8d + languageName: node + linkType: hard + +"readable-stream@npm:^4.0.0, readable-stream@npm:^4.5.2": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" + dependencies: + abort-controller: ^3.0.0 + buffer: ^6.0.3 + events: ^3.3.0 + process: ^0.11.10 + string_decoder: ^1.3.0 + checksum: c4030ccff010b83e4f33289c535f7830190773e274b3fcb6e2541475070bdfd69c98001c3b0cb78763fc00c8b62f514d96c2b10a8bd35d5ce45203a25fa1d33a + languageName: node + linkType: hard + +"readdir-glob@npm:^1.1.2": + version: 1.1.3 + resolution: "readdir-glob@npm:1.1.3" + dependencies: + minimatch: ^5.1.0 + checksum: 1dc0f7440ff5d9378b593abe9d42f34ebaf387516615e98ab410cf3a68f840abbf9ff1032d15e0a0dbffa78f9e2c46d4fafdbaac1ca435af2efe3264e3f21874 + languageName: node + linkType: hard + +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: ^2.2.1 + checksum: 1ced032e6e45670b6d7352d71d21ce7edf7b9b928494dcaba6f11fba63180d9da6cd7061ebc34175ffda6ff529f481818c962952004d273178acd70f7059b320 + languageName: node + linkType: hard + +"rechoir@npm:^0.8.0": + version: 0.8.0 + resolution: "rechoir@npm:0.8.0" + dependencies: + resolve: ^1.20.0 + checksum: ad3caed8afdefbc33fbc30e6d22b86c35b3d51c2005546f4e79bcc03c074df804b3640ad18945e6bef9ed12caedc035655ec1082f64a5e94c849ff939dc0a788 + languageName: node + linkType: hard + +"recursive-readdir@npm:^2.2.2": + version: 2.2.3 + resolution: "recursive-readdir@npm:2.2.3" + dependencies: + minimatch: ^3.0.5 + checksum: 88ec96e276237290607edc0872b4f9842837b95cfde0cdbb1e00ba9623dfdf3514d44cdd14496ab60a0c2dd180a6ef8a3f1c34599e6cf2273afac9b72a6fb2b5 + languageName: node + linkType: hard + +"redent@npm:^3.0.0": + version: 3.0.0 + resolution: "redent@npm:3.0.0" + dependencies: + indent-string: ^4.0.0 + strip-indent: ^3.0.0 + checksum: fa1ef20404a2d399235e83cc80bd55a956642e37dd197b4b612ba7327bf87fa32745aeb4a1634b2bab25467164ab4ed9c15be2c307923dd08b0fe7c52431ae6b + languageName: node + linkType: hard + +"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": + version: 1.2.0 + resolution: "redis-errors@npm:1.2.0" + checksum: f28ac2692113f6f9c222670735aa58aeae413464fd58ccf3fce3f700cae7262606300840c802c64f2b53f19f65993da24dc918afc277e9e33ac1ff09edb394f4 + languageName: node + linkType: hard + +"redis-parser@npm:^3.0.0": + version: 3.0.0 + resolution: "redis-parser@npm:3.0.0" + dependencies: + redis-errors: ^1.0.0 + checksum: 89290ae530332f2ae37577647fa18208d10308a1a6ba750b9d9a093e7398f5e5253f19855b64c98757f7129cccce958e4af2573fdc33bad41405f87f1943459a + languageName: node + linkType: hard + +"redux@npm:^4.0.0, redux@npm:^4.0.4": + version: 4.2.1 + resolution: "redux@npm:4.2.1" + dependencies: + "@babel/runtime": ^7.9.2 + checksum: f63b9060c3a1d930ae775252bb6e579b42415aee7a23c4114e21a0b4ba7ec12f0ec76936c00f546893f06e139819f0e2855e0d55ebfce34ca9c026241a6950dd + languageName: node + linkType: hard + +"reflect-metadata@npm:0.1.13": + version: 0.1.13 + resolution: "reflect-metadata@npm:0.1.13" + checksum: 798d379a7b6f6455501145419505c97dd11cbc23857a386add2b9ef15963ccf15a48d9d15507afe01d4cd74116df8a213247200bac00320bd7c11ddeaa5e8fb4 + languageName: node + linkType: hard + +"reflect-metadata@npm:^0.1.13": + version: 0.1.14 + resolution: "reflect-metadata@npm:0.1.14" + checksum: 155ad339319cec3c2d9d84719f730f8b6a6cd2a074733ec29dbae6c89d48a2914c7d07a2350212594f3aae160fa4da4f903e6512f27ceaf968443a7c692bcad0 + languageName: node + linkType: hard + +"reflect-metadata@npm:^0.2.1": + version: 0.2.2 + resolution: "reflect-metadata@npm:0.2.2" + checksum: a66c7b583e4efdd8f3c3124fbff33da2d0c86d8280617516308b32b2159af7a3698c961db3246387f56f6316b1d33a608f39bb2b49d813316dfc58f6d3bf3210 + languageName: node + linkType: hard + +"reflect.getprototypeof@npm:^1.0.4": + version: 1.0.6 + resolution: "reflect.getprototypeof@npm:1.0.6" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-abstract: ^1.23.1 + es-errors: ^1.3.0 + get-intrinsic: ^1.2.4 + globalthis: ^1.0.3 + which-builtin-type: ^1.1.3 + checksum: 88e9e65a7eaa0bf8e9a8bbf8ac07571363bc333ba8b6769ed5e013e0042ed7c385e97fae9049510b3b5fe4b42472d8f32de9ce8ce84902bc4297d4bbe3777dba + languageName: node + linkType: hard + +"refractor@npm:^3.6.0": + version: 3.6.0 + resolution: "refractor@npm:3.6.0" + dependencies: + hastscript: ^6.0.0 + parse-entities: ^2.0.0 + prismjs: ~1.27.0 + checksum: 39b01c4168c77c5c8486f9bf8907bbb05f257f15026057ba5728535815a2d90eed620468a4bfbb2b8ceefbb3ce3931a1be8b17152dbdbc8b0eef92450ff750a2 + languageName: node + linkType: hard + +"regenerate-unicode-properties@npm:^10.2.0": + version: 10.2.0 + resolution: "regenerate-unicode-properties@npm:10.2.0" + dependencies: + regenerate: ^1.4.2 + checksum: d5c5fc13f8b8d7e16e791637a4bfef741f8d70e267d51845ee7d5404a32fa14c75b181c4efba33e4bff8b0000a2f13e9773593713dfe5b66597df4259275ce63 + languageName: node + linkType: hard + +"regenerate@npm:^1.4.2": + version: 1.4.2 + resolution: "regenerate@npm:1.4.2" + checksum: 3317a09b2f802da8db09aa276e469b57a6c0dd818347e05b8862959c6193408242f150db5de83c12c3fa99091ad95fb42a6db2c3329bfaa12a0ea4cbbeb30cb0 + languageName: node + linkType: hard + +"regenerator-runtime@npm:^0.14.0": + version: 0.14.1 + resolution: "regenerator-runtime@npm:0.14.1" + checksum: 9f57c93277b5585d3c83b0cf76be47b473ae8c6d9142a46ce8b0291a04bb2cf902059f0f8445dcabb3fb7378e5fe4bb4ea1e008876343d42e46d3b484534ce38 + languageName: node + linkType: hard + +"regenerator-transform@npm:^0.15.2": + version: 0.15.2 + resolution: "regenerator-transform@npm:0.15.2" + dependencies: + "@babel/runtime": ^7.8.4 + checksum: 20b6f9377d65954980fe044cfdd160de98df415b4bff38fbade67b3337efaf078308c4fed943067cd759827cc8cfeca9cb28ccda1f08333b85d6a2acbd022c27 + languageName: node + linkType: hard + +"regexp.prototype.flags@npm:^1.5.1, regexp.prototype.flags@npm:^1.5.2": + version: 1.5.3 + resolution: "regexp.prototype.flags@npm:1.5.3" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-errors: ^1.3.0 + set-function-name: ^2.0.2 + checksum: 83ff0705b837f7cb6d664010a11642250f36d3f642263dd0f3bdfe8f150261aa7b26b50ee97f21c1da30ef82a580bb5afedbef5f45639d69edaafbeac9bbb0ed + languageName: node + linkType: hard + +"regexpu-core@npm:^6.1.1": + version: 6.1.1 + resolution: "regexpu-core@npm:6.1.1" + dependencies: + regenerate: ^1.4.2 + regenerate-unicode-properties: ^10.2.0 + regjsgen: ^0.8.0 + regjsparser: ^0.11.0 + unicode-match-property-ecmascript: ^2.0.0 + unicode-match-property-value-ecmascript: ^2.1.0 + checksum: ed8e3784e81b816b237313688f28b4695d30d4e0f823dfdf130fd4313c629ac6ec67650563867a6ca9a2435f33e79f3a5001c651aee52791e346213a948de0ff + languageName: node + linkType: hard + +"regjsgen@npm:^0.8.0": + version: 0.8.0 + resolution: "regjsgen@npm:0.8.0" + checksum: a1d925ff14a4b2be774e45775ee6b33b256f89c42d480e6d85152d2133f18bd3d6af662161b226fa57466f7efec367eaf7ccd2a58c0ec2a1306667ba2ad07b0d + languageName: node + linkType: hard + +"regjsparser@npm:^0.11.0": + version: 0.11.1 + resolution: "regjsparser@npm:0.11.1" + dependencies: + jsesc: ~3.0.2 + bin: + regjsparser: bin/parser + checksum: 231d60810ca12a760393d65d149aa9501ea28b02c27a61c551b4f9162fe3cf48b289423515b73b1aea52949346e78c76cd552ac7169817d31f34df348db90fb4 + languageName: node + linkType: hard + +"relateurl@npm:^0.2.7": + version: 0.2.7 + resolution: "relateurl@npm:0.2.7" + checksum: 5891e792eae1dfc3da91c6fda76d6c3de0333a60aa5ad848982ebb6dccaa06e86385fb1235a1582c680a3d445d31be01c6bfc0804ebbcab5aaf53fa856fde6b6 + languageName: node + linkType: hard + +"remark-gfm@npm:^3.0.1": + version: 3.0.1 + resolution: "remark-gfm@npm:3.0.1" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-gfm: ^2.0.0 + micromark-extension-gfm: ^2.0.0 + unified: ^10.0.0 + checksum: 02254f74d67b3419c2c9cf62d799ec35f6c6cd74db25c001361751991552a7ce86049a972107bff8122d85d15ae4a8d1a0618f3bc01a7df837af021ae9b2a04e + languageName: node + linkType: hard + +"remark-parse@npm:^10.0.0": + version: 10.0.2 + resolution: "remark-parse@npm:10.0.2" + dependencies: + "@types/mdast": ^3.0.0 + mdast-util-from-markdown: ^1.0.0 + unified: ^10.0.0 + checksum: 5041b4b44725f377e69986e02f8f072ae2222db5e7d3b6c80829756b842e811343ffc2069cae1f958a96bfa36104ab91a57d7d7e2f0cef521e210ab8c614d5c7 + languageName: node + linkType: hard + +"remark-rehype@npm:^10.0.0": + version: 10.1.0 + resolution: "remark-rehype@npm:10.1.0" + dependencies: + "@types/hast": ^2.0.0 + "@types/mdast": ^3.0.0 + mdast-util-to-hast: ^12.1.0 + unified: ^10.0.0 + checksum: b9ac8acff3383b204dfdc2599d0bdf86e6ca7e837033209584af2e6aaa6a9013e519a379afa3201299798cab7298c8f4b388de118c312c67234c133318aec084 + languageName: node + linkType: hard + +"remove-accents@npm:^0.4.2": + version: 0.4.4 + resolution: "remove-accents@npm:0.4.4" + checksum: 0219a20550f8e6d7ebb47993fc08633c9c10b171d573467671641178de7a00b47ffcdbbf3be323a22c45de8903ac869cc4707dfc5aa5f25b7711e0c9b4daf4f4 + languageName: node + linkType: hard + +"renderkid@npm:^3.0.0": + version: 3.0.0 + resolution: "renderkid@npm:3.0.0" + dependencies: + css-select: ^4.1.3 + dom-converter: ^0.2.0 + htmlparser2: ^6.1.0 + lodash: ^4.17.21 + strip-ansi: ^6.0.1 + checksum: 77162b62d6f33ab81f337c39efce0439ff0d1f6d441e29c35183151f83041c7850774fb904da163d6c844264d440d10557714e6daa0b19e4561a5cd4ef305d41 + languageName: node + linkType: hard + +"replace-in-file@npm:^7.1.0": + version: 7.2.0 + resolution: "replace-in-file@npm:7.2.0" + dependencies: + chalk: ^4.1.2 + glob: ^8.1.0 + yargs: ^17.7.2 + bin: + replace-in-file: bin/cli.js + checksum: 773cfff187a404a293ed0f8ee433fa6c14230b96c506455bd3a880a217b3a3ec31791b8acb3e32a629286e6d8a7825b94255f443d0873c52cb7593b05cda52ba + languageName: node + linkType: hard + +"request@npm:^2.88.0": + version: 2.88.2 + resolution: "request@npm:2.88.2" + dependencies: + aws-sign2: ~0.7.0 + aws4: ^1.8.0 + caseless: ~0.12.0 + combined-stream: ~1.0.6 + extend: ~3.0.2 + forever-agent: ~0.6.1 + form-data: ~2.3.2 + har-validator: ~5.1.3 + http-signature: ~1.2.0 + is-typedarray: ~1.0.0 + isstream: ~0.1.2 + json-stringify-safe: ~5.0.1 + mime-types: ~2.1.19 + oauth-sign: ~0.9.0 + performance-now: ^2.1.0 + qs: ~6.5.2 + safe-buffer: ^5.1.2 + tough-cookie: ~2.5.0 + tunnel-agent: ^0.6.0 + uuid: ^3.3.2 + checksum: 4e112c087f6eabe7327869da2417e9d28fcd0910419edd2eb17b6acfc4bfa1dad61954525949c228705805882d8a98a86a0ea12d7f739c01ee92af7062996983 + languageName: node + linkType: hard + +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80 + languageName: node + linkType: hard + +"require-from-string@npm:^2.0.2": + version: 2.0.2 + resolution: "require-from-string@npm:2.0.2" + checksum: a03ef6895445f33a4015300c426699bc66b2b044ba7b670aa238610381b56d3f07c686251740d575e22f4c87531ba662d06937508f0f3c0f1ddc04db3130560b + languageName: node + linkType: hard + +"requires-port@npm:^1.0.0": + version: 1.0.0 + resolution: "requires-port@npm:1.0.0" + checksum: eee0e303adffb69be55d1a214e415cf42b7441ae858c76dfc5353148644f6fd6e698926fc4643f510d5c126d12a705e7c8ed7e38061113bdf37547ab356797ff + languageName: node + linkType: hard + +"resize-observer-polyfill@npm:^1.5.1": + version: 1.5.1 + resolution: "resize-observer-polyfill@npm:1.5.1" + checksum: 57e7f79489867b00ba43c9c051524a5c8f162a61d5547e99333549afc23e15c44fd43f2f318ea0261ea98c0eb3158cca261e6f48d66e1ed1cd1f340a43977094 + languageName: node + linkType: hard + +"resolve-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "resolve-cwd@npm:3.0.0" + dependencies: + resolve-from: ^5.0.0 + checksum: 546e0816012d65778e580ad62b29e975a642989108d9a3c5beabfb2304192fa3c9f9146fbdfe213563c6ff51975ae41bac1d3c6e047dd9572c94863a057b4d81 + languageName: node + linkType: hard + +"resolve-dir@npm:^1.0.0, resolve-dir@npm:^1.0.1": + version: 1.0.1 + resolution: "resolve-dir@npm:1.0.1" + dependencies: + expand-tilde: ^2.0.0 + global-modules: ^1.0.0 + checksum: ef736b8ed60d6645c3b573da17d329bfb50ec4e1d6c5ffd6df49e3497acef9226f9810ea6823b8ece1560e01dcb13f77a9f6180d4f242d00cc9a8f4de909c65c + languageName: node + linkType: hard + +"resolve-from@npm:^4.0.0": + version: 4.0.0 + resolution: "resolve-from@npm:4.0.0" + checksum: f4ba0b8494846a5066328ad33ef8ac173801a51739eb4d63408c847da9a2e1c1de1e6cbbf72699211f3d13f8fc1325648b169bd15eb7da35688e30a5fb0e4a7f + languageName: node + linkType: hard + +"resolve-from@npm:^5.0.0": + version: 5.0.0 + resolution: "resolve-from@npm:5.0.0" + checksum: 4ceeb9113e1b1372d0cd969f3468fa042daa1dd9527b1b6bb88acb6ab55d8b9cd65dbf18819f9f9ddf0db804990901dcdaade80a215e7b2c23daae38e64f5bdf + languageName: node + linkType: hard + +"resolve-pkg-maps@npm:^1.0.0": + version: 1.0.0 + resolution: "resolve-pkg-maps@npm:1.0.0" + checksum: 1012afc566b3fdb190a6309cc37ef3b2dcc35dff5fa6683a9d00cd25c3247edfbc4691b91078c97adc82a29b77a2660c30d791d65dab4fc78bfc473f60289977 + languageName: node + linkType: hard + +"resolve.exports@npm:^2.0.0": + version: 2.0.2 + resolution: "resolve.exports@npm:2.0.2" + checksum: 1c7778ca1b86a94f8ab4055d196c7d87d1874b96df4d7c3e67bbf793140f0717fd506dcafd62785b079cd6086b9264424ad634fb904409764c3509c3df1653f2 + languageName: node + linkType: hard + +"resolve@npm:1.22.8, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:~1.22.1, resolve@npm:~1.22.2": + version: 1.22.8 + resolution: "resolve@npm:1.22.8" + dependencies: + is-core-module: ^2.13.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: f8a26958aa572c9b064562750b52131a37c29d072478ea32e129063e2da7f83e31f7f11e7087a18225a8561cfe8d2f0df9dbea7c9d331a897571c0a2527dbb4c + languageName: node + linkType: hard + +"resolve@npm:^2.0.0-next.5": + version: 2.0.0-next.5 + resolution: "resolve@npm:2.0.0-next.5" + dependencies: + is-core-module: ^2.13.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: a73ac69a1c4bd34c56b213d91f5b17ce390688fdb4a1a96ed3025cc7e08e7bfb90b3a06fcce461780cb0b589c958afcb0080ab802c71c01a7ecc8c64feafc89f + languageName: node + linkType: hard + +"resolve@patch:resolve@1.22.8#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@~1.22.1#~builtin, resolve@patch:resolve@~1.22.2#~builtin": + version: 1.22.8 + resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin::version=1.22.8&hash=07638b" + dependencies: + is-core-module: ^2.13.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: 5479b7d431cacd5185f8db64bfcb7286ae5e31eb299f4c4f404ad8aa6098b77599563ac4257cb2c37a42f59dfc06a1bec2bcf283bb448f319e37f0feb9a09847 + languageName: node + linkType: hard + +"resolve@patch:resolve@^2.0.0-next.5#~builtin": + version: 2.0.0-next.5 + resolution: "resolve@patch:resolve@npm%3A2.0.0-next.5#~builtin::version=2.0.0-next.5&hash=07638b" + dependencies: + is-core-module: ^2.13.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: 064d09c1808d0c51b3d90b5d27e198e6d0c5dad0eb57065fd40803d6a20553e5398b07f76739d69cbabc12547058bec6b32106ea66622375fb0d7e8fca6a846c + languageName: node + linkType: hard + +"restore-cursor@npm:^3.1.0": + version: 3.1.0 + resolution: "restore-cursor@npm:3.1.0" + dependencies: + onetime: ^5.1.0 + signal-exit: ^3.0.2 + checksum: f877dd8741796b909f2a82454ec111afb84eb45890eb49ac947d87991379406b3b83ff9673a46012fca0d7844bb989f45cc5b788254cf1a39b6b5a9659de0630 + languageName: node + linkType: hard + +"ret@npm:~0.1.10": + version: 0.1.15 + resolution: "ret@npm:0.1.15" + checksum: d76a9159eb8c946586567bd934358dfc08a36367b3257f7a3d7255fdd7b56597235af23c6afa0d7f0254159e8051f93c918809962ebd6df24ca2a83dbe4d4151 + languageName: node + linkType: hard + +"retry-request@npm:^7.0.0": + version: 7.0.2 + resolution: "retry-request@npm:7.0.2" + dependencies: + "@types/request": ^2.48.8 + extend: ^3.0.2 + teeny-request: ^9.0.0 + checksum: 2d7307422333f548e5f40524978a344b62193714f6209c4f6a41057ae279804eb9bc8e0a277791e7b6f2d5d76068bdaca8590662a909cf1e6cfc3ab789e4c6b6 + languageName: node + linkType: hard + +"retry@npm:0.13.1, retry@npm:^0.13.1": + version: 0.13.1 + resolution: "retry@npm:0.13.1" + checksum: 47c4d5be674f7c13eee4cfe927345023972197dbbdfba5d3af7e461d13b44de1bfd663bfc80d2f601f8ef3fc8164c16dd99655a221921954a65d044a2fc1233b + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 623bd7d2e5119467ba66202d733ec3c2e2e26568074923bc0585b6b99db14f357e79bdedb63cab56cec47491c4a0da7e6021a7465ca6dc4f481d3898fdd3158c + languageName: node + linkType: hard + +"reusify@npm:^1.0.4": + version: 1.0.4 + resolution: "reusify@npm:1.0.4" + checksum: c3076ebcc22a6bc252cb0b9c77561795256c22b757f40c0d8110b1300723f15ec0fc8685e8d4ea6d7666f36c79ccc793b1939c748bf36f18f542744a4e379fcc + languageName: node + linkType: hard + +"rfc4648@npm:^1.3.0": + version: 1.5.3 + resolution: "rfc4648@npm:1.5.3" + checksum: 19c81d502582e377125b00fbd7a5cdb0e351f9a1e40182fa9f608b48e1ab852d211b75facb2f4f3fa17f7c6ebc2ef4acca61ae7eb7fbcfa4768f11d2db678116 + languageName: node + linkType: hard + +"rfdc@npm:^1.3.0": + version: 1.4.1 + resolution: "rfdc@npm:1.4.1" + checksum: 3b05bd55062c1d78aaabfcea43840cdf7e12099968f368e9a4c3936beb744adb41cbdb315eac6d4d8c6623005d6f87fdf16d8a10e1ff3722e84afea7281c8d13 + languageName: node + linkType: hard + +"rifm@npm:^0.7.0": + version: 0.7.0 + resolution: "rifm@npm:0.7.0" + dependencies: + "@babel/runtime": ^7.3.1 + peerDependencies: + react: ">=16.8" + checksum: 7b89d9c5c92cb1b6848964ab5c5042d652ba803fe7ecea2282191e0e820b07fb3345306b2baf69af1cef2f0755c50e97efc51d0cfdd645b8956d05d5d19d381e + languageName: node + linkType: hard + +"rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: ^7.1.3 + bin: + rimraf: bin.js + checksum: 87f4164e396f0171b0a3386cc1877a817f572148ee13a7e113b238e48e8a9f2f31d009a92ec38a591ff1567d9662c6b67fd8818a2dbbaed74bc26a87a2a4a9a0 + languageName: node + linkType: hard + +"rimraf@npm:^5.0.5": + version: 5.0.10 + resolution: "rimraf@npm:5.0.10" + dependencies: + glob: ^10.3.7 + bin: + rimraf: dist/esm/bin.mjs + checksum: 50e27388dd2b3fa6677385fc1e2966e9157c89c86853b96d02e6915663a96b7ff4d590e14f6f70e90f9b554093aa5dbc05ac3012876be558c06a65437337bc05 + languageName: node + linkType: hard + +"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1": + version: 2.0.2 + resolution: "ripemd160@npm:2.0.2" + dependencies: + hash-base: ^3.0.0 + inherits: ^2.0.1 + checksum: 006accc40578ee2beae382757c4ce2908a826b27e2b079efdcd2959ee544ddf210b7b5d7d5e80467807604244e7388427330f5c6d4cd61e6edaddc5773ccc393 + languageName: node + linkType: hard + +"roarr@npm:^2.15.3": + version: 2.15.4 + resolution: "roarr@npm:2.15.4" + dependencies: + boolean: ^3.0.1 + detect-node: ^2.0.4 + globalthis: ^1.0.1 + json-stringify-safe: ^5.0.1 + semver-compare: ^1.0.0 + sprintf-js: ^1.1.2 + checksum: 682e28d5491e3ae99728a35ba188f4f0ccb6347dbd492f95dc9f4bfdfe8ee63d8203ad234766ee2db88c8d7a300714304976eb095ce5c9366fe586c03a21586c + languageName: node + linkType: hard + +"rollup-plugin-dts@npm:^6.1.0": + version: 6.1.1 + resolution: "rollup-plugin-dts@npm:6.1.1" + dependencies: + "@babel/code-frame": ^7.24.2 + magic-string: ^0.30.10 + peerDependencies: + rollup: ^3.29.4 || ^4 + typescript: ^4.5 || ^5.0 + dependenciesMeta: + "@babel/code-frame": + optional: true + checksum: e69da1a286570f5a8d990651a613b2063543a71ad3b3471a97e74ea328125ebee77a74b2c800031f8dcccdc92da0d086f833724d13a2c863a2cbdf7e8fc20329 + languageName: node + linkType: hard + +"rollup-plugin-esbuild@npm:^6.1.1": + version: 6.1.1 + resolution: "rollup-plugin-esbuild@npm:6.1.1" + dependencies: + "@rollup/pluginutils": ^5.0.5 + debug: ^4.3.4 + es-module-lexer: ^1.3.1 + get-tsconfig: ^4.7.2 + peerDependencies: + esbuild: ">=0.18.0" + rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 + checksum: b027ddfbc9519f6f6aa41537b102ea23a38df588686b86d62ebd40441dd7cc8ca8e227dcaea92fc7ae8a42dc57a9975a3b184771e0eeb4c1fbe6296f10ef9da5 + languageName: node + linkType: hard + +"rollup-plugin-postcss@npm:^4.0.0": + version: 4.0.2 + resolution: "rollup-plugin-postcss@npm:4.0.2" + dependencies: + chalk: ^4.1.0 + concat-with-sourcemaps: ^1.1.0 + cssnano: ^5.0.1 + import-cwd: ^3.0.0 + p-queue: ^6.6.2 + pify: ^5.0.0 + postcss-load-config: ^3.0.0 + postcss-modules: ^4.0.0 + promise.series: ^0.2.0 + resolve: ^1.19.0 + rollup-pluginutils: ^2.8.2 + safe-identifier: ^0.4.2 + style-inject: ^0.3.0 + peerDependencies: + postcss: 8.x + checksum: 67875e024fa36ba4bd43604dc50d02eabba0c93626cc372588260ae42aae3f98015ea1b0c3a78bcbd345ebea465ef636e5cb0f60dbc8b2e94fbe2514384395f0 + languageName: node + linkType: hard + +"rollup-pluginutils@npm:^2.8.2": + version: 2.8.2 + resolution: "rollup-pluginutils@npm:2.8.2" + dependencies: + estree-walker: ^0.6.1 + checksum: 339fdf866d8f4ff6e408fa274c0525412f7edb01dc46b5ccda51f575b7e0d20ad72965773376fb5db95a77a7fcfcab97bf841ec08dbadf5d6b08af02b7a2cf5e + languageName: node + linkType: hard + +"rollup@npm:^4.0.0": + version: 4.24.0 + resolution: "rollup@npm:4.24.0" + dependencies: + "@rollup/rollup-android-arm-eabi": 4.24.0 + "@rollup/rollup-android-arm64": 4.24.0 + "@rollup/rollup-darwin-arm64": 4.24.0 + "@rollup/rollup-darwin-x64": 4.24.0 + "@rollup/rollup-linux-arm-gnueabihf": 4.24.0 + "@rollup/rollup-linux-arm-musleabihf": 4.24.0 + "@rollup/rollup-linux-arm64-gnu": 4.24.0 + "@rollup/rollup-linux-arm64-musl": 4.24.0 + "@rollup/rollup-linux-powerpc64le-gnu": 4.24.0 + "@rollup/rollup-linux-riscv64-gnu": 4.24.0 + "@rollup/rollup-linux-s390x-gnu": 4.24.0 + "@rollup/rollup-linux-x64-gnu": 4.24.0 + "@rollup/rollup-linux-x64-musl": 4.24.0 + "@rollup/rollup-win32-arm64-msvc": 4.24.0 + "@rollup/rollup-win32-ia32-msvc": 4.24.0 + "@rollup/rollup-win32-x64-msvc": 4.24.0 + "@types/estree": 1.0.6 + fsevents: ~2.3.2 + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-powerpc64le-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: b7e915b0cc43749c2c71255ff58858496460b1a75148db2abecc8e9496af83f488517768593826715f610e20e480a5ae7f1132a1408eb1d364830d6b239325cf + languageName: node + linkType: hard + +"rtl-css-js@npm:^1.16.1": + version: 1.16.1 + resolution: "rtl-css-js@npm:1.16.1" + dependencies: + "@babel/runtime": ^7.1.2 + checksum: 7d9ab942098eee565784ccf957f6b7dfa78ea1eec7c6bffedc6641575d274189e90752537c7bdba1f43ae6534648144f467fd6d581527455ba626a4300e62c7a + languageName: node + linkType: hard + +"run-applescript@npm:^7.0.0": + version: 7.0.0 + resolution: "run-applescript@npm:7.0.0" + checksum: b02462454d8b182ad4117e5d4626e9e6782eb2072925c9fac582170b0627ae3c1ea92ee9b2df7daf84b5e9ffe14eb1cf5fb70bc44b15c8a0bfcdb47987e2410c + languageName: node + linkType: hard + +"run-async@npm:^2.4.0": + version: 2.4.1 + resolution: "run-async@npm:2.4.1" + checksum: a2c88aa15df176f091a2878eb840e68d0bdee319d8d97bbb89112223259cebecb94bc0defd735662b83c2f7a30bed8cddb7d1674eb48ae7322dc602b22d03797 + languageName: node + linkType: hard + +"run-parallel@npm:^1.1.9": + version: 1.2.0 + resolution: "run-parallel@npm:1.2.0" + dependencies: + queue-microtask: ^1.2.2 + checksum: cb4f97ad25a75ebc11a8ef4e33bb962f8af8516bb2001082ceabd8902e15b98f4b84b4f8a9b222e5d57fc3bd1379c483886ed4619367a7680dad65316993021d + languageName: node + linkType: hard + +"run-script-webpack-plugin@npm:^0.2.0": + version: 0.2.0 + resolution: "run-script-webpack-plugin@npm:0.2.0" + checksum: 1f5df65b726e098d602b4cc27472d9e2cd88841862f7ca2112f702b01f3c4fc1cd89b54fa63780691d988c9ab36cc9adc08a6fa056cdb9c7b85b027b21ba6cdd + languageName: node + linkType: hard + +"rxjs@npm:7.8.1, rxjs@npm:^7.5.5, rxjs@npm:^7.8.1": + version: 7.8.1 + resolution: "rxjs@npm:7.8.1" + dependencies: + tslib: ^2.1.0 + checksum: de4b53db1063e618ec2eca0f7965d9137cabe98cf6be9272efe6c86b47c17b987383df8574861bcced18ebd590764125a901d5506082be84a8b8e364bf05f119 + languageName: node + linkType: hard + +"rxjs@npm:^6.6.3": + version: 6.6.7 + resolution: "rxjs@npm:6.6.7" + dependencies: + tslib: ^1.9.0 + checksum: bc334edef1bb8bbf56590b0b25734ba0deaf8825b703256a93714308ea36dff8a11d25533671adf8e104e5e8f256aa6fdfe39b2e248cdbd7a5f90c260acbbd1b + languageName: node + linkType: hard + +"sade@npm:^1.7.3": + version: 1.8.1 + resolution: "sade@npm:1.8.1" + dependencies: + mri: ^1.1.0 + checksum: 0756e5b04c51ccdc8221ebffd1548d0ce5a783a44a0fa9017a026659b97d632913e78f7dca59f2496aa996a0be0b0c322afd87ca72ccd909406f49dbffa0f45d + languageName: node + linkType: hard + +"safe-array-concat@npm:^1.1.2": + version: 1.1.2 + resolution: "safe-array-concat@npm:1.1.2" + dependencies: + call-bind: ^1.0.7 + get-intrinsic: ^1.2.4 + has-symbols: ^1.0.3 + isarray: ^2.0.5 + checksum: a3b259694754ddfb73ae0663829e396977b99ff21cbe8607f35a469655656da8e271753497e59da8a7575baa94d2e684bea3e10ddd74ba046c0c9b4418ffa0c4 + languageName: node + linkType: hard + +"safe-buffer@npm:5.1.2, safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": + version: 5.1.2 + resolution: "safe-buffer@npm:5.1.2" + checksum: f2f1f7943ca44a594893a852894055cf619c1fbcb611237fc39e461ae751187e7baf4dc391a72125e0ac4fb2d8c5c0b3c71529622e6a58f46b960211e704903c + languageName: node + linkType: hard + +"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 + languageName: node + linkType: hard + +"safe-identifier@npm:^0.4.2": + version: 0.4.2 + resolution: "safe-identifier@npm:0.4.2" + checksum: 67e28ed89a74cf20b827419003d3cb60a0ebaec0771c2c818f4b2239bf4f96e01ad90aa8db6dc57ee90c0c438b6f46323e4b5a3d955d18d8c4e158ea035cabdd + languageName: node + linkType: hard + +"safe-regex-test@npm:^1.0.3": + version: 1.0.3 + resolution: "safe-regex-test@npm:1.0.3" + dependencies: + call-bind: ^1.0.6 + es-errors: ^1.3.0 + is-regex: ^1.1.4 + checksum: 6c7d392ff1ae7a3ae85273450ed02d1d131f1d2c76e177d6b03eb88e6df8fa062639070e7d311802c1615f351f18dc58f9454501c58e28d5ffd9b8f502ba6489 + languageName: node + linkType: hard + +"safe-stable-stringify@npm:^1.1": + version: 1.1.1 + resolution: "safe-stable-stringify@npm:1.1.1" + checksum: e32a30720e8a2e3043b8b96733f015c1aa7a21a5a328074ce917b8afe4d26b4308c186c74fa92131e5f794b1efc63caa32defafceaa2981accaaedbc8b2c861c + languageName: node + linkType: hard + +"safe-stable-stringify@npm:^2.2.0, safe-stable-stringify@npm:^2.3.1": + version: 2.5.0 + resolution: "safe-stable-stringify@npm:2.5.0" + checksum: d3ce103ed43c6c2f523e39607208bfb1c73aa48179fc5be53c3aa97c118390bffd4d55e012f5393b982b65eb3e0ee954dd57b547930d3f242b0053dcdb923d17 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.0.2, safer-buffer@npm:^2.1.0, safer-buffer@npm:~2.1.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 + languageName: node + linkType: hard + +"saxes@npm:^6.0.0": + version: 6.0.0 + resolution: "saxes@npm:6.0.0" + dependencies: + xmlchars: ^2.2.0 + checksum: d3fa3e2aaf6c65ed52ee993aff1891fc47d5e47d515164b5449cbf5da2cbdc396137e55590472e64c5c436c14ae64a8a03c29b9e7389fc6f14035cf4e982ef3b + languageName: node + linkType: hard + +"scheduler@npm:^0.23.2": + version: 0.23.2 + resolution: "scheduler@npm:0.23.2" + dependencies: + loose-envify: ^1.1.0 + checksum: 3e82d1f419e240ef6219d794ff29c7ee415fbdc19e038f680a10c067108e06284f1847450a210b29bbaf97b9d8a97ced5f624c31c681248ac84c80d56ad5a2c4 + languageName: node + linkType: hard + +"schema-utils@npm:2.7.0": + version: 2.7.0 + resolution: "schema-utils@npm:2.7.0" + dependencies: + "@types/json-schema": ^7.0.4 + ajv: ^6.12.2 + ajv-keywords: ^3.4.1 + checksum: 8889325b0ee1ae6a8f5d6aaa855c71e136ebbb7fd731b01a9d3ec8225dcb245f644c47c50104db4c741983b528cdff8558570021257d4d397ec6aaecd9172a8e + languageName: node + linkType: hard + +"schema-utils@npm:^3.0.0, schema-utils@npm:^3.1.1, schema-utils@npm:^3.2.0": + version: 3.3.0 + resolution: "schema-utils@npm:3.3.0" + dependencies: + "@types/json-schema": ^7.0.8 + ajv: ^6.12.5 + ajv-keywords: ^3.5.2 + checksum: ea56971926fac2487f0757da939a871388891bc87c6a82220d125d587b388f1704788f3706e7f63a7b70e49fc2db974c41343528caea60444afd5ce0fe4b85c0 + languageName: node + linkType: hard + +"schema-utils@npm:^4.0.0, schema-utils@npm:^4.2.0": + version: 4.2.0 + resolution: "schema-utils@npm:4.2.0" + dependencies: + "@types/json-schema": ^7.0.9 + ajv: ^8.9.0 + ajv-formats: ^2.1.1 + ajv-keywords: ^5.1.0 + checksum: 26a0463d47683258106e6652e9aeb0823bf0b85843039e068b57da1892f7ae6b6b1094d48e9ed5ba5cbe9f7166469d880858b9d91abe8bd249421eb813850cde + languageName: node + linkType: hard + +"schemes@npm:^1.4.0": + version: 1.4.0 + resolution: "schemes@npm:1.4.0" + dependencies: + extend: ^3.0.0 + checksum: 729646ac65fbf2b76529c8bbb3433b1079891c4916556d2e1302bfb0c6b84dafcc00ee56e76c4572becd70a2c22a8fd4690b656ea43d1e6b70c180720735948e + languageName: node + linkType: hard + +"screenfull@npm:^5.1.0": + version: 5.2.0 + resolution: "screenfull@npm:5.2.0" + checksum: 21eae33b780eb4679ea0ea2d14734b11168cf35049c45a2bf24ddeb39c67a788e7a8fb46d8b61ca6d8367fd67ce9dd4fc8bfe476489249c7189c2a79cf83f51a + languageName: node + linkType: hard + +"seedrandom@npm:^3.0.5": + version: 3.0.5 + resolution: "seedrandom@npm:3.0.5" + checksum: 728b56bc3bc1b9ddeabd381e449b51cb31bdc0aa86e27fcd0190cea8c44613d5bcb2f6bb63ed79f78180cbe791c20b8ec31a9627f7b7fc7f476fd2bdb7e2da9f + languageName: node + linkType: hard + +"select-hose@npm:^2.0.0": + version: 2.0.0 + resolution: "select-hose@npm:2.0.0" + checksum: d7e5fcc695a4804209d232a1b18624a5134be334d4e1114b0721f7a5e72bd73da483dcf41528c1af4f4f4892ad7cfd6a1e55c8ffb83f9c9fe723b738db609dbb + languageName: node + linkType: hard + +"selfsigned@npm:^2.0.0, selfsigned@npm:^2.4.1": + version: 2.4.1 + resolution: "selfsigned@npm:2.4.1" + dependencies: + "@types/node-forge": ^1.3.0 + node-forge: ^1 + checksum: 38b91c56f1d7949c0b77f9bbe4545b19518475cae15e7d7f0043f87b1626710b011ce89879a88969651f650a19d213bb15b7d5b4c2877df9eeeff7ba8f8b9bfa + languageName: node + linkType: hard + +"semver-compare@npm:^1.0.0": + version: 1.0.0 + resolution: "semver-compare@npm:1.0.0" + checksum: dd1d7e2909744cf2cf71864ac718efc990297f9de2913b68e41a214319e70174b1d1793ac16e31183b128c2b9812541300cb324db8168e6cf6b570703b171c68 + languageName: node + linkType: hard + +"semver@npm:7.6.3, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": + version: 7.6.3 + resolution: "semver@npm:7.6.3" + bin: + semver: bin/semver.js + checksum: 4110ec5d015c9438f322257b1c51fe30276e5f766a3f64c09edd1d7ea7118ecbc3f379f3b69032bacf13116dc7abc4ad8ce0d7e2bd642e26b0d271b56b61a7d8 + languageName: node + linkType: hard + +"semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": + version: 6.3.1 + resolution: "semver@npm:6.3.1" + bin: + semver: bin/semver.js + checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2 + languageName: node + linkType: hard + +"semver@npm:~7.5.4": + version: 7.5.4 + resolution: "semver@npm:7.5.4" + dependencies: + lru-cache: ^6.0.0 + bin: + semver: bin/semver.js + checksum: 12d8ad952fa353b0995bf180cdac205a4068b759a140e5d3c608317098b3575ac2f1e09182206bf2eb26120e1c0ed8fb92c48c592f6099680de56bb071423ca3 + languageName: node + linkType: hard + +"send@npm:0.19.0": + version: 0.19.0 + resolution: "send@npm:0.19.0" + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + etag: ~1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: ~1.2.1 + statuses: 2.0.1 + checksum: 5ae11bd900c1c2575525e2aa622e856804e2f96a09281ec1e39610d089f53aa69e13fd8db84b52f001d0318cf4bb0b3b904ad532fc4c0014eb90d32db0cff55f + languageName: node + linkType: hard + +"seq-queue@npm:^0.0.5": + version: 0.0.5 + resolution: "seq-queue@npm:0.0.5" + checksum: f8695a6cb613e1b378b9686cde4ea626944091a412fc1c9d24c5039283d4351dd115f4505e4cf103d3a2e4a9a6a72fc7698fdce703839fb1fec9627aa4ce5563 + languageName: node + linkType: hard + +"serialize-error@npm:^7.0.1": + version: 7.0.1 + resolution: "serialize-error@npm:7.0.1" + dependencies: + type-fest: ^0.13.1 + checksum: e0aba4dca2fc9fe74ae1baf38dbd99190e1945445a241ba646290f2176cdb2032281a76443b02ccf0caf30da5657d510746506368889a593b9835a497fc0732e + languageName: node + linkType: hard + +"serialize-error@npm:^8.0.1": + version: 8.1.0 + resolution: "serialize-error@npm:8.1.0" + dependencies: + type-fest: ^0.20.2 + checksum: 2eef236d50edd2d7926e602c14fb500dc3a125ee52e9f08f67033181b8e0be5d1122498bdf7c23c80683cddcad083a27974e9e7111ce23165f4d3bcdd6d65102 + languageName: node + linkType: hard + +"serialize-javascript@npm:^6.0.1": + version: 6.0.2 + resolution: "serialize-javascript@npm:6.0.2" + dependencies: + randombytes: ^2.1.0 + checksum: c4839c6206c1d143c0f80763997a361310305751171dd95e4b57efee69b8f6edd8960a0b7fbfc45042aadff98b206d55428aee0dc276efe54f100899c7fa8ab7 + languageName: node + linkType: hard + +"serve-index@npm:^1.9.1": + version: 1.9.1 + resolution: "serve-index@npm:1.9.1" + dependencies: + accepts: ~1.3.4 + batch: 0.6.1 + debug: 2.6.9 + escape-html: ~1.0.3 + http-errors: ~1.6.2 + mime-types: ~2.1.17 + parseurl: ~1.3.2 + checksum: e2647ce13379485b98a53ba2ea3fbad4d44b57540d00663b02b976e426e6194d62ac465c0d862cb7057f65e0de8ab8a684aa095427a4b8612412eca0d300d22f + languageName: node + linkType: hard + +"serve-static@npm:1.16.2": + version: 1.16.2 + resolution: "serve-static@npm:1.16.2" + dependencies: + encodeurl: ~2.0.0 + escape-html: ~1.0.3 + parseurl: ~1.3.3 + send: 0.19.0 + checksum: dffc52feb4cc5c68e66d0c7f3c1824d4e989f71050aefc9bd5f822a42c54c9b814f595fc5f2b717f4c7cc05396145f3e90422af31186a93f76cf15f707019759 + languageName: node + linkType: hard + +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 6e65a05f7cf7ebdf8b7c75b101e18c0b7e3dff4940d480efed8aad3a36a4005140b660fa1d804cb8bce911cac290441dc728084a30504d3516ac2ff7ad607b02 + languageName: node + linkType: hard + +"set-cookie-parser@npm:^2.4.6": + version: 2.7.0 + resolution: "set-cookie-parser@npm:2.7.0" + checksum: 1eed43d7b284b727b4e7d35e324a74c493469265488b0c8f464f5224186e7dbbdd1cb35c8822053581f807a10b930a628144041ad453db06548945c61d5a834f + languageName: node + linkType: hard + +"set-function-length@npm:^1.2.1": + version: 1.2.2 + resolution: "set-function-length@npm:1.2.2" + dependencies: + define-data-property: ^1.1.4 + es-errors: ^1.3.0 + function-bind: ^1.1.2 + get-intrinsic: ^1.2.4 + gopd: ^1.0.1 + has-property-descriptors: ^1.0.2 + checksum: a8248bdacdf84cb0fab4637774d9fb3c7a8e6089866d04c817583ff48e14149c87044ce683d7f50759a8c50fb87c7a7e173535b06169c87ef76f5fb276dfff72 + languageName: node + linkType: hard + +"set-function-name@npm:^2.0.1, set-function-name@npm:^2.0.2": + version: 2.0.2 + resolution: "set-function-name@npm:2.0.2" + dependencies: + define-data-property: ^1.1.4 + es-errors: ^1.3.0 + functions-have-names: ^1.2.3 + has-property-descriptors: ^1.0.2 + checksum: d6229a71527fd0404399fc6227e0ff0652800362510822a291925c9d7b48a1ca1a468b11b281471c34cd5a2da0db4f5d7ff315a61d26655e77f6e971e6d0c80f + languageName: node + linkType: hard + +"set-harmonic-interval@npm:^1.0.1": + version: 1.0.1 + resolution: "set-harmonic-interval@npm:1.0.1" + checksum: c122b831c2e0b1fb812e5e9d065094b9d174bd0576f9a779ab7a7d8881c8f6dd7d5fcab9a2553da15eea670eb598f9dd4d5162b626d45cc9c529706aa1444a84 + languageName: node + linkType: hard + +"setimmediate@npm:^1.0.4": + version: 1.0.5 + resolution: "setimmediate@npm:1.0.5" + checksum: c9a6f2c5b51a2dabdc0247db9c46460152ffc62ee139f3157440bd48e7c59425093f42719ac1d7931f054f153e2d26cf37dfeb8da17a794a58198a2705e527fd + languageName: node + linkType: hard + +"setprototypeof@npm:1.1.0": + version: 1.1.0 + resolution: "setprototypeof@npm:1.1.0" + checksum: 27cb44304d6c9e1a23bc6c706af4acaae1a7aa1054d4ec13c05f01a99fd4887109a83a8042b67ad90dbfcd100d43efc171ee036eb080667172079213242ca36e + languageName: node + linkType: hard + +"setprototypeof@npm:1.2.0": + version: 1.2.0 + resolution: "setprototypeof@npm:1.2.0" + checksum: be18cbbf70e7d8097c97f713a2e76edf84e87299b40d085c6bf8b65314e994cc15e2e317727342fa6996e38e1f52c59720b53fe621e2eb593a6847bf0356db89 + languageName: node + linkType: hard + +"sha.js@npm:^2.4.0, sha.js@npm:^2.4.11, sha.js@npm:^2.4.8, sha.js@npm:^2.4.9": + version: 2.4.11 + resolution: "sha.js@npm:2.4.11" + dependencies: + inherits: ^2.0.1 + safe-buffer: ^5.0.1 + bin: + sha.js: ./bin.js + checksum: ebd3f59d4b799000699097dadb831c8e3da3eb579144fd7eb7a19484cbcbb7aca3c68ba2bb362242eb09e33217de3b4ea56e4678184c334323eca24a58e3ad07 + languageName: node + linkType: hard + +"shebang-command@npm:^1.2.0": + version: 1.2.0 + resolution: "shebang-command@npm:1.2.0" + dependencies: + shebang-regex: ^1.0.0 + checksum: 9eed1750301e622961ba5d588af2212505e96770ec376a37ab678f965795e995ade7ed44910f5d3d3cb5e10165a1847f52d3348c64e146b8be922f7707958908 + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: ^3.0.0 + checksum: 6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa + languageName: node + linkType: hard + +"shebang-regex@npm:^1.0.0": + version: 1.0.0 + resolution: "shebang-regex@npm:1.0.0" + checksum: 404c5a752cd40f94591dfd9346da40a735a05139dac890ffc229afba610854d8799aaa52f87f7e0c94c5007f2c6af55bdcaeb584b56691926c5eaf41dc8f1372 + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222 + languageName: node + linkType: hard + +"shell-quote@npm:^1.7.3, shell-quote@npm:^1.8.1": + version: 1.8.1 + resolution: "shell-quote@npm:1.8.1" + checksum: 5f01201f4ef504d4c6a9d0d283fa17075f6770bfbe4c5850b074974c68062f37929ca61700d95ad2ac8822e14e8c4b990ca0e6e9272e64befd74ce5e19f0736b + languageName: node + linkType: hard + +"side-channel@npm:^1.0.4, side-channel@npm:^1.0.6": + version: 1.0.6 + resolution: "side-channel@npm:1.0.6" + dependencies: + call-bind: ^1.0.7 + es-errors: ^1.3.0 + get-intrinsic: ^1.2.4 + object-inspect: ^1.13.1 + checksum: bfc1afc1827d712271453e91b7cd3878ac0efd767495fd4e594c4c2afaa7963b7b510e249572bfd54b0527e66e4a12b61b80c061389e129755f34c493aad9b97 + languageName: node + linkType: hard + +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549 + languageName: node + linkType: hard + +"simple-concat@npm:^1.0.0": + version: 1.0.1 + resolution: "simple-concat@npm:1.0.1" + checksum: 4d211042cc3d73a718c21ac6c4e7d7a0363e184be6a5ad25c8a1502e49df6d0a0253979e3d50dbdd3f60ef6c6c58d756b5d66ac1e05cda9cacd2e9fc59e3876a + languageName: node + linkType: hard + +"simple-eval@npm:1.0.0": + version: 1.0.0 + resolution: "simple-eval@npm:1.0.0" + dependencies: + jsep: ^1.1.2 + checksum: 0f0719ae3a84d4b9c19366dc03065b1fe9638c982ed3e9d44ba541d25e3454e99419e3239034974fd6c5074b79c119419168b8f343fef4da6d7e35227cfd1f87 + languageName: node + linkType: hard + +"simple-get@npm:^3.0.3": + version: 3.1.1 + resolution: "simple-get@npm:3.1.1" + dependencies: + decompress-response: ^4.2.0 + once: ^1.3.1 + simple-concat: ^1.0.0 + checksum: 80195e70bf171486e75c31e28e5485468195cc42f85940f8b45c4a68472160144d223eb4d07bc82ef80cb974b7c401db021a540deb2d34ac4b3b8883da2d6401 + languageName: node + linkType: hard + +"simple-get@npm:^4.0.0, simple-get@npm:^4.0.1": + version: 4.0.1 + resolution: "simple-get@npm:4.0.1" + dependencies: + decompress-response: ^6.0.0 + once: ^1.3.1 + simple-concat: ^1.0.0 + checksum: e4132fd27cf7af230d853fa45c1b8ce900cb430dd0a3c6d3829649fe4f2b26574c803698076c4006450efb0fad2ba8c5455fbb5755d4b0a5ec42d4f12b31d27e + languageName: node + linkType: hard + +"simple-swizzle@npm:^0.2.2": + version: 0.2.2 + resolution: "simple-swizzle@npm:0.2.2" + dependencies: + is-arrayish: ^0.3.1 + checksum: a7f3f2ab5c76c4472d5c578df892e857323e452d9f392e1b5cf74b74db66e6294a1e1b8b390b519fa1b96b5b613f2a37db6cffef52c3f1f8f3c5ea64eb2d54c0 + languageName: node + linkType: hard + +"sisteransi@npm:^1.0.5": + version: 1.0.5 + resolution: "sisteransi@npm:1.0.5" + checksum: aba6438f46d2bfcef94cf112c835ab395172c75f67453fe05c340c770d3c402363018ae1ab4172a1026a90c47eaccf3af7b6ff6fa749a680c2929bd7fa2b37a4 + languageName: node + linkType: hard + +"slash@npm:^3.0.0": + version: 3.0.0 + resolution: "slash@npm:3.0.0" + checksum: 94a93fff615f25a999ad4b83c9d5e257a7280c90a32a7cb8b4a87996e4babf322e469c42b7f649fd5796edd8687652f3fb452a86dc97a816f01113183393f11c + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: b5167a7142c1da704c0e3af85c402002b597081dd9575031a90b4f229ca5678e9a36e8a374f1814c8156a725d17008ae3bde63b92f9cfd132526379e580bec8b + languageName: node + linkType: hard + +"smol-toml@npm:^1.3.0": + version: 1.3.0 + resolution: "smol-toml@npm:1.3.0" + checksum: 79e1db6b6cd32a13ad7602bfe1a02f20894fe599657a5cc2c8ffab7c3de4ba51f7426b701b513f9b859560918b36a63f7c73f7eaf6def8a1dc73db74ffd9b601 + languageName: node + linkType: hard + +"smtp-address-parser@npm:^1.0.3": + version: 1.1.0 + resolution: "smtp-address-parser@npm:1.1.0" + dependencies: + nearley: ^2.20.1 + checksum: 63314f22dfe6f2ab2845c4ffa68a48cbd1569507cf9ee429c45beff7c4b5957d6f63e84c31fe0d148f67e4b000c76cb1d8a9d1f0f6bd5a678fa9d6a80bac70e2 + languageName: node + linkType: hard + +"sockjs@npm:^0.3.24": + version: 0.3.24 + resolution: "sockjs@npm:0.3.24" + dependencies: + faye-websocket: ^0.11.3 + uuid: ^8.3.2 + websocket-driver: ^0.7.4 + checksum: 355309b48d2c4e9755349daa29cea1c0d9ee23e49b983841c6bf7a20276b00d3c02343f9f33f26d2ee8b261a5a02961b52a25c8da88b2538c5b68d3071b4934c + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^7.0.0": + version: 7.0.0 + resolution: "socks-proxy-agent@npm:7.0.0" + dependencies: + agent-base: ^6.0.2 + debug: ^4.3.3 + socks: ^2.6.2 + checksum: 720554370154cbc979e2e9ce6a6ec6ced205d02757d8f5d93fe95adae454fc187a5cbfc6b022afab850a5ce9b4c7d73e0f98e381879cf45f66317a4895953846 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.2, socks-proxy-agent@npm:^8.0.3, socks-proxy-agent@npm:^8.0.4": + version: 8.0.4 + resolution: "socks-proxy-agent@npm:8.0.4" + dependencies: + agent-base: ^7.1.1 + debug: ^4.3.4 + socks: ^2.8.3 + checksum: b2ec5051d85fe49072f9a250c427e0e9571fd09d5db133819192d078fd291276e1f0f50f6dbc04329b207738b1071314cee8bdbb4b12e27de42dbcf1d4233c67 + languageName: node + linkType: hard + +"socks@npm:^2.6.2, socks@npm:^2.8.3": + version: 2.8.3 + resolution: "socks@npm:2.8.3" + dependencies: + ip-address: ^9.0.5 + smart-buffer: ^4.2.0 + checksum: 7a6b7f6eedf7482b9e4597d9a20e09505824208006ea8f2c49b71657427f3c137ca2ae662089baa73e1971c62322d535d9d0cf1c9235cf6f55e315c18203eadd + languageName: node + linkType: hard + +"sorted-array-functions@npm:^1.3.0": + version: 1.3.0 + resolution: "sorted-array-functions@npm:1.3.0" + checksum: 673fd39ca3b6c92644d4483eac1700bb7d7555713a536822a7522a35af559bef3e72f10d89356b75042dc394cd7c2e2ab6f40024385218ec3c85bb7335032857 + languageName: node + linkType: hard + +"source-list-map@npm:^2.0.0": + version: 2.0.1 + resolution: "source-list-map@npm:2.0.1" + checksum: 806efc6f75e7cd31e4815e7a3aaf75a45c704871ea4075cb2eb49882c6fca28998f44fc5ac91adb6de03b2882ee6fb02f951fdc85e6a22b338c32bfe19557938 + languageName: node + linkType: hard + +"source-map-js@npm:^1.2.1": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 4eb0cd997cdf228bc253bcaff9340afeb706176e64868ecd20efbe6efea931465f43955612346d6b7318789e5265bdc419bc7669c1cebe3db0eb255f57efa76b + languageName: node + linkType: hard + +"source-map-support@npm:0.5.13": + version: 0.5.13 + resolution: "source-map-support@npm:0.5.13" + dependencies: + buffer-from: ^1.0.0 + source-map: ^0.6.0 + checksum: 933550047b6c1a2328599a21d8b7666507427c0f5ef5eaadd56b5da0fd9505e239053c66fe181bf1df469a3b7af9d775778eee283cbb7ae16b902ddc09e93a97 + languageName: node + linkType: hard + +"source-map-support@npm:~0.5.20": + version: 0.5.21 + resolution: "source-map-support@npm:0.5.21" + dependencies: + buffer-from: ^1.0.0 + source-map: ^0.6.0 + checksum: 43e98d700d79af1d36f859bdb7318e601dfc918c7ba2e98456118ebc4c4872b327773e5a1df09b0524e9e5063bb18f0934538eace60cca2710d1fa687645d137 + languageName: node + linkType: hard + +"source-map@npm:0.5.6": + version: 0.5.6 + resolution: "source-map@npm:0.5.6" + checksum: 390b3f5165c9631a74fb6fb55ba61e62a7f9b7d4026ae0e2bfc2899c241d71c1bccb8731c496dc7f7cb79a5f523406eb03d8c5bebe8448ee3fc38168e2d209c8 + languageName: node + linkType: hard + +"source-map@npm:^0.5.7": + version: 0.5.7 + resolution: "source-map@npm:0.5.7" + checksum: 5dc2043b93d2f194142c7f38f74a24670cd7a0063acdaf4bf01d2964b402257ae843c2a8fa822ad5b71013b5fcafa55af7421383da919752f22ff488bc553f4d + languageName: node + linkType: hard + +"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.0, source-map@npm:~0.6.1": + version: 0.6.1 + resolution: "source-map@npm:0.6.1" + checksum: 59ce8640cf3f3124f64ac289012c2b8bd377c238e316fb323ea22fbfe83da07d81e000071d7242cad7a23cd91c7de98e4df8830ec3f133cb6133a5f6e9f67bc2 + languageName: node + linkType: hard + +"source-map@npm:^0.7.3": + version: 0.7.4 + resolution: "source-map@npm:0.7.4" + checksum: 01cc5a74b1f0e1d626a58d36ad6898ea820567e87f18dfc9d24a9843a351aaa2ec09b87422589906d6ff1deed29693e176194dc88bcae7c9a852dc74b311dbf5 + languageName: node + linkType: hard + +"space-separated-tokens@npm:^1.0.0": + version: 1.1.5 + resolution: "space-separated-tokens@npm:1.1.5" + checksum: 8ef68f1cfa8ccad316b7f8d0df0919d0f1f6d32101e8faeee34ea3a923ce8509c1ad562f57388585ee4951e92d27afa211ed0a077d3d5995b5ba9180331be708 + languageName: node + linkType: hard + +"space-separated-tokens@npm:^2.0.0": + version: 2.0.2 + resolution: "space-separated-tokens@npm:2.0.2" + checksum: 202e97d7ca1ba0758a0aa4fe226ff98142073bcceeff2da3aad037968878552c3bbce3b3231970025375bbba5aee00c5b8206eda408da837ab2dc9c0f26be990 + languageName: node + linkType: hard + +"spawn-command@npm:^0.0.2-1": + version: 0.0.2 + resolution: "spawn-command@npm:0.0.2" + checksum: e35c5d28177b4d461d33c88cc11f6f3a5079e2b132c11e1746453bbb7a0c0b8a634f07541a2a234fa4758239d88203b758def509161b651e81958894c0b4b64b + languageName: node + linkType: hard + +"spawndamnit@npm:^2.0.0": + version: 2.0.0 + resolution: "spawndamnit@npm:2.0.0" + dependencies: + cross-spawn: ^5.1.0 + signal-exit: ^3.0.2 + checksum: c74b5e264ee5bc13d55692fd422d74c282e4607eb04ac64d19d06796718d89b14921620fa4237ec5635e7acdff21461670ff19850f210225410a353cad0d7fed + languageName: node + linkType: hard + +"spdy-transport@npm:^3.0.0": + version: 3.0.0 + resolution: "spdy-transport@npm:3.0.0" + dependencies: + debug: ^4.1.0 + detect-node: ^2.0.4 + hpack.js: ^2.1.6 + obuf: ^1.1.2 + readable-stream: ^3.0.6 + wbuf: ^1.7.3 + checksum: 0fcaad3b836fb1ec0bdd39fa7008b9a7a84a553f12be6b736a2512613b323207ffc924b9551cef0378f7233c85916cff1118652e03a730bdb97c0e042243d56c + languageName: node + linkType: hard + +"spdy@npm:^4.0.2": + version: 4.0.2 + resolution: "spdy@npm:4.0.2" + dependencies: + debug: ^4.1.0 + handle-thing: ^2.0.0 + http-deceiver: ^1.2.7 + select-hose: ^2.0.0 + spdy-transport: ^3.0.0 + checksum: 2c739d0ff6f56ad36d2d754d0261d5ec358457bea7cbf77b1b05b0c6464f2ce65b85f196305f50b7bd9120723eb94bae9933466f28e67e5cd8cde4e27f1d75f8 + languageName: node + linkType: hard + +"split-ca@npm:^1.0.1": + version: 1.0.1 + resolution: "split-ca@npm:1.0.1" + checksum: 1e7409938a95ee843fe2593156a5735e6ee63772748ee448ea8477a5a3e3abde193c3325b3696e56a5aff07c7dcf6b1f6a2f2a036895b4f3afe96abb366d893f + languageName: node + linkType: hard + +"split2@npm:^4.1.0": + version: 4.2.0 + resolution: "split2@npm:4.2.0" + checksum: 05d54102546549fe4d2455900699056580cca006c0275c334611420f854da30ac999230857a85fdd9914dc2109ae50f80fda43d2a445f2aa86eccdc1dfce779d + languageName: node + linkType: hard + +"split@npm:0.3": + version: 0.3.3 + resolution: "split@npm:0.3.3" + dependencies: + through: 2 + checksum: 2e076634c9637cfdc54ab4387b6a243b8c33b360874a25adf6f327a5647f07cb3bf1c755d515248eb3afee4e382278d01f62c62d87263c118f28065b86f74f02 + languageName: node + linkType: hard + +"sprintf-js@npm:^1.1.2, sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: a3fdac7b49643875b70864a9d9b469d87a40dfeaf5d34d9d0c5b1cda5fd7d065531fcb43c76357d62254c57184a7b151954156563a4d6a747015cfb41021cad0 + languageName: node + linkType: hard + +"sprintf-js@npm:~1.0.2": + version: 1.0.3 + resolution: "sprintf-js@npm:1.0.3" + checksum: 19d79aec211f09b99ec3099b5b2ae2f6e9cdefe50bc91ac4c69144b6d3928a640bb6ae5b3def70c2e85a2c3d9f5ec2719921e3a59d3ca3ef4b2fd1a4656a0df3 + languageName: node + linkType: hard + +"sqlstring@npm:^2.3.2": + version: 2.3.3 + resolution: "sqlstring@npm:2.3.3" + checksum: 1e7e2d51c38a0cf7372e875408ca100b6e0c9a941ab7773975ea41fb36e5528e404dc787689be855780cf6d0a829ff71027964ae3a05a7446e91dce26672fda7 + languageName: node + linkType: hard + +"ssh-remote-port-forward@npm:^1.0.4": + version: 1.0.4 + resolution: "ssh-remote-port-forward@npm:1.0.4" + dependencies: + "@types/ssh2": ^0.5.48 + ssh2: ^1.4.0 + checksum: c6c04c5ddfde7cb06e9a8655a152bd28fe6771c6fe62ff0bc08be229491546c410f30b153c968b8d6817a57d38678a270c228f30143ec0fe1be546efc4f6b65a + languageName: node + linkType: hard + +"ssh2@npm:^1.11.0, ssh2@npm:^1.15.0, ssh2@npm:^1.4.0": + version: 1.16.0 + resolution: "ssh2@npm:1.16.0" + dependencies: + asn1: ^0.2.6 + bcrypt-pbkdf: ^1.0.2 + cpu-features: ~0.0.10 + nan: ^2.20.0 + dependenciesMeta: + cpu-features: + optional: true + nan: + optional: true + checksum: c024c4a432aae2457852037f31c0d9bec323fb062ace3a31e4a6dd6c55842246c80e7d20ff93ffed22dde1e523250d8438bc2f7d4a1450cf4fa4887818176f0e + languageName: node + linkType: hard + +"sshpk@npm:^1.7.0": + version: 1.18.0 + resolution: "sshpk@npm:1.18.0" + dependencies: + asn1: ~0.2.3 + assert-plus: ^1.0.0 + bcrypt-pbkdf: ^1.0.0 + dashdash: ^1.12.0 + ecc-jsbn: ~0.1.1 + getpass: ^0.1.1 + jsbn: ~0.1.0 + safer-buffer: ^2.0.2 + tweetnacl: ~0.14.0 + bin: + sshpk-conv: bin/sshpk-conv + sshpk-sign: bin/sshpk-sign + sshpk-verify: bin/sshpk-verify + checksum: 01d43374eee3a7e37b3b82fdbecd5518cbb2e47ccbed27d2ae30f9753f22bd6ffad31225cb8ef013bc3fb7785e686cea619203ee1439a228f965558c367c3cfa + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.6 + resolution: "ssri@npm:10.0.6" + dependencies: + minipass: ^7.0.3 + checksum: 4603d53a05bcd44188747d38f1cc43833b9951b5a1ee43ba50535bdfc5fe4a0897472dbe69837570a5417c3c073377ef4f8c1a272683b401857f72738ee57299 + languageName: node + linkType: hard + +"ssri@npm:^9.0.0": + version: 9.0.1 + resolution: "ssri@npm:9.0.1" + dependencies: + minipass: ^3.1.1 + checksum: fb58f5e46b6923ae67b87ad5ef1c5ab6d427a17db0bead84570c2df3cd50b4ceb880ebdba2d60726588272890bae842a744e1ecce5bd2a2a582fccd5068309eb + languageName: node + linkType: hard + +"stable@npm:^0.1.8": + version: 0.1.8 + resolution: "stable@npm:0.1.8" + checksum: 2ff482bb100285d16dd75cd8f7c60ab652570e8952c0bfa91828a2b5f646a0ff533f14596ea4eabd48bb7f4aeea408dce8f8515812b975d958a4cc4fa6b9dfeb + languageName: node + linkType: hard + +"stack-generator@npm:^2.0.5": + version: 2.0.10 + resolution: "stack-generator@npm:2.0.10" + dependencies: + stackframe: ^1.3.4 + checksum: 4fc3978a934424218a0aa9f398034e1f78153d5ff4f4ff9c62478c672debb47dd58de05b09fc3900530cbb526d72c93a6e6c9353bacc698e3b1c00ca3dda0c47 + languageName: node + linkType: hard + +"stack-trace@npm:0.0.x": + version: 0.0.10 + resolution: "stack-trace@npm:0.0.10" + checksum: 473036ad32f8c00e889613153d6454f9be0536d430eb2358ca51cad6b95cea08a3cc33cc0e34de66b0dad221582b08ed2e61ef8e13f4087ab690f388362d6610 + languageName: node + linkType: hard + +"stack-utils@npm:^2.0.3": + version: 2.0.6 + resolution: "stack-utils@npm:2.0.6" + dependencies: + escape-string-regexp: ^2.0.0 + checksum: 052bf4d25bbf5f78e06c1d5e67de2e088b06871fa04107ca8d3f0e9d9263326e2942c8bedee3545795fc77d787d443a538345eef74db2f8e35db3558c6f91ff7 + languageName: node + linkType: hard + +"stackframe@npm:^1.3.4": + version: 1.3.4 + resolution: "stackframe@npm:1.3.4" + checksum: bae1596873595c4610993fa84f86a3387d67586401c1816ea048c0196800c0646c4d2da98c2ee80557fd9eff05877efe33b91ba6cd052658ed96ddc85d19067d + languageName: node + linkType: hard + +"stacktrace-gps@npm:^3.0.4": + version: 3.1.2 + resolution: "stacktrace-gps@npm:3.1.2" + dependencies: + source-map: 0.5.6 + stackframe: ^1.3.4 + checksum: 85daa232d138239b6ae0f4bcdd87d15d302a045d93625db17614030945b5314e204b5fbcf9bee5b6f4f9e6af5fca05f65c27fe910894b861ef6853b99470aa1c + languageName: node + linkType: hard + +"stacktrace-js@npm:^2.0.2": + version: 2.0.2 + resolution: "stacktrace-js@npm:2.0.2" + dependencies: + error-stack-parser: ^2.0.6 + stack-generator: ^2.0.5 + stacktrace-gps: ^3.0.4 + checksum: 081e786d56188ac04ac6604c09cd863b3ca2b4300ec061366cf68c3e4ad9edaa34fb40deea03cc23a05f442aa341e9171f47313f19bd588f9bec6c505a396286 + languageName: node + linkType: hard + +"standard-as-callback@npm:^2.1.0": + version: 2.1.0 + resolution: "standard-as-callback@npm:2.1.0" + checksum: 88bec83ee220687c72d94fd86a98d5272c91d37ec64b66d830dbc0d79b62bfa6e47f53b71646011835fc9ce7fae62739545d13124262b53be4fbb3e2ebad551c + languageName: node + linkType: hard + +"start-server-and-test@npm:2.0.8": + version: 2.0.8 + resolution: "start-server-and-test@npm:2.0.8" + dependencies: + arg: ^5.0.2 + bluebird: 3.7.2 + check-more-types: 2.24.0 + debug: 4.3.7 + execa: 5.1.1 + lazy-ass: 1.6.0 + ps-tree: 1.2.0 + wait-on: 8.0.1 + bin: + server-test: src/bin/start.js + start-server-and-test: src/bin/start.js + start-test: src/bin/start.js + checksum: 64cd27598348d8b276f489fa2b394b1141819719f729118b2ba19cfce32df134491c889163a1709013e630cbd988384be9b3e501e3062604b66b1fb3f4db9e50 + languageName: node + linkType: hard + +"static-eval@npm:2.0.2": + version: 2.0.2 + resolution: "static-eval@npm:2.0.2" + dependencies: + escodegen: ^1.8.1 + checksum: 335a923c5ccb29add404ac23d0a55c0da6cee3071f6f67a7053aeac0dedc6dbfc53ac9269e9c25f403f5b7603a291ef47d7114f99bde241184f7aa3f9286dc32 + languageName: node + linkType: hard + +"statuses@npm:2.0.1": + version: 2.0.1 + resolution: "statuses@npm:2.0.1" + checksum: 18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb + languageName: node + linkType: hard + +"statuses@npm:>= 1.4.0 < 2, statuses@npm:>= 1.5.0 < 2, statuses@npm:^1.5.0": + version: 1.5.0 + resolution: "statuses@npm:1.5.0" + checksum: c469b9519de16a4bb19600205cffb39ee471a5f17b82589757ca7bd40a8d92ebb6ed9f98b5a540c5d302ccbc78f15dc03cc0280dd6e00df1335568a5d5758a5c + languageName: node + linkType: hard + +"stop-iteration-iterator@npm:^1.0.0": + version: 1.0.0 + resolution: "stop-iteration-iterator@npm:1.0.0" + dependencies: + internal-slot: ^1.0.4 + checksum: d04173690b2efa40e24ab70e5e51a3ff31d56d699550cfad084104ab3381390daccb36652b25755e420245f3b0737de66c1879eaa2a8d4fc0a78f9bf892fcb42 + languageName: node + linkType: hard + +"stoppable@npm:^1.1.0": + version: 1.1.0 + resolution: "stoppable@npm:1.1.0" + checksum: 63104fcbdece130bc4906fd982061e763d2ef48065ed1ab29895e5ad00552c625f8a4c50c9cd2e3bfa805c8a2c3bfdda0f07c5ae39694bd2d5cb0bee1618d1e9 + languageName: node + linkType: hard + +"stream-browserify@npm:^2.0.1": + version: 2.0.2 + resolution: "stream-browserify@npm:2.0.2" + dependencies: + inherits: ~2.0.1 + readable-stream: ^2.0.2 + checksum: 8de7bcab5582e9a931ae1a4768be7efe8fa4b0b95fd368d16d8cf3e494b897d6b0a7238626de5d71686e53bddf417fd59d106cfa3af0ec055f61a8d1f8fc77b3 + languageName: node + linkType: hard + +"stream-buffers@npm:^3.0.2": + version: 3.0.3 + resolution: "stream-buffers@npm:3.0.3" + checksum: 3f0bdc4b1fd3ff370cef5a2103dd930b8981d42d97741eeb087a660771e27f0fc35fa8a351bb36e15bbbbce0eea00fefed60d6cdff4c6c3f527580529f183807 + languageName: node + linkType: hard + +"stream-combiner@npm:~0.0.4": + version: 0.0.4 + resolution: "stream-combiner@npm:0.0.4" + dependencies: + duplexer: ~0.1.1 + checksum: 844b622cfe8b9de45a6007404f613b60aaf85200ab9862299066204242f89a7c8033b1c356c998aa6cfc630f6cd9eba119ec1c6dc1f93e245982be4a847aee7d + languageName: node + linkType: hard + +"stream-events@npm:^1.0.5": + version: 1.0.5 + resolution: "stream-events@npm:1.0.5" + dependencies: + stubs: ^3.0.0 + checksum: 969ce82e34bfbef5734629cc06f9d7f3705a9ceb8fcd6a526332f9159f1f8bbfdb1a453f3ced0b728083454f7706adbbe8428bceb788a0287ca48ba2642dc3fc + languageName: node + linkType: hard + +"stream-http@npm:^2.7.2": + version: 2.8.3 + resolution: "stream-http@npm:2.8.3" + dependencies: + builtin-status-codes: ^3.0.0 + inherits: ^2.0.1 + readable-stream: ^2.3.6 + to-arraybuffer: ^1.0.0 + xtend: ^4.0.0 + checksum: f57dfaa21a015f72e6ce6b199cf1762074cfe8acf0047bba8f005593754f1743ad0a91788f95308d9f3829ad55742399ad27b4624432f2752a08e62ef4346e05 + languageName: node + linkType: hard + +"stream-shift@npm:^1.0.2": + version: 1.0.3 + resolution: "stream-shift@npm:1.0.3" + checksum: a24c0a3f66a8f9024bd1d579a533a53be283b4475d4e6b4b3211b964031447bdf6532dd1f3c2b0ad66752554391b7c62bd7ca4559193381f766534e723d50242 + languageName: node + linkType: hard + +"streamroller@npm:^3.1.5": + version: 3.1.5 + resolution: "streamroller@npm:3.1.5" + dependencies: + date-format: ^4.0.14 + debug: ^4.3.4 + fs-extra: ^8.1.0 + checksum: c1df5612b785ffa4b6bbf16460590b62994c57265bc55a5166eebeeb0daf648e84bc52dc6d57e0cd4e5c7609bda93076753c63ff54589febd1e0b95590f0e443 + languageName: node + linkType: hard + +"streamx@npm:^2.15.0, streamx@npm:^2.20.0": + version: 2.20.1 + resolution: "streamx@npm:2.20.1" + dependencies: + bare-events: ^2.2.0 + fast-fifo: ^1.3.2 + queue-tick: ^1.0.1 + text-decoder: ^1.1.0 + dependenciesMeta: + bare-events: + optional: true + checksum: 48605ddd3abdd86d2e3ee945ec7c9317f36abb5303347a8fff6e4c7926a72c33ec7ac86b50734ccd1cf65602b6a38e247966e8199b24e5a7485d9cec8f5327bd + languageName: node + linkType: hard + +"strict-event-emitter@npm:^0.2.4": + version: 0.2.8 + resolution: "strict-event-emitter@npm:0.2.8" + dependencies: + events: ^3.3.0 + checksum: 6ac06fe72a6ee6ae64d20f1dd42838ea67342f1b5f32b03b3050d73ee6ecee44b4d5c4ed2965a7154b47991e215f373d4e789e2b2be2769cd80e356126c2ca53 + languageName: node + linkType: hard + +"strict-event-emitter@npm:^0.4.3": + version: 0.4.6 + resolution: "strict-event-emitter@npm:0.4.6" + checksum: 4f4f2909613e7811de789991c06bfb770d6d6987e2ec5c66fa7485d0f07cc4e7e32eba0dcf26cee6d86af6c92946d7f4acdfaff57d0c4114df2cfa1bf0e3c091 + languageName: node + linkType: hard + +"string-argv@npm:~0.3.1": + version: 0.3.2 + resolution: "string-argv@npm:0.3.2" + checksum: 8703ad3f3db0b2641ed2adbb15cf24d3945070d9a751f9e74a924966db9f325ac755169007233e8985a39a6a292f14d4fee20482989b89b96e473c4221508a0f + languageName: node + linkType: hard + +"string-hash@npm:^1.1.1": + version: 1.1.3 + resolution: "string-hash@npm:1.1.3" + checksum: 104b8667a5e0dc71bfcd29fee09cb88c6102e27bfb07c55f95535d90587d016731d52299380052e514266f4028a7a5172e0d9ac58e2f8f5001be61dc77c0754d + languageName: node + linkType: hard + +"string-length@npm:^4.0.1": + version: 4.0.2 + resolution: "string-length@npm:4.0.2" + dependencies: + char-regex: ^1.0.2 + strip-ansi: ^6.0.0 + checksum: ce85533ef5113fcb7e522bcf9e62cb33871aa99b3729cec5595f4447f660b0cefd542ca6df4150c97a677d58b0cb727a3fe09ac1de94071d05526c73579bf505 + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: ^8.0.0 + is-fullwidth-code-point: ^3.0.0 + strip-ansi: ^6.0.1 + checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: ^0.2.0 + emoji-regex: ^9.2.2 + strip-ansi: ^7.0.1 + checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 + languageName: node + linkType: hard + +"string.prototype.includes@npm:^2.0.0": + version: 2.0.1 + resolution: "string.prototype.includes@npm:2.0.1" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-abstract: ^1.23.3 + checksum: ed4b7058b092f30d41c4df1e3e805eeea92479d2c7a886aa30f42ae32fde8924a10cc99cccc99c29b8e18c48216608a0fe6bf887f8b4aadf9559096a758f313a + languageName: node + linkType: hard + +"string.prototype.matchall@npm:^4.0.11": + version: 4.0.11 + resolution: "string.prototype.matchall@npm:4.0.11" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-abstract: ^1.23.2 + es-errors: ^1.3.0 + es-object-atoms: ^1.0.0 + get-intrinsic: ^1.2.4 + gopd: ^1.0.1 + has-symbols: ^1.0.3 + internal-slot: ^1.0.7 + regexp.prototype.flags: ^1.5.2 + set-function-name: ^2.0.2 + side-channel: ^1.0.6 + checksum: 6ac6566ed065c0c8489c91156078ca077db8ff64d683fda97ae652d00c52dfa5f39aaab0a710d8243031a857fd2c7c511e38b45524796764d25472d10d7075ae + languageName: node + linkType: hard + +"string.prototype.repeat@npm:^1.0.0": + version: 1.0.0 + resolution: "string.prototype.repeat@npm:1.0.0" + dependencies: + define-properties: ^1.1.3 + es-abstract: ^1.17.5 + checksum: 95dfc514ed7f328d80a066dabbfbbb1615c3e51490351085409db2eb7cbfed7ea29fdadaf277647fbf9f4a1e10e6dd9e95e78c0fd2c4e6bb6723ea6e59401004 + languageName: node + linkType: hard + +"string.prototype.trim@npm:^1.2.9": + version: 1.2.9 + resolution: "string.prototype.trim@npm:1.2.9" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-abstract: ^1.23.0 + es-object-atoms: ^1.0.0 + checksum: ea2df6ec1e914c9d4e2dc856fa08228e8b1be59b59e50b17578c94a66a176888f417264bb763d4aac638ad3b3dad56e7a03d9317086a178078d131aa293ba193 + languageName: node + linkType: hard + +"string.prototype.trimend@npm:^1.0.8": + version: 1.0.8 + resolution: "string.prototype.trimend@npm:1.0.8" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-object-atoms: ^1.0.0 + checksum: cc3bd2de08d8968a28787deba9a3cb3f17ca5f9f770c91e7e8fa3e7d47f079bad70fadce16f05dda9f261788be2c6e84a942f618c3bed31e42abc5c1084f8dfd + languageName: node + linkType: hard + +"string.prototype.trimstart@npm:^1.0.8": + version: 1.0.8 + resolution: "string.prototype.trimstart@npm:1.0.8" + dependencies: + call-bind: ^1.0.7 + define-properties: ^1.2.1 + es-object-atoms: ^1.0.0 + checksum: df1007a7f580a49d692375d996521dc14fd103acda7f3034b3c558a60b82beeed3a64fa91e494e164581793a8ab0ae2f59578a49896a7af6583c1f20472bce96 + languageName: node + linkType: hard + +"string_decoder@npm:^1.0.0, string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: ~5.2.0 + checksum: 8417646695a66e73aefc4420eb3b84cc9ffd89572861fe004e6aeb13c7bc00e2f616247505d2dbbef24247c372f70268f594af7126f43548565c68c117bdeb56 + languageName: node + linkType: hard + +"string_decoder@npm:~1.1.1": + version: 1.1.1 + resolution: "string_decoder@npm:1.1.1" + dependencies: + safe-buffer: ~5.1.0 + checksum: 9ab7e56f9d60a28f2be697419917c50cac19f3e8e6c28ef26ed5f4852289fe0de5d6997d29becf59028556f2c62983790c1d9ba1e2a3cc401768ca12d5183a5b + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:6.0, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: ^5.0.1 + checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c + languageName: node + linkType: hard + +"strip-ansi@npm:5.2.0": + version: 5.2.0 + resolution: "strip-ansi@npm:5.2.0" + dependencies: + ansi-regex: ^4.1.0 + checksum: bdb5f76ade97062bd88e7723aa019adbfacdcba42223b19ccb528ffb9fb0b89a5be442c663c4a3fb25268eaa3f6ea19c7c3fbae830bd1562d55adccae1fcec46 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: ^6.0.1 + checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d + languageName: node + linkType: hard + +"strip-bom@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-bom@npm:3.0.0" + checksum: 8d50ff27b7ebe5ecc78f1fe1e00fcdff7af014e73cf724b46fb81ef889eeb1015fc5184b64e81a2efe002180f3ba431bdd77e300da5c6685d702780fbf0c8d5b + languageName: node + linkType: hard + +"strip-bom@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-bom@npm:4.0.0" + checksum: 9dbcfbaf503c57c06af15fe2c8176fb1bf3af5ff65003851a102749f875a6dbe0ab3b30115eccf6e805e9d756830d3e40ec508b62b3f1ddf3761a20ebe29d3f3 + languageName: node + linkType: hard + +"strip-final-newline@npm:^2.0.0": + version: 2.0.0 + resolution: "strip-final-newline@npm:2.0.0" + checksum: 69412b5e25731e1938184b5d489c32e340605bb611d6140344abc3421b7f3c6f9984b21dff296dfcf056681b82caa3bb4cc996a965ce37bcfad663e92eae9c64 + languageName: node + linkType: hard + +"strip-indent@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-indent@npm:3.0.0" + dependencies: + min-indent: ^1.0.0 + checksum: 18f045d57d9d0d90cd16f72b2313d6364fd2cb4bf85b9f593523ad431c8720011a4d5f08b6591c9d580f446e78855c5334a30fb91aa1560f5d9f95ed1b4a0530 + languageName: node + linkType: hard + +"strip-json-comments@npm:5.0.1": + version: 5.0.1 + resolution: "strip-json-comments@npm:5.0.1" + checksum: b314af70c6666a71133e309a571bdb87687fc878d9fd8b38ebed393a77b89835b92f191aa6b0bc10dfd028ba99eed6b6365985001d64c5aef32a4a82456a156b + languageName: node + linkType: hard + +"strip-json-comments@npm:^3.1.1, strip-json-comments@npm:~3.1.1": + version: 3.1.1 + resolution: "strip-json-comments@npm:3.1.1" + checksum: 492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 + languageName: node + linkType: hard + +"strip-json-comments@npm:~2.0.1": + version: 2.0.1 + resolution: "strip-json-comments@npm:2.0.1" + checksum: 1074ccb63270d32ca28edfb0a281c96b94dc679077828135141f27d52a5a398ef5e78bcf22809d23cadc2b81dfbe345eb5fd8699b385c8b1128907dec4a7d1e1 + languageName: node + linkType: hard + +"strnum@npm:^1.0.5": + version: 1.0.5 + resolution: "strnum@npm:1.0.5" + checksum: 651b2031db5da1bf4a77fdd2f116a8ac8055157c5420f5569f64879133825915ad461513e7202a16d7fec63c54fd822410d0962f8ca12385c4334891b9ae6dd2 + languageName: node + linkType: hard + +"stubs@npm:^3.0.0": + version: 3.0.0 + resolution: "stubs@npm:3.0.0" + checksum: dec7b82186e3743317616235c59bfb53284acc312cb9f4c3e97e2205c67a5c158b0ca89db5927e52351582e90a2672822eeaec9db396e23e56893d2a8676e024 + languageName: node + linkType: hard + +"style-inject@npm:^0.3.0": + version: 0.3.0 + resolution: "style-inject@npm:0.3.0" + checksum: fa5f5f6730c3eb4ccc5735347935703c7c02759d4ddb5983d037ed0efda3c50a80640c2fed4f4d4c5ea600c97cdfdb45f79f734630324fa21a3a86723c0472da + languageName: node + linkType: hard + +"style-loader@npm:^3.3.1": + version: 3.3.4 + resolution: "style-loader@npm:3.3.4" + peerDependencies: + webpack: ^5.0.0 + checksum: caac3f2fe2c3c89e49b7a2a9329e1cfa515ecf5f36b9c4885f9b218019fda207a9029939b2c35821dec177a264a007e7c391ccdd3ff7401881ce6287b9c8f38b + languageName: node + linkType: hard + +"style-to-object@npm:^0.4.0": + version: 0.4.4 + resolution: "style-to-object@npm:0.4.4" + dependencies: + inline-style-parser: 0.1.1 + checksum: 41656c06f93ac0a7ac260ebc2f9d09a8bd74b8ec1836f358cc58e169235835a3a356977891d2ebbd76f0e08a53616929069199f9cce543214d3dc98346e19c9a + languageName: node + linkType: hard + +"stylehacks@npm:^5.1.1": + version: 5.1.1 + resolution: "stylehacks@npm:5.1.1" + dependencies: + browserslist: ^4.21.4 + postcss-selector-parser: ^6.0.4 + peerDependencies: + postcss: ^8.2.15 + checksum: 11175366ef52de65bf06cefba0ddc9db286dc3a1451fd2989e74c6ea47091a02329a4bf6ce10b1a36950056927b6bbbe47c5ab3a1f4c7032df932d010fbde5a2 + languageName: node + linkType: hard + +"stylis@npm:4.2.0": + version: 4.2.0 + resolution: "stylis@npm:4.2.0" + checksum: 0eb6cc1b866dc17a6037d0a82ac7fa877eba6a757443e79e7c4f35bacedbf6421fadcab4363b39667b43355cbaaa570a3cde850f776498e5450f32ed2f9b7584 + languageName: node + linkType: hard + +"stylis@npm:^4.3.0": + version: 4.3.4 + resolution: "stylis@npm:4.3.4" + checksum: 7e3a482c7bba6e0e9e3187972e958acf800b1abe99f23e081fcb5dea8e4a05eca44286c1381ce2bc7179245ddbd7bf1f74237ed413fce7491320a543bcfebda9 + languageName: node + linkType: hard + +"sucrase@npm:^3.20.2": + version: 3.35.0 + resolution: "sucrase@npm:3.35.0" + dependencies: + "@jridgewell/gen-mapping": ^0.3.2 + commander: ^4.0.0 + glob: ^10.3.10 + lines-and-columns: ^1.1.6 + mz: ^2.7.0 + pirates: ^4.0.1 + ts-interface-checker: ^0.1.9 + bin: + sucrase: bin/sucrase + sucrase-node: bin/sucrase-node + checksum: 9fc5792a9ab8a14dcf9c47dcb704431d35c1cdff1d17d55d382a31c2e8e3063870ad32ce120a80915498486246d612e30cda44f1624d9d9a10423e1a43487ad1 + languageName: node + linkType: hard + +"summary@npm:2.1.0": + version: 2.1.0 + resolution: "summary@npm:2.1.0" + checksum: 10ac12ce12c013b56ad44c37cfac206961f0993d98867b33b1b03a27b38a1cf8dd2db0b788883356c5335bbbb37d953772ef4a381d6fc8f408faf99f2bc54af5 + languageName: node + linkType: hard + +"superagent@npm:^8.1.2": + version: 8.1.2 + resolution: "superagent@npm:8.1.2" + dependencies: + component-emitter: ^1.3.0 + cookiejar: ^2.1.4 + debug: ^4.3.4 + fast-safe-stringify: ^2.1.1 + form-data: ^4.0.0 + formidable: ^2.1.2 + methods: ^1.1.2 + mime: 2.6.0 + qs: ^6.11.0 + semver: ^7.3.8 + checksum: f3601c5ccae34d5ba684a03703394b5d25931f4ae2e1e31a1de809f88a9400e997ece037f9accf148a21c408f950dc829db1e4e23576a7f9fe0efa79fd5c9d2f + languageName: node + linkType: hard + +"supertest@npm:6.3.4": + version: 6.3.4 + resolution: "supertest@npm:6.3.4" + dependencies: + methods: ^1.1.2 + superagent: ^8.1.2 + checksum: 875c6fa7940f21e5be9bb646579cdb030d4057bf2da643e125e1f0480add1200395d2b17e10b8e54e1009efc63e047422501e9eb30e12828668498c0910f295f + languageName: node + linkType: hard + +"supports-color@npm:^5.3.0": + version: 5.5.0 + resolution: "supports-color@npm:5.5.0" + dependencies: + has-flag: ^3.0.0 + checksum: 95f6f4ba5afdf92f495b5a912d4abee8dcba766ae719b975c56c084f5004845f6f5a5f7769f52d53f40e21952a6d87411bafe34af4a01e65f9926002e38e1dac + languageName: node + linkType: hard + +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: ^4.0.0 + checksum: 3dda818de06ebbe5b9653e07842d9479f3555ebc77e9a0280caf5a14fb877ffee9ed57007c3b78f5a6324b8dbeec648d9e97a24e2ed9fdb81ddc69ea07100f4a + languageName: node + linkType: hard + +"supports-color@npm:^8.0.0, supports-color@npm:^8.1.0, supports-color@npm:~8.1.1": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" + dependencies: + has-flag: ^4.0.0 + checksum: c052193a7e43c6cdc741eb7f378df605636e01ad434badf7324f17fb60c69a880d8d8fcdcb562cf94c2350e57b937d7425ab5b8326c67c2adc48f7c87c1db406 + languageName: node + linkType: hard + +"supports-preserve-symlinks-flag@npm:^1.0.0": + version: 1.0.0 + resolution: "supports-preserve-symlinks-flag@npm:1.0.0" + checksum: 53b1e247e68e05db7b3808b99b892bd36fb096e6fba213a06da7fab22045e97597db425c724f2bbd6c99a3c295e1e73f3e4de78592289f38431049e1277ca0ae + languageName: node + linkType: hard + +"svg-parser@npm:^2.0.4": + version: 2.0.4 + resolution: "svg-parser@npm:2.0.4" + checksum: b3de6653048212f2ae7afe4a423e04a76ec6d2d06e1bf7eacc618a7c5f7df7faa5105561c57b94579ec831fbbdbf5f190ba56a9205ff39ed13eabdf8ab086ddf + languageName: node + linkType: hard + +"svgo@npm:^2.7.0, svgo@npm:^2.8.0": + version: 2.8.0 + resolution: "svgo@npm:2.8.0" + dependencies: + "@trysound/sax": 0.2.0 + commander: ^7.2.0 + css-select: ^4.1.3 + css-tree: ^1.1.3 + csso: ^4.2.0 + picocolors: ^1.0.0 + stable: ^0.1.8 + bin: + svgo: bin/svgo + checksum: b92f71a8541468ffd0b81b8cdb36b1e242eea320bf3c1a9b2c8809945853e9d8c80c19744267eb91cabf06ae9d5fff3592d677df85a31be4ed59ff78534fa420 + languageName: node + linkType: hard + +"swc-loader@npm:^0.2.3": + version: 0.2.6 + resolution: "swc-loader@npm:0.2.6" + dependencies: + "@swc/counter": ^0.1.3 + peerDependencies: + "@swc/core": ^1.2.147 + webpack: ">=2" + checksum: fe90948c02a51bb8ffcff1ce3590e01dc12860b0bb7c9e22052b14fa846ed437781ae265614a5e14344bea22001108780f00a6e350e28c0b3499bc4cd11335fb + languageName: node + linkType: hard + +"swr@npm:^2.0.0": + version: 2.2.5 + resolution: "swr@npm:2.2.5" + dependencies: + client-only: ^0.0.1 + use-sync-external-store: ^1.2.0 + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + checksum: c6e6a5bd254951b22e5fd0930a95c7f79b5d0657f803c41ba1542cd6376623fb70b1895049d54ddde26da63b91951ae9d62a06772f82be28c1014d421e5b7aa9 + languageName: node + linkType: hard + +"symbol-tree@npm:^3.2.4": + version: 3.2.4 + resolution: "symbol-tree@npm:3.2.4" + checksum: 6e8fc7e1486b8b54bea91199d9535bb72f10842e40c79e882fc94fb7b14b89866adf2fd79efa5ebb5b658bc07fb459ccce5ac0e99ef3d72f474e74aaf284029d + languageName: node + linkType: hard + +"tapable@npm:^1.0.0": + version: 1.1.3 + resolution: "tapable@npm:1.1.3" + checksum: 53ff4e7c3900051c38cc4faab428ebfd7e6ad0841af5a7ac6d5f3045c5b50e88497bfa8295b4b3fbcadd94993c9e358868b78b9fb249a76cb8b018ac8dccafd7 + languageName: node + linkType: hard + +"tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": + version: 2.2.1 + resolution: "tapable@npm:2.2.1" + checksum: 3b7a1b4d86fa940aad46d9e73d1e8739335efd4c48322cb37d073eb6f80f5281889bf0320c6d8ffcfa1a0dd5bfdbd0f9d037e252ef972aca595330538aac4d51 + languageName: node + linkType: hard + +"tar-fs@npm:^2.0.0": + version: 2.1.1 + resolution: "tar-fs@npm:2.1.1" + dependencies: + chownr: ^1.1.1 + mkdirp-classic: ^0.5.2 + pump: ^3.0.0 + tar-stream: ^2.1.4 + checksum: f5b9a70059f5b2969e65f037b4e4da2daf0fa762d3d232ffd96e819e3f94665dbbbe62f76f084f1acb4dbdcce16c6e4dac08d12ffc6d24b8d76720f4d9cf032d + languageName: node + linkType: hard + +"tar-fs@npm:^3.0.6": + version: 3.0.6 + resolution: "tar-fs@npm:3.0.6" + dependencies: + bare-fs: ^2.1.1 + bare-path: ^2.1.0 + pump: ^3.0.0 + tar-stream: ^3.1.5 + dependenciesMeta: + bare-fs: + optional: true + bare-path: + optional: true + checksum: b4fa09c70f75caf05bf5cf87369cd2862f1ac5fb75c4ddf9d25d55999f7736a94b58ad679d384196cba837c5f5ff14086e060fafccef5474a16e2d3058ffa488 + languageName: node + linkType: hard + +"tar-fs@npm:~2.0.1": + version: 2.0.1 + resolution: "tar-fs@npm:2.0.1" + dependencies: + chownr: ^1.1.1 + mkdirp-classic: ^0.5.2 + pump: ^3.0.0 + tar-stream: ^2.0.0 + checksum: 26cd297ed2421bc8038ce1a4ca442296b53739f409847d495d46086e5713d8db27f2c03ba2f461d0f5ddbc790045628188a8544f8ae32cbb6238b279b68d0247 + languageName: node + linkType: hard + +"tar-stream@npm:^2.0.0, tar-stream@npm:^2.1.4": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: ^4.0.3 + end-of-stream: ^1.4.1 + fs-constants: ^1.0.0 + inherits: ^2.0.3 + readable-stream: ^3.1.1 + checksum: 699831a8b97666ef50021c767f84924cfee21c142c2eb0e79c63254e140e6408d6d55a065a2992548e72b06de39237ef2b802b99e3ece93ca3904a37622a66f3 + languageName: node + linkType: hard + +"tar-stream@npm:^3.0.0, tar-stream@npm:^3.1.5": + version: 3.1.7 + resolution: "tar-stream@npm:3.1.7" + dependencies: + b4a: ^1.6.4 + fast-fifo: ^1.2.0 + streamx: ^2.15.0 + checksum: 6393a6c19082b17b8dcc8e7fd349352bb29b4b8bfe1075912b91b01743ba6bb4298f5ff0b499a3bbaf82121830e96a1a59d4f21a43c0df339e54b01789cb8cc6 + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.1.2, tar@npm:^6.2.1": + version: 6.2.1 + resolution: "tar@npm:6.2.1" + dependencies: + chownr: ^2.0.0 + fs-minipass: ^2.0.0 + minipass: ^5.0.0 + minizlib: ^2.1.1 + mkdirp: ^1.0.3 + yallist: ^4.0.0 + checksum: f1322768c9741a25356c11373bce918483f40fa9a25c69c59410c8a1247632487edef5fe76c5f12ac51a6356d2f1829e96d2bc34098668a2fc34d76050ac2b6c + languageName: node + linkType: hard + +"tar@npm:^7.0.0": + version: 7.4.3 + resolution: "tar@npm:7.4.3" + dependencies: + "@isaacs/fs-minipass": ^4.0.0 + chownr: ^3.0.0 + minipass: ^7.1.2 + minizlib: ^3.0.1 + mkdirp: ^3.0.1 + yallist: ^5.0.0 + checksum: 8485350c0688331c94493031f417df069b778aadb25598abdad51862e007c39d1dd5310702c7be4a6784731a174799d8885d2fde0484269aea205b724d7b2ffa + languageName: node + linkType: hard + +"tarn@npm:^3.0.2": + version: 3.0.2 + resolution: "tarn@npm:3.0.2" + checksum: 27a69658f02504979c5b02e500522e78ec12ef893b90cb00fdef794f9d847a92ed78f6c0ad12e82b8919519bded6a8d6d0000442cd0c6d6ea83cd9b7297729af + languageName: node + linkType: hard + +"teeny-request@npm:^9.0.0": + version: 9.0.0 + resolution: "teeny-request@npm:9.0.0" + dependencies: + http-proxy-agent: ^5.0.0 + https-proxy-agent: ^5.0.0 + node-fetch: ^2.6.9 + stream-events: ^1.0.5 + uuid: ^9.0.0 + checksum: 9cb0ad83f9ca6ce6515b3109cbb30ceb2533cdeab8e41c3a0de89f509bd92c5a9aabd27b3adf7f3e49516e106a358859b19fa4928a1937a4ab95809ccb7d52eb + languageName: node + linkType: hard + +"term-size@npm:^2.1.0": + version: 2.2.1 + resolution: "term-size@npm:2.2.1" + checksum: 1ed981335483babc1e8206f843e06bd2bf89b85f0bf5a9a9d928033a0fcacdba183c03ba7d91814643015543ba002f1339f7112402a21da8f24b6c56b062a5a9 + languageName: node + linkType: hard + +"terser-webpack-plugin@npm:^5.1.3, terser-webpack-plugin@npm:^5.3.10": + version: 5.3.10 + resolution: "terser-webpack-plugin@npm:5.3.10" + dependencies: + "@jridgewell/trace-mapping": ^0.3.20 + jest-worker: ^27.4.5 + schema-utils: ^3.1.1 + serialize-javascript: ^6.0.1 + terser: ^5.26.0 + peerDependencies: + webpack: ^5.1.0 + peerDependenciesMeta: + "@swc/core": + optional: true + esbuild: + optional: true + uglify-js: + optional: true + checksum: bd6e7596cf815f3353e2a53e79cbdec959a1b0276f5e5d4e63e9d7c3c5bb5306df567729da287d1c7b39d79093e56863c569c42c6c24cc34c76aa313bd2cbcea + languageName: node + linkType: hard + +"terser@npm:^5.10.0, terser@npm:^5.26.0": + version: 5.36.0 + resolution: "terser@npm:5.36.0" + dependencies: + "@jridgewell/source-map": ^0.3.3 + acorn: ^8.8.2 + commander: ^2.20.0 + source-map-support: ~0.5.20 + bin: + terser: bin/terser + checksum: 489afd31901a2b170f7766948a3aa0e25da0acb41e9e35bd9f9b4751dfa2fc846e485f6fb9d34f0839a96af77f675b5fbf0a20c9aa54e0b8d7c219cf0b55e508 + languageName: node + linkType: hard + +"test-exclude@npm:^6.0.0": + version: 6.0.0 + resolution: "test-exclude@npm:6.0.0" + dependencies: + "@istanbuljs/schema": ^0.1.2 + glob: ^7.1.4 + minimatch: ^3.0.4 + checksum: 3b34a3d77165a2cb82b34014b3aba93b1c4637a5011807557dc2f3da826c59975a5ccad765721c4648b39817e3472789f9b0fa98fc854c5c1c7a1e632aacdc28 + languageName: node + linkType: hard + +"testcontainers@npm:^10.0.0": + version: 10.13.2 + resolution: "testcontainers@npm:10.13.2" + dependencies: + "@balena/dockerignore": ^1.0.2 + "@types/dockerode": ^3.3.29 + archiver: ^7.0.1 + async-lock: ^1.4.1 + byline: ^5.0.0 + debug: ^4.3.5 + docker-compose: ^0.24.8 + dockerode: ^3.3.5 + get-port: ^5.1.1 + proper-lockfile: ^4.1.2 + properties-reader: ^2.3.0 + ssh-remote-port-forward: ^1.0.4 + tar-fs: ^3.0.6 + tmp: ^0.2.3 + undici: ^5.28.4 + checksum: dd115745369981d159b9e74ce2461c2d7c9f3cfbe747e021c8268913b0b20beb5234cb160f22743cb40b38442dbcdfb5f985c63aa14d3b367493d0bfece6afe3 + languageName: node + linkType: hard + +"text-decoder@npm:^1.1.0": + version: 1.2.1 + resolution: "text-decoder@npm:1.2.1" + checksum: 0f42deda4a8f111af67f81f292e823f2bdcc85057fdeef35e3a5dda6b501605a1d449927a4a440af4485fbd02198b5baf722d146a195c1b1b211cdd37292ac66 + languageName: node + linkType: hard + +"text-hex@npm:1.0.x": + version: 1.0.0 + resolution: "text-hex@npm:1.0.0" + checksum: 1138f68adc97bf4381a302a24e2352f04992b7b1316c5003767e9b0d3367ffd0dc73d65001ea02b07cd0ecc2a9d186de0cf02f3c2d880b8a522d4ccb9342244a + languageName: node + linkType: hard + +"text-table@npm:0.2.0, text-table@npm:^0.2.0": + version: 0.2.0 + resolution: "text-table@npm:0.2.0" + checksum: b6937a38c80c7f84d9c11dd75e49d5c44f71d95e810a3250bd1f1797fc7117c57698204adf676b71497acc205d769d65c16ae8fa10afad832ae1322630aef10a + languageName: node + linkType: hard + +"textextensions@npm:^5.16.0": + version: 5.16.0 + resolution: "textextensions@npm:5.16.0" + checksum: d2abd5c962760046aa85d9ca542bd8bdb451370fc0a5e5f807aa80dd2f50175ec10d5ce9d28ae96968aaf6a1b1bea254cf4715f24852d0dcf29c6a60af7f793c + languageName: node + linkType: hard + +"thenify-all@npm:^1.0.0": + version: 1.6.0 + resolution: "thenify-all@npm:1.6.0" + dependencies: + thenify: ">= 3.1.0 < 4" + checksum: dba7cc8a23a154cdcb6acb7f51d61511c37a6b077ec5ab5da6e8b874272015937788402fd271fdfc5f187f8cb0948e38d0a42dcc89d554d731652ab458f5343e + languageName: node + linkType: hard + +"thenify@npm:>= 3.1.0 < 4": + version: 3.3.1 + resolution: "thenify@npm:3.3.1" + dependencies: + any-promise: ^1.0.0 + checksum: 84e1b804bfec49f3531215f17b4a6e50fd4397b5f7c1bccc427b9c656e1ecfb13ea79d899930184f78bc2f57285c54d9a50a590c8868f4f0cef5c1d9f898b05e + languageName: node + linkType: hard + +"thingies@npm:^1.20.0": + version: 1.21.0 + resolution: "thingies@npm:1.21.0" + peerDependencies: + tslib: ^2 + checksum: 283a2785e513dc892822dd0bbadaa79e873a7fc90b84798164717bf7cf837553e0b4518d8027b2307d8f6fc6caab088fa717112cd9196c6222763cc3cc1b7e79 + languageName: node + linkType: hard + +"throttle-debounce@npm:^3.0.1": + version: 3.0.1 + resolution: "throttle-debounce@npm:3.0.1" + checksum: e34ef638e8df3a9154249101b68afcbf2652a139c803415ef8a2f6a8bc577bcd4d79e4bb914ad3cd206523ac78b9fb7e80885bfa049f64fbb1927f99d98b5736 + languageName: node + linkType: hard + +"through@npm:2, through@npm:^2.3.6, through@npm:~2.3, through@npm:~2.3.1": + version: 2.3.8 + resolution: "through@npm:2.3.8" + checksum: a38c3e059853c494af95d50c072b83f8b676a9ba2818dcc5b108ef252230735c54e0185437618596c790bbba8fcdaef5b290405981ffa09dce67b1f1bf190cbd + languageName: node + linkType: hard + +"thunky@npm:^1.0.2": + version: 1.1.0 + resolution: "thunky@npm:1.1.0" + checksum: 993096c472b6b8f30e29dc777a8d17720e4cab448375041f20c0cb802a09a7fb2217f2a3e8cdc11851faa71c957e2db309357367fc9d7af3cb7a4d00f4b66034 + languageName: node + linkType: hard + +"tildify@npm:2.0.0": + version: 2.0.0 + resolution: "tildify@npm:2.0.0" + checksum: 0f5fee93624c4afdf75ee224c3b65aece4817ba5317fd70f49eaf084ea720d73556a6ef3f50079425a773ba3b93805b4524d14057841d4e4336516fdbe80635b + languageName: node + linkType: hard + +"timers-browserify@npm:^2.0.4": + version: 2.0.12 + resolution: "timers-browserify@npm:2.0.12" + dependencies: + setimmediate: ^1.0.4 + checksum: ec37ae299066bef6c464dcac29c7adafba1999e7227a9bdc4e105a459bee0f0b27234a46bfd7ab4041da79619e06a58433472867a913d01c26f8a203f87cee70 + languageName: node + linkType: hard + +"tiny-case@npm:^1.0.3": + version: 1.0.3 + resolution: "tiny-case@npm:1.0.3" + checksum: 3f7a30c39d5b0e1bc097b0b271bec14eb5b836093db034f35a0de26c14422380b50dc12bfd37498cf35b192f5df06f28a710712c87ead68872a9e37ad6f6049d + languageName: node + linkType: hard + +"tiny-emitter@npm:^2.1.0": + version: 2.1.0 + resolution: "tiny-emitter@npm:2.1.0" + checksum: fbcfb5145751a0e3b109507a828eb6d6d4501352ab7bb33eccef46e22e9d9ad3953158870a6966a59e57ab7c3f9cfac7cab8521db4de6a5e757012f4677df2dd + languageName: node + linkType: hard + +"tiny-invariant@npm:^1.0.6": + version: 1.3.3 + resolution: "tiny-invariant@npm:1.3.3" + checksum: 5e185c8cc2266967984ce3b352a4e57cb89dad5a8abb0dea21468a6ecaa67cd5bb47a3b7a85d08041008644af4f667fb8b6575ba38ba5fb00b3b5068306e59fe + languageName: node + linkType: hard + +"tiny-warning@npm:^1.0.2": + version: 1.0.3 + resolution: "tiny-warning@npm:1.0.3" + checksum: da62c4acac565902f0624b123eed6dd3509bc9a8d30c06e017104bedcf5d35810da8ff72864400ad19c5c7806fc0a8323c68baf3e326af7cb7d969f846100d71 + languageName: node + linkType: hard + +"tmp@npm:^0.0.33": + version: 0.0.33 + resolution: "tmp@npm:0.0.33" + dependencies: + os-tmpdir: ~1.0.2 + checksum: 902d7aceb74453ea02abbf58c203f4a8fc1cead89b60b31e354f74ed5b3fb09ea817f94fb310f884a5d16987dd9fa5a735412a7c2dd088dd3d415aa819ae3a28 + languageName: node + linkType: hard + +"tmp@npm:^0.2.3": + version: 0.2.3 + resolution: "tmp@npm:0.2.3" + checksum: 73b5c96b6e52da7e104d9d44afb5d106bb1e16d9fa7d00dbeb9e6522e61b571fbdb165c756c62164be9a3bbe192b9b268c236d370a2a0955c7689cd2ae377b95 + languageName: node + linkType: hard + +"tmpl@npm:1.0.5": + version: 1.0.5 + resolution: "tmpl@npm:1.0.5" + checksum: cd922d9b853c00fe414c5a774817be65b058d54a2d01ebb415840960406c669a0fc632f66df885e24cb022ec812739199ccbdb8d1164c3e513f85bfca5ab2873 + languageName: node + linkType: hard + +"to-arraybuffer@npm:^1.0.0": + version: 1.0.1 + resolution: "to-arraybuffer@npm:1.0.1" + checksum: 31433c10b388722729f5da04c6b2a06f40dc84f797bb802a5a171ced1e599454099c6c5bc5118f4b9105e7d049d3ad9d0f71182b77650e4fdb04539695489941 + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: ^7.0.0 + checksum: f76fa01b3d5be85db6a2a143e24df9f60dd047d151062d0ba3df62953f2f697b16fe5dad9b0ac6191c7efc7b1d9dcaa4b768174b7b29da89d4428e64bc0a20ed + languageName: node + linkType: hard + +"toggle-selection@npm:^1.0.6": + version: 1.0.6 + resolution: "toggle-selection@npm:1.0.6" + checksum: a90dc80ed1e7b18db8f4e16e86a5574f87632dc729cfc07d9ea3ced50021ad42bb4e08f22c0913e0b98e3837b0b717e0a51613c65f30418e21eb99da6556a74c + languageName: node + linkType: hard + +"toidentifier@npm:1.0.1": + version: 1.0.1 + resolution: "toidentifier@npm:1.0.1" + checksum: 952c29e2a85d7123239b5cfdd889a0dde47ab0497f0913d70588f19c53f7e0b5327c95f4651e413c74b785147f9637b17410ac8c846d5d4a20a5a33eb6dc3a45 + languageName: node + linkType: hard + +"toposort@npm:^2.0.2": + version: 2.0.2 + resolution: "toposort@npm:2.0.2" + checksum: d64c74b570391c9432873f48e231b439ee56bc49f7cb9780b505cfdf5cb832f808d0bae072515d93834dd6bceca5bb34448b5b4b408335e4d4716eaf68195dcb + languageName: node + linkType: hard + +"tosource@npm:^2.0.0-alpha.3": + version: 2.0.0-alpha.3 + resolution: "tosource@npm:2.0.0-alpha.3" + checksum: bc03a7571de8ed4306e6721283fa891f2adcab9dd80c46f6f177d4259b34bb192fe3a2cb3e1e2ce16f9db0bc7e534acfcb5478ab094b0ba255f98abfce6dab46 + languageName: node + linkType: hard + +"tough-cookie@npm:^4.1.2": + version: 4.1.4 + resolution: "tough-cookie@npm:4.1.4" + dependencies: + psl: ^1.1.33 + punycode: ^2.1.1 + universalify: ^0.2.0 + url-parse: ^1.5.3 + checksum: 5815059f014c31179a303c673f753f7899a6fce94ac93712c88ea5f3c26e0c042b5f0c7a599a00f8e0feeca4615dba75c3dffc54f3c1a489978aa8205e09307c + languageName: node + linkType: hard + +"tough-cookie@npm:~2.5.0": + version: 2.5.0 + resolution: "tough-cookie@npm:2.5.0" + dependencies: + psl: ^1.1.28 + punycode: ^2.1.1 + checksum: 16a8cd090224dd176eee23837cbe7573ca0fa297d7e468ab5e1c02d49a4e9a97bb05fef11320605eac516f91d54c57838a25864e8680e27b069a5231d8264977 + languageName: node + linkType: hard + +"tr46@npm:^3.0.0": + version: 3.0.0 + resolution: "tr46@npm:3.0.0" + dependencies: + punycode: ^2.1.1 + checksum: 44c3cc6767fb800490e6e9fd64fd49041aa4e49e1f6a012b34a75de739cc9ed3a6405296072c1df8b6389ae139c5e7c6496f659cfe13a04a4bff3a1422981270 + languageName: node + linkType: hard + +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 726321c5eaf41b5002e17ffbd1fb7245999a073e8979085dacd47c4b4e8068ff5777142fc6726d6ca1fd2ff16921b48788b87225cbc57c72636f6efa8efbffe3 + languageName: node + linkType: hard + +"tree-dump@npm:^1.0.1": + version: 1.0.2 + resolution: "tree-dump@npm:1.0.2" + peerDependencies: + tslib: 2 + checksum: 3b0cae6cd74c208da77dac1c65e6a212f5678fe181f1dfffbe05752be188aa88e56d5d5c33f5701d1f603ffcf33403763f722c9e8e398085cde0c0994323cb8d + languageName: node + linkType: hard + +"tree-kill@npm:^1.2.2": + version: 1.2.2 + resolution: "tree-kill@npm:1.2.2" + bin: + tree-kill: cli.js + checksum: 49117f5f410d19c84b0464d29afb9642c863bc5ba40fcb9a245d474c6d5cc64d1b177a6e6713129eb346b40aebb9d4631d967517f9fbe8251c35b21b13cd96c7 + languageName: node + linkType: hard + +"trim-lines@npm:^3.0.0": + version: 3.0.1 + resolution: "trim-lines@npm:3.0.1" + checksum: e241da104682a0e0d807222cc1496b92e716af4db7a002f4aeff33ae6a0024fef93165d49eab11aa07c71e1347c42d46563f91dfaa4d3fb945aa535cdead53ed + languageName: node + linkType: hard + +"triple-beam@npm:^1.3.0, triple-beam@npm:^1.4.1": + version: 1.4.1 + resolution: "triple-beam@npm:1.4.1" + checksum: 2e881a3e8e076b6f2b85b9ec9dd4a900d3f5016e6d21183ed98e78f9abcc0149e7d54d79a3f432b23afde46b0885bdcdcbff789f39bc75de796316961ec07f61 + languageName: node + linkType: hard + +"trough@npm:^2.0.0": + version: 2.2.0 + resolution: "trough@npm:2.2.0" + checksum: 6097df63169aca1f9b08c263b1b501a9b878387f46e161dde93f6d0bba7febba93c95f876a293c5ea370f6cb03bcb687b2488c8955c3cfb66c2c0161ea8c00f6 + languageName: node + linkType: hard + +"tryer@npm:^1.0.1": + version: 1.0.1 + resolution: "tryer@npm:1.0.1" + checksum: 1cf14d7f67c79613f054b569bfc9a89c7020d331573a812dfcf7437244e8f8e6eb6893b210cbd9cc217f67c1d72617f89793df231e4fe7d53634ed91cf3a89d1 + languageName: node + linkType: hard + +"ts-api-utils@npm:^1.0.1, ts-api-utils@npm:^1.3.0": + version: 1.3.0 + resolution: "ts-api-utils@npm:1.3.0" + peerDependencies: + typescript: ">=4.2.0" + checksum: c746ddabfdffbf16cb0b0db32bb287236a19e583057f8649ee7c49995bb776e1d3ef384685181c11a1a480369e022ca97512cb08c517b2d2bd82c83754c97012 + languageName: node + linkType: hard + +"ts-easing@npm:^0.2.0": + version: 0.2.0 + resolution: "ts-easing@npm:0.2.0" + checksum: e67ee862acca3b2e2718e736f31999adcef862d0df76d76a0e138588728d8a87dfec9978556044640bd0e90203590ad88ac2fe8746d0e9959b8d399132315150 + languageName: node + linkType: hard + +"ts-interface-checker@npm:^0.1.9": + version: 0.1.13 + resolution: "ts-interface-checker@npm:0.1.13" + checksum: 20c29189c2dd6067a8775e07823ddf8d59a33e2ffc47a1bd59a5cb28bb0121a2969a816d5e77eda2ed85b18171aa5d1c4005a6b88ae8499ec7cc49f78571cb5e + languageName: node + linkType: hard + +"ts-invariant@npm:^0.9.3": + version: 0.9.4 + resolution: "ts-invariant@npm:0.9.4" + dependencies: + tslib: ^2.1.0 + checksum: c9e5726361fa266916966b2070605f8664b6dd1d8b0ef7565dbf056abb6a87be26195985ef62dd97aeb0894cf2f4ad5b7f0d89dadadc197eaa38e99222afa29c + languageName: node + linkType: hard + +"ts-morph@npm:^23.0.0": + version: 23.0.0 + resolution: "ts-morph@npm:23.0.0" + dependencies: + "@ts-morph/common": ~0.24.0 + code-block-writer: ^13.0.1 + checksum: 3282eb0f8bd4577770874736c3259b97501da9a86137160b5d68f106b7848ea7b1fbccf9e198a3d930ec40c993e9951d4bfae31e2562dac8f3de0d7bb0e23615 + languageName: node + linkType: hard + +"ts-node@npm:^10.9.1": + version: 10.9.2 + resolution: "ts-node@npm:10.9.2" + dependencies: + "@cspotcode/source-map-support": ^0.8.0 + "@tsconfig/node10": ^1.0.7 + "@tsconfig/node12": ^1.0.7 + "@tsconfig/node14": ^1.0.0 + "@tsconfig/node16": ^1.0.2 + acorn: ^8.4.1 + acorn-walk: ^8.1.1 + arg: ^4.1.0 + create-require: ^1.1.0 + diff: ^4.0.1 + make-error: ^1.1.1 + v8-compile-cache-lib: ^3.0.1 + yn: 3.1.1 + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-esm: dist/bin-esm.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: fde256c9073969e234526e2cfead42591b9a2aec5222bac154b0de2fa9e4ceb30efcd717ee8bc785a56f3a119bdd5aa27b333d9dbec94ed254bd26f8944c67ac + languageName: node + linkType: hard + +"tsconfig-paths@npm:^3.15.0": + version: 3.15.0 + resolution: "tsconfig-paths@npm:3.15.0" + dependencies: + "@types/json5": ^0.0.29 + json5: ^1.0.2 + minimist: ^1.2.6 + strip-bom: ^3.0.0 + checksum: 59f35407a390d9482b320451f52a411a256a130ff0e7543d18c6f20afab29ac19fbe55c360a93d6476213cc335a4d76ce90f67df54c4e9037f7d240920832201 + languageName: node + linkType: hard + +"tslib@npm:2.7.0": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 1606d5c89f88d466889def78653f3aab0f88692e80bb2066d090ca6112ae250ec1cfa9dbfaab0d17b60da15a4186e8ec4d893801c67896b277c17374e36e1d28 + languageName: node + linkType: hard + +"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1, tslib@npm:^2.5.0, tslib@npm:^2.6.0, tslib@npm:^2.6.2": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: e4aba30e632b8c8902b47587fd13345e2827fa639e7c3121074d5ee0880723282411a8838f830b55100cbe4517672f84a2472667d355b81e8af165a55dc6203a + languageName: node + linkType: hard + +"tslib@npm:^1.14.1, tslib@npm:^1.8.1, tslib@npm:^1.9.0": + version: 1.14.1 + resolution: "tslib@npm:1.14.1" + checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd + languageName: node + linkType: hard + +"tsscmp@npm:1.0.6": + version: 1.0.6 + resolution: "tsscmp@npm:1.0.6" + checksum: 1512384def36bccc9125cabbd4c3b0e68608d7ee08127ceaa0b84a71797263f1a01c7f82fa69be8a3bd3c1396e2965d2f7b52d581d3a5eeaf3967fbc52e3b3bf + languageName: node + linkType: hard + +"tsutils@npm:^3.21.0": + version: 3.21.0 + resolution: "tsutils@npm:3.21.0" + dependencies: + tslib: ^1.8.1 + peerDependencies: + typescript: ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + checksum: 1843f4c1b2e0f975e08c4c21caa4af4f7f65a12ac1b81b3b8489366826259323feb3fc7a243123453d2d1a02314205a7634e048d4a8009921da19f99755cdc48 + languageName: node + linkType: hard + +"tty-browserify@npm:0.0.0": + version: 0.0.0 + resolution: "tty-browserify@npm:0.0.0" + checksum: a06f746acc419cb2527ba19b6f3bd97b4a208c03823bfb37b2982629d2effe30ebd17eaed0d7e2fc741f3c4f2a0c43455bd5fb4194354b378e78cfb7ca687f59 + languageName: node + linkType: hard + +"tunnel-agent@npm:^0.6.0": + version: 0.6.0 + resolution: "tunnel-agent@npm:0.6.0" + dependencies: + safe-buffer: ^5.0.1 + checksum: 05f6510358f8afc62a057b8b692f05d70c1782b70db86d6a1e0d5e28a32389e52fa6e7707b6c5ecccacc031462e4bc35af85ecfe4bbc341767917b7cf6965711 + languageName: node + linkType: hard + +"tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0": + version: 0.14.5 + resolution: "tweetnacl@npm:0.14.5" + checksum: 6061daba1724f59473d99a7bb82e13f211cdf6e31315510ae9656fefd4779851cb927adad90f3b488c8ed77c106adc0421ea8055f6f976ff21b27c5c4e918487 + languageName: node + linkType: hard + +"type-check@npm:^0.4.0, type-check@npm:~0.4.0": + version: 0.4.0 + resolution: "type-check@npm:0.4.0" + dependencies: + prelude-ls: ^1.2.1 + checksum: ec688ebfc9c45d0c30412e41ca9c0cdbd704580eb3a9ccf07b9b576094d7b86a012baebc95681999dd38f4f444afd28504cb3a89f2ef16b31d4ab61a0739025a + languageName: node + linkType: hard + +"type-check@npm:~0.3.2": + version: 0.3.2 + resolution: "type-check@npm:0.3.2" + dependencies: + prelude-ls: ~1.1.2 + checksum: dd3b1495642731bc0e1fc40abe5e977e0263005551ac83342ecb6f4f89551d106b368ec32ad3fb2da19b3bd7b2d1f64330da2ea9176d8ddbfe389fb286eb5124 + languageName: node + linkType: hard + +"type-detect@npm:4.0.8": + version: 4.0.8 + resolution: "type-detect@npm:4.0.8" + checksum: 62b5628bff67c0eb0b66afa371bd73e230399a8d2ad30d852716efcc4656a7516904570cd8631a49a3ce57c10225adf5d0cbdcb47f6b0255fe6557c453925a15 + languageName: node + linkType: hard + +"type-fest@npm:^0.13.1": + version: 0.13.1 + resolution: "type-fest@npm:0.13.1" + checksum: e6bf2e3c449f27d4ef5d56faf8b86feafbc3aec3025fc9a5fbe2db0a2587c44714521f9c30d8516a833c8c506d6263f5cc11267522b10c6ccdb6cc55b0a9d1c4 + languageName: node + linkType: hard + +"type-fest@npm:^0.20.2": + version: 0.20.2 + resolution: "type-fest@npm:0.20.2" + checksum: 4fb3272df21ad1c552486f8a2f8e115c09a521ad7a8db3d56d53718d0c907b62c6e9141ba5f584af3f6830d0872c521357e512381f24f7c44acae583ad517d73 + languageName: node + linkType: hard + +"type-fest@npm:^0.21.3": + version: 0.21.3 + resolution: "type-fest@npm:0.21.3" + checksum: e6b32a3b3877f04339bae01c193b273c62ba7bfc9e325b8703c4ee1b32dc8fe4ef5dfa54bf78265e069f7667d058e360ae0f37be5af9f153b22382cd55a9afe0 + languageName: node + linkType: hard + +"type-fest@npm:^2.19.0": + version: 2.19.0 + resolution: "type-fest@npm:2.19.0" + checksum: a4ef07ece297c9fba78fc1bd6d85dff4472fe043ede98bd4710d2615d15776902b595abf62bd78339ed6278f021235fb28a96361f8be86ed754f778973a0d278 + languageName: node + linkType: hard + +"type-is@npm:^1.6.16, type-is@npm:~1.6.18": + version: 1.6.18 + resolution: "type-is@npm:1.6.18" + dependencies: + media-typer: 0.3.0 + mime-types: ~2.1.24 + checksum: 2c8e47675d55f8b4e404bcf529abdf5036c537a04c2b20177bcf78c9e3c1da69da3942b1346e6edb09e823228c0ee656ef0e033765ec39a70d496ef601a0c657 + languageName: node + linkType: hard + +"typed-array-buffer@npm:^1.0.2": + version: 1.0.2 + resolution: "typed-array-buffer@npm:1.0.2" + dependencies: + call-bind: ^1.0.7 + es-errors: ^1.3.0 + is-typed-array: ^1.1.13 + checksum: 02ffc185d29c6df07968272b15d5319a1610817916ec8d4cd670ded5d1efe72901541ff2202fcc622730d8a549c76e198a2f74e312eabbfb712ed907d45cbb0b + languageName: node + linkType: hard + +"typed-array-byte-length@npm:^1.0.1": + version: 1.0.1 + resolution: "typed-array-byte-length@npm:1.0.1" + dependencies: + call-bind: ^1.0.7 + for-each: ^0.3.3 + gopd: ^1.0.1 + has-proto: ^1.0.3 + is-typed-array: ^1.1.13 + checksum: f65e5ecd1cf76b1a2d0d6f631f3ea3cdb5e08da106c6703ffe687d583e49954d570cc80434816d3746e18be889ffe53c58bf3e538081ea4077c26a41055b216d + languageName: node + linkType: hard + +"typed-array-byte-offset@npm:^1.0.2": + version: 1.0.2 + resolution: "typed-array-byte-offset@npm:1.0.2" + dependencies: + available-typed-arrays: ^1.0.7 + call-bind: ^1.0.7 + for-each: ^0.3.3 + gopd: ^1.0.1 + has-proto: ^1.0.3 + is-typed-array: ^1.1.13 + checksum: c8645c8794a621a0adcc142e0e2c57b1823bbfa4d590ad2c76b266aa3823895cf7afb9a893bf6685e18454ab1b0241e1a8d885a2d1340948efa4b56add4b5f67 + languageName: node + linkType: hard + +"typed-array-length@npm:^1.0.6": + version: 1.0.6 + resolution: "typed-array-length@npm:1.0.6" + dependencies: + call-bind: ^1.0.7 + for-each: ^0.3.3 + gopd: ^1.0.1 + has-proto: ^1.0.3 + is-typed-array: ^1.1.13 + possible-typed-array-names: ^1.0.0 + checksum: f0315e5b8f0168c29d390ff410ad13e4d511c78e6006df4a104576844812ee447fcc32daab1f3a76c9ef4f64eff808e134528b5b2439de335586b392e9750e5c + languageName: node + linkType: hard + +"typed-function@npm:^4.1.1": + version: 4.2.1 + resolution: "typed-function@npm:4.2.1" + checksum: 00d2dbbc61cf238fda6e0359eee8c5d344e92de3c54588a6da202be24dd8d31a5c87715a8401a65d384b8fdba7c971b19ac86e572f27e23976cccbd6ed842487 + languageName: node + linkType: hard + +"typedarray@npm:^0.0.6": + version: 0.0.6 + resolution: "typedarray@npm:0.0.6" + checksum: 33b39f3d0e8463985eeaeeacc3cb2e28bc3dfaf2a5ed219628c0b629d5d7b810b0eb2165f9f607c34871d5daa92ba1dc69f49051cf7d578b4cbd26c340b9d1b1 + languageName: node + linkType: hard + +"typeorm-adapter@npm:^1.6.1": + version: 1.7.0 + resolution: "typeorm-adapter@npm:1.7.0" + dependencies: + casbin: ^5.27.0 + reflect-metadata: ^0.1.13 + typeorm: ^0.3.17 + checksum: 9762691d3f8105a8333eee71eb809841810166f6fc385134fe4a27e3ea6d71fb773a9ec19e9bcb9c013d31d5b181c30e563d7d9848ae05a045f305f28b0de186 + languageName: node + linkType: hard + +"typeorm@npm:^0.3.17": + version: 0.3.20 + resolution: "typeorm@npm:0.3.20" + dependencies: + "@sqltools/formatter": ^1.2.5 + app-root-path: ^3.1.0 + buffer: ^6.0.3 + chalk: ^4.1.2 + cli-highlight: ^2.1.11 + dayjs: ^1.11.9 + debug: ^4.3.4 + dotenv: ^16.0.3 + glob: ^10.3.10 + mkdirp: ^2.1.3 + reflect-metadata: ^0.2.1 + sha.js: ^2.4.11 + tslib: ^2.5.0 + uuid: ^9.0.0 + yargs: ^17.6.2 + peerDependencies: + "@google-cloud/spanner": ^5.18.0 + "@sap/hana-client": ^2.12.25 + better-sqlite3: ^7.1.2 || ^8.0.0 || ^9.0.0 + hdb-pool: ^0.1.6 + ioredis: ^5.0.4 + mongodb: ^5.8.0 + mssql: ^9.1.1 || ^10.0.1 + mysql2: ^2.2.5 || ^3.0.1 + oracledb: ^6.3.0 + pg: ^8.5.1 + pg-native: ^3.0.0 + pg-query-stream: ^4.0.0 + redis: ^3.1.1 || ^4.0.0 + sql.js: ^1.4.0 + sqlite3: ^5.0.3 + ts-node: ^10.7.0 + typeorm-aurora-data-api-driver: ^2.0.0 + peerDependenciesMeta: + "@google-cloud/spanner": + optional: true + "@sap/hana-client": + optional: true + better-sqlite3: + optional: true + hdb-pool: + optional: true + ioredis: + optional: true + mongodb: + optional: true + mssql: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + redis: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + ts-node: + optional: true + typeorm-aurora-data-api-driver: + optional: true + bin: + typeorm: cli.js + typeorm-ts-node-commonjs: cli-ts-node-commonjs.js + typeorm-ts-node-esm: cli-ts-node-esm.js + checksum: 9d6e5ecd0688eed5a151f33fb26b6d7c3e0af0eaf14ceaefe4c1c1863cdda6eabf2e865c9df2321f7dce26e40f320b76b811e2d1d58bf43f6ce19b36aa1ed1c4 + languageName: node + linkType: hard + +"typescript-json-schema@npm:^0.65.0": + version: 0.65.1 + resolution: "typescript-json-schema@npm:0.65.1" + dependencies: + "@types/json-schema": ^7.0.9 + "@types/node": ^18.11.9 + glob: ^7.1.7 + path-equal: ^1.2.5 + safe-stable-stringify: ^2.2.0 + ts-node: ^10.9.1 + typescript: ~5.5.0 + yargs: ^17.1.1 + bin: + typescript-json-schema: bin/typescript-json-schema + checksum: f67af357d3ba7f7953124437f7d36e15ad4171c35b2db945a4b1b3c77114f35328825e9109a3b71150cfc2a1e942da589d956a065eb31eb1dfca6c67fa54e30f + languageName: node + linkType: hard + +"typescript@npm:5.4.2": + version: 5.4.2 + resolution: "typescript@npm:5.4.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 96d80fde25a09bcb04d399082fb27a808a9e17c2111e43849d2aafbd642d835e4f4ef0de09b0ba795ec2a700be6c4c2c3f62bf4660c05404c948727b5bbfb32a + languageName: node + linkType: hard + +"typescript@npm:~5.3.0": + version: 5.3.3 + resolution: "typescript@npm:5.3.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 2007ccb6e51bbbf6fde0a78099efe04dc1c3dfbdff04ca3b6a8bc717991862b39fd6126c0c3ebf2d2d98ac5e960bcaa873826bb2bb241f14277034148f41f6a2 + languageName: node + linkType: hard + +"typescript@npm:~5.5.0": + version: 5.5.4 + resolution: "typescript@npm:5.5.4" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: b309040f3a1cd91c68a5a58af6b9fdd4e849b8c42d837b2c2e73f9a4f96a98c4f1ed398a9aab576ee0a4748f5690cf594e6b99dbe61de7839da748c41e6d6ca8 + languageName: node + linkType: hard + +"typescript@patch:typescript@5.4.2#~builtin": + version: 5.4.2 + resolution: "typescript@patch:typescript@npm%3A5.4.2#~builtin::version=5.4.2&hash=a1c5e5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: c1b669146bca5529873aae60870e243fa8140c85f57ca32c42f898f586d73ce4a6b4f6bb02ae312729e214d7f5859a0c70da3e527a116fdf5ad00c9fc733ecc6 + languageName: node + linkType: hard + +"typescript@patch:typescript@~5.3.0#~builtin": + version: 5.3.3 + resolution: "typescript@patch:typescript@npm%3A5.3.3#~builtin::version=5.3.3&hash=a1c5e5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: f61375590b3162599f0f0d5b8737877ac0a7bc52761dbb585d67e7b8753a3a4c42d9a554c4cc929f591ffcf3a2b0602f65ae3ce74714fd5652623a816862b610 + languageName: node + linkType: hard + +"typescript@patch:typescript@~5.5.0#~builtin": + version: 5.5.4 + resolution: "typescript@patch:typescript@npm%3A5.5.4#~builtin::version=5.5.4&hash=a1c5e5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: fc52962f31a5bcb716d4213bef516885e4f01f30cea797a831205fc9ef12b405a40561c40eae3127ab85ba1548e7df49df2bcdee6b84a94bfbe3a0d7eff16b14 + languageName: node + linkType: hard + +"uglify-js@npm:^3.1.4": + version: 3.19.3 + resolution: "uglify-js@npm:3.19.3" + bin: + uglifyjs: bin/uglifyjs + checksum: 7ed6272fba562eb6a3149cfd13cda662f115847865c03099e3995a0e7a910eba37b82d4fccf9e88271bb2bcbe505bb374967450f433c17fa27aa36d94a8d0553 + languageName: node + linkType: hard + +"uid@npm:2.0.2": + version: 2.0.2 + resolution: "uid@npm:2.0.2" + dependencies: + "@lukeed/csprng": ^1.0.0 + checksum: 98aabddcd6fe46f9b331b0378a93ee9cc51474348ada02006df9d10b4abc783ed596748ed9f20d7f6c5ff395dbcd1e764a65a68db6f39a31c95ae85ef13fe979 + languageName: node + linkType: hard + +"unbox-primitive@npm:^1.0.2": + version: 1.0.2 + resolution: "unbox-primitive@npm:1.0.2" + dependencies: + call-bind: ^1.0.2 + has-bigints: ^1.0.2 + has-symbols: ^1.0.3 + which-boxed-primitive: ^1.0.2 + checksum: b7a1cf5862b5e4b5deb091672ffa579aa274f648410009c81cca63fed3b62b610c4f3b773f912ce545bb4e31edc3138975b5bc777fc6e4817dca51affb6380e9 + languageName: node + linkType: hard + +"underscore@npm:1.12.1": + version: 1.12.1 + resolution: "underscore@npm:1.12.1" + checksum: ec327603aa112b99fe9d74cd9bf3b3b7451465a9d2610ceab269a532e3f191650ab017903be34dc86fe406a11d04d8905a3b04dd4c129493e51bee09a3f3074c + languageName: node + linkType: hard + +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 3192ef6f3fd5df652f2dc1cd782b49d6ff14dc98e5dced492aa8a8c65425227da5da6aafe22523c67f035a272c599bb89cfe803c1db6311e44bed3042fc25487 + languageName: node + linkType: hard + +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: de51f1b447d22571cf155dfe14ff6d12c5bdaec237c765085b439c38ca8518fc360e88c70f99469162bf2e14188a7b0bcb06e1ed2dc031042b984b0bb9544017 + languageName: node + linkType: hard + +"undici@npm:^5.28.4": + version: 5.28.4 + resolution: "undici@npm:5.28.4" + dependencies: + "@fastify/busboy": ^2.0.0 + checksum: a8193132d84540e4dc1895ecc8dbaa176e8a49d26084d6fbe48a292e28397cd19ec5d13bc13e604484e76f94f6e334b2bdc740d5f06a6e50c44072818d0c19f9 + languageName: node + linkType: hard + +"unicode-canonical-property-names-ecmascript@npm:^2.0.0": + version: 2.0.1 + resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.1" + checksum: 3c3dabdb1d22aef4904399f9e810d0b71c0b12b3815169d96fac97e56d5642840c6071cf709adcace2252bc6bb80242396c2ec74b37224eb015c5f7aca40bad7 + languageName: node + linkType: hard + +"unicode-match-property-ecmascript@npm:^2.0.0": + version: 2.0.0 + resolution: "unicode-match-property-ecmascript@npm:2.0.0" + dependencies: + unicode-canonical-property-names-ecmascript: ^2.0.0 + unicode-property-aliases-ecmascript: ^2.0.0 + checksum: 1f34a7434a23df4885b5890ac36c5b2161a809887000be560f56ad4b11126d433c0c1c39baf1016bdabed4ec54829a6190ee37aa24919aa116dc1a5a8a62965a + languageName: node + linkType: hard + +"unicode-match-property-value-ecmascript@npm:^2.1.0": + version: 2.2.0 + resolution: "unicode-match-property-value-ecmascript@npm:2.2.0" + checksum: 9e3151e1d0bc6be35c4cef105e317c04090364173e8462005b5cde08a1e7c858b6586486cfebac39dc2c6c8c9ee24afb245de6d527604866edfa454fe2a35fae + languageName: node + linkType: hard + +"unicode-property-aliases-ecmascript@npm:^2.0.0": + version: 2.1.0 + resolution: "unicode-property-aliases-ecmascript@npm:2.1.0" + checksum: 243524431893649b62cc674d877bd64ef292d6071dd2fd01ab4d5ad26efbc104ffcd064f93f8a06b7e4ec54c172bf03f6417921a0d8c3a9994161fe1f88f815b + languageName: node + linkType: hard + +"unified@npm:^10.0.0": + version: 10.1.2 + resolution: "unified@npm:10.1.2" + dependencies: + "@types/unist": ^2.0.0 + bail: ^2.0.0 + extend: ^3.0.0 + is-buffer: ^2.0.0 + is-plain-obj: ^4.0.0 + trough: ^2.0.0 + vfile: ^5.0.0 + checksum: 053e7c65ede644607f87bd625a299e4b709869d2f76ec8138569e6e886903b6988b21cd9699e471eda42bee189527be0a9dac05936f1d069a5e65d0125d5d756 + languageName: node + linkType: hard + +"unique-filename@npm:^2.0.0": + version: 2.0.1 + resolution: "unique-filename@npm:2.0.1" + dependencies: + unique-slug: ^3.0.0 + checksum: 807acf3381aff319086b64dc7125a9a37c09c44af7620bd4f7f3247fcd5565660ac12d8b80534dcbfd067e6fe88a67e621386dd796a8af828d1337a8420a255f + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: ^4.0.0 + checksum: 8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df + languageName: node + linkType: hard + +"unique-slug@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-slug@npm:3.0.0" + dependencies: + imurmurhash: ^0.1.4 + checksum: 49f8d915ba7f0101801b922062ee46b7953256c93ceca74303bd8e6413ae10aa7e8216556b54dc5382895e8221d04f1efaf75f945c2e4a515b4139f77aa6640c + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: ^0.1.4 + checksum: 0884b58365af59f89739e6f71e3feacb5b1b41f2df2d842d0757933620e6de08eff347d27e9d499b43c40476cbaf7988638d3acb2ffbcb9d35fd035591adfd15 + languageName: node + linkType: hard + +"unist-util-generated@npm:^2.0.0": + version: 2.0.1 + resolution: "unist-util-generated@npm:2.0.1" + checksum: 6221ad0571dcc9c8964d6b054f39ef6571ed59cc0ce3e88ae97ea1c70afe76b46412a5ffaa91f96814644ac8477e23fb1b477d71f8d70e625728c5258f5c0d99 + languageName: node + linkType: hard + +"unist-util-is@npm:^5.0.0": + version: 5.2.1 + resolution: "unist-util-is@npm:5.2.1" + dependencies: + "@types/unist": ^2.0.0 + checksum: ae76fdc3d35352cd92f1bedc3a0d407c3b9c42599a52ab9141fe89bdd786b51f0ec5a2ab68b93fb532e239457cae62f7e39eaa80229e1cb94875da2eafcbe5c4 + languageName: node + linkType: hard + +"unist-util-position@npm:^4.0.0": + version: 4.0.4 + resolution: "unist-util-position@npm:4.0.4" + dependencies: + "@types/unist": ^2.0.0 + checksum: e7487b6cec9365299695e3379ded270a1717074fa11fd2407c9b934fb08db6fe1d9077ddeaf877ecf1813665f8ccded5171693d3d9a7a01a125ec5cdd5e88691 + languageName: node + linkType: hard + +"unist-util-stringify-position@npm:^3.0.0": + version: 3.0.3 + resolution: "unist-util-stringify-position@npm:3.0.3" + dependencies: + "@types/unist": ^2.0.0 + checksum: dbd66c15183607ca942a2b1b7a9f6a5996f91c0d30cf8966fb88955a02349d9eefd3974e9010ee67e71175d784c5a9fea915b0aa0b0df99dcb921b95c4c9e124 + languageName: node + linkType: hard + +"unist-util-visit-parents@npm:^5.0.0, unist-util-visit-parents@npm:^5.1.1": + version: 5.1.3 + resolution: "unist-util-visit-parents@npm:5.1.3" + dependencies: + "@types/unist": ^2.0.0 + unist-util-is: ^5.0.0 + checksum: 8ecada5978994f846b64658cf13b4092cd78dea39e1ba2f5090a5de842ba4852712c02351a8ae95250c64f864635e7b02aedf3b4a093552bb30cf1bd160efbaa + languageName: node + linkType: hard + +"unist-util-visit@npm:^4.0.0": + version: 4.1.2 + resolution: "unist-util-visit@npm:4.1.2" + dependencies: + "@types/unist": ^2.0.0 + unist-util-is: ^5.0.0 + unist-util-visit-parents: ^5.1.1 + checksum: 95a34e3f7b5b2d4b68fd722b6229972099eb97b6df18913eda44a5c11df8b1e27efe7206dd7b88c4ed244a48c474a5b2e2629ab79558ff9eb936840295549cee + languageName: node + linkType: hard + +"universal-github-app-jwt@npm:^1.1.1": + version: 1.2.0 + resolution: "universal-github-app-jwt@npm:1.2.0" + dependencies: + "@types/jsonwebtoken": ^9.0.0 + jsonwebtoken: ^9.0.2 + checksum: e5d1f80ec3b0fa3eb28049d39e624ca51cd367aaeabebb5858cdf7d2a04d19b70446b6fcdaa01e26e550a93aba43754729372e44908e036d409e65a4b17acb2a + languageName: node + linkType: hard + +"universal-user-agent@npm:^6.0.0": + version: 6.0.1 + resolution: "universal-user-agent@npm:6.0.1" + checksum: fdc8e1ae48a05decfc7ded09b62071f571c7fe0bd793d700704c80cea316101d4eac15cc27ed2bb64f4ce166d2684777c3198b9ab16034f547abea0d3aa1c93c + languageName: node + linkType: hard + +"universalify@npm:^0.1.0": + version: 0.1.2 + resolution: "universalify@npm:0.1.2" + checksum: 40cdc60f6e61070fe658ca36016a8f4ec216b29bf04a55dce14e3710cc84c7448538ef4dad3728d0bfe29975ccd7bfb5f414c45e7b78883567fb31b246f02dff + languageName: node + linkType: hard + +"universalify@npm:^0.2.0": + version: 0.2.0 + resolution: "universalify@npm:0.2.0" + checksum: e86134cb12919d177c2353196a4cc09981524ee87abf621f7bc8d249dbbbebaec5e7d1314b96061497981350df786e4c5128dbf442eba104d6e765bc260678b5 + languageName: node + linkType: hard + +"universalify@npm:^2.0.0": + version: 2.0.1 + resolution: "universalify@npm:2.0.1" + checksum: ecd8469fe0db28e7de9e5289d32bd1b6ba8f7183db34f3bfc4ca53c49891c2d6aa05f3fb3936a81285a905cc509fb641a0c3fc131ec786167eff41236ae32e60 + languageName: node + linkType: hard + +"unpipe@npm:1.0.0, unpipe@npm:~1.0.0": + version: 1.0.0 + resolution: "unpipe@npm:1.0.0" + checksum: 4fa18d8d8d977c55cb09715385c203197105e10a6d220087ec819f50cb68870f02942244f1017565484237f1f8c5d3cd413631b1ae104d3096f24fdfde1b4aa2 + languageName: node + linkType: hard + +"upath@npm:2.0.1": + version: 2.0.1 + resolution: "upath@npm:2.0.1" + checksum: 2db04f24a03ef72204c7b969d6991abec9e2cb06fb4c13a1fd1c59bc33b46526b16c3325e55930a11ff86a77a8cbbcda8f6399bf914087028c5beae21ecdb33c + languageName: node + linkType: hard + +"update-browserslist-db@npm:^1.1.0": + version: 1.1.1 + resolution: "update-browserslist-db@npm:1.1.1" + dependencies: + escalade: ^3.2.0 + picocolors: ^1.1.0 + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 2ea11bd2562122162c3e438d83a1f9125238c0844b6d16d366e3276d0c0acac6036822dc7df65fc5a89c699cdf9f174acf439c39bedf3f9a2f3983976e4b4c3e + languageName: node + linkType: hard + +"uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": + version: 4.4.1 + resolution: "uri-js@npm:4.4.1" + dependencies: + punycode: ^2.1.0 + checksum: 7167432de6817fe8e9e0c9684f1d2de2bb688c94388f7569f7dbdb1587c9f4ca2a77962f134ec90be0cc4d004c939ff0d05acc9f34a0db39a3c797dada262633 + languageName: node + linkType: hard + +"uri-template@npm:^2.0.0": + version: 2.0.0 + resolution: "uri-template@npm:2.0.0" + dependencies: + pct-encode: ~1.0.0 + checksum: 6eb3254368ca11330502525c6c0ab42af3cb646bfc96a4021666d6ac6653ede1ac0df7fde84a2e35e7f03f42d91b41251963122cfb3de9b54b84bc0ef3583ffc + languageName: node + linkType: hard + +"urijs@npm:^1.19.11": + version: 1.19.11 + resolution: "urijs@npm:1.19.11" + checksum: f9b95004560754d30fd7dbee44b47414d662dc9863f1cf5632a7c7983648df11d23c0be73b9b4f9554463b61d5b0a520b70df9e1ee963ebb4af02e6da2cc80f3 + languageName: node + linkType: hard + +"url-join@npm:^4.0.1": + version: 4.0.1 + resolution: "url-join@npm:4.0.1" + checksum: f74e868bf25dbc8be6a8d7237d4c36bb5b6c62c72e594d5ab1347fe91d6af7ccd9eb5d621e30152e4da45c2e9a26bec21390e911ab54a62d4d82e76028374ee5 + languageName: node + linkType: hard + +"url-parse@npm:^1.5.3": + version: 1.5.10 + resolution: "url-parse@npm:1.5.10" + dependencies: + querystringify: ^2.1.1 + requires-port: ^1.0.0 + checksum: fbdba6b1d83336aca2216bbdc38ba658d9cfb8fc7f665eb8b17852de638ff7d1a162c198a8e4ed66001ddbf6c9888d41e4798912c62b4fd777a31657989f7bdf + languageName: node + linkType: hard + +"url@npm:^0.11.0": + version: 0.11.4 + resolution: "url@npm:0.11.4" + dependencies: + punycode: ^1.4.1 + qs: ^6.12.3 + checksum: c25e587723d343d5d4248892393bfa5039ded9c2c07095a9d005bc64b7cb8956d623c0d8da8d1a28f71986a7a8d80fc2e9f9cf84235e48fa435a5cb4451062c6 + languageName: node + linkType: hard + +"use-memo-one@npm:^1.1.1": + version: 1.1.3 + resolution: "use-memo-one@npm:1.1.3" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 8f08eba26d69406b61bb4b8dacdd5a92bd6aef5b53d346dfe87954f7330ee10ecabc937cc7854635155d46053828e85c10b5a5aff7a04720e6a97b9f42999bac + languageName: node + linkType: hard + +"use-sync-external-store@npm:^1.2.0": + version: 1.2.2 + resolution: "use-sync-external-store@npm:1.2.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: fe07c071c4da3645f112c38c0e57beb479a8838616ff4e92598256ecce527f2888c08febc7f9b2f0ce2f0e18540ba3cde41eb2035e4fafcb4f52955037098a81 + languageName: node + linkType: hard + +"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 + languageName: node + linkType: hard + +"util@npm:^0.10.4": + version: 0.10.4 + resolution: "util@npm:0.10.4" + dependencies: + inherits: 2.0.3 + checksum: 913f9a90d05a60e91f91af01b8bd37e06bca4cc02d7b49e01089f9d5b78be2fffd61fb1a41b517de7238c5fc7337fa939c62d1fb4eb82e014894c7bee6637aaf + languageName: node + linkType: hard + +"util@npm:^0.11.0": + version: 0.11.1 + resolution: "util@npm:0.11.1" + dependencies: + inherits: 2.0.3 + checksum: 80bee6a2edf5ab08dcb97bfe55ca62289b4e66f762ada201f2c5104cb5e46474c8b334f6504d055c0e6a8fda10999add9bcbd81ba765e7f37b17dc767331aa55 + languageName: node + linkType: hard + +"util@npm:^0.12.3": + version: 0.12.5 + resolution: "util@npm:0.12.5" + dependencies: + inherits: ^2.0.3 + is-arguments: ^1.0.4 + is-generator-function: ^1.0.7 + is-typed-array: ^1.1.3 + which-typed-array: ^1.1.2 + checksum: 705e51f0de5b446f4edec10739752ac25856541e0254ea1e7e45e5b9f9b0cb105bc4bd415736a6210edc68245a7f903bf085ffb08dd7deb8a0e847f60538a38a + languageName: node + linkType: hard + +"utila@npm:~0.4": + version: 0.4.0 + resolution: "utila@npm:0.4.0" + checksum: 97ffd3bd2bb80c773429d3fb8396469115cd190dded1e733f190d8b602bd0a1bcd6216b7ce3c4395ee3c79e3c879c19d268dbaae3093564cb169ad1212d436f4 + languageName: node + linkType: hard + +"utility-types@npm:^3.10.0": + version: 3.11.0 + resolution: "utility-types@npm:3.11.0" + checksum: 35a4866927bbea5d037726744028d05c6e37772ded2aabaca21480ce9380185436aef586ead525e327c7f3c640b1a3287769a12ef269c7b165a2ddd50ea6ad61 + languageName: node + linkType: hard + +"utils-merge@npm:1.0.1, utils-merge@npm:^1.0.1": + version: 1.0.1 + resolution: "utils-merge@npm:1.0.1" + checksum: c81095493225ecfc28add49c106ca4f09cdf56bc66731aa8dabc2edbbccb1e1bfe2de6a115e5c6a380d3ea166d1636410b62ef216bb07b3feb1cfde1d95d5080 + languageName: node + linkType: hard + +"uuid@npm:^3.3.2, uuid@npm:^3.4.0": + version: 3.4.0 + resolution: "uuid@npm:3.4.0" + bin: + uuid: ./bin/uuid + checksum: 58de2feed61c59060b40f8203c0e4ed7fd6f99d42534a499f1741218a1dd0c129f4aa1de797bcf822c8ea5da7e4137aa3673431a96dae729047f7aca7b27866f + languageName: node + linkType: hard + +"uuid@npm:^8.0.0, uuid@npm:^8.3.0, uuid@npm:^8.3.2": + version: 8.3.2 + resolution: "uuid@npm:8.3.2" + bin: + uuid: dist/bin/uuid + checksum: 5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df + languageName: node + linkType: hard + +"uuid@npm:^9.0.0, uuid@npm:^9.0.1": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" + bin: + uuid: dist/bin/uuid + checksum: 39931f6da74e307f51c0fb463dc2462807531dc80760a9bff1e35af4316131b4fc3203d16da60ae33f07fdca5b56f3f1dd662da0c99fea9aaeab2004780cc5f4 + languageName: node + linkType: hard + +"uvu@npm:^0.5.0": + version: 0.5.6 + resolution: "uvu@npm:0.5.6" + dependencies: + dequal: ^2.0.0 + diff: ^5.0.0 + kleur: ^4.0.3 + sade: ^1.7.3 + bin: + uvu: bin.js + checksum: 09460a37975627de9fcad396e5078fb844d01aaf64a6399ebfcfd9e55f1c2037539b47611e8631f89be07656962af0cf48c334993db82b9ae9c3d25ce3862168 + languageName: node + linkType: hard + +"v8-compile-cache-lib@npm:^3.0.1": + version: 3.0.1 + resolution: "v8-compile-cache-lib@npm:3.0.1" + checksum: 78089ad549e21bcdbfca10c08850022b22024cdcc2da9b168bcf5a73a6ed7bf01a9cebb9eac28e03cd23a684d81e0502797e88f3ccd27a32aeab1cfc44c39da0 + languageName: node + linkType: hard + +"v8-to-istanbul@npm:^9.0.1": + version: 9.3.0 + resolution: "v8-to-istanbul@npm:9.3.0" + dependencies: + "@jridgewell/trace-mapping": ^0.3.12 + "@types/istanbul-lib-coverage": ^2.0.1 + convert-source-map: ^2.0.0 + checksum: ded42cd535d92b7fd09a71c4c67fb067487ef5551cc227bfbf2a1f159a842e4e4acddaef20b955789b8d3b455b9779d036853f4a27ce15007f6364a4d30317ae + languageName: node + linkType: hard + +"validate.io-array@npm:^1.0.3": + version: 1.0.6 + resolution: "validate.io-array@npm:1.0.6" + checksum: 54eca83ebc702e3e46499f9d9e77287a95ae25c4e727cd2fafee29c7333b3a36cca0c5d8f090b9406262786de80750fba85e7e7ef41e20bf8cc67d5570de449b + languageName: node + linkType: hard + +"validate.io-function@npm:^1.0.2": + version: 1.0.2 + resolution: "validate.io-function@npm:1.0.2" + checksum: e4cce2479a20cb7c42e8630c777fb107059c27bc32925f769e3a73ca5fd62b4892d897b3c80227e14d5fcd1c5b7d05544e0579d63e59f14034c0052cda7f7c44 + languageName: node + linkType: hard + +"validate.io-integer-array@npm:^1.0.0": + version: 1.0.0 + resolution: "validate.io-integer-array@npm:1.0.0" + dependencies: + validate.io-array: ^1.0.3 + validate.io-integer: ^1.0.4 + checksum: 5f6d7fab8df7d2bf546a05e830201768464605539c75a2c2417b632b4411a00df84b462f81eac75e1be95303e7e0ac92f244c137424739f4e15cd21c2eb52c7f + languageName: node + linkType: hard + +"validate.io-integer@npm:^1.0.4": + version: 1.0.5 + resolution: "validate.io-integer@npm:1.0.5" + dependencies: + validate.io-number: ^1.0.3 + checksum: 88b3f8bb5a5277a95305d64abbfc437079220ce4f57a148cc6113e7ccec03dd86b10a69d413982602aa90a62b8d516148a78716f550dcd3aff863ac1c2a7a5e6 + languageName: node + linkType: hard + +"validate.io-number@npm:^1.0.3": + version: 1.0.3 + resolution: "validate.io-number@npm:1.0.3" + checksum: 42418aeb6c969efa745475154fe576809b02eccd0961aad0421b090d6e7a12d23a3e28b0d5dddd2c6347c1a6bdccb82bba5048c716131cd20207244d50e07282 + languageName: node + linkType: hard + +"vary@npm:^1, vary@npm:^1.1.2, vary@npm:~1.1.2": + version: 1.1.2 + resolution: "vary@npm:1.1.2" + checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b + languageName: node + linkType: hard + +"verror@npm:1.10.0": + version: 1.10.0 + resolution: "verror@npm:1.10.0" + dependencies: + assert-plus: ^1.0.0 + core-util-is: 1.0.2 + extsprintf: ^1.2.0 + checksum: c431df0bedf2088b227a4e051e0ff4ca54df2c114096b0c01e1cbaadb021c30a04d7dd5b41ab277bcd51246ca135bf931d4c4c796ecae7a4fef6d744ecef36ea + languageName: node + linkType: hard + +"vfile-message@npm:^3.0.0": + version: 3.1.4 + resolution: "vfile-message@npm:3.1.4" + dependencies: + "@types/unist": ^2.0.0 + unist-util-stringify-position: ^3.0.0 + checksum: d0ee7da1973ad76513c274e7912adbed4d08d180eaa34e6bd40bc82459f4b7bc50fcaff41556135e3339995575eac5f6f709aba9332b80f775618ea4880a1367 + languageName: node + linkType: hard + +"vfile@npm:^5.0.0": + version: 5.3.7 + resolution: "vfile@npm:5.3.7" + dependencies: + "@types/unist": ^2.0.0 + is-buffer: ^2.0.0 + unist-util-stringify-position: ^3.0.0 + vfile-message: ^3.0.0 + checksum: 642cce703afc186dbe7cabf698dc954c70146e853491086f5da39e1ce850676fc96b169fcf7898aa3ff245e9313aeec40da93acd1e1fcc0c146dc4f6308b4ef9 + languageName: node + linkType: hard + +"vm-browserify@npm:^1.0.1": + version: 1.1.2 + resolution: "vm-browserify@npm:1.1.2" + checksum: 10a1c50aab54ff8b4c9042c15fc64aefccce8d2fb90c0640403242db0ee7fb269f9b102bdb69cfb435d7ef3180d61fd4fb004a043a12709abaf9056cfd7e039d + languageName: node + linkType: hard + +"w3c-xmlserializer@npm:^4.0.0": + version: 4.0.0 + resolution: "w3c-xmlserializer@npm:4.0.0" + dependencies: + xml-name-validator: ^4.0.0 + checksum: eba070e78deb408ae8defa4d36b429f084b2b47a4741c4a9be3f27a0a3d1845e277e3072b04391a138f7e43776842627d1334e448ff13ff90ad9fb1214ee7091 + languageName: node + linkType: hard + +"wait-on@npm:8.0.1": + version: 8.0.1 + resolution: "wait-on@npm:8.0.1" + dependencies: + axios: ^1.7.7 + joi: ^17.13.3 + lodash: ^4.17.21 + minimist: ^1.2.8 + rxjs: ^7.8.1 + bin: + wait-on: bin/wait-on + checksum: 20e670a7c7ef8959a859c27d269297e11a6be0324f4f0d0b494dfc2d43582f66a70f5a6ead158ed47a840632706ff2b9c939284bea7856bf283e9de9e33d84f3 + languageName: node + linkType: hard + +"walker@npm:^1.0.8": + version: 1.0.8 + resolution: "walker@npm:1.0.8" + dependencies: + makeerror: 1.0.12 + checksum: ad7a257ea1e662e57ef2e018f97b3c02a7240ad5093c392186ce0bcf1f1a60bbadd520d073b9beb921ed99f64f065efb63dfc8eec689a80e569f93c1c5d5e16c + languageName: node + linkType: hard + +"watchpack@npm:^2.4.1": + version: 2.4.2 + resolution: "watchpack@npm:2.4.2" + dependencies: + glob-to-regexp: ^0.4.1 + graceful-fs: ^4.1.2 + checksum: 92d9d52ce3d16fd83ed6994d1dd66a4d146998882f4c362d37adfea9ab77748a5b4d1e0c65fa104797928b2d40f635efa8f9b925a6265428a69f1e1852ca3441 + languageName: node + linkType: hard + +"wbuf@npm:^1.1.0, wbuf@npm:^1.7.3": + version: 1.7.3 + resolution: "wbuf@npm:1.7.3" + dependencies: + minimalistic-assert: ^1.0.0 + checksum: 2abc306c96930b757972a1c4650eb6b25b5d99f24088714957f88629e137db569368c5de0e57986c89ea70db2f1df9bba11a87cb6d0c8694b6f53a0159fab3bf + languageName: node + linkType: hard + +"wcwidth@npm:>=1.0.1, wcwidth@npm:^1.0.1": + version: 1.0.1 + resolution: "wcwidth@npm:1.0.1" + dependencies: + defaults: ^1.0.3 + checksum: 814e9d1ddcc9798f7377ffa448a5a3892232b9275ebb30a41b529607691c0491de47cba426e917a4d08ded3ee7e9ba2f3fe32e62ee3cd9c7d3bafb7754bd553c + languageName: node + linkType: hard + +"web-encoding@npm:^1.1.5": + version: 1.1.5 + resolution: "web-encoding@npm:1.1.5" + dependencies: + "@zxing/text-encoding": 0.9.0 + util: ^0.12.3 + dependenciesMeta: + "@zxing/text-encoding": + optional: true + checksum: 2234a2b122f41006ce07859b3c0bf2e18f46144fda2907d5db0b571b76aa5c26977c646100ad9c00d2f8a4f6f2b848bc02147845d8c447ab365ec4eff376338d + languageName: node + linkType: hard + +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: c92a0a6ab95314bde9c32e1d0a6dfac83b578f8fa5f21e675bc2706ed6981bc26b7eb7e6a1fab158e5ce4adf9caa4a0aee49a52505d4d13c7be545f15021b17c + languageName: node + linkType: hard + +"webidl-conversions@npm:^7.0.0": + version: 7.0.0 + resolution: "webidl-conversions@npm:7.0.0" + checksum: f05588567a2a76428515333eff87200fae6c83c3948a7482ebb109562971e77ef6dc49749afa58abb993391227c5697b3ecca52018793e0cb4620a48f10bd21b + languageName: node + linkType: hard + +"webpack-dev-middleware@npm:^7.4.2": + version: 7.4.2 + resolution: "webpack-dev-middleware@npm:7.4.2" + dependencies: + colorette: ^2.0.10 + memfs: ^4.6.0 + mime-types: ^2.1.31 + on-finished: ^2.4.1 + range-parser: ^1.2.1 + schema-utils: ^4.0.0 + peerDependencies: + webpack: ^5.0.0 + peerDependenciesMeta: + webpack: + optional: true + checksum: 39314ec5e4468d177dd61fb51af87ec097e920fe0f0dc101e1bf71796740a7e49fd4f7f939cf91e130232714d6d2fffd948d72dc65dec10f87ac30339929f018 + languageName: node + linkType: hard + +"webpack-dev-server@npm:^5.0.0": + version: 5.1.0 + resolution: "webpack-dev-server@npm:5.1.0" + dependencies: + "@types/bonjour": ^3.5.13 + "@types/connect-history-api-fallback": ^1.5.4 + "@types/express": ^4.17.21 + "@types/serve-index": ^1.9.4 + "@types/serve-static": ^1.15.5 + "@types/sockjs": ^0.3.36 + "@types/ws": ^8.5.10 + ansi-html-community: ^0.0.8 + bonjour-service: ^1.2.1 + chokidar: ^3.6.0 + colorette: ^2.0.10 + compression: ^1.7.4 + connect-history-api-fallback: ^2.0.0 + express: ^4.19.2 + graceful-fs: ^4.2.6 + html-entities: ^2.4.0 + http-proxy-middleware: ^2.0.3 + ipaddr.js: ^2.1.0 + launch-editor: ^2.6.1 + open: ^10.0.3 + p-retry: ^6.2.0 + schema-utils: ^4.2.0 + selfsigned: ^2.4.1 + serve-index: ^1.9.1 + sockjs: ^0.3.24 + spdy: ^4.0.2 + webpack-dev-middleware: ^7.4.2 + ws: ^8.18.0 + peerDependencies: + webpack: ^5.0.0 + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + bin: + webpack-dev-server: bin/webpack-dev-server.js + checksum: 3128fffeb76b97cc4c506607f81bb644437f6961cf310915e22ecaf79a45c185893d7fc8e1844183fb44827061ec2f3d321e937840f02d4989959a09551a8e35 + languageName: node + linkType: hard + +"webpack-node-externals@npm:^3.0.0": + version: 3.0.0 + resolution: "webpack-node-externals@npm:3.0.0" + checksum: 355080c35c821115b97dda8c93d9d0565a90a6012a532324eb0d6a64f8f0d609431fd29504fc7ce414755841ac14f601f3eef99472c2c5dc00233b504ebe73f2 + languageName: node + linkType: hard + +"webpack-sources@npm:^1.4.3": + version: 1.4.3 + resolution: "webpack-sources@npm:1.4.3" + dependencies: + source-list-map: ^2.0.0 + source-map: ~0.6.1 + checksum: 37463dad8d08114930f4bc4882a9602941f07c9f0efa9b6bc78738cd936275b990a596d801ef450d022bb005b109b9f451dd087db2f3c9baf53e8e22cf388f79 + languageName: node + linkType: hard + +"webpack-sources@npm:^3.2.3": + version: 3.2.3 + resolution: "webpack-sources@npm:3.2.3" + checksum: 989e401b9fe3536529e2a99dac8c1bdc50e3a0a2c8669cbafad31271eadd994bc9405f88a3039cd2e29db5e6d9d0926ceb7a1a4e7409ece021fe79c37d9c4607 + languageName: node + linkType: hard + +"webpack@npm:^5.94.0": + version: 5.95.0 + resolution: "webpack@npm:5.95.0" + dependencies: + "@types/estree": ^1.0.5 + "@webassemblyjs/ast": ^1.12.1 + "@webassemblyjs/wasm-edit": ^1.12.1 + "@webassemblyjs/wasm-parser": ^1.12.1 + acorn: ^8.7.1 + acorn-import-attributes: ^1.9.5 + browserslist: ^4.21.10 + chrome-trace-event: ^1.0.2 + enhanced-resolve: ^5.17.1 + es-module-lexer: ^1.2.1 + eslint-scope: 5.1.1 + events: ^3.2.0 + glob-to-regexp: ^0.4.1 + graceful-fs: ^4.2.11 + json-parse-even-better-errors: ^2.3.1 + loader-runner: ^4.2.0 + mime-types: ^2.1.27 + neo-async: ^2.6.2 + schema-utils: ^3.2.0 + tapable: ^2.1.1 + terser-webpack-plugin: ^5.3.10 + watchpack: ^2.4.1 + webpack-sources: ^3.2.3 + peerDependenciesMeta: + webpack-cli: + optional: true + bin: + webpack: bin/webpack.js + checksum: 0c3dfe288de4d62f8f3dc25478a618894883cab739121330763b7847e43304630ea2815ae2351a5f8ff6ab7c9642caf530d503d89bda261fe2cd220e524dd5d1 + languageName: node + linkType: hard + +"websocket-driver@npm:>=0.5.1, websocket-driver@npm:^0.7.4": + version: 0.7.4 + resolution: "websocket-driver@npm:0.7.4" + dependencies: + http-parser-js: ">=0.5.1" + safe-buffer: ">=5.1.0" + websocket-extensions: ">=0.1.1" + checksum: fffe5a33fe8eceafd21d2a065661d09e38b93877eae1de6ab5d7d2734c6ed243973beae10ae48c6613cfd675f200e5a058d1e3531bc9e6c5d4f1396ff1f0bfb9 + languageName: node + linkType: hard + +"websocket-extensions@npm:>=0.1.1": + version: 0.1.4 + resolution: "websocket-extensions@npm:0.1.4" + checksum: 5976835e68a86afcd64c7a9762ed85f2f27d48c488c707e67ba85e717b90fa066b98ab33c744d64255c9622d349eedecf728e65a5f921da71b58d0e9591b9038 + languageName: node + linkType: hard + +"whatwg-encoding@npm:^2.0.0": + version: 2.0.0 + resolution: "whatwg-encoding@npm:2.0.0" + dependencies: + iconv-lite: 0.6.3 + checksum: 7087810c410aa9b689cbd6af8773341a53cdc1f3aae2a882c163bd5522ec8ca4cdfc269aef417a5792f411807d5d77d50df4c24e3abb00bb60192858a40cc675 + languageName: node + linkType: hard + +"whatwg-mimetype@npm:^3.0.0": + version: 3.0.0 + resolution: "whatwg-mimetype@npm:3.0.0" + checksum: ce08bbb36b6aaf64f3a84da89707e3e6a31e5ab1c1a2379fd68df79ba712a4ab090904f0b50e6693b0dafc8e6343a6157e40bf18fdffd26e513cf95ee2a59824 + languageName: node + linkType: hard + +"whatwg-url@npm:^11.0.0": + version: 11.0.0 + resolution: "whatwg-url@npm:11.0.0" + dependencies: + tr46: ^3.0.0 + webidl-conversions: ^7.0.0 + checksum: ed4826aaa57e66bb3488a4b25c9cd476c46ba96052747388b5801f137dd740b73fde91ad207d96baf9f17fbcc80fc1a477ad65181b5eb5fa718d27c69501d7af + languageName: node + linkType: hard + +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: ~0.0.3 + webidl-conversions: ^3.0.0 + checksum: b8daed4ad3356cc4899048a15b2c143a9aed0dfae1f611ebd55073310c7b910f522ad75d727346ad64203d7e6c79ef25eafd465f4d12775ca44b90fa82ed9e2c + languageName: node + linkType: hard + +"which-boxed-primitive@npm:^1.0.2": + version: 1.0.2 + resolution: "which-boxed-primitive@npm:1.0.2" + dependencies: + is-bigint: ^1.0.1 + is-boolean-object: ^1.1.0 + is-number-object: ^1.0.4 + is-string: ^1.0.5 + is-symbol: ^1.0.3 + checksum: 53ce774c7379071729533922adcca47220228405e1895f26673bbd71bdf7fb09bee38c1d6399395927c6289476b5ae0629863427fd151491b71c4b6cb04f3a5e + languageName: node + linkType: hard + +"which-builtin-type@npm:^1.1.3": + version: 1.1.4 + resolution: "which-builtin-type@npm:1.1.4" + dependencies: + function.prototype.name: ^1.1.6 + has-tostringtag: ^1.0.2 + is-async-function: ^2.0.0 + is-date-object: ^1.0.5 + is-finalizationregistry: ^1.0.2 + is-generator-function: ^1.0.10 + is-regex: ^1.1.4 + is-weakref: ^1.0.2 + isarray: ^2.0.5 + which-boxed-primitive: ^1.0.2 + which-collection: ^1.0.2 + which-typed-array: ^1.1.15 + checksum: 1f413025250072534de2a2ee25139a24d477512b532b05c85fb9aa05aef04c6e1ca8e2668acf971b777e602721dbdec4b9d6a4f37c6b9ff8f026ad030352707f + languageName: node + linkType: hard + +"which-collection@npm:^1.0.1, which-collection@npm:^1.0.2": + version: 1.0.2 + resolution: "which-collection@npm:1.0.2" + dependencies: + is-map: ^2.0.3 + is-set: ^2.0.3 + is-weakmap: ^2.0.2 + is-weakset: ^2.0.3 + checksum: c51821a331624c8197916598a738fc5aeb9a857f1e00d89f5e4c03dc7c60b4032822b8ec5696d28268bb83326456a8b8216344fb84270d18ff1d7628051879d9 + languageName: node + linkType: hard + +"which-typed-array@npm:^1.1.13, which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.15, which-typed-array@npm:^1.1.2": + version: 1.1.15 + resolution: "which-typed-array@npm:1.1.15" + dependencies: + available-typed-arrays: ^1.0.7 + call-bind: ^1.0.7 + for-each: ^0.3.3 + gopd: ^1.0.1 + has-tostringtag: ^1.0.2 + checksum: 65227dcbfadf5677aacc43ec84356d17b5500cb8b8753059bb4397de5cd0c2de681d24e1a7bd575633f976a95f88233abfd6549c2105ef4ebd58af8aa1807c75 + languageName: node + linkType: hard + +"which@npm:^1.2.14, which@npm:^1.2.9, which@npm:^1.3.1": + version: 1.3.1 + resolution: "which@npm:1.3.1" + dependencies: + isexe: ^2.0.0 + bin: + which: ./bin/which + checksum: f2e185c6242244b8426c9df1510e86629192d93c1a986a7d2a591f2c24869e7ffd03d6dac07ca863b2e4c06f59a4cc9916c585b72ee9fa1aa609d0124df15e04 + languageName: node + linkType: hard + +"which@npm:^2.0.1, which@npm:^2.0.2": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: ^2.0.0 + bin: + node-which: ./bin/node-which + checksum: 1a5c563d3c1b52d5f893c8b61afe11abc3bab4afac492e8da5bde69d550de701cf9806235f20a47b5c8fa8a1d6a9135841de2596535e998027a54589000e66d1 + languageName: node + linkType: hard + +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" + dependencies: + isexe: ^3.1.1 + bin: + node-which: bin/which.js + checksum: f17e84c042592c21e23c8195108cff18c64050b9efb8459589116999ea9da6dd1509e6a1bac3aeebefd137be00fabbb61b5c2bc0aa0f8526f32b58ee2f545651 + languageName: node + linkType: hard + +"wide-align@npm:^1.1.2, wide-align@npm:^1.1.5": + version: 1.1.5 + resolution: "wide-align@npm:1.1.5" + dependencies: + string-width: ^1.0.2 || 2 || 3 || 4 + checksum: d5fc37cd561f9daee3c80e03b92ed3e84d80dde3365a8767263d03dacfc8fa06b065ffe1df00d8c2a09f731482fcacae745abfbb478d4af36d0a891fad4834d3 + languageName: node + linkType: hard + +"winston-transport@npm:^4.5.0, winston-transport@npm:^4.7.0": + version: 4.8.0 + resolution: "winston-transport@npm:4.8.0" + dependencies: + logform: ^2.6.1 + readable-stream: ^4.5.2 + triple-beam: ^1.3.0 + checksum: f84092188176d49a6f4f75321ba3e50107ac0942a51a6d7e36b80af19dafb22b57258aaa6d8220763044ea23e30bffd597d3280d2a2298e6a491fe424896bac7 + languageName: node + linkType: hard + +"winston@npm:^3.2.1": + version: 3.15.0 + resolution: "winston@npm:3.15.0" + dependencies: + "@colors/colors": ^1.6.0 + "@dabh/diagnostics": ^2.0.2 + async: ^3.2.3 + is-stream: ^2.0.0 + logform: ^2.6.0 + one-time: ^1.0.0 + readable-stream: ^3.4.0 + safe-stable-stringify: ^2.3.1 + stack-trace: 0.0.x + triple-beam: ^1.3.0 + winston-transport: ^4.7.0 + checksum: 2ae6f3a3359fadd90f69a4db20d78aba6901e18114648e48c8538e925511e4820f8d488f19b1c026096ece614732338aa138f4a0fa2c5e29e8fbc53029f55473 + languageName: node + linkType: hard + +"word-wrap@npm:^1.2.5, word-wrap@npm:~1.2.3": + version: 1.2.5 + resolution: "word-wrap@npm:1.2.5" + checksum: f93ba3586fc181f94afdaff3a6fef27920b4b6d9eaefed0f428f8e07adea2a7f54a5f2830ce59406c8416f033f86902b91eb824072354645eea687dff3691ccb + languageName: node + linkType: hard + +"wordwrap@npm:^1.0.0": + version: 1.0.0 + resolution: "wordwrap@npm:1.0.0" + checksum: 2a44b2788165d0a3de71fd517d4880a8e20ea3a82c080ce46e294f0b68b69a2e49cff5f99c600e275c698a90d12c5ea32aff06c311f0db2eb3f1201f3e7b2a04 + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b + languageName: node + linkType: hard + +"wrap-ansi@npm:^6.0.1": + version: 6.2.0 + resolution: "wrap-ansi@npm:6.2.0" + dependencies: + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: 6cd96a410161ff617b63581a08376f0cb9162375adeb7956e10c8cd397821f7eb2a6de24eb22a0b28401300bf228c86e50617cd568209b5f6775b93c97d2fe3a + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: ^6.1.0 + string-width: ^5.0.1 + strip-ansi: ^7.0.1 + checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 + languageName: node + linkType: hard + +"write-file-atomic@npm:^4.0.2": + version: 4.0.2 + resolution: "write-file-atomic@npm:4.0.2" + dependencies: + imurmurhash: ^0.1.4 + signal-exit: ^3.0.7 + checksum: 5da60bd4eeeb935eec97ead3df6e28e5917a6bd317478e4a85a5285e8480b8ed96032bbcc6ecd07b236142a24f3ca871c924ec4a6575e623ec1b11bf8c1c253c + languageName: node + linkType: hard + +"ws@npm:8.18.0, ws@npm:^8.11.0, ws@npm:^8.18.0": + version: 8.18.0 + resolution: "ws@npm:8.18.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 91d4d35bc99ff6df483bdf029b9ea4bfd7af1f16fc91231a96777a63d263e1eabf486e13a2353970efc534f9faa43bdbf9ee76525af22f4752cbc5ebda333975 + languageName: node + linkType: hard + +"xml-name-validator@npm:^4.0.0": + version: 4.0.0 + resolution: "xml-name-validator@npm:4.0.0" + checksum: af100b79c29804f05fa35aa3683e29a321db9b9685d5e5febda3fa1e40f13f85abc40f45a6b2bf7bee33f68a1dc5e8eaef4cec100a304a9db565e6061d4cb5ad + languageName: node + linkType: hard + +"xmlchars@npm:^2.2.0": + version: 2.2.0 + resolution: "xmlchars@npm:2.2.0" + checksum: 8c70ac94070ccca03f47a81fcce3b271bd1f37a591bf5424e787ae313fcb9c212f5f6786e1fa82076a2c632c0141552babcd85698c437506dfa6ae2d58723062 + languageName: node + linkType: hard + +"xtend@npm:^4.0.0": + version: 4.0.2 + resolution: "xtend@npm:4.0.2" + checksum: ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a + languageName: node + linkType: hard + +"xterm-addon-attach@npm:^0.9.0": + version: 0.9.0 + resolution: "xterm-addon-attach@npm:0.9.0" + peerDependencies: + xterm: ^5.0.0 + checksum: 70e5d3ecf139c04fae13c644b79c33858ef1a6e28dfe78f91dad3e34f5a155579029b87e91d1d016575acaf17f74e6c59402bde4bcff03461595bea0870f1ec1 + languageName: node + linkType: hard + +"xterm-addon-fit@npm:^0.8.0": + version: 0.8.0 + resolution: "xterm-addon-fit@npm:0.8.0" + peerDependencies: + xterm: ^5.0.0 + checksum: 5af2041b442f7c804eda2e6f62e3b68b5159b0ae6bd96e2aa8d85b26441df57291cbfed653d1196d4af5d9b94bfc39993df8b409a25c35e0d36bdaf6f5cdfe5f + languageName: node + linkType: hard + +"xterm@npm:^5.3.0": + version: 5.3.0 + resolution: "xterm@npm:5.3.0" + checksum: 1bdfdfe4cae4412128376180d85e476b43fb021cdd1114b18acad821c9ea44b5b600e0d88febf2b3572f38fad7741e5161ce0178a44369617cf937222cc6e011 + languageName: node + linkType: hard + +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 54f0fb95621ee60898a38c572c515659e51cc9d9f787fb109cef6fde4befbe1c4602dc999d30110feee37456ad0f1660fa2edcfde6a9a740f86a290999550d30 + languageName: node + linkType: hard + +"yallist@npm:^2.1.2": + version: 2.1.2 + resolution: "yallist@npm:2.1.2" + checksum: 9ba99409209f485b6fcb970330908a6d41fa1c933f75e08250316cce19383179a6b70a7e0721b89672ebb6199cc377bf3e432f55100da6a7d6e11902b0a642cb + languageName: node + linkType: hard + +"yallist@npm:^3.0.2": + version: 3.1.1 + resolution: "yallist@npm:3.1.1" + checksum: 48f7bb00dc19fc635a13a39fe547f527b10c9290e7b3e836b9a8f1ca04d4d342e85714416b3c2ab74949c9c66f9cebb0473e6bc353b79035356103b47641285d + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 + languageName: node + linkType: hard + +"yallist@npm:^5.0.0": + version: 5.0.0 + resolution: "yallist@npm:5.0.0" + checksum: eba51182400b9f35b017daa7f419f434424410691bbc5de4f4240cc830fdef906b504424992700dc047f16b4d99100a6f8b8b11175c193f38008e9c96322b6a5 + languageName: node + linkType: hard + +"yaml-ast-parser@npm:^0.0.43": + version: 0.0.43 + resolution: "yaml-ast-parser@npm:0.0.43" + checksum: fb5df4c067b6ccbd00953a46faf6ff27f0e290d623c712dc41f330251118f110e22cfd184bbff498bd969cbcda3cd27e0f9d0adb9e6d90eb60ccafc0d8e28077 + languageName: node + linkType: hard + +"yaml-diff-patch@npm:^2.0.0": + version: 2.0.0 + resolution: "yaml-diff-patch@npm:2.0.0" + dependencies: + fast-json-patch: ^3.1.0 + oppa: ^0.4.0 + yaml: ^2.0.0-10 + bin: + yaml-diff-patch: dist/bin/yaml-patch.js + yaml-overwrite: dist/bin/yaml-patch.js + yaml-patch: dist/bin/yaml-patch.js + checksum: 5207d8523584eb6088fe32a0c6010599260ecfa5f959d120a1bad02f19143d1ddeafe10c37ccf125ac04d079072a5ead92b55c6787fd64d12f5acbb0d172e7ec + languageName: node + linkType: hard + +"yaml@npm:^1.10.0, yaml@npm:^1.10.2, yaml@npm:^1.7.2": + version: 1.10.2 + resolution: "yaml@npm:1.10.2" + checksum: ce4ada136e8a78a0b08dc10b4b900936912d15de59905b2bf415b4d33c63df1d555d23acb2a41b23cf9fb5da41c256441afca3d6509de7247daa062fd2c5ea5f + languageName: node + linkType: hard + +"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.2.2": + version: 2.6.0 + resolution: "yaml@npm:2.6.0" + bin: + yaml: bin.mjs + checksum: e5e74fd75e01bde2c09333d529af9fbb5928c5f7f01bfdefdcb2bf753d4ef489a45cab4deac01c9448f55ca27e691612b81fe3c3a59bb8cb5b0069da0f92cf0b + languageName: node + linkType: hard + +"yargs-parser@npm:^20.2.2": + version: 20.2.9 + resolution: "yargs-parser@npm:20.2.9" + checksum: 8bb69015f2b0ff9e17b2c8e6bfe224ab463dd00ca211eece72a4cd8a906224d2703fb8a326d36fdd0e68701e201b2a60ed7cf81ce0fd9b3799f9fe7745977ae3 + languageName: node + linkType: hard + +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c + languageName: node + linkType: hard + +"yargs@npm:^16.0.0, yargs@npm:^16.2.0": + version: 16.2.0 + resolution: "yargs@npm:16.2.0" + dependencies: + cliui: ^7.0.2 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 + require-directory: ^2.1.1 + string-width: ^4.2.0 + y18n: ^5.0.5 + yargs-parser: ^20.2.2 + checksum: b14afbb51e3251a204d81937c86a7e9d4bdbf9a2bcee38226c900d00f522969ab675703bee2a6f99f8e20103f608382936034e64d921b74df82b63c07c5e8f59 + languageName: node + linkType: hard + +"yargs@npm:^17.1.1, yargs@npm:^17.3.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: ^8.0.1 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 + require-directory: ^2.1.1 + string-width: ^4.2.3 + y18n: ^5.0.5 + yargs-parser: ^21.1.1 + checksum: 73b572e863aa4a8cbef323dd911d79d193b772defd5a51aab0aca2d446655216f5002c42c5306033968193bdbf892a7a4c110b0d77954a7fdf563e653967b56a + languageName: node + linkType: hard + +"yauzl@npm:^3.0.0": + version: 3.1.3 + resolution: "yauzl@npm:3.1.3" + dependencies: + buffer-crc32: ~0.2.3 + pend: ~1.2.0 + checksum: 5b782f6e99361a9c715e7a82e7aae3d983b6ddff6ebe3a66d2dd3f4ee601ec41c55fa88587bf6de0acbc013aac0b2cac84f9f0cd48372fd5329ee5e273f46f2c + languageName: node + linkType: hard + +"ylru@npm:^1.2.0": + version: 1.4.0 + resolution: "ylru@npm:1.4.0" + checksum: e0bf797476487e3d57a6e8790cbb749cff2089e2afc87e46bc84ce7605c329d578ff422c8e8c2ddf167681ddd218af0f58e099733ae1044cba9e9472ebedc01d + languageName: node + linkType: hard + +"yml-loader@npm:^2.1.0": + version: 2.1.0 + resolution: "yml-loader@npm:2.1.0" + dependencies: + js-yaml: ^3.8.3 + loader-utils: ^1.1.0 + checksum: 7afc624b3c9d3520698d275069b891a826ecb1ecf3c37e8312737067b23427f1e0d5c4b05cb08bea85d675c0a4f883831bcc82fda34f79158c0659a2d09de920 + languageName: node + linkType: hard + +"yn@npm:3.1.1": + version: 3.1.1 + resolution: "yn@npm:3.1.1" + checksum: 2c487b0e149e746ef48cda9f8bad10fc83693cd69d7f9dcd8be4214e985de33a29c9e24f3c0d6bcf2288427040a8947406ab27f7af67ee9456e6b84854f02dd6 + languageName: node + linkType: hard + +"yn@npm:^4.0.0": + version: 4.0.0 + resolution: "yn@npm:4.0.0" + checksum: 2d60113b6f43f7c29a0a97719d8da4f626b755f5bb2fd19b00d1fe732db1900ad3f1785811a86d941cbe2800f02773af00d0ed99201333eeb3618db8502f7e96 + languageName: node + linkType: hard + +"yocto-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "yocto-queue@npm:0.1.0" + checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 + languageName: node + linkType: hard + +"yup@npm:^1.3.2": + version: 1.4.0 + resolution: "yup@npm:1.4.0" + dependencies: + property-expr: ^2.0.5 + tiny-case: ^1.0.3 + toposort: ^2.0.2 + type-fest: ^2.19.0 + checksum: 20a2ee0c1e891979ca16b34805b3a3be9ab4bea6ea3d2f9005b998b4dc992d0e4d7b53e5f4d8d9423420046630fb44fdf0ecf7e83bc34dd83392bca046c5229d + languageName: node + linkType: hard + +"zen-observable@npm:^0.10.0": + version: 0.10.0 + resolution: "zen-observable@npm:0.10.0" + checksum: cee4e8902fcf4ed49f96937e9bc30b980ad3311b85e94b836c77f787163c98f19c65d2d8ac80990e8ecf4c1497d84821d58580d5ee20015f55516de146e8b7af + languageName: node + linkType: hard + +"zip-stream@npm:^6.0.1": + version: 6.0.1 + resolution: "zip-stream@npm:6.0.1" + dependencies: + archiver-utils: ^5.0.0 + compress-commons: ^6.0.2 + readable-stream: ^4.0.0 + checksum: aa5abd6a89590eadeba040afbc375f53337f12637e5e98330012a12d9886cde7a3ccc28bd91aafab50576035bbb1de39a9a316eecf2411c8b9009c9f94f0db27 + languageName: node + linkType: hard + +"zod-to-json-schema@npm:^3.20.4, zod-to-json-schema@npm:^3.21.4": + version: 3.23.3 + resolution: "zod-to-json-schema@npm:3.23.3" + peerDependencies: + zod: ^3.23.3 + checksum: 0d51cf64b54fd39e86434cd5d2239c2981808e6461d022e4c68a1dec67fff28ef2b7bb5733dfd40eb50d6ce6d252288f3989d67134fa81401c36469bb26f13ec + languageName: node + linkType: hard + +"zod-validation-error@npm:^3.0.3, zod-validation-error@npm:^3.4.0": + version: 3.4.0 + resolution: "zod-validation-error@npm:3.4.0" + peerDependencies: + zod: ^3.18.0 + checksum: b07fbfc39582dbdf6972f5f5f0c3bac9e6b5e6d2e55ef3dd891fd08f1966ebf1023a4bc270e9b569eaa48ed1684ac2252c9f260b0bd07b167671596e6e4d0fa8 + languageName: node + linkType: hard + +"zod@npm:^3.22.4": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 15949ff82118f59c893dacd9d3c766d02b6fa2e71cf474d5aa888570c469dbf5446ac5ad562bb035bf7ac9650da94f290655c194f4a6de3e766f43febd432c5c + languageName: node + linkType: hard + +"zwitch@npm:^2.0.0": + version: 2.0.4 + resolution: "zwitch@npm:2.0.4" + checksum: f22ec5fc2d5f02c423c93d35cdfa83573a3a3bd98c66b927c368ea4d0e7252a500df2a90a6b45522be536a96a73404393c958e945fdba95e6832c200791702b6 + languageName: node + linkType: hard