diff --git a/workspaces/rbac/e2e-tests/.env.sample b/workspaces/rbac/e2e-tests/.env.sample new file mode 100644 index 000000000..ad59d1823 --- /dev/null +++ b/workspaces/rbac/e2e-tests/.env.sample @@ -0,0 +1,7 @@ +# Keycloak API credentials for catalog-users e2e tests. +# Copy to .env and set values (e.g. from deployment secrets or CI). +# KEYCLOAK_BASE_URL=https://keycloak.example.com +# KEYCLOAK_REALM=your-realm +# KEYCLOAK_CLIENT_ID=admin-cli +# KEYCLOAK_CLIENT_SECRET= +# RBAC_ADMIN_PASSWORD= diff --git a/workspaces/rbac/e2e-tests/.yarnrc.yml b/workspaces/rbac/e2e-tests/.yarnrc.yml new file mode 100644 index 000000000..3186f3f07 --- /dev/null +++ b/workspaces/rbac/e2e-tests/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/workspaces/rbac/e2e-tests/eslint.config.js b/workspaces/rbac/e2e-tests/eslint.config.js new file mode 100644 index 000000000..d8d041826 --- /dev/null +++ b/workspaces/rbac/e2e-tests/eslint.config.js @@ -0,0 +1,3 @@ +import { createEslintConfig } from "@red-hat-developer-hub/e2e-test-utils/eslint"; + +export default createEslintConfig(import.meta.dirname); diff --git a/workspaces/rbac/e2e-tests/package.json b/workspaces/rbac/e2e-tests/package.json new file mode 100644 index 000000000..48cb8cd6b --- /dev/null +++ b/workspaces/rbac/e2e-tests/package.json @@ -0,0 +1,38 @@ +{ + "name": "rbac-e2e-tests", + "version": "1.0.0", + "private": true, + "type": "module", + "engines": { + "node": ">=22", + "yarn": ">=3" + }, + "packageManager": "yarn@3.8.7", + "description": "E2E tests for RBAC", + "scripts": { + "test": "playwright test", + "report": "playwright show-report", + "test:ui": "playwright test --ui", + "test:headed": "playwright test --headed", + "lint:check": "eslint .", + "lint:fix": "eslint . --fix", + "prettier:check": "prettier --check .", + "prettier:fix": "prettier --write .", + "tsc:check": "tsc --noEmit", + "check": "tsc --noEmit && yarn lint:check && yarn prettier:check" + }, + "devDependencies": { + "@backstage-community/plugin-rbac-common": "1.23.0", + "@eslint/js": "^9.39.2", + "@playwright/test": "1.57.0", + "@red-hat-developer-hub/e2e-test-utils": "1.1.15", + "@types/node": "^24.10.1", + "dotenv": "^16.4.7", + "eslint": "^9.39.2", + "eslint-plugin-check-file": "^3.3.1", + "eslint-plugin-playwright": "^2.4.0", + "prettier": "^3.7.4", + "typescript": "^5.9.3", + "typescript-eslint": "^8.50.0" + } +} diff --git a/workspaces/rbac/e2e-tests/playwright.config.ts b/workspaces/rbac/e2e-tests/playwright.config.ts new file mode 100644 index 000000000..d8c5ed66f --- /dev/null +++ b/workspaces/rbac/e2e-tests/playwright.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "@red-hat-developer-hub/e2e-test-utils/playwright-config"; +import dotenv from "dotenv"; + +dotenv.config({ path: `${import.meta.dirname}/.env` }); + +export default defineConfig({ + projects: [ + { + name: "rbac", + }, + ], +}); diff --git a/workspaces/rbac/e2e-tests/support/constants/roles.ts b/workspaces/rbac/e2e-tests/support/constants/roles.ts new file mode 100644 index 000000000..6bef12c61 --- /dev/null +++ b/workspaces/rbac/e2e-tests/support/constants/roles.ts @@ -0,0 +1,46 @@ +export type RbacRef = { + name: string; + ref: string; +}; + +/** + * All roles referenced by the RBAC e2e test suite. + * + * Note: `rbacAdmin` and `guest` are system-managed roles (sourced from app-config + * and a CSV policy file respectively) and cannot be deleted via the API — they are + * skipped during cleanup in `cleanupRoles`. + */ +export const RBAC_ROLES: Record = { + rbacOwnership: { + name: "rbac-ownership-role", + ref: "role:default/rbac-ownership-role", + }, + rbacConditional: { + name: "rbac-conditional-role", + ref: "role:default/rbac-conditional-role", + }, + conditionalResource: { + name: "rbac-conditional-resource-role", + ref: "role:default/rbac-conditional-resource-role", + }, + overviewListEdit: { + name: "rbac-list-edit-role", + ref: "role:default/rbac-list-edit-role", + }, + overviewMembers: { + name: "rbac-overview-members-role", + ref: "role:default/rbac-overview-members-role", + }, + overviewPolicies: { + name: "rbac-overview-policies-role", + ref: "role:default/rbac-overview-policies-role", + }, + rbacAdmin: { + name: "rbac_admin", + ref: "role:default/rbac_admin", + }, + guest: { + name: "guests", + ref: "role:default/guests", + }, +}; diff --git a/workspaces/rbac/e2e-tests/support/constants/users-and-groups.ts b/workspaces/rbac/e2e-tests/support/constants/users-and-groups.ts new file mode 100644 index 000000000..6adccd64b --- /dev/null +++ b/workspaces/rbac/e2e-tests/support/constants/users-and-groups.ts @@ -0,0 +1,161 @@ +export type RbacUser = { + username: string; + firstName: string; + lastName: string; + email: string; + password: string; + groups: string[]; +}; + +export type RBACGroup = { + name: string; + keycloak?: boolean; +}; + +/** + * Users created in Keycloak for RBAC e2e tests. + * Each key describes the scenario the user serves. + * + * - rbacAdmin: RBAC plugin admin; configured as admin in app-config. + * - noAccess: No permissions; verifies RBAC sidebar hidden and direct nav blocked. + * - tara: Fixture member used when constructing roles via the UI. + * - jonathon: Fixture member used when editing role membership via the UI. + * - currentUserOwner: Member of rhdh-qe-2-team. Tests $currentUser: can unregister own + * components but not group-owned ones. + * - conditionalManager: Gets conditional RBAC manage permission via rbac-ownership-role + * Used in the serial IsOwner suite. + * - allowAllowUser: catalog_reader. Both static allow AND conditional IS_ENTITY_OWNER + * allow read — tests policyDecisionPrecedence allow+allow case. + * - conditionalAllowUser: Has static deny (all_resource_denier) but conditional policy allows + * read — tests conditional overrides deny. + * - conditionalDenyUser: Has static allow but conditional deny wins — sees empty catalog. + * - conditionalDenier: all_resource_reader + conditional_denier roles — conditional deny + * overrides static allow. + * - childGroupMember: Member of rhdh-qe-child-team. Tests transitive $ownerRefs: can read + * components owned by the parent group rhdh-qe-parent-team. + * - subChildGroupMember: Member of rhdh-qe-sub-child-team. Tests deep transitive $ownerRefs: + * can read components owned by parent and grandparent groups. + * + * Passwords are generated at module-load time using `crypto.randomUUID()` trimmed + * to 21 characters with hyphens replaced by zeros. This satisfies typical minimum + * length and complexity requirements while staying fully random per test run. + * The rbacAdmin password can be overridden via the `RBAC_ADMIN_PASSWORD` env var + * so that a stable value can be used in CI where needed. + */ +export const RBAC_DESCRIPTIVE_USERS: Record = { + rbacAdmin: { + username: "rbac-admin", + firstName: "RBAC", + lastName: "Admin", + email: "rbac-admin@example.com", + password: + process.env.RBAC_ADMIN_PASSWORD ?? + crypto.randomUUID().substring(0, 21).replaceAll("-", "0"), + groups: [], + }, + noAccess: { + username: "no-access", + firstName: "No", + lastName: "Access", + email: "no-access@example.com", + password: crypto.randomUUID().substring(0, 21).replaceAll("-", "0"), + groups: [], + }, + tara: { + username: "tara", + firstName: "Tara", + lastName: "MacGovern", + email: "tara@example.com", + password: crypto.randomUUID().substring(0, 21).replaceAll("-", "0"), + groups: [], + }, + jonathon: { + username: "jonathon", + firstName: "Jonathon", + lastName: "Page", + email: "jonathon@example.com", + password: crypto.randomUUID().substring(0, 21).replaceAll("-", "0"), + groups: [], + }, + currentUserOwner: { + username: "current-user-owner", + firstName: "Current", + lastName: "User-Owner", + email: "current-user-owner@example.com", + password: crypto.randomUUID().substring(0, 21).replaceAll("-", "0"), + groups: ["rhdh-qe-2-team"], + }, + conditionalManager: { + username: "conditional-manager", + firstName: "Conditional", + lastName: "Manager", + email: "conditional-manager@example.com", + password: crypto.randomUUID().substring(0, 21).replaceAll("-", "0"), + groups: [], + }, + allowAllowUser: { + username: "allow-allow-user", + firstName: "Allow", + lastName: "Allow-User", + email: "allow-allow-user@example.com", + password: crypto.randomUUID().substring(0, 21).replaceAll("-", "0"), + groups: [], + }, + conditionalAllowUser: { + username: "conditional-allow-user", + firstName: "Conditional", + lastName: "Allow-User", + email: "conditional-allow-user@example.com", + password: crypto.randomUUID().substring(0, 21).replaceAll("-", "0"), + groups: [], + }, + conditionalDenyUser: { + username: "conditional-deny-user", + firstName: "Conditional", + lastName: "Deny-User", + email: "conditional-deny-user@example.com", + password: crypto.randomUUID().substring(0, 21).replaceAll("-", "0"), + groups: [], + }, + childGroupMember: { + username: "child-group-member", + firstName: "Child", + lastName: "Group-Member", + email: "child-group-member@example.com", + password: crypto.randomUUID().substring(0, 21).replaceAll("-", "0"), + groups: [], + }, + subChildGroupMember: { + username: "sub-child-group-member", + firstName: "Sub-Child", + lastName: "Group-Member", + email: "sub-child-group-member@example.com", + password: crypto.randomUUID().substring(0, 21).replaceAll("-", "0"), + groups: [], + }, +}; + +/** + * Groups created in Keycloak for RBAC e2e tests. + * The transitive parent/child/sub-child groups are not created via Keycloak + * (configureForRHDH does not support sub-group membership); they are managed + * separately in the catalog config. + */ +export const RBAC_GROUPS: Record = { + backstage: { name: "backstage", keycloak: true }, + currentUserOwnerTeam: { name: "rhdh-qe-2-team", keycloak: true }, + rhdhParentTeam: { name: "rhdh-qe-parent-team" }, + rhdhChildTeam: { name: "rhdh-qe-child-team" }, + rhdhSubChildTeam: { name: "rhdh-qe-sub-child-team" }, +}; + +/** + * Returns the UI display name for a user in RBAC_DESCRIPTIVE_USERS. + * Used when selecting members from the role creation/edit form dropdowns. + */ +export const displayName = (key: keyof typeof RBAC_DESCRIPTIVE_USERS): string => + `${RBAC_DESCRIPTIVE_USERS[key].firstName} ${RBAC_DESCRIPTIVE_USERS[key].lastName}`; + +/** Returns the Backstage entity reference string for a given user, e.g. `user:default/rbac-admin`. */ +export const userEntityRef = (user: RbacUser): string => + `user:default/${user.username}`; diff --git a/workspaces/rbac/e2e-tests/support/pages/rbac-obj.ts b/workspaces/rbac/e2e-tests/support/pages/rbac-obj.ts new file mode 100644 index 000000000..01355baad --- /dev/null +++ b/workspaces/rbac/e2e-tests/support/pages/rbac-obj.ts @@ -0,0 +1,127 @@ +import { + type Locator, + type Page, +} from "@red-hat-developer-hub/e2e-test-utils/test"; + +/** + * ROLES_PAGE_COMPONENTS - Roles list page: edit and delete buttons + */ +export const ROLES_PAGE_COMPONENTS = { + getEditRoleButton: (page: Page, name: string): Locator => + page.getByTestId(`edit-role-${name}`), + + getDeleteRoleButton: (page: Page, name: string): Locator => + page.getByTestId(`delete-role-${name}`), + + getFilterInput: (page: Page): Locator => page.getByPlaceholder("Filter"), +}; + +/** + * DELETE_ROLE_COMPONENTS - Delete role confirmation dialog + */ +export const DELETE_ROLE_COMPONENTS = { + getRoleNameInput: (page: Page): Locator => + page.locator('input[name="delete-role"]'), +}; + +/** + * ROLE_OVERVIEW_COMPONENTS - Role overview page action buttons + */ +export const ROLE_OVERVIEW_COMPONENTS = { + getUpdatePoliciesButton: (page: Page): Locator => + page.getByTestId("update-policies"), + + getUpdateMembersButton: (page: Page): Locator => + page.getByTestId("update-members"), +}; + +/** + * ROLE_FORM_COMPONENTS - Role creation / edit form fields + */ +export const ROLE_FORM_COMPONENTS = { + getRoleNameInput: (page: Page): Locator => page.locator('input[name="name"]'), + + getRoleOwnerInput: (page: Page): Locator => + page.locator('textarea[name="owner"]'), + + getUsersAndGroupsField: (page: Page): Locator => + page.locator('input[name="add-users-and-groups"]'), + + getMemberOption: (page: Page, label: string): Locator => + page.locator(`span[data-testid="${label}"]`), + + getDropdownToggle: (page: Page): Locator => + page.getByTestId("ArrowDropDownIcon"), + + getSelectPluginsCombobox: (page: Page): Locator => + page.getByRole("combobox", { name: "Select plugins" }), + + getExpandCatalogRow: (page: Page): Locator => + page.getByTestId("expand-row-catalog"), + + getNextButton: (page: Page): Locator => + page.getByTestId("nextButton-0").first(), + + getUserAndGroupNextButton: (page: Page): Locator => + page.getByTestId("nextButton-1").first(), + + getPermissionPolicyNextButton: (page: Page): Locator => + page.getByTestId("nextButton-2").first(), + + getPermissionsSelectPlaceholder: (page: Page): Locator => + page.getByText("Select..."), +}; + +/** + * CONDITIONAL_RULE_COMPONENTS - Conditions sidebar and rule builder + */ +export const CONDITIONAL_RULE_COMPONENTS = { + getRulesSidebar: (page: Page): Locator => page.getByTestId("rules-sidebar"), + + getSaveConditionsButton: (page: Page): Locator => + page.getByTestId("save-conditions"), + + getAnyOfButton: (page: Page): Locator => + page.getByRole("button", { name: "AnyOf" }), + + getNotButton: (page: Page): Locator => + page.getByRole("button", { name: "Not" }), + + getAddRuleButton: (page: Page): Locator => + page.getByRole("button", { name: "Add rule" }), + + getAddNestedConditionButton: (page: Page): Locator => + page.getByRole("button", { name: "Add Nested Condition" }), + + getHasSpecButton: (page: Page): Locator => page.getByText("HAS_SPEC"), + + getHasAnnotationButton: (page: Page): Locator => + page.getByText("HAS_ANNOTATION"), + + getHasLabelButton: (page: Page): Locator => page.getByText("HAS_LABEL"), + + getIsEntityKindButton: (page: Page): Locator => + page.getByText("IS_ENTITY_KIND"), + + getIsOwnerButton: (page: Page): Locator => page.getByText("IS_OWNER"), + + getKeyInput: (page: Page): Locator => page.getByLabel("key *"), + + getAnnotationInput: (page: Page): Locator => page.getByLabel("annotation *"), + + getLabelInput: (page: Page): Locator => page.getByLabel("label *"), + + getRuleBadge: (page: Page, count: string): Locator => + page.locator('span[class*="MuiBadge-badge"]').filter({ hasText: count }), +}; + +/** + * SEARCH_COMPONENTS - Search inputs on the roles list page + */ +export const SEARCH_COMPONENTS = { + getAriaLabelSearchInput: (page: Page): Locator => + page.locator('input[aria-label="Search"]'), + + getPlaceholderSearchInput: (page: Page): Locator => + page.locator('input[placeholder="Search"]'), +}; diff --git a/workspaces/rbac/e2e-tests/support/pages/rbac-po.ts b/workspaces/rbac/e2e-tests/support/pages/rbac-po.ts new file mode 100644 index 000000000..9f73f3eaa --- /dev/null +++ b/workspaces/rbac/e2e-tests/support/pages/rbac-po.ts @@ -0,0 +1,551 @@ +import { expect, Page } from "@red-hat-developer-hub/e2e-test-utils/test"; +import { type RoleBasedPolicy } from "@backstage-community/plugin-rbac-common"; +import { + CONDITIONAL_RULE_COMPONENTS, + DELETE_ROLE_COMPONENTS, + ROLE_FORM_COMPONENTS, + ROLE_OVERVIEW_COMPONENTS, + ROLES_PAGE_COMPONENTS, + SEARCH_COMPONENTS, +} from "./rbac-obj"; +import { UIhelper } from "@red-hat-developer-hub/e2e-test-utils/helpers"; +type PermissionPolicyType = "anyOf" | "not"; + +export class RbacPO { + private readonly page: Page; + private readonly uiHelper: UIhelper; + + constructor(page: Page, uiHelper: UIhelper) { + this.page = page; + this.uiHelper = uiHelper; + } + + public async go(): Promise { + await this.page.goto("/rbac"); + } + + public async navigateToRBACPage(timeout?: number) { + await this.go(); + await this.uiHelper.waitForLoad(); + await this.uiHelper.verifyHeading("RBAC", timeout); + } + + /** + * Builds a regex string that matches the UI's "X users, Y groups" / "Y groups, X users" + * summary text in either order (the backend can return them in either sequence). + * Zero counts are omitted, e.g. 0 groups + 2 users → matches "2 users". + * The result is wrapped in a non-capturing group so it can be composed into a + * larger pattern by `regexpLongUsersAndGroups`. + */ + private readonly stringForRegexUsersAndGroups = ( + numUsers: number, + numGroups: number, + ): string => { + const userPluralized = numUsers === 1 ? "user" : "users"; + const usersText = numUsers === 0 ? "" : `${numUsers} ${userPluralized}`; + + const groupsPluralized = numGroups === 1 ? "group" : "groups"; + const groupsText = + numGroups === 0 ? "" : `${numGroups} ${groupsPluralized}`; + + return `(${groupsText}${numGroups === 0 ? "" : ", "}${usersText}|${usersText}${numUsers === 0 ? "" : ", "}${groupsText})`; + }; + + public regexpShortUsersAndGroups = ( + numUsers: number, + numGroups: number, + ): RegExp => { + return new RegExp(this.stringForRegexUsersAndGroups(numUsers, numGroups)); + }; + + private readonly regexpLongUsersAndGroups = ( + numUsers: number, + numGroups: number, + ): RegExp => { + return new RegExp( + String.raw`Users and groups \(${this.stringForRegexUsersAndGroups(numUsers, numGroups)}\)`, + ); + }; + + public async verifyGeneralRbacViewHeading() { + await this.uiHelper.verifyHeading(/All roles \(\d+\)/); + } + + private async verifyRoleHeading(role: string) { + await this.uiHelper.verifyHeading(role); + } + + private async verifyRoleIsListed(role: string) { + await this.uiHelper.verifyLink(role); + } + + private async clickOnRoleLink(role: string) { + await this.uiHelper.clickLink(role); + } + + private async switchToOverView() { + await this.uiHelper.clickTab("Overview"); + } + + public async verifyRoleAndSwitchToOverview( + role: string, + description: string, + headings: (string | RegExp)[], + ) { + await this.verifyRoleIsListed(role); + await this.clickOnRoleLink(role); + await this.verifyRoleHeading(role); + await this.switchToOverView(); + await this.uiHelper.verifyText("About"); + + await this.uiHelper.verifyText(description); + + for (const heading of headings) { + await this.uiHelper.verifyHeading(heading); + } + } + + private async verifyPermissionPoliciesHeader(policies: number) { + await this.uiHelper.verifyText(`Permission policies (${policies})`); + } + + private async next() { + await this.uiHelper.clickButton("Next"); + } + + private async create() { + await this.uiHelper.clickButton("Create"); + } + + public async openPluginsDropdown() { + await ROLE_FORM_COMPONENTS.getSelectPluginsCombobox(this.page).click(); + } + + public async selectOption( + option: + | "catalog" + | "kubernetes" + | "catalog.entity.read" + | "scaffolder" + | "scaffolder-template.read" + | "permission", + ) { + const optionSelector = `li[role="option"]:has-text("${option}")`; + await this.page.waitForSelector(optionSelector); + await this.page.click(optionSelector); + } + + private async clickOpenSidebar() { + await CONDITIONAL_RULE_COMPONENTS.getRulesSidebar(this.page) + .getByLabel("Open") + .click(); + } + + public async selectPermissionCheckbox(name: string) { + await this.page + .getByRole("cell", { name: name }) + .getByRole("checkbox") + .click(); + } + + private async pluginRuleCount(number: string) { + await expect( + CONDITIONAL_RULE_COMPONENTS.getRuleBadge(this.page, number), + ).toBeVisible(); + } + + private async searchAndVerifyRoleHeading( + role: string, + header: string = "All roles (1)", + ) { + const searchInput = SEARCH_COMPONENTS.getAriaLabelSearchInput(this.page); + await searchInput.waitFor(); + await searchInput.fill(role); + await this.uiHelper.verifyHeading(header); + } + + public async filterRolesList(roleName: string): Promise { + await ROLES_PAGE_COMPONENTS.getFilterInput(this.page).fill(roleName); + } + + public async verifyRoleOverviewTables( + allGridColumnsText: RegExp[] | string[], + allCellsIdentifier: RegExp[] | string[], + ): Promise { + await this.uiHelper.verifyColumnHeading(allGridColumnsText); + await this.uiHelper.verifyCellsInTable(allCellsIdentifier); + } + + public async navigateToCatalogComponent( + componentName: string, + ): Promise { + await this.uiHelper.goToPageUrl("/catalog"); + await this.uiHelper.selectMuiBox("Kind", "Component"); + await this.uiHelper.searchInputPlaceholder(componentName); + await expect( + this.page.getByRole("link", { name: componentName, exact: true }), + ).toBeVisible(); + await this.page + .getByRole("link", { name: componentName, exact: true }) + .click(); + } + + public async verifyComponentOwner(ownerPattern: string): Promise { + await expect( + this.page.getByRole("article").getByRole("link", { + name: new RegExp(ownerPattern), + }), + ).toBeVisible(); + } + + private async openPermissionsDropdown(): Promise { + await ROLE_FORM_COMPONENTS.getPermissionsSelectPlaceholder( + this.page, + ).click(); + } + + private async createRoleUsers( + name: string, + users: string[], + groups: string[], + owner?: string, + ) { + if (!this.page.url().includes("rbac")) await this.go(); + await this.create(); + await this.uiHelper.verifyHeading("Create role"); + await ROLE_FORM_COMPONENTS.getRoleNameInput(this.page).fill(name); + if (owner) { + await ROLE_FORM_COMPONENTS.getRoleOwnerInput(this.page).fill(owner); + } + await this.uiHelper.clickButton("Next"); + await ROLE_FORM_COMPONENTS.getUsersAndGroupsField(this.page).click(); + + for (const userOrGroup of users.concat(groups)) { + await ROLE_FORM_COMPONENTS.getMemberOption( + this.page, + userOrGroup, + ).click(); + } + + // Close dropdown after selecting users and groups + await ROLE_FORM_COMPONENTS.getDropdownToggle(this.page).click(); + + // Dynamically verify the heading based on users and groups added + await this.uiHelper.verifyHeading( + this.regexpShortUsersAndGroups(users.length, groups.length), + ); + + await this.next(); + } + + async createRole( + name: string, + users: string[], + groups: string[], + policies: RoleBasedPolicy[], + pluginId: "catalog" | "kubernetes" | "scaffolder" = "catalog", + owner?: string, + ) { + await this.createRoleUsers(name, users, groups, owner); + + await this.openPluginsDropdown(); + await this.selectOption(pluginId); + await this.openPermissionsDropdown(); + + for (const policy of policies) { + if (!policy.permission) continue; + await this.selectPermissionCheckbox(policy.permission); + } + + await this.next(); + await this.uiHelper.verifyHeading("Review and create"); + await this.uiHelper.verifyText( + this.regexpLongUsersAndGroups(users.length, groups.length), + ); + await this.verifyPermissionPoliciesHeader(policies.length); + await this.create(); + + // Check for error alert first + const errorAlert = this.page + .getByRole("alert") + .filter({ hasText: /error/i }); + const errorCount = await errorAlert.count(); + + if (errorCount > 0) { + const errorMessage = await errorAlert.textContent(); + throw new Error( + `Failed to create role: ${errorMessage}. This may indicate insufficient permissions.`, + ); + } + + // Wait for success message before proceeding to roles list + await this.uiHelper.verifyText( + `Role role:default/${name} created successfully`, + ); + + // Now we should be on the roles list page + await this.searchAndVerifyRoleHeading(name); + } + + async createConditionalRole( + name: string, + users: string[], + groups: string[], + permissionPolicyType: PermissionPolicyType, + pluginId: "catalog" | "kubernetes" | "scaffolder" = "catalog", + owner?: string, + ) { + await this.createRoleUsers(name, users, groups, owner); + + await this.openPluginsDropdown(); + await this.selectOption(pluginId); + await this.openPermissionsDropdown(); + + if (permissionPolicyType === "anyOf") { + // Conditional Scenario 1: Permission policies using AnyOf + await this.selectPermissionCheckbox("catalog.entity.read"); + await this.page + .getByRole("row", { name: "catalog.entity.read" }) + .getByLabel("remove") + .click(); + await CONDITIONAL_RULE_COMPONENTS.getAnyOfButton(this.page).click(); + await this.clickOpenSidebar(); + await CONDITIONAL_RULE_COMPONENTS.getIsEntityKindButton( + this.page, + ).click(); + await this.page.getByPlaceholder("string, string").click(); + await this.page + .getByPlaceholder("string, string") + .fill("component,template,user,group"); + await CONDITIONAL_RULE_COMPONENTS.getAddRuleButton(this.page).click(); + await this.page.getByLabel("Open").nth(2).click(); + await CONDITIONAL_RULE_COMPONENTS.getHasSpecButton(this.page).click(); + const keyInput = CONDITIONAL_RULE_COMPONENTS.getKeyInput(this.page); + await keyInput.click(); + await keyInput.fill("lifecycle"); + await keyInput.press("Tab"); + await keyInput.fill("experimental"); + await CONDITIONAL_RULE_COMPONENTS.getAddRuleButton(this.page).click(); + await this.page.getByLabel("Open").nth(3).click(); + await CONDITIONAL_RULE_COMPONENTS.getHasLabelButton(this.page).click(); + const labelInput = CONDITIONAL_RULE_COMPONENTS.getLabelInput(this.page); + await labelInput.click(); + await labelInput.fill("partner"); + // Add nested condition + await CONDITIONAL_RULE_COMPONENTS.getAddNestedConditionButton( + this.page, + ).click(); + await this.page.getByLabel("Open").nth(4).click(); + await CONDITIONAL_RULE_COMPONENTS.getHasAnnotationButton( + this.page, + ).click(); + const annotationInput = CONDITIONAL_RULE_COMPONENTS.getAnnotationInput( + this.page, + ); + await annotationInput.click(); + await annotationInput.fill("test"); + await CONDITIONAL_RULE_COMPONENTS.getSaveConditionsButton( + this.page, + ).click(); + await this.pluginRuleCount("4"); + await this.next(); + await this.uiHelper.verifyHeading("Review and create"); + await this.uiHelper.verifyText( + this.regexpLongUsersAndGroups(users.length, groups.length), + ); + await this.verifyPermissionPoliciesHeader(1); + await this.uiHelper.verifyText("4 rules"); + await this.uiHelper.clickButton("Create"); + await this.uiHelper.verifyText( + `Role role:default/${name} created successfully`, + ); + } else if (permissionPolicyType === "not") { + // Conditional Scenario 2: Permission policies using Not + await this.selectPermissionCheckbox("catalog.entity.read"); + await this.page + .getByRole("row", { name: "catalog.entity.read" }) + .getByLabel("remove") + .click(); + await CONDITIONAL_RULE_COMPONENTS.getNotButton(this.page).click(); + await this.clickOpenSidebar(); + await CONDITIONAL_RULE_COMPONENTS.getHasSpecButton(this.page).click(); + const keyInput = CONDITIONAL_RULE_COMPONENTS.getKeyInput(this.page); + await keyInput.click(); + await keyInput.fill("lifecycle"); + await keyInput.press("Tab"); + await keyInput.fill("experimental"); + await CONDITIONAL_RULE_COMPONENTS.getSaveConditionsButton( + this.page, + ).click(); + await this.pluginRuleCount("1"); + await this.next(); + await this.uiHelper.verifyHeading("Review and create"); + await this.verifyPermissionPoliciesHeader(1); + await this.uiHelper.verifyText("1 rule"); + await this.uiHelper.clickButton("Create"); + await this.uiHelper.verifyText(`role:default/${name}`); + + await this.searchAndVerifyRoleHeading(name); + } + } + + async deleteRole( + name: string, + header: string = "All roles (0)", + skipVerify?: boolean, + ) { + // Ensure we always navigate back to the RBAC page + await this.go(); + + await this.uiHelper.searchInputAriaLabel(name); + const button = ROLES_PAGE_COMPONENTS.getDeleteRoleButton(this.page, name); + await button.waitFor({ state: "visible" }); + await button.click(); + await this.uiHelper.verifyHeading("Delete this role?"); + const roleNameInput = DELETE_ROLE_COMPONENTS.getRoleNameInput(this.page); + await roleNameInput.click(); + await roleNameInput.fill(name); + await this.uiHelper.clickButton("Delete"); + + if (!skipVerify) { + await this.uiHelper.verifyText(`Role ${name} deleted successfully`); + + await this.searchAndVerifyRoleHeading(name, header); + } + } + + /** + * Adds an `IS_ENTITY_OWNER` conditional policy for each of the three + * policy-management permissions. These are the permissions that allow a + * non-admin user to manage RBAC — the IsOwner condition scopes them to + * entities the user already owns so they cannot escalate their own access. + */ + private async createRBACConditions(owner: string) { + const permissions = [ + "policy.entity.read", + "policy.entity.update", + "policy.entity.delete", + ]; + for (const permission of permissions) { + await this.selectPermissionCheckbox(permission); + await this.page + .getByRole("row", { name: permission }) + .getByLabel("remove") + .click(); + await this.clickOpenSidebar(); + await CONDITIONAL_RULE_COMPONENTS.getIsOwnerButton(this.page).click(); + await this.page.getByPlaceholder("string, string").click(); + await this.page.getByPlaceholder("string, string").fill(owner); + await CONDITIONAL_RULE_COMPONENTS.getSaveConditionsButton( + this.page, + ).click(); + } + } + + async createRBACConditionRole(name: string, users: string[], owner: string) { + if (!this.page.url().includes("rbac")) await this.go(); + await this.createRoleUsers(name, users, [], owner); + + await this.openPluginsDropdown(); + await this.selectOption("catalog"); + await this.openPermissionsDropdown(); + + await this.selectPermissionCheckbox("catalog.entity.read"); + await ROLE_FORM_COMPONENTS.getExpandCatalogRow(this.page).click(); + + await this.openPluginsDropdown(); + await this.selectOption("permission"); + await this.openPermissionsDropdown(); + + await this.selectPermissionCheckbox("policy.entity.create"); + + await this.createRBACConditions(owner); + + await this.next(); + await this.uiHelper.verifyHeading("Review and create"); + await this.verifyPermissionPoliciesHeader(5); + await this.create(); + + await this.searchAndVerifyRoleHeading(name); + } + + public async editRoleMembers( + role: string, + user: string, + numUsers: number, + numGroups: number, + ) { + await this.uiHelper.verifyHeading("Edit Role"); + // When navigating from the roles list inline-edit button the form opens + // directly at the users/groups step, but when invoked from the overview + // page an initial "Next" button is shown on a preceding step — handle both + const isNextButtonVisible = await ROLE_FORM_COMPONENTS.getNextButton( + this.page, + ).isVisible(); + + if (isNextButtonVisible) + await ROLE_FORM_COMPONENTS.getNextButton(this.page).click(); + + // Wait for users and groups step to be ready + await expect(this.page.getByLabel("Select users and groups")).toBeVisible(); + + await ROLE_FORM_COMPONENTS.getUsersAndGroupsField(this.page).click(); + await ROLE_FORM_COMPONENTS.getMemberOption(this.page, user).click(); + // Close dropdown after selecting users and groups + await ROLE_FORM_COMPONENTS.getDropdownToggle(this.page).click(); + + await this.uiHelper.verifyHeading( + this.regexpShortUsersAndGroups(numUsers, numGroups), + ); + + await ROLE_FORM_COMPONENTS.getUserAndGroupNextButton(this.page).click(); + + // Wait for permissions step to be ready (use .first() to handle multiple Next buttons) + await this.page.getByText(/\d plugins/).waitFor({ state: "visible" }); + const nextPermissionPolicyButton = + ROLE_FORM_COMPONENTS.getPermissionPolicyNextButton(this.page); + await expect(nextPermissionPolicyButton).toBeVisible(); + await expect(nextPermissionPolicyButton).toBeEnabled(); + await nextPermissionPolicyButton.click(); + + // The "users are not granted access" banner is shown while the review step + // is still loading; wait for it to disappear before the Save button becomes + // clickable + await this.page + .getByText("users are not granted access") + .waitFor({ state: "hidden" }); + const saveButton = this.page.getByRole("button", { name: "Save" }); + await expect(saveButton).toBeVisible(); + await expect(saveButton).toBeEnabled(); + await saveButton.click(); + await this.uiHelper.verifyText(`Role ${role} updated successfully`); + } + + public async editRolePermissions() { + await ROLE_OVERVIEW_COMPONENTS.getUpdatePoliciesButton(this.page).click(); + await this.uiHelper.verifyHeading("Edit Role"); + await this.openPluginsDropdown(); + await this.selectOption("scaffolder"); + + // Close the plugins dropdown to access the permissions table + await this.page.getByRole("button", { name: "Close" }).click(); + + // Expand the Scaffolder row to access its permissions + await this.page + .getByRole("row", { name: /Scaffolder/i }) + .getByRole("button", { name: "expand row" }) + .click(); + + await this.selectPermissionCheckbox("scaffolder.template.parameter"); + await ROLE_FORM_COMPONENTS.getPermissionPolicyNextButton(this.page).click(); + + // The "users are not granted access" banner is shown while the review step + // is still loading; wait for it to disappear before the Save button becomes + // clickable + await this.page + .getByText("users are not granted access") + .waitFor({ state: "hidden" }); + await expect(this.page.getByRole("button", { name: "Save" })).toBeVisible(); + await this.uiHelper.clickButton("Save"); + } +} diff --git a/workspaces/rbac/e2e-tests/support/pages/rbac.ts b/workspaces/rbac/e2e-tests/support/pages/rbac.ts new file mode 100644 index 000000000..4b14d31f3 --- /dev/null +++ b/workspaces/rbac/e2e-tests/support/pages/rbac.ts @@ -0,0 +1,41 @@ +export class RolesPage { + static getRolesListCellsIdentifier() { + 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 permissionPolicies = /\d/; + return [roleName, usersAndGroups, permissionPolicies]; + } + + static getUsersAndGroupsListCellsIdentifier() { + const name = new RegExp(/^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/); + const type = new RegExp(/^(User|Group)$/); + const members = /^(-|\d+)$/; + return [name, type, members]; + } + + static getPermissionPoliciesListCellsIdentifier() { + const policies = + /^(?:(Read|Create|Update|Delete)(?:, (?:Read|Create|Update|Delete))*|Use)$/; + return [policies]; + } + + //Depending on the version of the Backstage, it can be 'Permission Policies' or 'Accessible Plugins' + // Accepts either term + static getRolesListColumnsText() { + return [ + /^Name$/, + /^Users and groups$/, + /Permission Policies|Accessible plugins/, + ]; + } + + static getUsersAndGroupsListColumnsText() { + return ["Name", "Type", "Members"]; + } + + static getPermissionPoliciesListColumnsText() { + return ["Plugin", "Permission", "Policies"]; + } +} diff --git a/workspaces/rbac/e2e-tests/support/utils/cleanup.ts b/workspaces/rbac/e2e-tests/support/utils/cleanup.ts new file mode 100644 index 000000000..828df174d --- /dev/null +++ b/workspaces/rbac/e2e-tests/support/utils/cleanup.ts @@ -0,0 +1,83 @@ +import { APIResponse } from "@playwright/test"; +import { RbacRef } from "../constants/roles"; +import { + PermissionAction, + RoleConditionalPolicyDecision, +} from "@backstage-community/plugin-rbac-common"; +import { + RbacApiHelper, + Policy, + Response, +} from "@red-hat-developer-hub/e2e-test-utils/helpers"; + +// Roles that cannot be deleted and will throw a 403 — skip to avoid noise +const SKIPPABLE_ROLES: Set = new Set(["rbac_admin", "guests"]); + +async function deletePoliciesForRole( + rbacApi: RbacApiHelper, + roleName: string, + policiesResponse: APIResponse, +): Promise { + if (!policiesResponse.ok()) return; + const policies = await Response.removeMetadataFromResponse(policiesResponse); + if (Array.isArray(policies) && policies.length > 0) { + await rbacApi.deletePolicy(roleName, policies as Policy[]); + } +} + +async function deleteConditionsForRole( + rbacApi: RbacApiHelper, + conditionResponse: APIResponse, + remainingConditions: RoleConditionalPolicyDecision[], +): Promise { + if (!conditionResponse.ok()) return; + for (const condition of remainingConditions) { + await rbacApi.deleteCondition(condition.id); + } +} + +async function cleanupRole( + rbacApi: RbacApiHelper, + role: RbacRef, +): Promise { + const policiesResponse = await rbacApi.getPoliciesByRole(role.name); + const conditionResponse = await rbacApi.getConditions(); + const remainingConditions = await rbacApi.getConditionsByRole( + role.ref, + await conditionResponse.json(), + ); + + if (policiesResponse.status() === 404 && remainingConditions.length === 0) { + return; + } + + await deletePoliciesForRole(rbacApi, role.name, policiesResponse); + await deleteConditionsForRole( + rbacApi, + conditionResponse, + remainingConditions, + ); + + const deleteRoleResponse = await rbacApi.deleteRole(role.name); + if (!deleteRoleResponse.ok() && deleteRoleResponse.status() !== 404) { + console.error( + `Unexpected error deleting role ${role.name}: ${deleteRoleResponse.status()}`, + ); + } +} + +export async function cleanupRoles( + roles: Record, + apiToken: string, +): Promise { + const rbacApi = await RbacApiHelper.build(apiToken); + + for (const role of Object.values(roles)) { + if (SKIPPABLE_ROLES.has(role.name)) continue; + try { + await cleanupRole(rbacApi, role); + } catch (error) { + console.error(`Error during cleanup for role ${role.name}:`, error); + } + } +} diff --git a/workspaces/rbac/e2e-tests/support/utils/create-users.ts b/workspaces/rbac/e2e-tests/support/utils/create-users.ts new file mode 100644 index 000000000..4dd8cafc4 --- /dev/null +++ b/workspaces/rbac/e2e-tests/support/utils/create-users.ts @@ -0,0 +1,26 @@ +import { KeycloakHelper } from "@red-hat-developer-hub/e2e-test-utils/keycloak"; +import { + RBAC_DESCRIPTIVE_USERS, + RBAC_GROUPS, +} from "../constants/users-and-groups"; + +export async function createUsersAndGroups(): Promise { + const keycloak = new KeycloakHelper(); + + await keycloak.deploy(); + + // Check if users already exist due to a test failure/restart + const realm = process.env.KEYCLOAK_REALM ?? ""; + if (await keycloak.getUsers(realm)) { + // Randomly generated passwords will be recreated everytime the tests are restarted + // We need to clean up the old users so that the new passwords can take affect + for (const user of Object.values(RBAC_DESCRIPTIVE_USERS)) { + await keycloak.deleteUser(realm, user.username); + } + } + + await keycloak.configureForRHDH({ + groups: Object.values(RBAC_GROUPS).filter((g) => g.keycloak), + users: Object.values(RBAC_DESCRIPTIVE_USERS), + }); +} diff --git a/workspaces/rbac/e2e-tests/support/utils/helper.ts b/workspaces/rbac/e2e-tests/support/utils/helper.ts new file mode 100644 index 000000000..7879a6025 --- /dev/null +++ b/workspaces/rbac/e2e-tests/support/utils/helper.ts @@ -0,0 +1,24 @@ +import { + type Locator, + type Page, +} from "@red-hat-developer-hub/e2e-test-utils/test"; +import fs from "node:fs"; + +export async function downloadAndReadFile( + page: Page, + locator: Locator, +): Promise { + const [download] = await Promise.all([ + page.waitForEvent("download"), + locator.click(), + ]); + + const filePath = await download.path(); + + if (filePath) { + return fs.readFileSync(filePath, "utf-8"); + } else { + console.error("Download failed or path is not available"); + return undefined; + } +} diff --git a/workspaces/rbac/e2e-tests/support/utils/login.ts b/workspaces/rbac/e2e-tests/support/utils/login.ts new file mode 100644 index 000000000..c6c0a8030 --- /dev/null +++ b/workspaces/rbac/e2e-tests/support/utils/login.ts @@ -0,0 +1,8 @@ +import { LoginHelper } from "@red-hat-developer-hub/e2e-test-utils/helpers"; +import { RbacUser } from "../constants/users-and-groups"; + +export const loginAs = ( + loginHelper: LoginHelper, + user: RbacUser, +): Promise => + loginHelper.loginAsKeycloakUser(user.username, user.password); diff --git a/workspaces/rbac/e2e-tests/tests/config/app-config-rhdh.yaml b/workspaces/rbac/e2e-tests/tests/config/app-config-rhdh.yaml new file mode 100644 index 000000000..795b3f1e4 --- /dev/null +++ b/workspaces/rbac/e2e-tests/tests/config/app-config-rhdh.yaml @@ -0,0 +1,25 @@ +catalog: + processingInterval: { hours: 24 } + import: + entityFilename: catalog-info.yaml + rules: + - allow: [Component, Group] + locations: + - type: url + target: https://github.com/PatAKnight/rhdh-plugin-export-overlays/blob/rbac-test-migration/workspaces/rbac/e2e-tests/tests/config/catalog-entities.yaml +permission: + enabled: true + rbac: + maxDepth: 1 + policyFileReload: true + policies-csv-file: "./rbac/rbac-policy.csv" + conditionalPoliciesFile: "./rbac/conditional-policy.yaml" + pluginsWithPermission: + - catalog + - permission + - scaffolder + admin: + users: + - name: user:default/rbac-admin + policyDecisionPrecedence: conditional # default behavior +includeTransitiveGroupOwnership: true diff --git a/workspaces/rbac/e2e-tests/tests/config/catalog-entities.yaml b/workspaces/rbac/e2e-tests/tests/config/catalog-entities.yaml new file mode 100644 index 000000000..cc9fb1b2f --- /dev/null +++ b/workspaces/rbac/e2e-tests/tests/config/catalog-entities.yaml @@ -0,0 +1,100 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: mock-sub-child-site + description: A mock site for testing RBAC permissions with includeTransitiveGroupOwnership config. +spec: + type: website + lifecycle: experimental + owner: group:default/rhdh-qe-sub-child-team +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: mock-site + description: A mock site for testing RBAC permissions with includeTransitiveGroupOwnership config. +spec: + type: website + lifecycle: experimental + owner: group:default/rhdh-qe-parent-team +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: mock-child-site + description: A mock site for testing RBAC permissions with includeTransitiveGroupOwnership config. +spec: + type: website + lifecycle: experimental + owner: group:default/rhdh-qe-child-team +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test-rhdh-qe-2 + description: A mock component for testing RBAC permissions with $currentUser alias. +spec: + type: website + lifecycle: experimental + owner: user:default/current-user-owner +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test-rhdh-qe-2-team-owned + description: A mock component for testing RBAC permissions with $currentUser alias. +spec: + type: website + lifecycle: experimental + owner: group:janus-qe/rhdh-qe-2-team +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: mock-component + description: A mock component for testing RBAC policy precedence. +spec: + type: website + lifecycle: experimental + owner: user:default/conditional-allow-user +--- +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: rhdh-qe-parent-team + title: RHDH QE Parent Team +spec: + type: team + children: [rhdh-qe-child-team] +--- +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: rhdh-qe-child-team + title: RHDH QE Child Team +spec: + type: team + children: [rhdh-qe-sub-child-team] + members: [child-group-member] +--- +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: rhdh-qe-sub-child-team + title: RHDH QE Sub Child Team +spec: + type: team + children: [] + members: [sub-child-group-member] +--- +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: rhdh-qe-2-team + namespace: janus-qe + title: Team utilized for $currentUser alias testing +spec: + type: team + children: [] + members: [] +--- diff --git a/workspaces/rbac/e2e-tests/tests/config/rbac-configmap.yaml b/workspaces/rbac/e2e-tests/tests/config/rbac-configmap.yaml new file mode 100644 index 000000000..bd0d25a08 --- /dev/null +++ b/workspaces/rbac/e2e-tests/tests/config/rbac-configmap.yaml @@ -0,0 +1,108 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: rbac-policy + labels: + backstage.io/kubernetes-id: developer-hub +data: + rbac-policy.csv: | + p, role:default/guests, catalog.entity.create, create, allow + g, user:default/no-access, role:default/guests + p, role:default/team_a, catalog-entity, read, allow + g, user:default/user_team_a, role:default/team_a + g, user:xyz/user, role:xyz/team_a + g, group:default/rhdh-qe-2-team, role:default/test2-role + + p, role:xyz/team_a, catalog-entity, read, allow + p, role:xyz/team_a, catalog.entity.create, create, allow + p, role:xyz/team_a, catalog.location.create, create, allow + p, role:xyz/team_a, catalog.location.read, read, allow + + g, user:default/config-admin, role:default/team_a + g, user:default/config-admin, role:default/qe_config_admin + p, role:default/qe_config_admin, catalog.entity.create, create, allow + p, role:default/qe_config_admin, catalog.location.create, create, allow + p, role:default/qe_config_admin, catalog.location.read, read, allow + + g, group:default/rhdh-qe-parent-team, role:default/transitive-owner + g, group:default/rhdh-qe-child-team, role:default/transitive-owner + + p, role:default/all_resource_reader, catalog-entity, read, allow + p, role:default/all_resource_reader, catalog-entity, create, allow + g, user:default/conditional-allow-user, role:default/all_resource_reader + + p, role:default/all_resource_denier, catalog-entity, read, deny + p, role:default/all_resource_denier, catalog-entity, create, allow + g, user:default/conditional-deny-user, role:default/all_resource_denier + + g, user:default/conditional-allow-user, role:default/owned_resource_reader + g, user:default/conditional-deny-user, role:default/owned_resource_reader + + g, user:default/conditional-denier, role:default/all_resource_reader + g, user:default/conditional-denier, role:default/conditional_denier + conditional-policy.yaml: | + result: CONDITIONAL + roleEntityRef: 'role:default/test2-role' + pluginId: catalog + resourceType: catalog-entity + permissionMapping: + - read + - update + conditions: + rule: IS_ENTITY_OWNER + resourceType: catalog-entity + params: + claims: + - 'group:janus-qe/rhdh-qe-2-team' + - $currentUser + --- + result: CONDITIONAL + roleEntityRef: 'role:default/test2-role' + pluginId: catalog + resourceType: catalog-entity + permissionMapping: + - delete + conditions: + rule: IS_ENTITY_OWNER + resourceType: catalog-entity + params: + claims: + - $currentUser + --- + result: CONDITIONAL + roleEntityRef: 'role:default/transitive-owner' + pluginId: catalog + resourceType: catalog-entity + permissionMapping: + - read + conditions: + rule: IS_ENTITY_OWNER + resourceType: catalog-entity + params: + claims: + - $ownerRefs + --- + result: CONDITIONAL + roleEntityRef: 'role:default/owned_resource_reader' + pluginId: catalog + resourceType: catalog-entity + permissionMapping: + - read + conditions: + rule: IS_ENTITY_OWNER + resourceType: catalog-entity + params: + claims: + - $currentUser + --- + result: CONDITIONAL + roleEntityRef: 'role:default/conditional_denier' + pluginId: catalog + resourceType: catalog-entity + permissionMapping: + - read + conditions: + rule: HAS_LABEL + resourceType: catalog-entity + params: + label: test-label diff --git a/workspaces/rbac/e2e-tests/tests/config/values.yaml b/workspaces/rbac/e2e-tests/tests/config/values.yaml new file mode 100644 index 000000000..c9f50a990 --- /dev/null +++ b/workspaces/rbac/e2e-tests/tests/config/values.yaml @@ -0,0 +1,53 @@ +upstream: + backstage: + extraVolumeMounts: + # NOTE: Lists will not be merged with the default values file. So need to append all the defaults if you want to add a new item here + # See https://issues.redhat.com/browse/RHDHPLAN-869 + # The initContainer below will install dynamic plugins in this volume mount. + - name: dynamic-plugins-root + mountPath: /opt/app-root/src/dynamic-plugins-root + - name: extensions-catalog + mountPath: /extensions + - name: temp + mountPath: /tmp + - name: rbac-policy + mountPath: /opt/app-root/src/rbac + extraVolumes: + # NOTE: Lists will not be merged with the default values file. So need to append all the defaults if you want to add a new item here + # See https://issues.redhat.com/browse/RHDHPLAN-869 + # -- Ephemeral volume that will contain the dynamic plugins installed by the initContainer below at start. + # To have more control on underlying storage, the [emptyDir](https://docs.openshift.com/container-platform/4.13/storage/understanding-ephemeral-storage.html) + # could be changed to a [generic ephemeral volume](https://docs.openshift.com/container-platform/4.13/storage/generic-ephemeral-vols.html#generic-ephemeral-vols-procedure_generic-ephemeral-volumes). + - name: dynamic-plugins-root + emptyDir: {} + # Volume that will expose the `dynamic-plugins.yaml` file from the `dynamic-plugins` config map. + # The `dynamic-plugins` config map is created by the helm chart from the content of the `global.dynamic` field. + - name: dynamic-plugins + configMap: + defaultMode: 420 + name: '{{ printf "%s-dynamic-plugins" .Release.Name }}' + optional: true + # Optional volume that allows exposing the `.npmrc` file (through a `dynamic-plugins-npmrc` secret) + # to be used when running `npm pack` during the dynamic plugins installation by the initContainer. + - name: dynamic-plugins-npmrc + secret: + defaultMode: 420 + optional: true + secretName: '{{ printf "%s-dynamic-plugins-npmrc" .Release.Name }}' + - name: dynamic-plugins-registry-auth + secret: + defaultMode: 416 + optional: true + secretName: '{{ printf "%s-dynamic-plugins-registry-auth" .Release.Name }}' + - name: npmcacache + emptyDir: {} + # -- Ephemeral volume used by the install-dynamic-plugins init container to extract catalog entities from the catalog index image. + # Mounted at the /extensions path in the backstage-backend main container for automatic discovery by the extension catalog backend providers. + - name: extensions-catalog + emptyDir: {} + - name: temp + emptyDir: {} + - name: rbac-policy + configMap: + defaultMode: 420 + name: rbac-policy diff --git a/workspaces/rbac/e2e-tests/tests/specs/rbac.spec.ts b/workspaces/rbac/e2e-tests/tests/specs/rbac.spec.ts new file mode 100644 index 000000000..b193dd798 --- /dev/null +++ b/workspaces/rbac/e2e-tests/tests/specs/rbac.spec.ts @@ -0,0 +1,637 @@ +import { test, expect, Page } from "@red-hat-developer-hub/e2e-test-utils/test"; +import { RolesPage } from "../../support/pages/rbac"; +import { downloadAndReadFile } from "../../support/utils/helper"; +import { RbacPO } from "../../support/pages/rbac-po"; +import { + ROLE_OVERVIEW_COMPONENTS, + ROLES_PAGE_COMPONENTS, +} from "../../support/pages/rbac-obj"; + +import { $ } from "@red-hat-developer-hub/e2e-test-utils/utils"; +import * as path from "node:path"; +import { createUsersAndGroups } from "../../support/utils/create-users"; +import { cleanupRoles } from "../../support/utils/cleanup"; +import { + RBAC_DESCRIPTIVE_USERS, + RBAC_GROUPS, + displayName, + userEntityRef, +} from "../../support/constants/users-and-groups"; +import { RBAC_ROLES } from "../../support/constants/roles"; +import { loginAs } from "../../support/utils/login"; +import { + AuthApiHelper, + LoginHelper, + UIhelper, +} from "@red-hat-developer-hub/e2e-test-utils/helpers"; + +const rbacConfigmapPath = path.resolve( + process.cwd(), + "tests/config/rbac-configmap.yaml", +); + +test.describe("RBAC plugin", () => { + let rbacPO: RbacPO; + let apiToken: string; + + /** + * Shared helper used by both `beforeAll` (to grab the API token) and each + * `beforeEach` that needs an admin UI session. Extracting it avoids + * duplicating the login + navigation steps across multiple describe blocks. + */ + async function setupAdminSession({ + page, + uiHelper, + loginHelper, + }: { + page: Page; + uiHelper: UIhelper; + loginHelper: LoginHelper; + }) { + rbacPO = new RbacPO(page, uiHelper); + await loginAs(loginHelper, RBAC_DESCRIPTIVE_USERS.rbacAdmin); + await rbacPO.navigateToRBACPage(); + } + + test.beforeAll(async ({ rhdh, browser }) => { + await createUsersAndGroups(); + const namespace = rhdh.deploymentConfig.namespace; + await $`kubectl apply -f ${rbacConfigmapPath} -n ${namespace}`; + + await rhdh.configure({ + auth: "keycloak", + appConfig: "tests/config/app-config-rhdh.yaml", + valueFile: "tests/config/values.yaml", + }); + await rhdh.deploy(); + + // `beforeAll` does not receive a `page` fixture, so a temporary browser + // context is created solely to perform the admin login and extract the + // API token used by `afterAll` for programmatic cleanup. + const context = await browser.newContext({ + baseURL: process.env.RHDH_BASE_URL, + }); + + const page = await context.newPage(); + const uiHelper = new UIhelper(page); + const loginHelper = new LoginHelper(page); + await setupAdminSession({ page, uiHelper, loginHelper }); + const authApiHelper = new AuthApiHelper(page); + apiToken = await authApiHelper.getToken(); + await context.close(); + }); + + test.describe("RBAC plugin: admin user", () => { + test.beforeEach(async ({ page, uiHelper, loginHelper }) => { + await setupAdminSession({ page, uiHelper, loginHelper }); + }); + + test("Check Administration side nav has RBAC plugin", async ({ + page, + uiHelper, + }) => { + await uiHelper.goToPageUrl("/", "Welcome back!"); + await uiHelper.openSidebarButton("Administration"); + const rbacLink = page.getByRole("link", { name: "RBAC" }); + await expect(rbacLink).toBeVisible(); + await rbacLink.click(); + await uiHelper.verifyHeading("RBAC"); + expect(await page.title()).toContain("RBAC"); + + await rbacPO.verifyGeneralRbacViewHeading(); + const allGridColumnsText = RolesPage.getRolesListColumnsText(); + const allCellsIdentifier = RolesPage.getRolesListCellsIdentifier(); + + await rbacPO.verifyRoleOverviewTables( + allGridColumnsText, + allCellsIdentifier, + ); + }); + + test("Export CSV of the user list", async ({ page }) => { + await rbacPO.navigateToRBACPage(); + const exportCsvLink = page.getByRole("link", { name: "Export CSV" }); + await exportCsvLink.click(); + const fileContent = await downloadAndReadFile(page, exportCsvLink); + await test.info().attach("user-list-file", { + body: fileContent, + contentType: "text/plain", + }); + const lines = (fileContent ?? "").trim().split("\n"); + + const header = "userEntityRef,displayName,email,lastAuthTime"; + expect(lines[0], "Header needs to match the expected header").toBe( + header, + ); + + // Check that each subsequent line starts with "user:default" or "user:development" + const invalidLines = lines + .slice(1) + .filter( + (line) => + !line.startsWith("user:default") && + !line.startsWith("user:development"), + ); + + await test.step(`Validate user lines: ${invalidLines.length} invalid out of ${lines.length} total`, async () => { + expect(invalidLines, "All users should be valid").toHaveLength(0); + }); + }); + + test("View details of a role (rbac_admin)", async ({ uiHelper }) => { + await rbacPO.navigateToRBACPage(); + await rbacPO.verifyRoleAndSwitchToOverview(RBAC_ROLES.rbacAdmin.ref, "", [ + "1 user", + "5 permissions", + ]); + + const usersAndGroupsColumnsText = + RolesPage.getUsersAndGroupsListColumnsText(); + const usersAndGroupsCellsIdentifier = + RolesPage.getUsersAndGroupsListCellsIdentifier(); + + await rbacPO.verifyRoleOverviewTables( + usersAndGroupsColumnsText, + usersAndGroupsCellsIdentifier, + ); + + const permissionPoliciesColumnsText = + RolesPage.getPermissionPoliciesListColumnsText(); + const permissionPoliciesCellsIdentifier = + RolesPage.getPermissionPoliciesListCellsIdentifier(); + + await rbacPO.verifyRoleOverviewTables( + permissionPoliciesColumnsText, + permissionPoliciesCellsIdentifier, + ); + + await uiHelper.clickLink("RBAC"); + }); + + test("Cancel role creation exists without creating a role", async ({ + page, + uiHelper, + }) => { + await rbacPO.navigateToRBACPage(); + await uiHelper.clickButton("Create"); + await uiHelper.verifyHeading("Create role"); + await uiHelper.fillTextInputByLabel("name", "sample-role-1"); + await uiHelper.fillTextInputByLabel( + "description", + "Test Description data", + ); + + await uiHelper.clickButton("Next"); + // Wait for the users and groups step to be visible + await expect( + page.getByTestId("users-and-groups-text-field"), + ).toBeVisible(); + await uiHelper.fillTextInputByLabel( + "Select users and groups", + "sample-role-1", + ); + await page + .getByTestId("users-and-groups-text-field") + .getByLabel("clear search") + .click(); + await expect( + page.getByTestId("users-and-groups-text-field").getByRole("combobox"), + ).toBeEmpty(); + await uiHelper.verifyHeading("No users and groups selected"); + await uiHelper.clickButton("Cancel"); + await uiHelper.verifyText("Exit role creation?"); + await uiHelper.clickButton("Discard"); + await expect(page.getByRole("alert")).toHaveCount(0); + }); + + test("Edit role users via the inline edit button on the roles list", async ({ + page, + }) => { + await rbacPO.navigateToRBACPage(); + await rbacPO.createRole( + RBAC_ROLES.overviewListEdit.name, + [displayName("noAccess"), displayName("tara")], + [RBAC_GROUPS.backstage.name], + [{ permission: "catalog.entity.delete" }], + ); + + await rbacPO.filterRolesList(RBAC_ROLES.overviewListEdit.name); + await ROLES_PAGE_COMPONENTS.getEditRoleButton( + page, + RBAC_ROLES.overviewListEdit.ref, + ).click(); + await rbacPO.editRoleMembers( + RBAC_ROLES.overviewListEdit.ref, + displayName("jonathon"), + 3, + 1, + ); + + await rbacPO.filterRolesList(RBAC_ROLES.overviewListEdit.name); + + // Use semantic selector for table cell + const usersAndGroupsLocator = page + .getByRole("cell") + .filter({ hasText: rbacPO.regexpShortUsersAndGroups(3, 1) }); + await expect(usersAndGroupsLocator).toBeVisible(); + + await rbacPO.deleteRole(RBAC_ROLES.overviewListEdit.ref); + }); + + test("Edit role members via the updateMembers button on the overview page", async ({ + page, + uiHelper, + }) => { + await rbacPO.navigateToRBACPage(); + await rbacPO.createRole( + RBAC_ROLES.overviewMembers.name, + [displayName("noAccess"), displayName("tara")], + [RBAC_GROUPS.backstage.name], + [{ permission: "catalog.entity.delete" }], + ); + + await rbacPO.filterRolesList(RBAC_ROLES.overviewMembers.name); + + await rbacPO.verifyRoleAndSwitchToOverview( + RBAC_ROLES.overviewMembers.ref, + "", + [rbacPO.regexpShortUsersAndGroups(2, 1), "1 permission"], + ); + + await ROLE_OVERVIEW_COMPONENTS.getUpdateMembersButton(page).click(); + await rbacPO.editRoleMembers( + RBAC_ROLES.overviewMembers.ref, + displayName("noAccess"), + 1, + 1, + ); + + await uiHelper.verifyHeading(rbacPO.regexpShortUsersAndGroups(1, 1)); + + await rbacPO.deleteRole(RBAC_ROLES.overviewMembers.ref); + }); + + test("Edit role policies via the updatePolicies button on the overview page", async ({ + uiHelper, + }) => { + await rbacPO.navigateToRBACPage(); + await rbacPO.createRole( + RBAC_ROLES.overviewPolicies.name, + [displayName("noAccess"), displayName("tara")], + [RBAC_GROUPS.backstage.name], + [{ permission: "catalog.entity.delete" }], + ); + + await rbacPO.filterRolesList(RBAC_ROLES.overviewPolicies.name); + + await rbacPO.verifyRoleAndSwitchToOverview( + RBAC_ROLES.overviewPolicies.ref, + "", + [rbacPO.regexpShortUsersAndGroups(2, 1), "1 permission"], + ); + + await rbacPO.editRolePermissions(); + + await uiHelper.verifyText( + `Role ${RBAC_ROLES.overviewPolicies.ref} updated successfully`, + ); + await uiHelper.verifyHeading("2 permissions"); + + await rbacPO.deleteRole(RBAC_ROLES.overviewPolicies.ref); + }); + }); + + test.describe("RBAC Plugin: validate appropriate guest user handling", () => { + test.beforeEach(async ({ page, uiHelper, loginHelper }) => { + rbacPO = new RbacPO(page, uiHelper); + await loginAs(loginHelper, RBAC_DESCRIPTIVE_USERS.noAccess); + }); + + test("Administration side nav does not show RBAC plugin", async ({ + page, + uiHelper, + }) => { + await uiHelper.openSidebarButton("Administration"); + // Check specifically for RBAC link in sidebar navigation, not anywhere on the page + const rbacNavLink = page + .getByRole("navigation", { name: "sidebar nav" }) + .getByRole("link", { name: "RBAC" }); + await expect(rbacNavLink).toHaveCount(0); + }); + + test("Direct navigation to /rbac is denied", async ({ uiHelper }) => { + await rbacPO.go(); + await uiHelper.waitForLoad(); + await uiHelper.verifyText("ERROR : Not Found"); + }); + }); + + test.describe("RBAC plugin: permission policies loaded from files", () => { + test.beforeEach(async ({ page, uiHelper, loginHelper }) => { + await setupAdminSession({ page, uiHelper, loginHelper }); + }); + + test("Permission policies defined in a CSV file are loaded (guest role, 1 permission)", async ({ + uiHelper, + }) => { + await rbacPO.filterRolesList(RBAC_ROLES.guest.name); + await rbacPO.verifyRoleAndSwitchToOverview( + RBAC_ROLES.guest.ref, + "csv permission policy file", + ["1 user", "1 permission"], + ); + + const permissionPoliciesColumnsText = + RolesPage.getPermissionPoliciesListColumnsText(); + const permissionPoliciesCellsIdentifier = + RolesPage.getPermissionPoliciesListCellsIdentifier(); + + await rbacPO.verifyRoleOverviewTables( + permissionPoliciesColumnsText, + permissionPoliciesCellsIdentifier, + ); + + await uiHelper.verifyRowInTableByUniqueText(displayName("noAccess"), [ + "user", + "-", + ]); + await uiHelper.verifyRowInTableByUniqueText("catalog.entity.create", [ + "create", + "-", + ]); + }); + + test("CSV file-sourced role (guest role): Update policies is not available", async ({ + page, + }) => { + await rbacPO.filterRolesList(RBAC_ROLES.guest.name); + await rbacPO.verifyRoleAndSwitchToOverview( + RBAC_ROLES.guest.ref, + "csv permission policy file", + ["1 user", "1 permission"], + ); + + await rbacPO.editRolePermissions(); + + const errorAlert = page + .getByRole("alert") + .filter({ hasText: /Unable to edit the role/i }); + expect(errorAlert.count()).toBeTruthy(); + }); + + test("CSV file-sourced role (guest role): Delete is not available", async ({ + page, + }) => { + await rbacPO.filterRolesList(RBAC_ROLES.guest.name); + await rbacPO.deleteRole(RBAC_ROLES.guest.ref, "All roles (0)", true); + + const errorAlert = page + .getByRole("alert") + .filter({ hasText: /Unable to delete policy/i }); + expect(errorAlert.count()).toBeTruthy(); + }); + + test("Config-sourced role (rbac_admin): Update policies is not available", async ({ + page, + }) => { + await rbacPO.filterRolesList(RBAC_ROLES.rbacAdmin.name); + await rbacPO.verifyRoleAndSwitchToOverview(RBAC_ROLES.rbacAdmin.ref, "", [ + "1 user", + "5 permissions", + ]); + + await rbacPO.editRolePermissions(); + + const errorAlert = page + .getByRole("alert") + .filter({ hasText: /Unable to edit the role/i }); + expect(errorAlert.count()).toBeTruthy(); + }); + + test("Config-sourced role (rbac_admin): Delete is not available", async ({ + page, + }) => { + await rbacPO.filterRolesList(RBAC_ROLES.rbacAdmin.name); + await rbacPO.deleteRole(RBAC_ROLES.rbacAdmin.ref, "All roles (0)", true); + + const errorAlert = page + .getByRole("alert") + .filter({ hasText: /Unable to delete policy/i }); + expect(errorAlert.count()).toBeTruthy(); + }); + }); + + test.describe("RBAC conditional policies: $currentUser alias", () => { + test.beforeEach(async ({ page, uiHelper, loginHelper }) => { + rbacPO = new RbacPO(page, uiHelper); + await loginAs(loginHelper, RBAC_DESCRIPTIVE_USERS.currentUserOwner); + }); + + test("User can unregister own components but not group-owned components", async ({ + page, + uiHelper, + }) => { + await rbacPO.navigateToCatalogComponent("test-rhdh-qe-2"); + + // Verify component name in the main heading + await expect(page.getByRole("heading", { level: 1 })).toContainText( + "test-rhdh-qe-2", + ); + await page.getByTestId("menu-button").click(); + const unregisterUserOwned = page.getByRole("menuitem", { + name: "Unregister entity", + }); + await expect(unregisterUserOwned).toBeEnabled(); + + await page.getByRole("menuitem", { name: "Unregister entity" }).click(); + await expect(page.getByRole("dialog")).toContainText( + "Are you sure you want to unregister this entity?", + ); + await page.getByRole("button", { name: "Cancel" }).click(); + + await uiHelper.openSidebar("Catalog"); + await page + .getByRole("link", { name: "test-rhdh-qe-2-team-owned" }) + .click(); + // Verify owner group in the component metadata (scope to article to avoid duplicates) + await expect( + page + .getByRole("article") + .getByRole("link", { name: /janus-qe\/rhdh-qe-2-team/ }), + ).toBeVisible(); + await page.getByTestId("menu-button").click(); + const unregisterGroupOwned = page.getByRole("menuitem", { + name: "Unregister entity", + }); + await expect(unregisterGroupOwned).toBeDisabled(); + }); + }); + + test.describe("RBAC conditional policies: $ownerRefs transitive group ownership", () => { + test.beforeEach(({ page, uiHelper }) => { + rbacPO = new RbacPO(page, uiHelper); + }); + + test("User in child group can read components owned by the parent group", async ({ + loginHelper, + }) => { + // login as child-group-member: belongs in rhdh-qe-child-team, which is a sub group of rhdh-qe-parent-team + await loginAs(loginHelper, RBAC_DESCRIPTIVE_USERS.childGroupMember); + + // rhdh-qe-parent-team owns mock-site + await rbacPO.navigateToCatalogComponent("mock-site"); + // Verify owner group in the component metadata + await rbacPO.verifyComponentOwner(RBAC_GROUPS.rhdhParentTeam.name); + + // rhdh-qe-child-team owns mock-child-site, check that it can see it's own groups' components + await rbacPO.navigateToCatalogComponent("mock-child-site"); + // Verify owner group in the component metadata + await rbacPO.verifyComponentOwner(RBAC_GROUPS.rhdhChildTeam.name); + }); + + test("User in sub-child group can read components owned by grandparent group", async ({ + loginHelper, + }) => { + // login as sub-child-group-member: belongs in rhdh-qe-sub-child-team, which is a sub group of rhdh-qe-child-team + await loginAs(loginHelper, RBAC_DESCRIPTIVE_USERS.subChildGroupMember); + + // rhdh-qe-parent-team owns mock-site + await rbacPO.navigateToCatalogComponent("mock-site"); + // Verify owner group in the component metadata + await rbacPO.verifyComponentOwner(RBAC_GROUPS.rhdhParentTeam.name); + + // rhdh-qe-child-team owns mock-child-site + await rbacPO.navigateToCatalogComponent("mock-child-site"); + // Verify owner group in the component metadata + await rbacPO.verifyComponentOwner(RBAC_GROUPS.rhdhChildTeam.name); + + // rhdh-qe-sub-child-team owns mock-sub-child-site, check that it can see it's own groups' components + await rbacPO.navigateToCatalogComponent("mock-sub-child-site"); + // Verify owner group in the component metadata + await rbacPO.verifyComponentOwner(RBAC_GROUPS.rhdhSubChildTeam.name); + }); + }); + + test.describe("RBAC conditional policies: IsOwner ownership rule", () => { + test.describe.configure({ mode: "serial" }); + + test.beforeEach(async ({ page, uiHelper }) => { + rbacPO = new RbacPO(page, uiHelper); + }); + + test("Admin creates rbac-ownership-role with IsOwner rule for conditional-manager", async ({ + loginHelper, + }) => { + await loginAs(loginHelper, RBAC_DESCRIPTIVE_USERS.rbacAdmin); + + await rbacPO.navigateToRBACPage(); + await rbacPO.createRBACConditionRole( + RBAC_ROLES.rbacOwnership.name, + [displayName("conditionalManager")], + userEntityRef(RBAC_DESCRIPTIVE_USERS.conditionalManager), + ); + }); + + test("conditional-manager can access RBAC page, create a role, edit it, and delete it", async ({ + page, + loginHelper, + }) => { + await loginAs(loginHelper, RBAC_DESCRIPTIVE_USERS.conditionalManager); + + await rbacPO.navigateToRBACPage(); + await rbacPO.createRole( + RBAC_ROLES.rbacConditional.name, + [displayName("noAccess"), displayName("tara")], + [RBAC_GROUPS.backstage.name], + [{ permission: "catalog.entity.delete" }], + "catalog", + userEntityRef(RBAC_DESCRIPTIVE_USERS.conditionalManager), + ); + + await ROLES_PAGE_COMPONENTS.getEditRoleButton( + page, + RBAC_ROLES.rbacConditional.ref, + ).click(); + + await rbacPO.editRoleMembers( + RBAC_ROLES.rbacConditional.ref, + displayName("jonathon"), + 3, + 1, + ); + + await rbacPO.deleteRole(RBAC_ROLES.rbacConditional.ref, "All roles"); + }); + + test("Admin revokes access by deleting rbac-conditional-role", async ({ + loginHelper, + }) => { + await loginAs(loginHelper, RBAC_DESCRIPTIVE_USERS.rbacAdmin); + + await rbacPO.navigateToRBACPage(); + + await rbacPO.deleteRole(RBAC_ROLES.rbacOwnership.ref); + }); + + test("conditional-manager no longer sees RBAC in the sidebar after access is revoked", async ({ + page, + uiHelper, + loginHelper, + }) => { + await loginAs(loginHelper, RBAC_DESCRIPTIVE_USERS.conditionalManager); + + await uiHelper.openSidebarButton("Administration"); + const dropdownMenuLocator = page.getByText("RBAC"); + await expect(dropdownMenuLocator).toBeHidden(); + }); + }); + + test.describe("RBAC conditional policies: policyDecisionPrecedence", () => { + test.beforeEach(({ page, uiHelper }) => { + rbacPO = new RbacPO(page, uiHelper); + }); + + test("Conditional allow overrides basic deny (conditional-allow-user)", async ({ + loginHelper, + }) => { + // Should allow read: conditional policy takes precedence over static deny read via CSV + await loginAs(loginHelper, RBAC_DESCRIPTIVE_USERS.conditionalAllowUser); + await rbacPO.navigateToCatalogComponent("mock-component"); + }); + + test("Conditional deny overrides basic allow (conditional-deny-user)", async ({ + uiHelper, + loginHelper, + }) => { + // Should deny read: conditional deny policy takes precedence over allow read via basic + await loginAs(loginHelper, RBAC_DESCRIPTIVE_USERS.conditionalDenyUser); + await uiHelper.openSidebar("Catalog"); + await uiHelper.selectMuiBox("Kind", "Component"); + await uiHelper.verifyTableIsEmpty(); + }); + }); + + test.describe("RBAC conditional policies: permission policy per resource type", () => { + test.beforeEach(async ({ page, uiHelper, loginHelper }) => { + await setupAdminSession({ page, uiHelper, loginHelper }); + }); + + test("Create role with AnyOf conditional rules per resource type and verify only authorized users see appropriate catalog resources", async ({}) => { + await rbacPO.createConditionalRole( + RBAC_ROLES.conditionalResource.name, + [displayName("noAccess"), displayName("rbacAdmin")], + [RBAC_GROUPS.backstage.name], + "anyOf", + "catalog", + userEntityRef(RBAC_DESCRIPTIVE_USERS.rbacAdmin), + ); + + await rbacPO.deleteRole(RBAC_ROLES.conditionalResource.ref); + }); + }); + + // Ensure we clean up in the event that a test fails so that we do not impact other tests + test.afterAll(async () => { + await cleanupRoles(RBAC_ROLES, apiToken); + }); +}); diff --git a/workspaces/rbac/e2e-tests/tsconfig.json b/workspaces/rbac/e2e-tests/tsconfig.json new file mode 100644 index 000000000..ede55abc0 --- /dev/null +++ b/workspaces/rbac/e2e-tests/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@red-hat-developer-hub/e2e-test-utils/tsconfig", + "include": ["**/*.ts"] +} diff --git a/workspaces/rbac/e2e-tests/yarn.lock b/workspaces/rbac/e2e-tests/yarn.lock new file mode 100644 index 000000000..5c750dba1 --- /dev/null +++ b/workspaces/rbac/e2e-tests/yarn.lock @@ -0,0 +1,2483 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 6 + cacheKey: 8 + +"@axe-core/playwright@npm:^4.11.0": + version: 4.11.1 + resolution: "@axe-core/playwright@npm:4.11.1" + dependencies: + axe-core: ~4.11.1 + peerDependencies: + playwright-core: ">= 1.0.0" + checksum: 311f107688bfd930ee201dbab9c75048a6cdd39be7af0d8fdf05e5d01b76ef8d57ca687954102839eefa14ec23f96b6a2833d4bef82c4b280e43b7f4e15a39b3 + languageName: node + linkType: hard + +"@backstage-community/plugin-rbac-common@npm:1.23.0": + version: 1.23.0 + resolution: "@backstage-community/plugin-rbac-common@npm:1.23.0" + peerDependencies: + "@backstage/errors": ^1.2.7 + "@backstage/plugin-permission-common": ^0.9.5 + checksum: 8cbb67a4854b9a72d329459cf37d05628da4ca4c665fb1e9c34e2dcbcd8d4399264b00bab8e4ee786717e2c0c5b0e3db7ee4eee6705424047a799fbf311eb5f4 + languageName: node + linkType: hard + +"@eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": + version: 4.9.1 + resolution: "@eslint-community/eslint-utils@npm:4.9.1" + dependencies: + eslint-visitor-keys: ^3.4.3 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 0a27c2d676c4be6b329ebb5dd8f6c5ef5fae9a019ff575655306d72874bb26f3ab20e0b241a5f086464bb1f2511ca26a29ff6f80c1e2b0b02eca4686b4dfe1b5 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.12.2": + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 1770bc81f676a72f65c7200b5675ff7a349786521f30e66125faaf767fde1ba1c19c3790e16ba8508a62a3933afcfc806a893858b3b5906faf693d862b9e4120 + languageName: node + linkType: hard + +"@eslint/config-array@npm:^0.21.1": + version: 0.21.1 + resolution: "@eslint/config-array@npm:0.21.1" + dependencies: + "@eslint/object-schema": ^2.1.7 + debug: ^4.3.1 + minimatch: ^3.1.2 + checksum: fc5b57803b059f7c1f62950ef83baf045a01887fc00551f9e87ac119246fcc6d71c854a7f678accc79cbf829ed010e8135c755a154b0f54b129c538950cd7e6a + languageName: node + linkType: hard + +"@eslint/config-helpers@npm:^0.4.2": + version: 0.4.2 + resolution: "@eslint/config-helpers@npm:0.4.2" + dependencies: + "@eslint/core": ^0.17.0 + checksum: 63ff6a0730c9fff2edb80c89b39b15b28d6a635a1c3f32cf0d7eb3e2625f2efbc373c5531ae84e420ae36d6e37016dd40c365b6e5dee6938478e9907aaadae0b + languageName: node + linkType: hard + +"@eslint/core@npm:^0.17.0": + version: 0.17.0 + resolution: "@eslint/core@npm:0.17.0" + dependencies: + "@types/json-schema": ^7.0.15 + checksum: ff9b5b4987f0bae4f2a4cfcdc7ae584ad3b0cb58526ca562fb281d6837700a04c7f3c86862e95126462318f33f60bf38e1cb07ed0e2449532d4b91cd5f4ab1f2 + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^3.3.1": + version: 3.3.4 + resolution: "@eslint/eslintrc@npm:3.3.4" + dependencies: + ajv: ^6.14.0 + debug: ^4.3.2 + espree: ^10.0.1 + globals: ^14.0.0 + ignore: ^5.2.0 + import-fresh: ^3.2.1 + js-yaml: ^4.1.1 + minimatch: ^3.1.3 + strip-json-comments: ^3.1.1 + checksum: c16df92611b927af454d3ab9b1e003d75ae5a0e91009bdab8487bc37d4eb507adf071bd208857fd397a2c311cff1c28f617e01250078e532ab3ac7f1353cee13 + languageName: node + linkType: hard + +"@eslint/js@npm:9.39.3, @eslint/js@npm:^9.39.1, @eslint/js@npm:^9.39.2": + version: 9.39.3 + resolution: "@eslint/js@npm:9.39.3" + checksum: 6018c13073204cf1b79de561cca74284c0387bf753e0dcd85ff750f1441c4c2914896d8feff3afd8c07d6934ac6f8ae36a5cc241f5645041b645dad588442d46 + languageName: node + linkType: hard + +"@eslint/object-schema@npm:^2.1.7": + version: 2.1.7 + resolution: "@eslint/object-schema@npm:2.1.7" + checksum: fc5708f192476956544def13455d60fd1bafbf8f062d1e05ec5c06dd470b02078eaf721e696a8b31c1c45d2056723a514b941ae5eea1398cc7e38eba6711a775 + languageName: node + linkType: hard + +"@eslint/plugin-kit@npm:^0.4.1": + version: 0.4.1 + resolution: "@eslint/plugin-kit@npm:0.4.1" + dependencies: + "@eslint/core": ^0.17.0 + levn: ^0.4.1 + checksum: 3f4492e02a3620e05d46126c5cfeff5f651ecf33466c8f88efb4812ae69db5f005e8c13373afabc070ecca7becd319b656d6670ad5093f05ca63c2a8841d99ba + languageName: node + linkType: hard + +"@gar/promise-retry@npm:^1.0.0": + version: 1.0.2 + resolution: "@gar/promise-retry@npm:1.0.2" + dependencies: + retry: ^0.13.1 + checksum: b91326999ce94677cbe91973079eabc689761a93a045f6a2d34d4070e9305b27f6c54e4021688c7080cb14caf89eafa0c0f300af741b94c20d18608bdb66ca46 + languageName: node + linkType: hard + +"@humanfs/core@npm:^0.19.1": + version: 0.19.1 + resolution: "@humanfs/core@npm:0.19.1" + checksum: 611e0545146f55ddfdd5c20239cfb7911f9d0e28258787c4fc1a1f6214250830c9367aaaeace0096ed90b6739bee1e9c52ad5ba8adaf74ab8b449119303babfe + languageName: node + linkType: hard + +"@humanfs/node@npm:^0.16.6": + version: 0.16.7 + resolution: "@humanfs/node@npm:0.16.7" + dependencies: + "@humanfs/core": ^0.19.1 + "@humanwhocodes/retry": ^0.4.0 + checksum: 7d2a396a94d80158ce320c0fd7df9aebb82edb8b667e5aaf8f87f4ca50518d0941ca494e0cd68e06b061e777ce5f7d26c45f93ac3fa9f7b11fd1ff26e3cd1440 + 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/retry@npm:^0.4.0, @humanwhocodes/retry@npm:^0.4.2": + version: 0.4.3 + resolution: "@humanwhocodes/retry@npm:0.4.3" + checksum: d423455b9d53cf01f778603404512a4246fb19b83e74fe3e28c70d9a80e9d4ae147d2411628907ca983e91a855a52535859a8bb218050bc3f6dbd7a553b7b442 + 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 + +"@jsep-plugin/assignment@npm:^1.3.0": + version: 1.3.0 + resolution: "@jsep-plugin/assignment@npm:1.3.0" + peerDependencies: + jsep: ^0.4.0||^1.0.0 + checksum: 5549497d403a6c00969d61202a6d3dafc5a349929d190a524363dcfacb3436dbda3d9f88b2ec1330247a594ad3c5f1c17b0997769d0b206802281bad6cf9a410 + languageName: node + linkType: hard + +"@jsep-plugin/regex@npm:^1.0.4": + version: 1.0.4 + resolution: "@jsep-plugin/regex@npm:1.0.4" + peerDependencies: + jsep: ^0.4.0||^1.0.0 + checksum: 78ef01554535ac6c108851a2a6d86377bce10de01a263ad7b31f9b37c8aa9fc6c49f24b753e5da7d771c5921c913e43c1c33e0bc0fa5d02562d906c83a237836 + languageName: node + linkType: hard + +"@keycloak/keycloak-admin-client@npm:^26.0.0": + version: 26.5.4 + resolution: "@keycloak/keycloak-admin-client@npm:26.5.4" + dependencies: + camelize-ts: ^3.0.0 + url-template: ^3.1.1 + checksum: ce9a3d1d1ed91e6685f0141f0f1b060f16ae84439b0cc713b6c45aa5067ac703f40d038e403ecdaf77e33fafbe26bd3f991d7de50562a6cdfdd3ae4e0ec2d3b9 + languageName: node + linkType: hard + +"@kubernetes/client-node@npm:^1.4.0": + version: 1.4.0 + resolution: "@kubernetes/client-node@npm:1.4.0" + dependencies: + "@types/js-yaml": ^4.0.1 + "@types/node": ^24.0.0 + "@types/node-fetch": ^2.6.13 + "@types/stream-buffers": ^3.0.3 + form-data: ^4.0.0 + hpagent: ^1.2.0 + isomorphic-ws: ^5.0.0 + js-yaml: ^4.1.0 + jsonpath-plus: ^10.3.0 + node-fetch: ^2.7.0 + openid-client: ^6.1.3 + rfc4648: ^1.3.0 + socks-proxy-agent: ^8.0.4 + stream-buffers: ^3.0.2 + tar-fs: ^3.0.9 + ws: ^8.18.2 + checksum: d62b22db84d1832c904ca1489ca2de029b1698ae111cf0fb1dac96ae9e5a68414e1c128adf9ffdd0fd75c937aef831a3e39c48c1b43850c62765d6e61ce25efd + languageName: node + linkType: hard + +"@npmcli/agent@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/agent@npm:4.0.0" + dependencies: + agent-base: ^7.1.0 + http-proxy-agent: ^7.0.0 + https-proxy-agent: ^7.0.1 + lru-cache: ^11.2.1 + socks-proxy-agent: ^8.0.3 + checksum: 89ae20b44859ff8d4de56ade319d8ceaa267a0742d6f7345fe98aa5cd8614ced7db85ea4dc5bfbd6614dbb200a10b134e087143582534c939e8a02219e8665c8 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^5.0.0": + version: 5.0.0 + resolution: "@npmcli/fs@npm:5.0.0" + dependencies: + semver: ^7.3.5 + checksum: 897dac32eb37e011800112d406b9ea2ebd96f1dab01bb8fbeb59191b86f6825dffed6a89f3b6c824753d10f8735b76d630927bd7610e9e123b129ef2e5f02cb5 + languageName: node + linkType: hard + +"@otplib/core@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/core@npm:12.0.1" + checksum: b3c34bc20b31bc3f49cc0dc3c0eb070491c0101e8c1efa83cec48ca94158bd736aaca8187df667fc0c4a239d4ac52076bc44084bee04a50c80c3630caf77affa + languageName: node + linkType: hard + +"@otplib/plugin-crypto@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/plugin-crypto@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + checksum: 6867c74ee8aca6c2db9670362cf51e44f3648602c39318bf537421242e33f0012a172acd43bbed9a21d706e535dc4c66aff965380673391e9fd74cf685b5b13a + languageName: node + linkType: hard + +"@otplib/plugin-thirty-two@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/plugin-thirty-two@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + thirty-two: ^1.0.2 + checksum: 920099e40d3e8c2941291c84c70064c2d86d0d1ed17230d650445d5463340e406bc413ddf2e40c374ddc4ee988ef1e3facacab9b5248b1ff361fd13df52bf88f + languageName: node + linkType: hard + +"@otplib/preset-default@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/preset-default@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + "@otplib/plugin-crypto": ^12.0.1 + "@otplib/plugin-thirty-two": ^12.0.1 + checksum: 8133231384f6277f77eb8e42ef83bc32a8b01059bef147d1c358d9e9bfd292e1c239f581fe008367a48489dd68952b7ac0948e6c41412fc06079da2c91b71d16 + languageName: node + linkType: hard + +"@otplib/preset-v11@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/preset-v11@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + "@otplib/plugin-crypto": ^12.0.1 + "@otplib/plugin-thirty-two": ^12.0.1 + checksum: 367cb09397e617c21ec748d54e920ab43f1c5dfba70cbfd88edf73aecca399cf0c09fefe32518f79c7ee8a06e7058d14b200da378cc7d46af3cac4e22a153e2f + languageName: node + linkType: hard + +"@playwright/test@npm:1.57.0": + version: 1.57.0 + resolution: "@playwright/test@npm:1.57.0" + dependencies: + playwright: 1.57.0 + bin: + playwright: cli.js + checksum: 1a84783a240d69c2c8081a127b446f812a8dc86fe6f60a9511dd501cc0e6229cbec7e7753972678f3f063ad2bebb2cedbe9caebc5faa41014aebed35773ea242 + languageName: node + linkType: hard + +"@red-hat-developer-hub/e2e-test-utils@npm:1.1.15": + version: 1.1.15 + resolution: "@red-hat-developer-hub/e2e-test-utils@npm:1.1.15" + dependencies: + "@axe-core/playwright": ^4.11.0 + "@backstage-community/plugin-rbac-common": 1.23.0 + "@eslint/js": ^9.39.1 + "@keycloak/keycloak-admin-client": ^26.0.0 + "@kubernetes/client-node": ^1.4.0 + eslint: ^9.39.1 + eslint-plugin-check-file: ^3.3.1 + eslint-plugin-playwright: ^2.4.0 + fs-extra: ^11.3.2 + js-yaml: ^4.1.1 + lodash.clonedeepwith: ^4.5.0 + lodash.mergewith: ^4.6.2 + otplib: 12.0.1 + prettier: ^3.7.4 + proper-lockfile: ^4.1.2 + typescript: ^5.9.3 + typescript-eslint: ^8.48.1 + zx: ^8.8.5 + peerDependencies: + "@playwright/test": ^1.57.0 + checksum: 9b92ffbec69b77f263c875a9f3edb672fbe165f3cee8f84c5e2e7b9dd2428b693a2eb9b3375f42c0706960dd7ea2d434e7917f2eb5b6a3762d8f80c18a936c70 + languageName: node + linkType: hard + +"@types/estree@npm:^1.0.6": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: bd93e2e415b6f182ec4da1074e1f36c480f1d26add3e696d54fb30c09bc470897e41361c8fd957bf0985024f8fbf1e6e2aff977d79352ef7eb93a5c6dcff6c11 + 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/json-schema@npm:^7.0.15": + version: 7.0.15 + resolution: "@types/json-schema@npm:7.0.15" + checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98 + languageName: node + linkType: hard + +"@types/node-fetch@npm:^2.6.13": + version: 2.6.13 + resolution: "@types/node-fetch@npm:2.6.13" + dependencies: + "@types/node": "*" + form-data: ^4.0.4 + checksum: e4b4db3a8c23309dadf0beb87e88882af1157f0c08b7b76027ac40add6ed363c924e2fa275f42ae45eacf776b25ed439d14400d9d6372eb39634dd4c7e7e1ad8 + languageName: node + linkType: hard + +"@types/node@npm:*": + version: 25.3.1 + resolution: "@types/node@npm:25.3.1" + dependencies: + undici-types: ~7.18.0 + checksum: e073f3abb8be5e1c918e4787a6cc320b00bdb7d9c5dd0c58e32330819eb918d3b20f66060c8d42757e246e14829c3a2396cc305bb28e33b253d0d6b940548e2b + languageName: node + linkType: hard + +"@types/node@npm:^24.0.0, @types/node@npm:^24.10.1": + version: 24.10.14 + resolution: "@types/node@npm:24.10.14" + dependencies: + undici-types: ~7.16.0 + checksum: c0432511a83947a6e0de5f206604275bd7676ddc6304dee09059dbbd20e75a03a7394fa95a87b07f1761474d5adac5a0d06d86b71926808728f358843ebb8b4b + languageName: node + linkType: hard + +"@types/stream-buffers@npm:^3.0.3": + version: 3.0.8 + resolution: "@types/stream-buffers@npm:3.0.8" + dependencies: + "@types/node": "*" + checksum: 2e269491769f3c529236cdd743a505d1dca52d14b3672714730d8940000d948bdf9468b2e54609b9d591d31e51e8e043eb44a830a6189bc727bd9dfc4dd70cdf + languageName: node + linkType: hard + +"@typescript-eslint/eslint-plugin@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/eslint-plugin@npm:8.56.1" + dependencies: + "@eslint-community/regexpp": ^4.12.2 + "@typescript-eslint/scope-manager": 8.56.1 + "@typescript-eslint/type-utils": 8.56.1 + "@typescript-eslint/utils": 8.56.1 + "@typescript-eslint/visitor-keys": 8.56.1 + ignore: ^7.0.5 + natural-compare: ^1.4.0 + ts-api-utils: ^2.4.0 + peerDependencies: + "@typescript-eslint/parser": ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 40f5e1f73a9c268fb4ae5da88f56dd92bebe0185b0228fbbf26df00fc9fdfb41024d9dcb0443f171a34a09214444b20e1ff3f144c3070b7772d93444b2b4c37e + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/parser@npm:8.56.1" + dependencies: + "@typescript-eslint/scope-manager": 8.56.1 + "@typescript-eslint/types": 8.56.1 + "@typescript-eslint/typescript-estree": 8.56.1 + "@typescript-eslint/visitor-keys": 8.56.1 + debug: ^4.4.3 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 96035fd94147ea6940a5e6f030f1068ae246e24c0b1bcf8b1a9f0d205ea7678c46c7f9c27f16313302fdf115d94fc83b3e2d1403f0df1c04d0ffe85e88a554ee + languageName: node + linkType: hard + +"@typescript-eslint/project-service@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/project-service@npm:8.56.1" + dependencies: + "@typescript-eslint/tsconfig-utils": ^8.56.1 + "@typescript-eslint/types": ^8.56.1 + debug: ^4.4.3 + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: aeb8f9e34185781d83a67dfef8dc80b3d0d1d832b592a86aaecada1311e7920cb8fd1a044986a6a1c50cdcadfb1e80a3c173cd17b4e8abe8d6bf072e9a904474 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/scope-manager@npm:8.56.1" + dependencies: + "@typescript-eslint/types": 8.56.1 + "@typescript-eslint/visitor-keys": 8.56.1 + checksum: 55a593a2c74bb7cff5136d48c242e7bcf8c54bdd4ae20db2bedf4756f795adda85f9971b7d871991f19ae3cd1cf4b6f854a589bd5fd00f0d77e2906880034155 + languageName: node + linkType: hard + +"@typescript-eslint/tsconfig-utils@npm:8.56.1, @typescript-eslint/tsconfig-utils@npm:^8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.56.1" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: b54be8d46963958d8dc763099d632a83384dda2832c57a121df853c2fbd01042eb5c02d427303c021920d8312fcafb987ef734dea2a4ebcf1fe3dc956fd62f42 + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/type-utils@npm:8.56.1" + dependencies: + "@typescript-eslint/types": 8.56.1 + "@typescript-eslint/typescript-estree": 8.56.1 + "@typescript-eslint/utils": 8.56.1 + debug: ^4.4.3 + ts-api-utils: ^2.4.0 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: e2c8345d0b82977d3fb7f432d90b02773384e139644e7c214f4e6a1e2d3bcc231a2f2f57d9b507d37aabd4eb984b8523edc254b9400bf66f988475aaf225adfc + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:8.56.1, @typescript-eslint/types@npm:^8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/types@npm:8.56.1" + checksum: 67716a6699becdef1dd23acb0762b940caa2c541fd27573328b5047a7cd08d19f775eb01cd11080d16dc2d2a23af8f9b8fc5813f935b12170769de074af3c77a + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.56.1" + dependencies: + "@typescript-eslint/project-service": 8.56.1 + "@typescript-eslint/tsconfig-utils": 8.56.1 + "@typescript-eslint/types": 8.56.1 + "@typescript-eslint/visitor-keys": 8.56.1 + debug: ^4.4.3 + minimatch: ^10.2.2 + semver: ^7.7.3 + tinyglobby: ^0.2.15 + ts-api-utils: ^2.4.0 + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: e28b4874b8d65c22075b3ada91931d17d892f16aa39a7f65cf726476085c0ef9620945c558b3c00ed363f88bd6ebd5372267510b640687370c608a0298bbf5cf + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/utils@npm:8.56.1" + dependencies: + "@eslint-community/eslint-utils": ^4.9.1 + "@typescript-eslint/scope-manager": 8.56.1 + "@typescript-eslint/types": 8.56.1 + "@typescript-eslint/typescript-estree": 8.56.1 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: bcef244cdad1ede9726e6e01e60077056b5ce1136ac0637446b05975a1936a2d424159471eebaa9f8746659bdca567f5e50d63bdbb6369bc48d2f5af4627d1ca + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.56.1" + dependencies: + "@typescript-eslint/types": 8.56.1 + eslint-visitor-keys: ^5.0.0 + checksum: cd8cb26d855b09103bd8a5d5d3eb9eb9eecb7cf7276fd2bebf3f79deb6cd4d81b400b229fb8485cd7376e3c25bbd07b2d1674f47794ff84ffcc817859cd0cb62 + languageName: node + linkType: hard + +"abbrev@npm:^4.0.0": + version: 4.0.0 + resolution: "abbrev@npm:4.0.0" + checksum: d0344b63d28e763f259b4898c41bdc92c08e9d06d0da5617d0bbe4d78244e46daea88c510a2f9472af59b031d9060ec1a999653144e793fd029a59dae2f56dc8 + 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@npm:^8.15.0": + version: 8.16.0 + resolution: "acorn@npm:8.16.0" + bin: + acorn: bin/acorn + checksum: bbfa466cd0dbd18b4460a85e9d0fc2f35db999380892403c573261beda91f23836db2aa71fd3ae65e94424ad14ff8e2b7bd37c7a2624278fd89137cd6e448c41 + languageName: node + linkType: hard + +"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": + version: 7.1.4 + resolution: "agent-base@npm:7.1.4" + checksum: 86a7f542af277cfbd77dd61e7df8422f90bac512953709003a1c530171a9d019d072e2400eab2b59f84b49ab9dd237be44315ca663ac73e82b3922d10ea5eafa + languageName: node + linkType: hard + +"ajv@npm:^6.12.4, ajv@npm:^6.14.0": + version: 6.14.0 + resolution: "ajv@npm:6.14.0" + 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: 7bb3ea97bb8af52521589079f427e799b6561acaa94f50e13410cb87588c51df8db1afe1157b3e48f1a829269adaa11116e0c2cafe2b998add1523789809a3c5 + languageName: node + linkType: hard + +"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 + +"argparse@npm:^2.0.1": + version: 2.0.1 + resolution: "argparse@npm:2.0.1" + checksum: 83644b56493e89a254bae05702abf3a1101b4fa4d0ca31df1c9985275a5a5bd47b3c27b7fa0b71098d41114d8ca000e6ed90cad764b306f8a503665e4d517ced + languageName: node + linkType: hard + +"async-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-function@npm:1.0.0" + checksum: 9102e246d1ed9b37ac36f57f0a6ca55226876553251a31fc80677e71471f463a54c872dc78d5d7f80740c8ba624395cccbe8b60f7b690c4418f487d8e9fd1106 + languageName: node + linkType: hard + +"async-generator-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-generator-function@npm:1.0.0" + checksum: 74a71a4a2dd7afd06ebb612f6d612c7f4766a351bedffde466023bf6dae629e46b0d2cd38786239e0fbf245de0c7df76035465e16d1213774a0efb22fec0d713 + 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 + +"axe-core@npm:~4.11.1": + version: 4.11.1 + resolution: "axe-core@npm:4.11.1" + checksum: 92b3c79af3695bcebac0e7f3f90f4bc11d2b39ccdc670937290e8dacbc943473713cc06b771dea0563c66d57d93d940ed89e082bfdecccf9dd70782d4bb243c0 + languageName: node + linkType: hard + +"b4a@npm:^1.6.4": + version: 1.8.0 + resolution: "b4a@npm:1.8.0" + peerDependencies: + react-native-b4a: "*" + peerDependenciesMeta: + react-native-b4a: + optional: true + checksum: 92a3addf120a69c26c8dfcda1537eb63e10e682fb44a7f2d78f3347fe12c61b4022152b5a7f16bd71872f34eda84c4b8ce441cbe02a07e9a7cd7c1a3cb0ecf07 + 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 + +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 + languageName: node + linkType: hard + +"bare-events@npm:^2.5.4, bare-events@npm:^2.7.0": + version: 2.8.2 + resolution: "bare-events@npm:2.8.2" + peerDependencies: + bare-abort-controller: "*" + peerDependenciesMeta: + bare-abort-controller: + optional: true + checksum: 97e6fc825bc984363a1f695c9a962b1b9f0ea467baa95d7059565a0aa31f4b5fc0f5eb71c087456ae3a436f39fce14a6147a12dd63aac0ff1d20cc5ad64cd780 + languageName: node + linkType: hard + +"bare-fs@npm:^4.0.1": + version: 4.5.5 + resolution: "bare-fs@npm:4.5.5" + dependencies: + bare-events: ^2.5.4 + bare-path: ^3.0.0 + bare-stream: ^2.6.4 + bare-url: ^2.2.2 + fast-fifo: ^1.3.2 + peerDependencies: + bare-buffer: "*" + peerDependenciesMeta: + bare-buffer: + optional: true + checksum: f4c9cc7cd6b21d7c69c59d0f325e6cdd37ed2c505ef68db77c2ebced10b71a0a37cd89c2301d36b43692ea7b3fcaa025224f04ac7579e07c133e92ec545c2bd4 + languageName: node + linkType: hard + +"bare-os@npm:^3.0.1": + version: 3.7.0 + resolution: "bare-os@npm:3.7.0" + checksum: d770682f46b7b1e19db266b99083d8eeca7a8366850fe48a974130ebf665d25dbaefb10ccc56332f95353f5e3544e40cd771e18ed7568ec83bf175aad3271091 + languageName: node + linkType: hard + +"bare-path@npm:^3.0.0": + version: 3.0.0 + resolution: "bare-path@npm:3.0.0" + dependencies: + bare-os: ^3.0.1 + checksum: 51d559515f332f62cf9c37c38f2640c1b84b5e8c9de454b70baf029f806058cf94c51d6a0dfec0025cc7760f2069dc3e16c82f0d24f4a9ddb18c829bf9c0206d + languageName: node + linkType: hard + +"bare-stream@npm:^2.6.4": + version: 2.8.0 + resolution: "bare-stream@npm:2.8.0" + dependencies: + streamx: ^2.21.0 + teex: ^1.0.1 + peerDependencies: + bare-buffer: "*" + bare-events: "*" + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + checksum: 32d92d59f5de5dec3466ff9c380bf9e44f316c75fb7423e60ea37b5d941fdacf22f0c2e8b548a5e0169500be9a49f76bbcfb12ac89c966aa184bc9861ebda44a + languageName: node + linkType: hard + +"bare-url@npm:^2.2.2": + version: 2.3.2 + resolution: "bare-url@npm:2.3.2" + dependencies: + bare-path: ^3.0.0 + checksum: 7b2a6335a55a010ffcc863f62cc5bfaa216b383bc05a8e7fb30caccb5600e09d403ad482fc671582eba531bbca4a891dba8eefa866f2e2d222b0a72f2460c340 + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.12 + resolution: "brace-expansion@npm:1.1.12" + dependencies: + balanced-match: ^1.0.0 + concat-map: 0.0.1 + checksum: 12cb6d6310629e3048cadb003e1aca4d8c9bb5c67c3c321bafdd7e7a50155de081f78ea3e0ed92ecc75a9015e784f301efc8132383132f4f7904ad1ac529c562 + languageName: node + linkType: hard + +"brace-expansion@npm:^5.0.2": + version: 5.0.3 + resolution: "brace-expansion@npm:5.0.3" + dependencies: + balanced-match: ^4.0.2 + checksum: 8fea33ebbf4eac08d578fad7727c25c18a86e453bbd95d6312ecd87db73effe5607c77ed03d81ad7f56b4d84a4e355abbda939fdcab0d8a820bdb56d6587a7c5 + languageName: node + linkType: hard + +"braces@npm:^3.0.3": + version: 3.0.3 + resolution: "braces@npm:3.0.3" + dependencies: + fill-range: ^7.1.1 + checksum: b95aa0b3bd909f6cd1720ffcf031aeaf46154dd88b4da01f9a1d3f7ea866a79eba76a6d01cbc3c422b2ee5cdc39a4f02491058d5df0d7bf6e6a162a832df1f69 + languageName: node + linkType: hard + +"cacache@npm:^20.0.1": + version: 20.0.3 + resolution: "cacache@npm:20.0.3" + dependencies: + "@npmcli/fs": ^5.0.0 + fs-minipass: ^3.0.0 + glob: ^13.0.0 + lru-cache: ^11.1.0 + minipass: ^7.0.3 + minipass-collect: ^2.0.1 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + p-map: ^7.0.2 + ssri: ^13.0.0 + unique-filename: ^5.0.0 + checksum: 595e6b91d72972d596e1e9ccab8ddbf08b773f27240220b1b5b1b7b3f52173cfbcf095212e5d7acd86c3bd453c28e69b116469889c511615ef3589523d542639 + languageName: node + linkType: hard + +"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: ^1.3.0 + function-bind: ^1.1.2 + checksum: b2863d74fcf2a6948221f65d95b91b4b2d90cfe8927650b506141e669f7d5de65cea191bf788838bc40d13846b7886c5bc5c84ab96c3adbcf88ad69a72fcdc6b + 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 + +"camelize-ts@npm:^3.0.0": + version: 3.0.0 + resolution: "camelize-ts@npm:3.0.0" + checksum: 835f7f79ddec6e6e0364c6a8294ce82586bca5d9443001f28077169181801cb126d8bc608c85504aa6c877de6fe5f7c9533f80996dc81117d865ff92c676d680 + languageName: node + linkType: hard + +"chalk@npm:^4.0.0": + 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 + +"chownr@npm:^3.0.0": + version: 3.0.0 + resolution: "chownr@npm:3.0.0" + checksum: fd73a4bab48b79e66903fe1cafbdc208956f41ea4f856df883d0c7277b7ab29fd33ee65f93b2ec9192fc0169238f2f8307b7735d27c155821d886b84aa97aa8d + 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.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: ~1.0.0 + checksum: 49fa4aeb4916567e33ea81d088f6584749fc90c7abec76fd516bf1c5aa5c79f3584b5ba3de6b86d26ddd64bae5329c4c7479343250cfe71c75bb366eae53bb7c + 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 + +"cross-spawn@npm:^7.0.6": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: ^3.1.0 + shebang-command: ^2.0.0 + which: ^2.0.1 + checksum: 8d306efacaf6f3f60e0224c287664093fa9185680b2d195852ba9a863f85d02dcc737094c6e512175f8ee0161f9b87c73c6826034c2422e39de7d6569cf4503b + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: ^2.1.3 + peerDependenciesMeta: + supports-color: + optional: true + checksum: 4805abd570e601acdca85b6aa3757186084a45cff9b2fa6eee1f3b173caa776b45f478b2a71a572d616d2010cea9211d0ac4a02a610e4c18ac4324bde3760834 + languageName: node + linkType: hard + +"deep-is@npm:^0.1.3": + version: 0.1.4 + resolution: "deep-is@npm:0.1.4" + checksum: edb65dd0d7d1b9c40b2f50219aef30e116cedd6fc79290e740972c132c09106d2e80aa0bc8826673dd5a00222d4179c84b36a790eef63a4c4bca75a37ef90804 + 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 + +"dotenv@npm:^16.4.7": + version: 16.6.1 + resolution: "dotenv@npm:16.6.1" + checksum: e8bd63c9a37f57934f7938a9cf35de698097fadf980cb6edb61d33b3e424ceccfe4d10f37130b904a973b9038627c2646a3365a904b4406514ea94d7f1816b69 + languageName: node + linkType: hard + +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: ^1.0.1 + es-errors: ^1.3.0 + gopd: ^1.2.0 + checksum: 149207e36f07bd4941921b0ca929e3a28f1da7bd6b6ff8ff7f4e2f2e460675af4576eeba359c635723dc189b64cdd4787e0255897d5b135ccc5d15cb8685fc90 + languageName: node + linkType: hard + +"end-of-stream@npm:^1.1.0": + version: 1.4.5 + resolution: "end-of-stream@npm:1.4.5" + dependencies: + once: ^1.4.0 + checksum: 1e0cfa6e7f49887544e03314f9dfc56a8cb6dde910cbb445983ecc2ff426fc05946df9d75d8a21a3a64f2cecfe1bf88f773952029f46756b2ed64a24e95b1fb8 + 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 + +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 0512f4e5d564021c9e3a644437b0155af2679d10d80f21adaf868e64d30efdfbd321631956f20f42d655fedb2e3a027da479fad3fa6048f768eb453a80a5f80a + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: ec1414527a0ccacd7f15f4a3bc66e215f04f595ba23ca75cdae0927af099b5ec865f9f4d33e9d7e86f512f252876ac77d4281a7871531a50678132429b1271b5 + languageName: node + linkType: hard + +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" + dependencies: + es-errors: ^1.3.0 + checksum: 214d3767287b12f36d3d7267ef342bbbe1e89f899cfd67040309fc65032372a8e60201410a99a1645f2f90c1912c8c49c8668066f6bdd954bcd614dda2e3da97 + languageName: node + linkType: hard + +"es-set-tostringtag@npm:^2.1.0": + version: 2.1.0 + resolution: "es-set-tostringtag@npm:2.1.0" + dependencies: + es-errors: ^1.3.0 + get-intrinsic: ^1.2.6 + has-tostringtag: ^1.0.2 + hasown: ^2.0.2 + checksum: 789f35de4be3dc8d11fdcb91bc26af4ae3e6d602caa93299a8c45cf05d36cc5081454ae2a6d3afa09cceca214b76c046e4f8151e092e6fc7feeb5efb9e794fc6 + 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 + +"eslint-plugin-check-file@npm:^3.3.1": + version: 3.3.1 + resolution: "eslint-plugin-check-file@npm:3.3.1" + dependencies: + is-glob: ^4.0.3 + micromatch: ^4.0.8 + peerDependencies: + eslint: ">=9.0.0" + checksum: 32b5c3954d4710901b0365308996432596ae1373e678255e016522bbefe19dfe5510dc9b661db937eeb8f3ebb11efa58b2d39c6554a31ae8e5edd2b864c7a79c + languageName: node + linkType: hard + +"eslint-plugin-playwright@npm:^2.4.0": + version: 2.7.1 + resolution: "eslint-plugin-playwright@npm:2.7.1" + dependencies: + globals: ^17.3.0 + peerDependencies: + eslint: ">=8.40.0" + checksum: c882fcabc4f443d22f3d2dba28f3387cb89feb52d4a05dab928b3907421e19d2ffd4c9c9ea52d3afacc57fcf25d6f593ac95d665baaf002ea9c796c22a52380b + languageName: node + linkType: hard + +"eslint-scope@npm:^8.4.0": + version: 8.4.0 + resolution: "eslint-scope@npm:8.4.0" + dependencies: + esrecurse: ^4.3.0 + estraverse: ^5.2.0 + checksum: cf88f42cd5e81490d549dc6d350fe01e6fe420f9d9ea34f134bb359b030e3c4ef888d36667632e448937fe52449f7181501df48c08200e3d3b0fee250d05364e + languageName: node + linkType: hard + +"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-visitor-keys@npm:^4.2.1": + version: 4.2.1 + resolution: "eslint-visitor-keys@npm:4.2.1" + checksum: 3a77e3f99a49109f6fb2c5b7784bc78f9743b834d238cdba4d66c602c6b52f19ed7bcd0a5c5dbbeae3a8689fd785e76c001799f53d2228b278282cf9f699fff5 + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^5.0.0": + version: 5.0.1 + resolution: "eslint-visitor-keys@npm:5.0.1" + checksum: d6cc6830536ab4a808f25325686c2c27862f27aab0c1ffed39627293b06cee05d95187da113cafd366314ea5be803b456115de71ad625e365020f20e2a6af89b + languageName: node + linkType: hard + +"eslint@npm:^9.39.1, eslint@npm:^9.39.2": + version: 9.39.3 + resolution: "eslint@npm:9.39.3" + dependencies: + "@eslint-community/eslint-utils": ^4.8.0 + "@eslint-community/regexpp": ^4.12.1 + "@eslint/config-array": ^0.21.1 + "@eslint/config-helpers": ^0.4.2 + "@eslint/core": ^0.17.0 + "@eslint/eslintrc": ^3.3.1 + "@eslint/js": 9.39.3 + "@eslint/plugin-kit": ^0.4.1 + "@humanfs/node": ^0.16.6 + "@humanwhocodes/module-importer": ^1.0.1 + "@humanwhocodes/retry": ^0.4.2 + "@types/estree": ^1.0.6 + ajv: ^6.12.4 + chalk: ^4.0.0 + cross-spawn: ^7.0.6 + debug: ^4.3.2 + escape-string-regexp: ^4.0.0 + eslint-scope: ^8.4.0 + eslint-visitor-keys: ^4.2.1 + espree: ^10.4.0 + esquery: ^1.5.0 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^8.0.0 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + ignore: ^5.2.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + json-stable-stringify-without-jsonify: ^1.0.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.3 + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + bin: + eslint: bin/eslint.js + checksum: c242078b30198a1fb358adac08803553f7071bec76138f16977e64a49e0a5bcf7b41aea00fbeede0f7ed86c4c4f5744b2dd9a340567ecc5fdf779f2227651d57 + languageName: node + linkType: hard + +"espree@npm:^10.0.1, espree@npm:^10.4.0": + version: 10.4.0 + resolution: "espree@npm:10.4.0" + dependencies: + acorn: ^8.15.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^4.2.1 + checksum: 5f9d0d7c81c1bca4bfd29a55270067ff9d575adb8c729a5d7f779c2c7b910bfc68ccf8ec19b29844b707440fc159a83868f22c8e87bbf7cbcb225ed067df6c85 + languageName: node + linkType: hard + +"esquery@npm:^1.5.0": + version: 1.7.0 + resolution: "esquery@npm:1.7.0" + dependencies: + estraverse: ^5.1.0 + checksum: 3239792b68cf39fe18966d0ca01549bb15556734f0144308fd213739b0f153671ae916013fce0bca032044a4dbcda98b43c1c667f20c20a54dec3597ac0d7c27 + 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:^5.1.0, estraverse@npm:^5.2.0": + version: 5.3.0 + resolution: "estraverse@npm:5.3.0" + checksum: 072780882dc8416ad144f8fe199628d2b3e7bbc9989d9ed43795d2c90309a2047e6bc5979d7e2322a341163d22cfad9e21f4110597fe487519697389497e4e2b + 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 + +"events-universal@npm:^1.0.0": + version: 1.0.1 + resolution: "events-universal@npm:1.0.1" + dependencies: + bare-events: ^2.7.0 + checksum: fb8451c98535bde30585004303a368d55c38e5bc3ed6aa9b5d29fecaabaf8ec276a33ff77dcc1d1c05eecf83b8161f184cabc9a03b76a06c10e9a4ce827a6abc + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.3 + resolution: "exponential-backoff@npm:3.1.3" + checksum: 471fdb70fd3d2c08a74a026973bdd4105b7832911f610ca67bbb74e39279411c1eed2f2a110c9d41c2edd89459ba58fdaba1c174beed73e7a42d773882dcff82 + 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-json-stable-stringify@npm:^2.0.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": + version: 2.0.6 + resolution: "fast-levenshtein@npm:2.0.6" + checksum: 92cfec0a8dfafd9c7a15fba8f2cc29cd0b62b85f056d99ce448bbcd9f708e18ab2764bda4dd5158364f4145a7c72788538994f0d1787b956ef0d1062b0f7c24c + languageName: node + linkType: hard + +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: bd537daa9d3cd53887eed35efa0eab2dbb1ca408790e10e024120e7a36c6e9ae2b33710cb8381e35def01bc9c1d7eaba746f886338413e68ff6ebaee07b9a6e8 + languageName: node + linkType: hard + +"file-entry-cache@npm:^8.0.0": + version: 8.0.0 + resolution: "file-entry-cache@npm:8.0.0" + dependencies: + flat-cache: ^4.0.0 + checksum: f67802d3334809048c69b3d458f672e1b6d26daefda701761c81f203b80149c35dea04d78ea4238969dd617678e530876722a0634c43031a0957f10cc3ed190f + 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 + +"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:^4.0.0": + version: 4.0.1 + resolution: "flat-cache@npm:4.0.1" + dependencies: + flatted: ^3.2.9 + keyv: ^4.5.4 + checksum: 899fc86bf6df093547d76e7bfaeb900824b869d7d457d02e9b8aae24836f0a99fbad79328cfd6415ee8908f180699bf259dc7614f793447cb14f707caf5996f6 + languageName: node + linkType: hard + +"flatted@npm:^3.2.9": + version: 3.3.3 + resolution: "flatted@npm:3.3.3" + checksum: 8c96c02fbeadcf4e8ffd0fa24983241e27698b0781295622591fc13585e2f226609d95e422bcf2ef044146ffacb6b68b1f20871454eddf75ab3caa6ee5f4a1fe + languageName: node + linkType: hard + +"form-data@npm:^4.0.0, form-data@npm:^4.0.4": + version: 4.0.5 + resolution: "form-data@npm:4.0.5" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.8 + es-set-tostringtag: ^2.1.0 + hasown: ^2.0.2 + mime-types: ^2.1.12 + checksum: af8328413c16d0cded5fccc975a44d227c5120fd46a9e81de8acf619d43ed838414cc6d7792195b30b248f76a65246949a129a4dadd148721948f90cd6d4fb69 + languageName: node + linkType: hard + +"fs-extra@npm:^11.3.2": + version: 11.3.3 + resolution: "fs-extra@npm:11.3.3" + dependencies: + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: fb2acabbd1e04bcaca90eadfe98e6ffba1523b8009afbb9f4c0aae5efbca0bd0bf6c9a6831df5af5aaacb98d3e499898be848fb0c03d31ae7b9d1b053e81c151 + 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 + +"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@patch:fsevents@2.3.2#~builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=df0bf1" + 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 + +"generator-function@npm:^2.0.0": + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 3bf87f7b0230de5d74529677e6c3ceb3b7b5d9618b5a22d92b45ce3876defbaf5a77791b25a61b0fa7d13f95675b5ff67a7769f3b9af33f096e34653519e873d + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.2.6": + version: 1.3.1 + resolution: "get-intrinsic@npm:1.3.1" + dependencies: + async-function: ^1.0.0 + async-generator-function: ^1.0.0 + call-bind-apply-helpers: ^1.0.2 + es-define-property: ^1.0.1 + es-errors: ^1.3.0 + es-object-atoms: ^1.1.1 + function-bind: ^1.1.2 + generator-function: ^2.0.0 + get-proto: ^1.0.1 + gopd: ^1.2.0 + has-symbols: ^1.1.0 + hasown: ^2.0.2 + math-intrinsics: ^1.1.0 + checksum: c02b3b6a445f9cd53e14896303794ac60f9751f58a69099127248abdb0251957174c6524245fc68579dc8e6a35161d3d94c93e665f808274716f4248b269436a + languageName: node + linkType: hard + +"get-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: ^1.0.1 + es-object-atoms: ^1.0.0 + checksum: 4fc96afdb58ced9a67558698b91433e6b037aaa6f1493af77498d7c85b141382cf223c0e5946f334fb328ee85dfe6edd06d218eaf09556f4bc4ec6005d7f5f7b + 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@npm:^13.0.0": + version: 13.0.6 + resolution: "glob@npm:13.0.6" + dependencies: + minimatch: ^10.2.2 + minipass: ^7.1.3 + path-scurry: ^2.0.2 + checksum: 1eb421c696c66af3c26e4845dbdd222d3b982ede17448456b49272722d872e9a91741b50e4e827370c57d17a39a69790061f45033523f085c076d8fcc0f69d2b + languageName: node + linkType: hard + +"globals@npm:^14.0.0": + version: 14.0.0 + resolution: "globals@npm:14.0.0" + checksum: 534b8216736a5425737f59f6e6a5c7f386254560c9f41d24a9227d60ee3ad4a9e82c5b85def0e212e9d92162f83a92544be4c7fd4c902cb913736c10e08237ac + languageName: node + linkType: hard + +"globals@npm:^17.3.0": + version: 17.3.0 + resolution: "globals@npm:17.3.0" + checksum: 4316ace3d0890ac3d72d50bfd723df93fd375cb4b328c503e1fae81dd73e4ab9c76051ae28ce02c2a6b049e9a8149e86e8861d185433d3af65c774976802a718 + languageName: node + linkType: hard + +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: cc6d8e655e360955bdccaca51a12a474268f95bb793fc3e1f2bdadb075f28bfd1fd988dab872daf77a61d78cbaf13744bc8727a17cfb1d150d76047d805375f3 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 + 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-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: b2316c7302a0e8ba3aaba215f834e96c22c86f192e7310bdf689dd0e6999510c89b00fbc5742571507cebf25764d68c988b3a0da217369a73596191ac0ce694b + languageName: node + linkType: hard + +"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 + +"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 + +"hpagent@npm:^1.2.0": + version: 1.2.0 + resolution: "hpagent@npm:1.2.0" + checksum: b029da695edae438cee4da2a437386f9db4ac27b3ceb7306d02e1b586c9c194741ed2e943c8a222e0cfefaf27ee3f863aca7ba1721b0950a2a19bf25bc0d85e2 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.2.0 + resolution: "http-cache-semantics@npm:4.2.0" + checksum: 7a7246ddfce629f96832791176fd643589d954e6f3b49548dadb4290451961237fab8fcea41cd2008fe819d95b41c1e8b97f47d088afc0a1c81705287b4ddbcc + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + 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 + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.6 + resolution: "https-proxy-agent@npm:7.0.6" + dependencies: + agent-base: ^7.1.2 + debug: 4 + checksum: b882377a120aa0544846172e5db021fa8afbf83fea2a897d397bd2ddd8095ab268c24bc462f40a15f2a8c600bf4aa05ce52927f70038d4014e68aefecfa94e8d + languageName: node + linkType: hard + +"iconv-lite@npm:^0.7.2": + version: 0.7.2 + resolution: "iconv-lite@npm:0.7.2" + dependencies: + safer-buffer: ">= 2.1.2 < 3.0.0" + checksum: faf884c1f631a5d676e3e64054bed891c7c5f616b790082d99ccfbfd017c661a39db8009160268fd65fae57c9154d4d491ebc9c301f3446a078460ef114dc4b8 + languageName: node + linkType: hard + +"ignore@npm:^5.2.0": + version: 5.3.2 + resolution: "ignore@npm:5.3.2" + checksum: 2acfd32a573260ea522ea0bfeff880af426d68f6831f973129e2ba7363f422923cf53aab62f8369cbf4667c7b25b6f8a3761b34ecdb284ea18e87a5262a865be + languageName: node + linkType: hard + +"ignore@npm:^7.0.5": + version: 7.0.5 + resolution: "ignore@npm:7.0.5" + checksum: d0862bf64d3d58bf34d5fb0a9f725bec9ca5ce8cd1aecc8f28034269e8f69b8009ffd79ca3eda96962a6a444687781cd5efdb8c7c8ddc0a6996e36d31c217f14 + languageName: node + linkType: hard + +"import-fresh@npm:^3.2.1": + version: 3.3.1 + resolution: "import-fresh@npm:3.3.1" + dependencies: + parent-module: ^1.0.0 + resolve-from: ^4.0.0 + checksum: a06b19461b4879cc654d46f8a6244eb55eb053437afd4cbb6613cad6be203811849ed3e4ea038783092879487299fda24af932b86bdfff67c9055ba3612b8c87 + 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 + +"ip-address@npm:^10.0.1": + version: 10.1.0 + resolution: "ip-address@npm:10.1.0" + checksum: 76b1abcdf52a32e2e05ca1f202f3a8ab8547e5651a9233781b330271bd7f1a741067748d71c4cbb9d9906d9f1fa69e7ddc8b4a11130db4534fdab0e908c84e0d + 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-glob@npm:^4.0.0, is-glob@npm:^4.0.3": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: ^2.1.1 + checksum: d381c1319fcb69d341cc6e6c7cd588e17cd94722d9a32dbd60660b993c4fb7d0f19438674e68dfec686d09b7c73139c9166b47597f846af387450224a8101ab4 + 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 + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 + languageName: node + linkType: hard + +"isexe@npm:^4.0.0": + version: 4.0.0 + resolution: "isexe@npm:4.0.0" + checksum: 2ead327ef596042ef9c9ec5f236b316acfaedb87f4bb61b3c3d574fb2e9c8a04b67305e04733bde52c24d9622fdebd3270aadb632adfbf9cadef88fe30f479e5 + languageName: node + linkType: hard + +"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 + +"jose@npm:^6.1.3": + version: 6.1.3 + resolution: "jose@npm:6.1.3" + checksum: 7f51c7e77f82b70ef88ede9fd1760298bc0ffbf143b9d94f78c08462987ae61864535c1856bc6c26d335f857c7d41f4fffcc29134212c19ea929ce34a4c790f0 + languageName: node + linkType: hard + +"js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1": + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" + dependencies: + argparse: ^2.0.1 + bin: + js-yaml: bin/js-yaml.js + checksum: ea2339c6930fe048ec31b007b3c90be2714ab3e7defcc2c27ebf30c74fd940358f29070b4345af0019ef151875bf3bc3f8644bea1bab0372652b5044813ac02d + languageName: node + linkType: hard + +"jsep@npm:^1.4.0": + version: 1.4.0 + resolution: "jsep@npm:1.4.0" + checksum: 8e7af5ecb91483b227092b87a3e85b5df3e848dbe6f201b19efcb18047567530d21dfeecb0978e09d1f66554fcfaed84176819eeacdfc86f61dc05c40c18f824 + languageName: node + linkType: hard + +"json-buffer@npm:3.0.1": + version: 3.0.1 + resolution: "json-buffer@npm:3.0.1" + checksum: 9026b03edc2847eefa2e37646c579300a1f3a4586cfb62bf857832b60c852042d0d6ae55d1afb8926163fa54c2b01d83ae24705f34990348bdac6273a29d4581 + 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-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 + +"jsonfile@npm:^6.0.1": + version: 6.2.0 + resolution: "jsonfile@npm:6.2.0" + dependencies: + graceful-fs: ^4.1.6 + universalify: ^2.0.0 + dependenciesMeta: + graceful-fs: + optional: true + checksum: c3028ec5c770bb41290c9bb9ca04bdd0a1b698ddbdf6517c9453d3f90fc9e000c9675959fb46891d317690a93c62de03ff1735d8dbe02be83e51168ce85815d3 + languageName: node + linkType: hard + +"jsonpath-plus@npm:^10.3.0": + version: 10.4.0 + resolution: "jsonpath-plus@npm:10.4.0" + dependencies: + "@jsep-plugin/assignment": ^1.3.0 + "@jsep-plugin/regex": ^1.0.4 + jsep: ^1.4.0 + bin: + jsonpath: bin/jsonpath-cli.js + jsonpath-plus: bin/jsonpath-cli.js + checksum: c736fb09a477f98eb8ceb05a88b71b426ae636d6f9454da32f2bde802c5c0c2f5927f186786024ff9b3360543467b5423d6849d66f42864cc6e8d1ec16017cfb + languageName: node + linkType: hard + +"keyv@npm:^4.5.4": + version: 4.5.4 + resolution: "keyv@npm:4.5.4" + dependencies: + json-buffer: 3.0.1 + checksum: 74a24395b1c34bd44ad5cb2b49140d087553e170625240b86755a6604cd65aa16efdbdeae5cdb17ba1284a0fbb25ad06263755dbc71b8d8b06f74232ce3cdd72 + 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 + +"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.clonedeepwith@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.clonedeepwith@npm:4.5.0" + checksum: 9fbf4ebfa04b381df226a2298eba680327bea3d0d5d19c5118de7ae218fd219186e30e9fd0d33b13729f34ffbc83c1cf09cb27aff265ba94cb602b8a2b1e71c9 + 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.mergewith@npm:^4.6.2": + version: 4.6.2 + resolution: "lodash.mergewith@npm:4.6.2" + checksum: a6db2a9339752411f21b956908c404ec1e088e783a65c8b29e30ae5b3b6384f82517662d6f425cc97c2070b546cc2c7daaa8d33f78db7b6e9be06cd834abdeb8 + languageName: node + linkType: hard + +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": + version: 11.2.6 + resolution: "lru-cache@npm:11.2.6" + checksum: 26fe602c92a0cb7a8da9a85db162ddd810d84507d9c4ef8d95a785a805648f9579e1148aaeac260f6b6315197bcf27c1b7e60a0a066621d6e95b3587699a0c70 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^15.0.0": + version: 15.0.4 + resolution: "make-fetch-happen@npm:15.0.4" + dependencies: + "@gar/promise-retry": ^1.0.0 + "@npmcli/agent": ^4.0.0 + cacache: ^20.0.1 + http-cache-semantics: ^4.1.1 + minipass: ^7.0.2 + minipass-fetch: ^5.0.0 + minipass-flush: ^1.0.5 + minipass-pipeline: ^1.2.4 + negotiator: ^1.0.0 + proc-log: ^6.0.0 + ssri: ^13.0.0 + checksum: eb875e1fbdf58b4d6d7c6c2ee97a2521b407f85d1fb2c0e6bc2c2d9d13fa34c3198eff3512589dc26e8fb32684e4e52f3c9d844a704ac96b393559a88b5e57ca + languageName: node + linkType: hard + +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 0e513b29d120f478c85a70f49da0b8b19bc638975eca466f2eeae0071f3ad00454c621bf66e16dd435896c208e719fc91ad79bbfba4e400fe0b372e7c1c9c9a2 + languageName: node + linkType: hard + +"micromatch@npm:^4.0.8": + 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 + +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 0d99a03585f8b39d68182803b12ac601d9c01abfa28ec56204fa330bc9f3d1c5e14beb049bafadb3dbdf646dfb94b87e24d4ec7b31b7279ef906a8ea9b6a513f + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: 1.52.0 + checksum: 89a5b7f1def9f3af5dad6496c5ed50191ae4331cc5389d7c521c8ad28d5fdad2d06fd81baf38fed813dc4e46bb55c8145bb0ff406330818c9cf712fb2e9b3836 + languageName: node + linkType: hard + +"minimatch@npm:^10.2.2": + version: 10.2.4 + resolution: "minimatch@npm:10.2.4" + dependencies: + brace-expansion: ^5.0.2 + checksum: 56dce6b04c6b30b500d81d7a29822c108b7d58c46696ec7332d04a2bd104a5cb69e5c7ce93e1783dc66d61400d831e6e226ca101ac23665aff32ca303619dc3d + languageName: node + linkType: hard + +"minimatch@npm:^3.1.2, minimatch@npm:^3.1.3": + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" + dependencies: + brace-expansion: ^1.1.7 + checksum: 47ef6f412c08be045a7291d11b1c40777925accf7252dc6d3caa39b1bfbb3a7ea390ba7aba464d762d783265c644143d2c8a204e6b5763145024d52ee65a1941 + 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:^5.0.0": + version: 5.0.2 + resolution: "minipass-fetch@npm:5.0.2" + dependencies: + iconv-lite: ^0.7.2 + minipass: ^7.0.3 + minipass-sized: ^2.0.0 + minizlib: ^3.0.1 + dependenciesMeta: + iconv-lite: + optional: true + checksum: d4dfdd9700fc8aba445834f75f2abaf9e5d404c10eda06e2db4a8ba89fc66a26956d19703d0edf9be864cb30dec22356d343509ad0a105446516c0ead4330328 + 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:^2.0.0": + version: 2.0.0 + resolution: "minipass-sized@npm:2.0.0" + dependencies: + minipass: ^7.1.2 + checksum: 1a1fd251aef4e24050a04ea03fdc0514960f7304a374fd01f352bfdb72c0a2c084ad05d63e76011c181cadfb38dbf487f8782e1e778337f6a099ac2da26b6d5d + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: ^4.0.0 + checksum: a30d083c8054cee83cdcdc97f97e4641a3f58ae743970457b1489ce38ee1167b3aaf7d815cd39ec7a99b9c40397fd4f686e83750e73e652b21cb516f6d845e48 + languageName: node + linkType: hard + +"minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 2ede17c0bf8fec499be3360fd07f0ec7666189e3907320a9b653f1530cf84af98928c5b12d80bfb75f321833bf2e97785b940540213ebdafe97a5f10327e664d + languageName: node + linkType: hard + +"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" + dependencies: + minipass: ^7.1.2 + checksum: a15e6f0128f514b7d41a1c68ce531155447f4669e32d279bba1c1c071ef6c2abd7e4d4579bb59ccc2ed1531346749665968fdd7be8d83eb6b6ae2fe1f3d370a7 + languageName: node + linkType: hard + +"ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d + 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 + +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 20ebfe79b2d2e7cf9cbc8239a72662b584f71164096e6e8896c8325055497c96f6b80cd22c258e8a2f2aa382a787795ec3ee8b37b422a302c7d4381b0d5ecfbb + languageName: node + linkType: hard + +"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-gyp@npm:latest": + version: 12.2.0 + resolution: "node-gyp@npm:12.2.0" + dependencies: + env-paths: ^2.2.0 + exponential-backoff: ^3.1.1 + graceful-fs: ^4.2.6 + make-fetch-happen: ^15.0.0 + nopt: ^9.0.0 + proc-log: ^6.0.0 + semver: ^7.3.5 + tar: ^7.5.4 + tinyglobby: ^0.2.12 + which: ^6.0.0 + bin: + node-gyp: bin/node-gyp.js + checksum: d4ce0acd08bd41004f45e10cef468f4bd15eaafb3acc388a0c567416e1746dc005cc080b8a3495e4e2ae2eed170a2123ff622c2d6614062f4a839837dcf1dd9d + languageName: node + linkType: hard + +"nopt@npm:^9.0.0": + version: 9.0.0 + resolution: "nopt@npm:9.0.0" + dependencies: + abbrev: ^4.0.0 + bin: + nopt: bin/nopt.js + checksum: 7a5d9ab0629eaec1944a95438cc4efa6418ed2834aa8eb21a1bea579a7d8ac3e30120131855376a96ef59ab0e23ad8e0bc94d3349770a95e5cb7119339f7c7fb + languageName: node + linkType: hard + +"oauth4webapi@npm:^3.8.4": + version: 3.8.5 + resolution: "oauth4webapi@npm:3.8.5" + checksum: 4e4915e38e12e1807f84b185676b2a7bd79ebdeb46b35dc772d4abf0b0ef0a690613558298db6453006bc350ca883ff6bdb22717fe3d7e5101155562b30b0df1 + languageName: node + linkType: hard + +"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 + +"openid-client@npm:^6.1.3": + version: 6.8.2 + resolution: "openid-client@npm:6.8.2" + dependencies: + jose: ^6.1.3 + oauth4webapi: ^3.8.4 + checksum: 35f76a03f33a1d87f8ebeaebd6b6263eee17905823ae448f590e82e5a9168e3fec98e0788e541d251352b17560c7a20ce90acee798ecae907cea6fb675a4eab8 + 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 + +"otplib@npm:12.0.1": + version: 12.0.1 + resolution: "otplib@npm:12.0.1" + dependencies: + "@otplib/core": ^12.0.1 + "@otplib/preset-default": ^12.0.1 + "@otplib/preset-v11": ^12.0.1 + checksum: 4a1b91cf1b8e920b50ad4bac2ef2a89126630c62daf68e9b32ff15106b2551db905d3b979955cf5f8f114da0a8883cec3d636901d65e793c1745bb4174e2a572 + languageName: node + linkType: hard + +"p-limit@npm:^3.0.2": + 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:^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:^7.0.2": + version: 7.0.4 + resolution: "p-map@npm:7.0.4" + checksum: 4be2097e942f2fd3a4f4b0c6585c721f23851de8ad6484d20c472b3ea4937d5cd9a59914c832b1bceac7bf9d149001938036b82a52de0bc381f61ff2d35d26a5 + 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 + +"path-exists@npm:^4.0.0": + version: 4.0.0 + resolution: "path-exists@npm:4.0.0" + checksum: 505807199dfb7c50737b057dd8d351b82c033029ab94cb10a657609e00c1bc53b951cfdbccab8de04c5584d5eff31128ce6afd3db79281874a5ef2adbba55ed1 + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 + languageName: node + linkType: hard + +"path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" + dependencies: + lru-cache: ^11.0.0 + minipass: ^7.1.2 + checksum: a723afe86e342e19dd1b49ce4f5b64a9a84b1e2e07ffc62f051c11623ecd461b1bf1599eee1ecacfce03dda8b6bb866a5df80c0ded45375d258ff22f631920a7 + languageName: node + linkType: hard + +"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.3": + version: 4.0.3 + resolution: "picomatch@npm:4.0.3" + checksum: 6817fb74eb745a71445debe1029768de55fd59a42b75606f478ee1d0dc1aa6e78b711d041a7c9d5550e042642029b7f373dc1a43b224c4b7f12d23436735dba0 + languageName: node + linkType: hard + +"playwright-core@npm:1.57.0": + version: 1.57.0 + resolution: "playwright-core@npm:1.57.0" + bin: + playwright-core: cli.js + checksum: 960e80d6ec06305b11a3ca9e78e8e4201cc17f37dd37279cb6fece4df43d74bf589833f4f94535fadd284b427f98c5f1cf09368e22f0f00b6a9477571ce6b03b + languageName: node + linkType: hard + +"playwright@npm:1.57.0": + version: 1.57.0 + resolution: "playwright@npm:1.57.0" + dependencies: + fsevents: 2.3.2 + playwright-core: 1.57.0 + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 176fd9fd890f390e0aa00d42697b70072d534243b15467d9430f3af329e77b3225b67a0afa12ea76fb440300dabd92d4cf040baf5edceee8eeff0ee1590ae5b7 + 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 + +"prettier@npm:^3.7.4": + version: 3.8.1 + resolution: "prettier@npm:3.8.1" + bin: + prettier: bin/prettier.cjs + checksum: 36fe4ecd95751aa17fea70b48afd5086e88002988238112fc1be30a5307af6983e1833be790b0cc1c54702b71f73b12bfec12c05166d7619e3151ab221654297 + languageName: node + linkType: hard + +"proc-log@npm:^6.0.0": + version: 6.1.0 + resolution: "proc-log@npm:6.1.0" + checksum: ac450ff8244e95b0c9935b52d629fef92ae69b7e39aea19972a8234259614d644402dd62ce9cb094f4a637d8a4514cba90c1456ad785a40ad5b64d502875a817 + 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 + +"pump@npm:^3.0.0": + version: 3.0.3 + resolution: "pump@npm:3.0.3" + dependencies: + end-of-stream: ^1.1.0 + once: ^1.3.1 + checksum: 52843fc933b838c0330f588388115a1b28ef2a5ffa7774709b142e35431e8ab0c2edec90de3fa34ebb72d59fef854f151eea7dfc211b6dcf586b384556bd2f39 + languageName: node + linkType: hard + +"punycode@npm:^2.1.0": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: bb0a0ceedca4c3c57a9b981b90601579058903c62be23c5e8e843d2c2d4148a3ecf029d5133486fb0e1822b098ba8bba09e89d6b21742d02fa26bda6441a6fb2 + languageName: node + linkType: hard + +"rbac-e2e-tests@workspace:.": + version: 0.0.0-use.local + resolution: "rbac-e2e-tests@workspace:." + dependencies: + "@backstage-community/plugin-rbac-common": 1.23.0 + "@eslint/js": ^9.39.2 + "@playwright/test": 1.57.0 + "@red-hat-developer-hub/e2e-test-utils": 1.1.15 + "@types/node": ^24.10.1 + dotenv: ^16.4.7 + eslint: ^9.39.2 + eslint-plugin-check-file: ^3.3.1 + eslint-plugin-playwright: ^2.4.0 + prettier: ^3.7.4 + typescript: ^5.9.3 + typescript-eslint: ^8.50.0 + languageName: unknown + linkType: soft + +"resolve-from@npm:^4.0.0": + version: 4.0.0 + resolution: "resolve-from@npm:4.0.0" + checksum: f4ba0b8494846a5066328ad33ef8ac173801a51739eb4d63408c847da9a2e1c1de1e6cbbf72699211f3d13f8fc1325648b169bd15eb7da35688e30a5fb0e4a7f + 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 + +"retry@npm:^0.13.1": + version: 0.13.1 + resolution: "retry@npm:0.13.1" + checksum: 47c4d5be674f7c13eee4cfe927345023972197dbbdfba5d3af7e461d13b44de1bfd663bfc80d2f601f8ef3fc8164c16dd99655a221921954a65d044a2fc1233b + languageName: node + linkType: hard + +"rfc4648@npm:^1.3.0": + version: 1.5.4 + resolution: "rfc4648@npm:1.5.4" + checksum: 4bfd555f16b8ed1bceb3cf4d79242cf3253a2600de51f45ac19549f366b4b8edc0d9b8feaae778059c137f1d10ebd87e125fc839636f6267a7b6badaa95a9f00 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 + languageName: node + linkType: hard + +"semver@npm:^7.3.5, semver@npm:^7.7.3": + version: 7.7.4 + resolution: "semver@npm:7.7.4" + bin: + semver: bin/semver.js + checksum: 9b4a6a58e98b9723fafcafa393c9d4e8edefaa60b8dfbe39e30892a3604cf1f45f52df9cfb1ae1a22b44c8b3d57fec8a9bb7b3e1645431587cb272399ede152e + 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:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222 + languageName: node + linkType: hard + +"signal-exit@npm:^3.0.2": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 + 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 + +"socks-proxy-agent@npm:^8.0.3, socks-proxy-agent@npm:^8.0.4": + version: 8.0.5 + resolution: "socks-proxy-agent@npm:8.0.5" + dependencies: + agent-base: ^7.1.2 + debug: ^4.3.4 + socks: ^2.8.3 + checksum: b4fbcdb7ad2d6eec445926e255a1fb95c975db0020543fbac8dfa6c47aecc6b3b619b7fb9c60a3f82c9b2969912a5e7e174a056ae4d98cb5322f3524d6036e1d + languageName: node + linkType: hard + +"socks@npm:^2.8.3": + version: 2.8.7 + resolution: "socks@npm:2.8.7" + dependencies: + ip-address: ^10.0.1 + smart-buffer: ^4.2.0 + checksum: 4bbe2c88cf0eeaf49f94b7f11564a99b2571bde6fd1e714ff95b38f89e1f97858c19e0ab0e6d39eb7f6a984fa67366825895383ed563fe59962a1d57a1d55318 + languageName: node + linkType: hard + +"ssri@npm:^13.0.0": + version: 13.0.1 + resolution: "ssri@npm:13.0.1" + dependencies: + minipass: ^7.0.3 + checksum: 42acbdbd485e9a5a198de2198b6fd474d1e84bff6bea5d95aa0a8aa26ea78ce44f2097ac481e767f0406de7ceccfa4669584116d4fcf2d4e2dba7034d7c34930 + 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 + +"streamx@npm:^2.12.5, streamx@npm:^2.15.0, streamx@npm:^2.21.0": + version: 2.23.0 + resolution: "streamx@npm:2.23.0" + dependencies: + events-universal: ^1.0.0 + fast-fifo: ^1.3.2 + text-decoder: ^1.1.0 + checksum: d57de47db76ffd926842afab562fc8b139d7333269c6db11da5a233e8524cd326161b48bdddd28cefc010cec0b143af04dbb6f5e92d5034839ce7428928dfcae + languageName: node + linkType: hard + +"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 + +"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 + +"tar-fs@npm:^3.0.9": + version: 3.1.1 + resolution: "tar-fs@npm:3.1.1" + dependencies: + bare-fs: ^4.0.1 + bare-path: ^3.0.0 + pump: ^3.0.0 + tar-stream: ^3.1.5 + dependenciesMeta: + bare-fs: + optional: true + bare-path: + optional: true + checksum: eaae4c5410897e00bd03c44bcaf9bad43f793d1dd8fa2fe0ba808c809c8b169d6f40c093eab0ce20e12e9b6b8bf63f11438b2a6a8695c1dfc441fa71a5013896 + languageName: node + linkType: hard + +"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:^7.5.4": + version: 7.5.9 + resolution: "tar@npm:7.5.9" + dependencies: + "@isaacs/fs-minipass": ^4.0.0 + chownr: ^3.0.0 + minipass: ^7.1.2 + minizlib: ^3.1.0 + yallist: ^5.0.0 + checksum: 26fbbdf536895814167d03e4883f80febb6520729169c54d0f29ee8a163557283862752493f0e5b60800a6f3608aac3250c41fac8e20a4f056ba4fa63f3dbad7 + languageName: node + linkType: hard + +"teex@npm:^1.0.1": + version: 1.0.1 + resolution: "teex@npm:1.0.1" + dependencies: + streamx: ^2.12.5 + checksum: 36bf7ce8bb5eb428ad7b14b695ee7fb0a02f09c1a9d8181cc42531208543a920b299d711bf78dad4ff9bcf36ac437ae8e138053734746076e3e0e7d6d76eef64 + languageName: node + linkType: hard + +"text-decoder@npm:^1.1.0": + version: 1.2.7 + resolution: "text-decoder@npm:1.2.7" + dependencies: + b4a: ^1.6.4 + checksum: a544f8490806675986e9703cda2d0809d7bd010adf0cc19ac9975791912ea9e6998cd4696d7d7e9392c5648e660111a865c983e8adfeb29699fab471548f82a3 + languageName: node + linkType: hard + +"thirty-two@npm:^1.0.2": + version: 1.0.2 + resolution: "thirty-two@npm:1.0.2" + checksum: f6700b31d16ef942fdc0d14daed8a2f69ea8b60b0e85db8b83adf58d84bbeafe95a17d343ab55efaae571bb5148b62fc0ee12b04781323bf7af7d7e9693eec76 + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: ^6.5.0 + picomatch: ^4.0.3 + checksum: 0e33b8babff966c6ab86e9b825a350a6a98a63700fa0bb7ae6cf36a7770a508892383adc272f7f9d17aaf46a9d622b455e775b9949a3f951eaaf5dfb26331d44 + 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 + +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 726321c5eaf41b5002e17ffbd1fb7245999a073e8979085dacd47c4b4e8068ff5777142fc6726d6ca1fd2ff16921b48788b87225cbc57c72636f6efa8efbffe3 + languageName: node + linkType: hard + +"ts-api-utils@npm:^2.4.0": + version: 2.4.0 + resolution: "ts-api-utils@npm:2.4.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: beae72a4fa22a7cc91a8a0f3dfb487d72e30f06ac50ff72f327d061dea2d4940c6451d36578d949caad3893d4d2c7d42d53b7663597ccda54ad32cdb842c3e34 + 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 + +"typescript-eslint@npm:^8.48.1, typescript-eslint@npm:^8.50.0": + version: 8.56.1 + resolution: "typescript-eslint@npm:8.56.1" + dependencies: + "@typescript-eslint/eslint-plugin": 8.56.1 + "@typescript-eslint/parser": 8.56.1 + "@typescript-eslint/typescript-estree": 8.56.1 + "@typescript-eslint/utils": 8.56.1 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: aa153347f6a2b0be1d2b522927d8d71c8159d6cf7ddeb6f3f9ca94efd70e3872ee7406a28c92b90df920b219af665411237fe1c92614049c214f3e9769df119b + languageName: node + linkType: hard + +"typescript@npm:^5.9.3": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 0d0ffb84f2cd072c3e164c79a2e5a1a1f4f168e84cb2882ff8967b92afe1def6c2a91f6838fb58b168428f9458c57a2ba06a6737711fdd87a256bbe83e9a217f + languageName: node + linkType: hard + +"typescript@patch:typescript@^5.9.3#~builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#~builtin::version=5.9.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: a5a6dc399d3761ded54192031f11d3ad5df8001c7febe3fbbc8098efcb552cdf8f2f402b3618c56dafcd04fef63dee005f4900f608e185404caedc46480539ed + languageName: node + linkType: hard + +"undici-types@npm:~7.16.0": + version: 7.16.0 + resolution: "undici-types@npm:7.16.0" + checksum: 1ef68fc6c5bad200c8b6f17de8e5bc5cfdcadc164ba8d7208cd087cfa8583d922d8316a7fd76c9a658c22b4123d3ff847429185094484fbc65377d695c905857 + languageName: node + linkType: hard + +"undici-types@npm:~7.18.0": + version: 7.18.2 + resolution: "undici-types@npm:7.18.2" + checksum: 23da306c8366574adec305b06a8519ab5c7d09e3f5d16c1a98709a34fae17da09ec95198f30f86c00055e02efa8bfcc843e84e8aebeb9b8d6bb3e06afccae07a + languageName: node + linkType: hard + +"unique-filename@npm:^5.0.0": + version: 5.0.0 + resolution: "unique-filename@npm:5.0.0" + dependencies: + unique-slug: ^6.0.0 + checksum: a5f67085caef74bdd2a6869a200ed5d68d171f5cc38435a836b5fd12cce4e4eb55e6a190298035c325053a5687ed7a3c96f0a91e82215fd14729769d9ac57d9b + languageName: node + linkType: hard + +"unique-slug@npm:^6.0.0": + version: 6.0.0 + resolution: "unique-slug@npm:6.0.0" + dependencies: + imurmurhash: ^0.1.4 + checksum: ad6cf238b10292d944521714d31bc9f3ca79fa80cb7a154aad183056493f98e85de669412c6bbfe527ffa9bdeff36d3dd4d5bccaf562c794f2580ab11932b691 + 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 + +"uri-js@npm:^4.2.2": + version: 4.4.1 + resolution: "uri-js@npm:4.4.1" + dependencies: + punycode: ^2.1.0 + checksum: 7167432de6817fe8e9e0c9684f1d2de2bb688c94388f7569f7dbdb1587c9f4ca2a77962f134ec90be0cc4d004c939ff0d05acc9f34a0db39a3c797dada262633 + languageName: node + linkType: hard + +"url-template@npm:^3.1.1": + version: 3.1.1 + resolution: "url-template@npm:3.1.1" + checksum: ac09daaeaec55a6b070b838ed161d66b050a46fc12ac251cb2db1ce356e786cfb117ee4391d943aaaa757971c509a0142b3cd83dfd8cc3d7b6d90a99d001a5f9 + 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 + +"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@npm:^2.0.1": + 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:^6.0.0": + version: 6.0.1 + resolution: "which@npm:6.0.1" + dependencies: + isexe: ^4.0.0 + bin: + node-which: bin/which.js + checksum: dbea77c7d3058bf6c78bf9659d2dce4d2b57d39a15b826b2af6ac2e5a219b99dc8a831b79fdbc453c0598adb4f3f84cf9c2491fd52beb9f5d2dececcad117f68 + languageName: node + linkType: hard + +"word-wrap@npm:^1.2.5": + version: 1.2.5 + resolution: "word-wrap@npm:1.2.5" + checksum: f93ba3586fc181f94afdaff3a6fef27920b4b6d9eaefed0f428f8e07adea2a7f54a5f2830ce59406c8416f033f86902b91eb824072354645eea687dff3691ccb + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 + languageName: node + linkType: hard + +"ws@npm:^8.18.2": + version: 8.19.0 + resolution: "ws@npm:8.19.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 7a426122c373e053a65a2affbcdcdbf8f643ba0265577afd4e08595397ca244c05de81570300711e2363a9dab5aea3ae644b445bc7468b1ebbb51bfe2efb20e1 + 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 + +"yocto-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "yocto-queue@npm:0.1.0" + checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 + languageName: node + linkType: hard + +"zx@npm:^8.8.5": + version: 8.8.5 + resolution: "zx@npm:8.8.5" + bin: + zx: build/cli.js + checksum: bf3bb23ae0d0a1198ea53a7ac7e341900b33251aeb4ed657dcbfcc76ed7b0411bd47cc6bdaf67f7b2b79f83339e25d1e47bbf359a8ededfdc33792fb22a92c4c + languageName: node + linkType: hard