From 2817ebb4fdd8985103460feb2b75972f1654b26c Mon Sep 17 00:00:00 2001 From: Dominika Zemanovicova Date: Mon, 14 Apr 2025 09:06:55 +0200 Subject: [PATCH] Introduce rbac auditor tests Signed-off-by: Dominika Zemanovicova --- e2e-tests/playwright.config.ts | 12 +- .../e2e/audit-log/auditor-rbac.spec.ts | 341 ++++++++++++++++++ .../playwright/e2e/audit-log/catalog.spec.ts | 92 +++-- .../playwright/e2e/audit-log/log-utils.ts | 76 ++-- e2e-tests/playwright/e2e/audit-log/logs.ts | 39 +- .../playwright/e2e/audit-log/scaffold.spec.ts | 66 ++-- .../playwright/e2e/plugins/rbac/rbac.spec.ts | 2 +- e2e-tests/playwright/support/api/rbac-api.ts | 36 +- 8 files changed, 520 insertions(+), 144 deletions(-) create mode 100644 e2e-tests/playwright/e2e/audit-log/auditor-rbac.spec.ts diff --git a/e2e-tests/playwright.config.ts b/e2e-tests/playwright.config.ts index 30d4fbf321..4b54afd16f 100644 --- a/e2e-tests/playwright.config.ts +++ b/e2e-tests/playwright.config.ts @@ -55,7 +55,7 @@ export default defineConfig({ name: "showcase", testIgnore: [ "**/playwright/e2e/plugins/rbac/**/*.spec.ts", - "**/playwright/e2e/plugins/analytics/analytics-disabled-rbac.spec.ts", + "**/playwright/e2e/**/*-rbac.spec.ts", "**/playwright/e2e/verify-tls-config-with-external-postgres-db.spec.ts", "**/playwright/e2e/authProviders/**/*.spec.ts", "**/playwright/e2e/plugins/bulk-import.spec.ts", @@ -68,7 +68,7 @@ export default defineConfig({ name: "showcase-rbac", testMatch: [ "**/playwright/e2e/plugins/rbac/**/*.spec.ts", - "**/playwright/e2e/plugins/analytics/analytics-disabled-rbac.spec.ts", + "**/playwright/e2e/**/*-rbac.spec.ts", "**/playwright/e2e/verify-tls-config-with-external-postgres-db.spec.ts", "**/playwright/e2e/plugins/bulk-import.spec.ts", ], @@ -100,7 +100,7 @@ export default defineConfig({ testIgnore: [ "**/playwright/e2e/smoke-test.spec.ts", "**/playwright/e2e/plugins/rbac/**/*.spec.ts", - "**/playwright/e2e/plugins/analytics/analytics-disabled-rbac.spec.ts", + "**/playwright/e2e/**/*-rbac.spec.ts", "**/playwright/e2e/verify-tls-config-with-external-postgres-db.spec.ts", "**/playwright/e2e/authProviders/**/*.spec.ts", "**/playwright/e2e/plugins/bulk-import.spec.ts", @@ -118,7 +118,7 @@ export default defineConfig({ dependencies: ["smoke-test"], testMatch: [ "**/playwright/e2e/plugins/rbac/**/*.spec.ts", - "**/playwright/e2e/plugins/analytics/analytics-disabled-rbac.spec.ts", + "**/playwright/e2e/**/*-rbac.spec.ts", "**/playwright/e2e/plugins/bulk-import.spec.ts", ], }, @@ -126,7 +126,7 @@ export default defineConfig({ name: "showcase-operator", testIgnore: [ "**/playwright/e2e/plugins/rbac/**/*.spec.ts", - "**/playwright/e2e/plugins/analytics/analytics-disabled-rbac.spec.ts", + "**/playwright/e2e/**/*-rbac.spec.ts", "**/playwright/e2e/verify-tls-config-with-external-postgres-db.spec.ts", "**/playwright/e2e/authProviders/**/*.spec.ts", "**/playwright/e2e/plugins/bulk-import.spec.ts", @@ -142,7 +142,7 @@ export default defineConfig({ name: "showcase-operator-rbac", testMatch: [ "**/playwright/e2e/plugins/rbac/**/*.spec.ts", - "**/playwright/e2e/plugins/analytics/analytics-disabled-rbac.spec.ts", + "**/playwright/e2e/**/*-rbac.spec.ts", "**/playwright/e2e/plugins/bulk-import.spec.ts", ], }, diff --git a/e2e-tests/playwright/e2e/audit-log/auditor-rbac.spec.ts b/e2e-tests/playwright/e2e/audit-log/auditor-rbac.spec.ts new file mode 100644 index 0000000000..e18b5cc66a --- /dev/null +++ b/e2e-tests/playwright/e2e/audit-log/auditor-rbac.spec.ts @@ -0,0 +1,341 @@ +import { test } from "@playwright/test"; +import { Common, setupBrowser } from "../../utils/common"; +import { LogUtils } from "./log-utils"; +import { EventStatus, LogRequest } from "./logs"; +import { type JsonObject } from "@backstage/types"; +import { RhdhAuthApiHack } from "../../support/api/rhdh-auth-api-hack"; +import RhdhRbacApi from "../../support/api/rbac-api"; + +test.describe("Auditor check for RBAC Plugin", () => { + let common: Common; + let apiToken: string; + let rbacApi: RhdhRbacApi; + const userEntityRef = "user:default/rhdh-qe"; + const pluginActorId = "plugin:permission"; + + test.beforeAll(async ({ browser }, testInfo) => { + await LogUtils.loginToOpenShift(); + const page = (await setupBrowser(browser, testInfo)).page; + common = new Common(page); + await common.loginAsKeycloakUser(); + + apiToken = await RhdhAuthApiHack.getToken(page); + rbacApi = await RhdhRbacApi.build(apiToken); + }); + + /** + * Helper function to validate log events for RBAC Plugin + */ + async function validateRbacLogEvent( + eventId: string, + actorId: string, + request?: LogRequest, + meta?: JsonObject, + error?: string, + status: EventStatus = "succeeded", + ) { + await LogUtils.validateLogEvent( + eventId, + actorId, + request, + meta, + error, + status, + "permission", + ); + } + + test.describe("Logs should have correct structure for 'role-read' event", () => { + test("Validate 'role-read' queryType 'all'", async () => { + await rbacApi.getRoles(); + await validateRbacLogEvent( + "role-read", + userEntityRef, + { method: "GET", url: "/api/permission/roles" }, + { + queryType: "all", + source: "rest", + }, + ); + }); + + test("Validate 'role-read' queryType 'by-role'", async () => { + await rbacApi.getRole("default/rbac_admin"); + await validateRbacLogEvent( + "role-read", + userEntityRef, + { + method: "GET", + url: "/api/permission/roles/role/default/rbac_admin", + }, + { + queryType: "by-role", + source: "rest", + }, + ); + }); + }); + + test.describe("Logs should have correct structure for 'role-write' event", () => { + const error = + "NotAllowedError: Unable to {action} role: source does not match originating role role:default/rbac_admin, consider making changes to the 'CONFIGURATION'"; + const role = { + memberReferences: [userEntityRef], + name: "role:default/rbac_admin", + }; + + test("Validate 'role-write' actionType 'create'", async () => { + await rbacApi.createRoles(role); + await validateRbacLogEvent( + "role-write", + userEntityRef, + { method: "POST", url: "/api/permission/roles" }, + { + actionType: "create", + source: "rest", + }, + error.replace("{action}", "add"), + "failed", + ); + }); + + test("Validate 'role-write' actionType 'update'", async () => { + await rbacApi.updateRole("default/rbac_admin", role, role); + await validateRbacLogEvent( + "role-write", + userEntityRef, + { method: "PUT", url: "/api/permission/roles/role/default/rbac_admin" }, + { + actionType: "update", + source: "rest", + }, + error.replace("{action}", "edit"), + "failed", + ); + }); + + test("Validate 'role-write' actionType 'delete'", async () => { + await rbacApi.deleteRole("default/rbac_admin"); + await validateRbacLogEvent( + "role-write", + userEntityRef, + { + method: "DELETE", + url: "/api/permission/roles/role/default/rbac_admin", + }, + { + actionType: "delete", + source: "rest", + }, + error.replace("{action}", "delete"), + "failed", + ); + }); + }); + + test.describe("Logs should have correct structure for 'policy-read' event", () => { + test("Validate 'policy-read' queryType 'all'", async () => { + await rbacApi.getPolicies(); + await validateRbacLogEvent( + "policy-read", + userEntityRef, + { + method: "GET", + url: "/api/permission/policies", + }, + { + queryType: "all", + source: "rest", + }, + ); + }); + + test("Validate 'policy-read' queryType 'by-role'", async () => { + await rbacApi.getPoliciesByRole("default/rbac_admin"); + await validateRbacLogEvent( + "policy-read", + userEntityRef, + { + method: "GET", + url: "/api/permission/policies/role/default/rbac_admin", + }, + { + entityRef: "role:default/rbac_admin", + queryType: "by-role", + source: "rest", + }, + ); + }); + + test("Validate 'policy-read' queryType 'by-query'", async () => { + await rbacApi.getPoliciesByQuery({ + entityRef: userEntityRef, + permission: "policy-entity", + policy: "read", + effect: "allow", + }); + + await validateRbacLogEvent( + "policy-read", + userEntityRef, + { + method: "GET", + url: "/api/permission/policies?entityRef=user%3Adefault%2Frhdh-qe&permission=policy-entity&policy=read&effect=allow", + }, + { + query: { + effect: "ALLOW", + entityRef: userEntityRef, + permission: "policy-entity", + policy: "read", + }, + queryType: "by-query", + source: "rest", + }, + ); + }); + }); + + test.describe("Logs should have correct structure for 'policy-write' event", () => { + const updatePolicy = { + permission: "policy-entity", + policy: "read", + effect: "allow", + }; + const policy = { + entityReference: "role:default/rbac_admin", + ...updatePolicy, + }; + const error = + "NotAllowedError: Unable to {action} policy role:default/rbac_admin,policy-entity,read,allow: source does not match originating role role:default/rbac_admin, consider making changes to the 'CONFIGURATION'"; + + test("Validate 'policy-write' actionType 'create'", async () => { + await rbacApi.createPolicies([policy]); + await validateRbacLogEvent( + "policy-write", + userEntityRef, + { method: "POST", url: "/api/permission/policies" }, + { + actionType: "create", + source: "rest", + }, + error.replace("{action}", "add"), + "failed", + ); + }); + + test("Validate 'policy-write' actionType 'update'", async () => { + await rbacApi.updatePolicy( + "default/rbac_admin", + [updatePolicy], + [{ ...updatePolicy, effect: "deny" }], + ); + await validateRbacLogEvent( + "policy-write", + userEntityRef, + { + method: "PUT", + url: "/api/permission/policies/role/default/rbac_admin", + }, + { + actionType: "update", + source: "rest", + }, + error.replace("{action}", "edit"), + "failed", + ); + }); + + test("Validate 'policy-write' actionType 'delete'", async () => { + await rbacApi.deletePolicy("default/rbac_admin", [policy]); + await validateRbacLogEvent( + "policy-write", + userEntityRef, + { + method: "DELETE", + url: "/api/permission/policies/role/default/rbac_admin", + }, + { + actionType: "delete", + source: "rest", + }, + error.replace("{action}", "delete"), + "failed", + ); + }); + }); + + test.describe("Logs should have correct structure for 'condition-read' event", () => { + test("Validate 'condition-read' queryType 'all'", async () => { + await rbacApi.getConditions(); + await validateRbacLogEvent( + "condition-read", + userEntityRef, + { method: "GET", url: "/api/permission/roles/conditions" }, + { + queryType: "all", + source: "rest", + }, + ); + }); + + test("Validate 'condition-read' queryType 'by-query'", async () => { + await rbacApi.getConditionByQuery({ + roleEntityRef: "role:default/test2-role", + pluginId: "catalog", + resourceType: "catalog-entity", + actions: "read", + }); + await validateRbacLogEvent( + "condition-read", + userEntityRef, + { + method: "GET", + url: "/api/permission/roles/conditions?roleEntityRef=role%3Adefault%2Ftest2-role&pluginId=catalog&resourceType=catalog-entity&actions=read", + }, + { + query: { + actions: "read", + pluginId: "catalog", + resourceType: "catalog-entity", + roleEntityRef: "role:default/test2-role", + }, + queryType: "by-query", + source: "rest", + }, + ); + }); + + test("Validate 'condition-read' queryType 'by-id'", async () => { + await rbacApi.getConditionById(1); + await validateRbacLogEvent( + "condition-read", + userEntityRef, + { method: "GET", url: "/api/permission/roles/conditions/1" }, + { + id: "1", + queryType: "by-id", + source: "rest", + }, + ); + }); + }); + + test("Logs should have correct structure for 'permission-evaluation' event", async ({ + page, + }) => { + await page.goto("/rbac"); + await validateRbacLogEvent( + "permission-evaluation", + pluginActorId, + undefined, + { + action: "read", + permissionName: "policy.entity.read", + resourceType: "policy-entity", + result: "ALLOW", + userEntityRef, + }, + ); + }); +}); diff --git a/e2e-tests/playwright/e2e/audit-log/catalog.spec.ts b/e2e-tests/playwright/e2e/audit-log/catalog.spec.ts index d260fcf03a..d840d9ae17 100644 --- a/e2e-tests/playwright/e2e/audit-log/catalog.spec.ts +++ b/e2e-tests/playwright/e2e/audit-log/catalog.spec.ts @@ -3,13 +3,15 @@ import { Common } from "../../utils/common"; import { UIhelper } from "../../utils/ui-helper"; import { LogUtils } from "./log-utils"; import { CatalogImport } from "../../support/pages/catalog-import"; +import { type LogRequest } from "./logs"; // Re-enable with after adapting the tests to the new audit log service -test.describe.skip("Audit Log check for Catalog Plugin", () => { +test.describe.skip("Auditor check for Catalog Plugin", () => { let uiHelper: UIhelper; let common: Common; let catalogImport: CatalogImport; let baseApiUrl: string; + const actorId = "user:development/guest"; test.beforeAll(async ({ baseURL }) => { await LogUtils.loginToOpenShift(); @@ -29,88 +31,78 @@ test.describe.skip("Audit Log check for Catalog Plugin", () => { */ async function validateCatalogLogEvent( eventName: string, - message: string, - method: string, - apiPath: string, + actorId: string, + request?: LogRequest, plugin: string = "catalog", ) { await LogUtils.validateLogEvent( eventName, - message, - method, - apiPath, - baseApiUrl, + actorId, + request, + undefined, + undefined, + "succeeded", plugin, + undefined, + baseApiUrl, ); } - test("Should fetch logs for CatalogEntityFacetFetch event and validate log structure and values", async () => { - await validateCatalogLogEvent( - "CatalogEntityFacetFetch", - "Entity facet fetch attempt", - "GET", - "/api/catalog/entity-facets", - ); + test("Should fetch logs for 'entity-facets' event and validate log structure and values", async () => { + await validateCatalogLogEvent("entity-facets", actorId, { + method: "GET", + url: "/api/catalog/entity-facets", + }); }); - test("Should fetch logs for CatalogEntityFetchByName event and validate log structure and values", async () => { + test("Should fetch logs for 'entity-fetch' event queryType 'by-name' and validate log structure and values", async () => { await uiHelper.selectMuiBox("Kind", "Component"); await uiHelper.clickByDataTestId("user-picker-all"); await uiHelper.clickLink("backstage-janus"); - await validateCatalogLogEvent( - "CatalogEntityFetchByName", - "Fetch attempt for entity with entityRef component:default/backstage-janus", - "GET", - "/api/catalog/entities/by-name/component/default/backstage-janus", - ); + await validateCatalogLogEvent("entity-fetch", actorId, { + method: "GET", + url: "/api/catalog/entities/by-name/component/default/backstage-janus", + }); }); - test("Should fetch logs for CatalogEntityBatchFetch event and validate log structure and values", async () => { + test("Should fetch logs for 'entity-fetch' event queryType 'by-refs' and validate log structure and values", async () => { await uiHelper.selectMuiBox("Kind", "Component"); await uiHelper.clickByDataTestId("user-picker-all"); await uiHelper.clickLink("backstage-janus"); - await validateCatalogLogEvent( - "CatalogEntityBatchFetch", - "Batch entity fetch attempt", - "POST", - "/api/catalog/entities/by-refs", - ); + await validateCatalogLogEvent("entity-fetch", actorId, { + method: "POST", + url: "/api/catalog/entities/by-refs", + }); }); - test("Should fetch logs for CatalogEntityAncestryFetch event and validate log structure and values", async () => { + test("Should fetch logs for 'entity-fetch' event queryType 'ancestry' and validate log structure and values", async () => { await uiHelper.selectMuiBox("Kind", "Component"); await uiHelper.clickByDataTestId("user-picker-all"); await uiHelper.clickLink("backstage-janus"); - await validateCatalogLogEvent( - "CatalogEntityAncestryFetch", - "Fetch attempt for entity ancestor of entity component:default/backstage-janus", - "GET", - "/api/catalog/entities/by-name/component/default/backstage-janus/ancestry", - ); + await validateCatalogLogEvent("entity-fetch", actorId, { + method: "GET", + url: "/api/catalog/entities/by-name/component/default/backstage-janus/ancestry", + }); }); - test("Should fetch logs for QueriedCatalogEntityFetch event and validate log structure and values", async () => { + test("Should fetch logs for 'entity-fetch' event queryType 'by-query' and validate log structure and values", async () => { await uiHelper.clickButton("Self-service"); - await validateCatalogLogEvent( - "QueriedCatalogEntityFetch", - "Queried entity fetch attempt", - "GET", - "/api/catalog/entities/by-query", - ); + await validateCatalogLogEvent("entity-fetch", actorId, { + method: "GET", + url: "/api/catalog/entities/by-query", + }); }); - test("Should fetch logs for CatalogLocationCreation event and validate log structure and values", async () => { + test("Should fetch logs for 'location-mutate' event actionType 'create' and validate log structure and values", async () => { const template = "https://github.com/RoadieHQ/sample-service/blob/main/demo_template.yaml"; await uiHelper.clickButton("Self-service"); await uiHelper.clickButton("Register Existing Component"); await catalogImport.analyzeComponent(template); - await validateCatalogLogEvent( - "CatalogLocationCreation", - template, - "POST", - "/api/catalog/locations?dryRun=true", - ); + await validateCatalogLogEvent("location-mutate", actorId, { + method: "POST", + url: "/api/catalog/locations?dryRun=true", + }); }); }); diff --git a/e2e-tests/playwright/e2e/audit-log/log-utils.ts b/e2e-tests/playwright/e2e/audit-log/log-utils.ts index 20856149a0..32abc5bc4c 100644 --- a/e2e-tests/playwright/e2e/audit-log/log-utils.ts +++ b/e2e-tests/playwright/e2e/audit-log/log-utils.ts @@ -1,6 +1,12 @@ import { expect } from "@playwright/test"; import { execFile } from "child_process"; -import { Log } from "./logs"; +import { type JsonObject } from "@backstage/types"; +import { + Log, + type LogRequest, + type EventStatus, + type EventSeverityLevel, +} from "./logs"; export class LogUtils { /** @@ -106,19 +112,21 @@ export class LogUtils { /** * Fetches logs with retry logic in case the log is not immediately available. * + * @param eventId The event to filter the logs * @param filter The string to filter the logs * @param maxRetries Maximum number of retry attempts * @param retryDelay Delay (in milliseconds) between retries * @returns The log line matching the filter, or throws an error if not found */ static async getPodLogsWithRetry( - filter: string, + eventId?: string, + filter?: string, maxRetries: number = 3, retryDelay: number = 5000, ): Promise { const podSelector = "app.kubernetes.io/component=backstage,app.kubernetes.io/instance=rhdh,app.kubernetes.io/name=backstage"; - const tailNumber = 30; + const tailNumber = 50; const namespace = process.env.NAME_SPACE || "showcase-ci-nightly"; let attempt = 0; @@ -144,7 +152,11 @@ export class LogUtils { console.log("Raw log output:", output); const logLines = output.split("\n"); - const filteredLines = logLines.filter((line) => line.includes(filter)); + const filteredLines = logLines.filter((line) => { + const matchEvent = !eventId || line.includes(eventId); + const matchFilter = !filter || line.includes(filter); + return matchEvent && matchFilter; + }); if (filteredLines.length > 0) { console.log("Matching log line found:", filteredLines[0]); @@ -152,7 +164,7 @@ export class LogUtils { } console.warn( - `No matching logs found for filter "${filter}" on attempt ${ + `No matching logs found for filter "${eventId} ${filter}" on attempt ${ attempt + 1 }. Retrying...`, ); @@ -171,7 +183,7 @@ export class LogUtils { } throw new Error( - `Failed to fetch logs for filter "${filter}" after ${maxRetries + 1} attempts.`, + `Failed to fetch logs for filter "${eventId} ${filter}" after ${maxRetries + 1} attempts.`, ); } @@ -211,23 +223,29 @@ export class LogUtils { * Validates if the actual log matches the expected log values for a specific event. * This is a reusable method for different log validations across various tests. * - * @param eventName The name of the event to filter in the logs - * @param message The expected log message - * @param method The HTTP method used in the log (GET, POST, etc.) - * @param url The URL endpoint that was hit in the log - * @param baseURL The base URL of the application, used to get the hostname + * @param eventId The id of the event to filter in the logs + * @param actorId The id of actor initiating the request + * @param request The url endpoint and HTTP method (GET, POST, etc.) hit + * @param meta The metadata about the event + * @param error The error that occurred + * @param status The status of event * @param plugin The plugin name that triggered the log event + * @param severityLevel The level of severity of the event + * @param baseURL The base URL of the application, used to get the hostname */ public static async validateLogEvent( - eventName: string, - message: string, - method: string, - url: string, - baseURL: string, - plugin: string, + eventId: string, + actorId: string, + request?: LogRequest, + meta?: JsonObject, + error?: string, + status: EventStatus = "succeeded", + plugin: string = "catalog", + severityLevel: EventSeverityLevel = "medium", + baseURL: string = process.env.BASE_URL, ) { try { - const actualLog = await LogUtils.getPodLogsWithRetry(eventName); + const actualLog = await LogUtils.getPodLogsWithRetry(eventId, status); console.log("Raw log output before filtering:", actualLog); let parsedLog: Log; @@ -240,24 +258,26 @@ export class LogUtils { const expectedLog: Partial = { actor: { - hostname: new URL(baseURL).hostname, + actorId, + ...(request && { hostname: new URL(baseURL).hostname }), }, - message, plugin, - request: { - method, - url, - }, + ...(request && { request }), + ...(meta && { meta }), + ...(error && { error }), + status, + severityLevel, }; console.log("Validating log with expected values:", expectedLog); LogUtils.validateLog(parsedLog, expectedLog); } catch (error) { console.error("Error validating log event:", error); - console.error("Event name:", eventName); - console.error("Expected message:", message); - console.error("Expected method:", method); - console.error("Expected URL:", url); + console.error("Event id:", eventId); + console.error("Actor id:", actorId); + console.error("Meta:", meta); + console.error("Expected method:", request?.method); + console.error("Expected URL:", request?.url); console.error("Base URL:", baseURL); console.error("Plugin:", plugin); throw error; diff --git a/e2e-tests/playwright/e2e/audit-log/logs.ts b/e2e-tests/playwright/e2e/audit-log/logs.ts index 36b4ee20ac..03a50cf0a0 100644 --- a/e2e-tests/playwright/e2e/audit-log/logs.ts +++ b/e2e-tests/playwright/e2e/audit-log/logs.ts @@ -1,9 +1,11 @@ +import { type JsonObject } from "@backstage/types"; + class Actor { actorId?: string; hostname: string; } -class LogRequest { +export class LogRequest { body?: object; method: string; params?: object; @@ -19,20 +21,23 @@ class LogResponse { status: number; } +export type EventStatus = "initiated" | "succeeded" | "failed"; + +export type EventSeverityLevel = "low" | "medium" | "high" | "critical"; + export class Log { actor: Actor; - eventName: string; - isAuditLog: boolean; - level: string; - message: string; - meta: object; + eventId: string; + isAuditEvent: boolean; + severityLevel: EventSeverityLevel; plugin: string; - request: LogRequest; - response: LogResponse; + request?: LogRequest; + response?: LogResponse; service: string; - stage: string; - status: string; + status: EventStatus; timestamp: string; + meta?: JsonObject; + error?: string; /** * Constructor for the Log class. @@ -43,7 +48,7 @@ export class Log { constructor(overrides: Partial = {}) { // Default value for status this.status = overrides.status || "succeeded"; - this.isAuditLog = overrides.isAuditLog || true; + this.isAuditEvent = overrides.isAuditEvent || true; // Default value for actorId, with other actor properties being optional this.actor = { @@ -52,15 +57,9 @@ export class Log { }; // Other properties without default values - this.eventName = overrides.eventName || ""; + this.eventId = overrides.eventId || ""; this.plugin = overrides.plugin || ""; - this.message = overrides.message || ""; - this.request = { - method: overrides.request?.method || "", - url: overrides.request?.url || "", - }; - this.response = { - status: overrides.response?.status || 0, - }; + this.request = overrides.request; + this.response = overrides.response; } } diff --git a/e2e-tests/playwright/e2e/audit-log/scaffold.spec.ts b/e2e-tests/playwright/e2e/audit-log/scaffold.spec.ts index 9559c048e9..1cf885ec50 100644 --- a/e2e-tests/playwright/e2e/audit-log/scaffold.spec.ts +++ b/e2e-tests/playwright/e2e/audit-log/scaffold.spec.ts @@ -3,6 +3,7 @@ import { Common } from "../../utils/common"; import { UIhelper } from "../../utils/ui-helper"; import { LogUtils } from "./log-utils"; import { CatalogImport } from "../../support/pages/catalog-import"; +import { type LogRequest } from "./logs"; test.describe.skip("Audit Log check for Catalog Plugin", () => { let uiHelper: UIhelper; @@ -25,8 +26,25 @@ test.describe.skip("Audit Log check for Catalog Plugin", () => { await uiHelper.clickLink({ ariaLabel: "Self-service" }); }); - test("Should fetch logs for ScaffolderParameterSchemaFetch event and validate log structure and values", async ({ - baseURL, + /** + * Helper function to validate log events for Scaffolder Plugin + */ + async function validateScaffolderLogEvent( + eventId: string, + request: LogRequest, + ) { + await LogUtils.validateLogEvent( + eventId, + "user:development/guest", + request, + undefined, + undefined, + "succeeded", + "scaffolder", + ); + } + + test("Should fetch logs for 'template-parameter-schema' event and validate log structure and values", async ({ page, }) => { await uiHelper.clickButton("Register Existing Component"); @@ -35,45 +53,29 @@ test.describe.skip("Audit Log check for Catalog Plugin", () => { await uiHelper.clickLink({ ariaLabel: "Self-service" }); await common.waitForLoad(); await uiHelper.clickBtnInCard("Hello World 2", "Choose"); - await LogUtils.validateLogEvent( - "ScaffolderParameterSchemaFetch", - "user:development/guest requested the parameter schema for template:default/hello-world-2", - "GET", - "/api/scaffolder/v2/templates/default/template/hello-world-2/parameter-schema", - baseURL!, - "scaffolder", - ); + await validateScaffolderLogEvent("template-parameter-schema", { + method: "GET", + url: "/api/scaffolder/v2/templates/default/template/hello-world-2/parameter-schema", + }); }); - test("Should fetch logs for ScaffolderInstalledActionsFetch event and validate log structure and values", async ({ - baseURL, - }) => { + test("Should fetch logs for 'action-fetch' event and validate log structure and values", async () => { await uiHelper.clickById("long-menu"); await uiHelper.clickSpanByText("Installed Actions"); - await LogUtils.validateLogEvent( - "ScaffolderInstalledActionsFetch", - "user:development/guest requested the list of installed actions", - "GET", - "/api/scaffolder/v2/actions", - baseURL!, - "scaffolder", - ); + await validateScaffolderLogEvent("action-fetch", { + method: "GET", + url: "/api/scaffolder/v2/actions", + }); }); - test("Should fetch logs for ScaffolderTaskListFetch event and validate log structure and values", async ({ - baseURL, - }) => { + test("Should fetch logs for 'task' event actionType 'list' and validate log structure and values", async () => { await uiHelper.clickById("long-menu"); await uiHelper.clickSpanByText("Task List"); - await LogUtils.validateLogEvent( - "ScaffolderTaskListFetch", - "user:development/guest requested for the list of scaffolder tasks", - "GET", - "/api/scaffolder/v2/tasks?createdBy=user%3Adevelopment%2Fguest", - baseURL!, - "scaffolder", - ); + await validateScaffolderLogEvent("task", { + method: "GET", + url: "/api/scaffolder/v2/tasks?createdBy=user%3Adevelopment%2Fguest", + }); }); }); diff --git a/e2e-tests/playwright/e2e/plugins/rbac/rbac.spec.ts b/e2e-tests/playwright/e2e/plugins/rbac/rbac.spec.ts index ce8586ee10..2973277675 100644 --- a/e2e-tests/playwright/e2e/plugins/rbac/rbac.spec.ts +++ b/e2e-tests/playwright/e2e/plugins/rbac/rbac.spec.ts @@ -643,7 +643,7 @@ test.describe.serial("Test RBAC", () => { try { const remainingPoliciesResponse = - await rbacApi.getPolicy("default/test"); + await rbacApi.getPoliciesByRole("default/test"); const remainingPolicies = await Response.removeMetadataFromResponse( remainingPoliciesResponse, diff --git a/e2e-tests/playwright/support/api/rbac-api.ts b/e2e-tests/playwright/support/api/rbac-api.ts index 2202782e9b..80bcf9c6d6 100644 --- a/e2e-tests/playwright/support/api/rbac-api.ts +++ b/e2e-tests/playwright/support/api/rbac-api.ts @@ -9,7 +9,7 @@ export default class RhdhRbacApi { Authorization: string; }; private myContext: APIRequestContext; - private readonly roleRegex = /^[a-zA-Z]+\/[a-zA-Z]+$/; + private readonly roleRegex = /^[a-zA-Z]+\/[a-zA-Z_]+$/; private constructor(private readonly token: string) { this.authHeader = { @@ -33,8 +33,8 @@ export default class RhdhRbacApi { return await this.myContext.get("roles"); } - public async getRole(): Promise { - return await this.myContext.get("role"); + public async getRole(role: string): Promise { + return await this.myContext.get(`roles/role/${role}`); } public async updateRole( role: string /* shall be like: default/admin */, @@ -60,8 +60,14 @@ export default class RhdhRbacApi { return await this.myContext.get("policies"); } - public async getPolicy(policy: string): Promise { - return await this.myContext.get(`policies/${policy}`); + public async getPoliciesByRole(policy: string): Promise { + return await this.myContext.get(`policies/role/${policy}`); + } + + public async getPoliciesByQuery( + params: string | { [key: string]: string | number | boolean }, + ): Promise { + return await this.myContext.get("policies", { params }); } public async createPolicies(policy: Policy[]): Promise { @@ -74,17 +80,33 @@ export default class RhdhRbacApi { newPolicy: Policy[], ): Promise { this.checkRoleFormat(role); - return await this.myContext.put(`/policies/role/${role}`, { + return await this.myContext.put(`policies/role/${role}`, { data: { oldPolicy, newPolicy }, }); } public async deletePolicy(policy: string, policies: Policy[]) { this.checkRoleFormat(policy); - return await this.myContext.delete(`/policies/role/${policy}`, { + return await this.myContext.delete(`policies/role/${policy}`, { data: policies, }); } + // Conditions + + public async getConditions(): Promise { + return await this.myContext.get("roles/conditions"); + } + + public async getConditionByQuery( + params: string | { [key: string]: string | number | boolean }, + ): Promise { + return await this.myContext.get("roles/conditions", { params }); + } + + public async getConditionById(id: number): Promise { + return await this.myContext.get(`roles/conditions/${id}`); + } + private checkRoleFormat(role: string) { if (!this.roleRegex.test(role)) throw Error(