Skip to content

feat(auth): OpenShift RBAC fine-grained permissions - #1734

Merged
andrewazores merged 68 commits into
cryostatio:mainfrom
andrewazores:fine-grained-permissions
Sep 15, 2026
Merged

andrewazores merged 68 commits into
cryostatio:mainfrom
andrewazores:fine-grained-permissions

Conversation

@andrewazores

@andrewazores andrewazores commented Aug 11, 2026

Copy link
Copy Markdown
Member

Welcome to Cryostat! 👋

Before contributing, make sure you have:

  • Read the contributing guidelines
  • Linked a relevant issue which this PR resolves
  • Linked any other relevant issues, PR's, or documentation, if any
  • Resolved all conflicts, if any
  • Rebased your branch PR on top of the latest upstream main branch
  • Attached at least one of the following labels to the PR: [chore, ci, docs, feat, fix, test]
  • Signed all commits using a GPG signature

To recreate commits with GPG signature git fetch upstream && git rebase --force --gpg-sign upstream/main


Fixes: #1733
Fixes #1750

Description of the change:

Reimplements the old Cryostat 2.x concept of optionally hooking in to the OpenShift cluster OAuth server to perform additional authorization checks on incoming requests. Various Cryostat API resource types are modelled such as targets, activerecordings, archivedrecordings, heapdumps, etc., each with verbs read, write, and delete. All REST API endpoints are annotated with an appropriate set of required permissions. All GraphQL queries and mutations are similarly annotated. When Cryostat is configured for PERMISSIVE RBAC mode (the default), it simply passes all requests - the same behaviour as before (relying on the auth proxy, if there is one). When configured for BASIC RBAC mode Cryostat passes all requests but asserts that the request originated from the auth proxy. When configured in OPENSHIFT RBAC mode Cryostat asserts that the request originated from the auth proxy, extracts the user's token passed back by the proxy, and uses that to perform a SelfSubjectAccessReview against the cluster OAuth to check if the user is authorized to perform the requested action(s). A cryostat.security.rbac.namespace property (CRYOSTAT_SECURITY_RBAC_NAMESPACE environment variable) is also exposed so that the Operator/Helm can control the context that the SSAR is performed in - if this property is blank then the SSAR requires the user to pass a ClusterRole check, otherwise if the property is set to a Namespace name then the user must pass a Role check scoped to that Namespace. Therefore, the Operator/Helm can set this environment variable to the Cryostat (CR) installation Namespace to retain the existing authz semantics.

Motivation for the change:

See #1733
This allows for fine-grained access controls within the Cryostat API. Previously, access to Cryostat was controlled by a single check at the auth proxy (if any) - when behind an oauth2-proxy, having valid authentication credentials implied authorization to read any data and perform any action, and when behind openshift-oauth-proxy passing an authentication check and single create pods/exec role check granted access to read any data and perform any action. This is a very broad, coarse level of authn/authz. Admins installing Cryostat may wish to set up user accounts or groups so that some users have full access, other users may only read existing data, other users may only read specific data, etc.

Not handled (yet): #630 multitenancy. API endpoints may leak metadata, ex. GET /api/v4/targets will return a full list of all targets to a user who has the targets:read permission, when really it should first check that the user has targets:read and then for each target check that the user has permission to that particular target (either a per-target target:read permission, or a scoped activerecordings:read on that target for example). This PR still assumes that the scenario is that all users given access to a Cryostat instance are part of the same team/organization and that per-namespace authorization is out of scope. This intentionally only handles the case of differing user permissions within Cryostat instance or within the set of target namespaces so that there can be ex. admin roles vs readonly roles within a team.

How to manually test:

See cryostatio/cryostat-operator#1428 . Outside of an OpenShift context, ex. on other Kubernetes or in smoketest, Cryostat should behave identically to how it does prior to this PR.

@andrewazores andrewazores added feat New feature or request safe-to-test labels Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Summary

Summary by CodeRabbit

  • New Features

    • Added fine-grained permission controls across REST and GraphQL APIs.
    • Added configurable RBAC modes, permission mappings, trusted proxy hosts, and authorization decision caching.
    • Added forwarded-credential and Bearer authentication support.
    • Added configurable Kubernetes client caching.
    • Added recording resynchronization support.
    • Added 401 and 403 responses to API documentation.
  • Bug Fixes

    • Improved agent-proxy authorization and credential handling.
    • Corrected target creation and dry-run response behavior.
    • Preserved specific client error responses during target operations.

Walkthrough

This change adds configurable fine-grained RBAC with Kubernetes permission mapping, HTTP and GraphQL enforcement, token and decision caching, endpoint annotations, OpenAPI security requirements, validation tests, agent-container networking updates, and schema-generator cleanup.

Changes

Fine-grained RBAC

Layer / File(s) Summary
RBAC core and request authorization
src/main/java/io/cryostat/security/rbac/*, src/main/resources/application.properties
Adds RBAC modes, permission mapping, HTTP authentication, trusted agent-proxy validation, Kubernetes SSAR checks, token and decision caches, and configurable defaults.
REST and GraphQL enforcement
src/main/java/io/cryostat/{audit,asyncprofiler,credentials,diagnostic,discovery,events,expressions,jmcagent,recordings,reports,rules,targets,triggers}/*, src/main/java/io/cryostat/graphql/*
Replaces role checks with resource-specific permissions and adds GraphQL resolver checks, including field-specific recording authorization.
Contracts and validation
schema/openapi.yaml, src/test/java/io/cryostat/security/*, src/test/java/io/cryostat/security/rbac/*, src/test/java/io/cryostat/{discovery,recordings}/*
Adds OpenAPI 401 and 403 responses, permission architecture rules, RBAC tests, and endpoint behavior coverage.
Build and integration support
pom.xml, schema-generator/*, src/main/java/io/cryostat/schema/PayloadTypeAnalyzer.java, src/test/java/io/cryostat/resources/AgentApplicationResource.java
Updates dependencies, simplifies schema generation, and adds Dev Services network support for agent integration tests.

Suggested reviewers: jtolentino1

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@github-actions github-actions Bot added the needs-triage Needs thorough attention from code reviewers label Aug 11, 2026
@andrewazores andrewazores removed the needs-triage Needs thorough attention from code reviewers label Aug 11, 2026
@andrewazores

Copy link
Copy Markdown
Member Author

/build_test

@github-actions

Copy link
Copy Markdown

Workflow started at 8/11/2026, 4:39:38 PM. View Actions Run.

@github-actions

Copy link
Copy Markdown

OpenAPI schema change detected:

diff --git a/schema/openapi.yaml b/schema/openapi.yaml
index 17dac91..4491cbd 100644
--- a/schema/openapi.yaml
+++ b/schema/openapi.yaml
@@ -1282,105 +1282,141 @@ paths:
     get:
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/ArchivedHeapDumpDirectory'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List Fs Heap Dumps
       tags:
         - Diagnostics
   /api/beta/diagnostics/fs/heapdumps/{jvmId}/{heapDumpId}:
     delete:
       parameters:
         - in: path
           name: heapDumpId
           required: true
           schema:
             type: string
         - in: path
           name: jvmId
           required: true
           schema:
             type: string
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete Heap Dump By Path
       tags:
         - Diagnostics
   /api/beta/diagnostics/fs/threaddumps:
     get:
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/ArchivedThreadDumpDirectory'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List Fs Thread Dumps
       tags:
         - Diagnostics
   /api/beta/diagnostics/fs/threaddumps/{jvmId}/{threadDumpId}:
     delete:
       parameters:
         - in: path
           name: jvmId
           required: true
           schema:
             type: string
         - in: path
           name: threadDumpId
           required: true
           schema:
             type: string
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete Thread Dump
       tags:
         - Diagnostics
   /api/beta/diagnostics/fs/unified-logs:
     get:
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/ArchivedUnifiedLogDirectory'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List Fs Unified Logs
       tags:
         - Unified Logs
   /api/beta/diagnostics/fs/unified-logs/{jvmId}/{logId}:
     delete:
       parameters:
         - in: path
           name: jvmId
           required: true
           schema:
             type: string
         - in: path
           name: logId
           required: true
           schema:
             type: string
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete Unified Log By Path
       tags:
         - Unified Logs
     patch:
       parameters:
         - in: path
           name: jvmId
           required: true
           schema:
             type: string
@@ -1395,20 +1431,26 @@ paths:
             schema:
               $ref: '#/components/schemas/MetadataBody'
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/UnifiedLog'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Patch Fs Unified Log Metadata
       tags:
         - Unified Logs
   /api/beta/diagnostics/heapdump/download/{encodedKey}:
     get:
       parameters:
         - in: path
           name: encodedKey
           required: true
           schema:
@@ -1416,20 +1458,26 @@ paths:
         - in: query
           name: filename
           schema:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema: {}
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Handle Heap Dumps Storage Download
       tags:
         - Diagnostics
   /api/beta/diagnostics/heapdump/upload/{jvmId}:
     post:
       parameters:
         - in: path
           name: jvmId
           required: true
           schema:
@@ -1446,20 +1494,26 @@ paths:
                   type: string
                 labels:
                   $ref: '#/components/schemas/JsonObject'
               type: object
         required: true
       responses:
         "201":
           description: Created
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Upload Heap Dump
       tags:
         - Diagnostics
   /api/beta/diagnostics/targets/{jvmId}/heapdump/{heapDumpId}/analyze:
     post:
       parameters:
         - in: path
           name: heapDumpId
           required: true
           schema:
@@ -1476,20 +1530,26 @@ paths:
               $ref: '#/components/schemas/HttpServerResponse'
         required: true
       responses:
         "200":
           content:
             application/json:
               schema: {}
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Analyze Heap Dump
       tags:
         - Diagnostics
   /api/beta/diagnostics/targets/{jvmId}/threaddump/{threadDumpId}/analyze:
     post:
       parameters:
         - in: path
           name: jvmId
           required: true
           schema:
@@ -1499,38 +1559,50 @@ paths:
           required: true
           schema:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/ThreadDumpAnalysis'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Analyze Thread Dump
       tags:
         - Diagnostics
   /api/beta/diagnostics/targets/{targetId}/gc:
     post:
       description: |
         Request the remote target to perform a garbage collection. The target JVM is free to ignore this
         request. This is generally equivalent to a System.gc() call made within the target JVM.
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "201":
           description: Created
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Initiate a garbage collection on the specified target
       tags:
         - Diagnostics
   /api/beta/diagnostics/targets/{targetId}/heapdump:
     get:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
@@ -1538,20 +1610,26 @@ paths:
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/HeapDump'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Get Heap Dumps
       tags:
         - Diagnostics
     post:
       description: |
         Request the remote target to perform a heap dump.
       parameters:
         - in: path
           name: targetId
           required: true
@@ -1566,40 +1644,52 @@ paths:
         required: true
       responses:
         "200":
           content:
             text/plain:
               schema:
                 type: string
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Initiates a heap dump on the specified target
       tags:
         - Diagnostics
   /api/beta/diagnostics/targets/{targetId}/heapdump/{heapDumpId}:
     delete:
       parameters:
         - in: path
           name: heapDumpId
           required: true
           schema:
             type: string
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete Heap Dump
       tags:
         - Diagnostics
   /api/beta/diagnostics/targets/{targetId}/threaddump:
     get:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
@@ -1607,20 +1697,26 @@ paths:
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/ThreadDump'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Get Thread Dumps
       tags:
         - Diagnostics
     post:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
@@ -1638,73 +1734,97 @@ paths:
         required: true
       responses:
         "200":
           content:
             text/plain:
               schema:
                 type: string
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Thread Dump
       tags:
         - Diagnostics
   /api/beta/diagnostics/targets/{targetId}/threaddump/{threadDumpId}:
     delete:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
             type: integer
         - in: path
           name: threadDumpId
           required: true
           schema:
             type: string
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete Thread Dump
       tags:
         - Diagnostics
   /api/beta/diagnostics/targets/{targetId}/unified-logging:
     delete:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Disable Unified Logging
       tags:
         - Unified Logs
     get:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/UnifiedLogStatus'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Unified Logging Status
       tags:
         - Unified Logs
     patch:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
@@ -1717,20 +1837,26 @@ paths:
           name: what
           schema:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/UnifiedLog'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Reconfigure Unified Logging
       tags:
         - Unified Logs
     post:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
@@ -1743,39 +1869,51 @@ paths:
           name: what
           schema:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/UnifiedLog'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Enable Unified Logging
       tags:
         - Unified Logs
   /api/beta/diagnostics/targets/{targetId}/unified-logging/pull:
     post:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/UnifiedLog'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Pull Unified Log
       tags:
         - Unified Logs
   /api/beta/diagnostics/targets/{targetId}/unified-logs:
     get:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
@@ -1783,40 +1921,52 @@ paths:
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/UnifiedLog'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List Unified Logs
       tags:
         - Unified Logs
   /api/beta/diagnostics/targets/{targetId}/unified-logs/{logId}:
     delete:
       parameters:
         - in: path
           name: logId
           required: true
           schema:
             type: string
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete Unified Log
       tags:
         - Unified Logs
     get:
       parameters:
         - in: path
           name: logId
           required: true
           schema:
             type: string
@@ -1829,20 +1979,26 @@ paths:
         - in: query
           name: filename
           schema:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema: {}
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Download Unified Log
       tags:
         - Unified Logs
     patch:
       parameters:
         - in: path
           name: logId
           required: true
           schema:
             type: string
@@ -1858,20 +2014,26 @@ paths:
             schema:
               $ref: '#/components/schemas/MetadataBody'
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/UnifiedLog'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Patch Unified Log Metadata
       tags:
         - Unified Logs
   /api/beta/diagnostics/threaddump/download/{encodedKey}:
     get:
       parameters:
         - in: path
           name: encodedKey
           required: true
           schema:
@@ -1879,20 +2041,26 @@ paths:
         - in: query
           name: filename
           schema:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema: {}
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Handle Thread Dumps Storage Download
       tags:
         - Diagnostics
   /api/beta/diagnostics/unified-logs/download/{encodedKey}:
     get:
       parameters:
         - in: path
           name: encodedKey
           required: true
           schema:
@@ -1900,20 +2068,26 @@ paths:
         - in: query
           name: filename
           schema:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema: {}
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Handle Unified Log Storage Download
       tags:
         - Unified Logs
   /api/beta/discovery/credential_exists:
     post:
       requestBody:
         content:
           application/x-www-form-urlencoded:
             schema:
               properties:
@@ -1929,73 +2103,97 @@ paths:
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/Credential'
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Check if a Credential already exists with an identical MatchExpression script.
       tags:
         - Discovery
   /api/beta/fs/recordings:
     get:
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/ArchivedRecordingDirectory'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List all archived recordings grouped by target
       tags:
         - Archived Recordings
   /api/beta/fs/recordings/{jvmId}:
     get:
       parameters:
         - in: path
           name: jvmId
           required: true
           schema:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/ArchivedRecordingDirectory'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List all archived recordings belonging to the specified target
       tags:
         - Archived Recordings
   /api/beta/fs/recordings/{jvmId}/{filename}:
     delete:
       parameters:
         - in: path
           name: filename
           required: true
           schema:
             type: string
         - in: path
           name: jvmId
           required: true
           schema:
             type: string
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete an archived recording by name belonging to the specified target
       tags:
         - Archived Recordings
   /api/beta/recording_analytics/{jvmId}/{filename}:
     post:
       parameters:
         - in: path
           name: filename
           required: true
           schema:
@@ -2020,60 +2218,78 @@ paths:
             application/json:
               schema:
                 items:
                   items:
                     type: string
                   type: array
                 type: array
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Execute Query
       tags:
         - Jfr Analytics
   /api/beta/recordings/{connectUrl}/{filename}:
     delete:
       parameters:
         - description: the connection URL associated with the target
           in: path
           name: connectUrl
           required: true
           schema:
             type: string
         - in: path
           name: filename
           required: true
           schema:
             type: string
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete an archived recording belonging to the specified target
       tags:
         - Archived Recordings
   /api/beta/recordings/{jvmId}:
     get:
       parameters:
         - in: path
           name: jvmId
           required: true
           schema:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/ArchivedRecording'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List archived recordings belonging to the specified target
       tags:
         - Archived Recordings
     post:
       description: |
         Upload a JFR binary file into the archives, associating the archived recording with a particular
         target JVM. This is primarily used by the Cryostat Agent for pushing harvested recording files.
       parameters:
         - in: path
           name: jvmId
@@ -2093,40 +2309,52 @@ paths:
                 recording:
                   format: binary
                   type: string
               type: object
         required: true
       responses:
         "201":
           description: Created
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Upload a JFR binary file to archives, associated with a particular target
       tags:
         - Archived Recordings
   /api/beta/targets/{jvmId}/smart_triggers/sync:
     post:
       parameters:
         - in: path
           name: jvmId
           required: true
           schema:
             type: string
       requestBody:
         content:
           text/plain:
             schema:
               type: string
         required: true
       responses:
         "201":
           description: Created
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Sync Recordings
       tags:
         - Smart Triggers
   /api/beta/targets/{targetId}/async-profiler:
     get:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
@@ -2134,20 +2362,26 @@ paths:
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/AsyncProfile'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List existing async-profiler profiles on the specified target
       tags:
         - Async Profiler
     post:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
@@ -2160,59 +2394,77 @@ paths:
         required: true
       responses:
         "200":
           content:
             text/plain:
               schema:
                 type: string
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Create a new async-profiler profile on the specified target
       tags:
         - Async Profiler
   /api/beta/targets/{targetId}/async-profiler/status:
     get:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/AsyncProfilerStatus'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Get specified target's async-profiler status
       tags:
         - Async Profiler
   /api/beta/targets/{targetId}/async-profiler/{profileId}:
     delete:
       parameters:
         - in: path
           name: profileId
           required: true
           schema:
             type: string
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete an async-profiler profile from the specified target
       tags:
         - Async Profiler
     get:
       parameters:
         - in: path
           name: profileId
           required: true
           schema:
             type: string
@@ -2223,20 +2475,26 @@ paths:
             format: int64
             type: integer
       responses:
         "200":
           content:
             application/octet-stream:
               schema:
                 format: binary
                 type: string
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Download an async-profiler binary file in JFR format
       tags:
         - Async Profiler
   /api/beta/targets/{targetId}/smart_triggers:
     get:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
@@ -2244,20 +2502,26 @@ paths:
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/SmartTrigger'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Retrieve all currently active Smart Triggers for a target
       tags:
         - Smart Triggers
     post:
       description: |
         Define a new Smart Trigger. A custom trigger definition must consist of both an expression
         that defines the overall trigger condition and the name of an event template that is used
         for the JFR Recording. The entire trigger expression must be enclosed in square brackets,
         with the recording template name specified after a ~. For an example definition:
         [ProcessCpuLoad > 0.2 ; TargetDuration > duration("30s")]~profile
@@ -2275,20 +2539,26 @@ paths:
               properties:
                 definition:
                   type: string
               type: object
         required: true
       responses:
         "201":
           description: Created
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Define a new Smart Trigger for a target
       tags:
         - Smart Triggers
   /api/beta/targets/{targetId}/smart_triggers/{uuid}:
     delete:
       description: |
         Delete an active Smart Trigger. A custom trigger definition must consist of both an expression
         that defines the overall trigger condition and the name of an event template that is used
         for the JFR Recording. The entire trigger expression must be enclosed in square brackets,
         with the recording template name specified after a ~. For an example definition:
@@ -2301,20 +2571,26 @@ paths:
             format: int64
             type: integer
         - in: path
           name: uuid
           required: true
           schema:
             type: string
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete a currently active Smart Trigger for a target
       tags:
         - Smart Triggers
   /api/v4.1/metrics/reports:
     get:
       description: |
         Retrieve the latest aggregate report data across all targets with recent automated analysis reports
         scores. These are multi-dimensional metrics in Prometheus format.
       parameters:
         - in: query
@@ -2325,20 +2601,26 @@ paths:
             type: number
       responses:
         "200":
           content:
             text/plain:
               schema:
                 items:
                   type: string
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Retrieve the latest aggregate report data
       tags:
         - Analysis Report Aggregator
   /api/v4.1/metrics/reports/{jvmId}:
     get:
       description: |
         Retrieve the latest aggregate report data for a given target's recent automated analysis reports
         scores. These are multi-dimensional metrics in Prometheus format.
       parameters:
         - in: path
@@ -2346,34 +2628,46 @@ paths:
           required: true
           schema:
             type: string
       responses:
         "200":
           content:
             text/plain:
               schema:
                 type: string
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Retrieve the latest aggregate report data for the specified target
       tags:
         - Analysis Report Aggregator
   /api/v4.1/reports_rules:
     get:
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/ReportRule'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List Report Rules
       tags:
         - Reports
   /api/v4.1/targets/{targetId}/reports:
     get:
       description: |
         Get the current cached automated analysis report for the specified target, if any. If no such
         report currently exists for the specified target then the response will be an HTTP 404 Not Found,
         and automated analysis report generation will not be triggered.
       parameters:
@@ -2385,20 +2679,26 @@ paths:
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 additionalProperties:
                   $ref: '#/components/schemas/AnalysisResult1'
                 type: object
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Retrieve current automated analysis report for a target
       tags:
         - Reports
     post:
       description: |
         Composite action that 1) creates a Snapshot active recording on the specified target, 2) archives
         that Snapshot immediately, 3) performs automated analysis report generation on the archived file.
         The response will include a Location header pointing the client to an endpoint where the report can
         be retrieved, which may require the client to wait for a Job UUID notification.
       parameters:
@@ -2420,20 +2720,26 @@ paths:
               $ref: '#/components/schemas/HttpServerResponse'
         required: true
       responses:
         "200":
           content:
             application/json:
               schema: {}
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Perform "target analysis" on the specified target
       tags:
         - Reports
   /api/v4.2/discovery/{id}:
     post:
       description: |
         Using its plugin ID and current token, a discovery plugin uses this endpoint to publish a JSON
         request body containing a list of discovery nodes. The discovery plugin itself is a Realm node in
         the overall discovery tree, so the published list of nodes here will replace the plugin Realm
         node's list of children.
@@ -2476,20 +2782,26 @@ paths:
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/PluginRegistration'
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Register and publish a Cryostat Agent
       tags:
         - Discovery
   /api/v4/activedownload/{id}:
     get:
       description: |
         Given a recording ID and a remote recording ID within that target, Cryostat will open a remote
         connection to the target and pipe back a data stream containing the Flight Recording binary file
         format for that recording. The client can feed this data to other tooling which ingests the JFR
         binary file format.
@@ -2501,20 +2813,26 @@ paths:
             format: int64
             type: integer
       responses:
         "200":
           content:
             application/octet-stream:
               schema:
                 format: binary
                 type: string
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Download a Flight Recording binary file
       tags:
         - Active Recordings Download
   /api/v4/auth:
     post:
       description: |
         In modern Cryostat deployments it is customary to deploy Cryostat behind an authenticating reverse
         proxy, so authentication is not actually handled by Cryostat itself. This endpoint is used by the
         Cryostat Web UI client to send an authenticated client request, including some authentication
         headers, to the Cryostat server so that it can extract information about the logged-in user. The
@@ -2538,20 +2856,26 @@ paths:
         and are therefore candidates for Cryostat to select this Credential.
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/CredentialMatchResult'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List information about all of the available Stored Credentials.
       tags:
         - Credentials
     post:
       description: |
         Define a new Stored Credential. Requires a match expression which defines which targets require
         this credential, and the username and password to use to pass authentication checks on those
         targets. Stored Credentials are stored in an encrypted keyring using symmetric encryption and an
         encryption key configured on the Cryostat database.
       requestBody:
@@ -2569,20 +2893,26 @@ paths:
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/Credential'
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Define a new Stored Credential
       tags:
         - Credentials
   /api/v4/credentials/test/{targetId}:
     post:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
@@ -2601,35 +2931,47 @@ paths:
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/CredentialTestResult'
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Test if the supplied username/password are valid credentials for the specified target.
       tags:
         - Credentials
   /api/v4/credentials/{id}:
     delete:
       parameters:
         - in: path
           name: id
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete a Stored Credential
       tags:
         - Credentials
     get:
       description: |
         Get match result information about a specific Stored Credential. A match result includes the Stored
         Credential's ID, its Match Expression, and a list of currently discovered Targets which match that
         expression and are therefore candidates for Cryostat to select this Credential.
       parameters:
         - in: path
@@ -2638,38 +2980,50 @@ paths:
           schema:
             format: int64
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/CredentialMatchResult'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Get information about a Stored Credential
       tags:
         - Credentials
   /api/v4/discovery:
     get:
       parameters:
         - in: query
           name: mergeRealms
           schema:
             default: false
             type: boolean
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/DiscoveryNode'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Retrieve the entire discovery tree.
       tags:
         - Discovery
     post:
       description: |
         Register a new discovery plugin, or refresh an existing plugin's registration and generate a new
         token. New registrations require the realm and callback fields. Registration refreshers
         additionally require the id and token fields, which are supplied in the response to the original
         registration.
       requestBody:
@@ -2680,20 +3034,26 @@ paths:
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/PluginRegistration'
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Register as a new discovery plugin or refresh existing registration
       tags:
         - Discovery
   /api/v4/discovery/{id}:
     delete:
       description: |
         Delete the plugin's registration along with its discovery Realm node and all of its children. This
         is used when a discovery plugin is shutting down.
       parameters:
         - in: path
@@ -2721,20 +3081,26 @@ paths:
           required: true
           schema:
             $ref: '#/components/schemas/UUID'
         - in: header
           name: Cryostat-Discovery-Authentication
           schema:
             type: string
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Endpoint for discovery plugins to check their own registration status
       tags:
         - Discovery
     post:
       description: |
         Using its plugin ID and current token, a discovery plugin uses this endpoint to publish a JSON
         request body containing a list of discovery nodes. The discovery plugin itself is a Realm node in
         the overall discovery tree, so the published list of nodes here will replace the plugin Realm
         node's list of children.
       parameters:
@@ -2774,20 +3140,26 @@ paths:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/DiscoveryPlugin_Flat'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List currently registered discovery plugins
       tags:
         - Discovery
   /api/v4/discovery_plugins/{id}:
     get:
       description: |
         Retrieve information about a specific discovery plugin, including its discovery Realm node and
         subtree.
       parameters:
         - in: path
@@ -2795,20 +3167,26 @@ paths:
           required: true
           schema:
             $ref: '#/components/schemas/UUID'
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/DiscoveryPlugin'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Retrieve a specific discovery plugin
       tags:
         - Discovery
   /api/v4/download/{encodedKey}:
     get:
       description: |
         Get a download URL for an archived recording. The response will be an HTTP redirect with a Location
         header pointing to the location where the client can download the recording JFR binary file.
       parameters:
         - in: path
@@ -2819,38 +3197,50 @@ paths:
         - in: query
           name: filename
           schema:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema: {}
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Get a download URL for an archived recording
       tags:
         - Archived Recordings
   /api/v4/event_templates:
     get:
       description: |
         Retrieve a list of templates available on this Cryostat server. These templates can be applied to
         recordings started on any discovered target, but any event configurations within the template which
         reference events that do not exist on the target will be ignored.
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/Template'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List server event templates
       tags:
         - Event Templates
     post:
       description: |
         Upload a new custom event template to the server. This must be in OpenJDK .jfc (XML) format.
       requestBody:
         content:
           application/x-www-form-urlencoded:
             schema:
@@ -2862,57 +3252,75 @@ paths:
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/Template'
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Upload a custom event template
       tags:
         - Event Templates
   /api/v4/event_templates/{templateName}:
     delete:
       description: |
         Delete a custom event template from the server. Only previously uploaded custom event templates can
         be deleted.
       parameters:
         - in: path
           name: templateName
           required: true
           schema:
             type: string
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete a custom event template
       tags:
         - Event Templates
   /api/v4/event_templates/{templateType}:
     get:
       parameters:
         - in: path
           name: templateType
           required: true
           schema:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/Template'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List server event templates of the given type
       tags:
         - Event Templates
   /api/v4/event_templates/{templateType}/{templateName}:
     get:
       description: |
         Get the .jfc (XML) file definition for the given server event template. This is the same type of
         event configuration file that ships with OpenJDK distributions.
       parameters:
         - in: path
@@ -2925,20 +3333,26 @@ paths:
           required: true
           schema:
             type: string
       responses:
         "200":
           content:
             application/xml:
               schema:
                 type: string
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Get a specific event template
       tags:
         - Event Templates
   /api/v4/grafana/{encodedKey}:
     post:
       description: |
         Upload an archived recording to the jfr-datasource for later online analysis in the associated
         Grafana dashboard.
       parameters:
         - in: path
@@ -2954,20 +3368,26 @@ paths:
         required: true
       responses:
         "200":
           content:
             text/plain:
               schema:
                 type: string
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Upload an archived recording to Grafana for online analysis
       tags:
         - Archived Recordings
   /api/v4/grafana_dashboard_url:
     get:
       description: |
         Returns the URL for the associated Grafana dashboard instance. If there is an internally-accessible
         (for Cryostat) URL and an externally-accessible URL (for users) URL, the externally-accessible URL
         is preferred. If neither are configured then the response is an HTTP 400 Bad Request.
       responses:
@@ -3020,20 +3440,26 @@ paths:
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   additionalProperties: {}
                   type: object
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Retrieve a list of all currently defined Match Expressions
       tags:
         - Match Expressions
     post:
       description: |
         Given a list of Target IDs, retrieve each Target instance from the database, then return the list
         filtered by the Targets which satisfy the Match Expression. If a given ID does not exist in the
         database then the whole request will fail. The expression must evaluate to a boolean value for each
         target. The match expression will not be stored.
       requestBody:
@@ -3044,39 +3470,51 @@ paths:
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/MatchedExpression'
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Test a MatchExpression against a list of Targets
       tags:
         - Match Expressions
   /api/v4/matchExpressions/{id}:
     get:
       parameters:
         - in: path
           name: id
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/MatchedExpression'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Retrieve a single Match Expression
       tags:
         - Match Expressions
   /api/v4/probes:
     get:
       responses:
         "200":
           content:
             application/json:
               schema:
@@ -3154,20 +3592,26 @@ paths:
         List all archived recordings from all targets, including (re-)uploaded files.
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/ArchivedRecording'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List all archived recordings
       tags:
         - Archived Recordings
     post:
       description: |
         (Re-)upload a JFR binary file into the archives. This allows for the restoration of archived files
         after they have been otherwise removed, or for portability across Cryostat instances or between
         Cryostat version upgrades. This can also be used to upload JFR files which were not collected by
         Cryostat, so that Cryostat can be used to perform online analysis of the file.
       requestBody:
@@ -3185,35 +3629,47 @@ paths:
       responses:
         "200":
           content:
             application/json:
               schema:
                 additionalProperties: {}
                 type: object
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Upload a JFR binary file to archives
       tags:
         - Archived Recordings
   /api/v4/recordings/{filename}:
     delete:
       deprecated: true
       parameters:
         - in: path
           name: filename
           required: true
           schema:
             type: string
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete an archived recording by filename
       tags:
         - Archived Recordings
   /api/v4/reports/{encodedKey}:
     get:
       description: |
         Given an encoded key from another endpoint, request an actual automated analysis report. If the
         requested report already exists in one of the tiered caching layers then the report will be
         directly returned as a JSON response body. If the report does not yet exist then the response will
         contain a Job UUID as a plain text response body. Cryostat will emit a WebSocket notification later
@@ -3236,34 +3692,46 @@ paths:
           application/json:
             schema:
               $ref: '#/components/schemas/HttpServerResponse'
         required: true
       responses:
         "200":
           content:
             application/json:
               schema: {}
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Get an automated analysis report
       tags:
         - Reports
   /api/v4/rules:
     get:
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/Rule'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List all Automated Rules
       tags:
         - Rules
     post:
       requestBody:
         content:
           application/json:
             schema:
               $ref: '#/components/schemas/Rule'
           application/x-www-form-urlencoded:
@@ -3335,56 +3803,74 @@ paths:
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/Rule'
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Create a new Automated Rule
       tags:
         - Rules
   /api/v4/rules/{name}:
     delete:
       parameters:
         - in: path
           name: name
           required: true
           schema:
             type: string
         - in: query
           name: clean
           schema:
             default: false
             type: boolean
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete an Automated Rule by name
       tags:
         - Rules
     get:
       parameters:
         - in: path
           name: name
           required: true
           schema:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/Rule'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Get an Automated Rule by name
       tags:
         - Rules
     patch:
       description: |
         Update Automated Rule parameters, such as whether the rule is currently active or not.
       parameters:
         - in: path
           name: name
           required: true
@@ -3401,37 +3887,49 @@ paths:
             schema:
               $ref: '#/components/schemas/JsonObject'
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/Rule'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Update an Automated Rule
       tags:
         - Rules
   /api/v4/targets:
     get:
       description: |
         Get a list of the currently discovered targets. These are essentialy the same as the leaf nodes of
         the discovery tree. See 'GET /api/v4/discovery'.
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/Target'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List currently discovered targets
       tags:
         - Targets
     post:
       description: |
         Create a target definition given a JSON request body target stub. The target stub must contain the
         connectUrl and alias, and optionally contain a username and password to create a Stored Credential
         associated with this target. The dryrun parameter can be used to perform this operation as a check,
         to verify if such a target could be created (no connectUrl conflict and acceptable credentials).
       parameters:
@@ -3477,59 +3975,77 @@ paths:
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/Target'
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Create a target definition
       tags:
         - Custom Discovery
   /api/v4/targets/{id}:
     delete:
       description: |
         Delete the specified target by ID. Only allows deletion of targets that were defined by the same
         Custom Target discovery API. Other targets must be removed by the discovery mechanisms which
         discovered them.
       parameters:
         - in: path
           name: id
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete the specified target
       tags:
         - Custom Discovery
     get:
       description: |
         Get details about a particular target given its ID.
       parameters:
         - in: path
           name: id
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/Target'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Get a target by ID
       tags:
         - Targets
   /api/v4/targets/{id}/event_templates:
     get:
       description: |
         Retrieve a list of event templates available on the given target when starting recordings on the
         same target. This includes all of the server's available templates, plus the templates available
         specifically from the target (ex. within /usr/lib/jvm/java/lib/jfr).
       parameters:
@@ -3541,20 +4057,26 @@ paths:
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/Template'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Retrieve a list of event templates available on the given target
       tags:
         - Target Event Templates
   /api/v4/targets/{id}/event_templates/{templateType}/{templateName}:
     get:
       description: |
         Get the .jfc (XML) file definition for the given target event template.
       parameters:
         - in: path
           name: id
@@ -3572,20 +4094,26 @@ paths:
           required: true
           schema:
             $ref: '#/components/schemas/TemplateType'
       responses:
         "200":
           content:
             application/xml:
               schema:
                 type: string
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Get a specific event template
       tags:
         - Target Event Templates
   /api/v4/targets/{id}/events:
     get:
       description: |
         Retrieve a list of JFR event types registered within the given target. This will include all
         built-in JFR types emitted by the target JVM, as well as custom event types specific to that
         target JVM if they are correctly registered. Custom event types, or event types emitted by plugins
         and extensions, may not always appear in this list.
@@ -3602,20 +4130,26 @@ paths:
             type: string
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/SerializableEventTypeInfo'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List JFR event types registered within the given target
       tags:
         - Events
   /api/v4/targets/{id}/probes:
     delete:
       parameters:
         - in: path
           name: id
           required: true
           schema:
@@ -3681,20 +4215,26 @@ paths:
             format: int64
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 additionalProperties: {}
                 type: object
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Get the current set of options for the specified target
       tags:
         - Recording Options
     patch:
       description: |
         Set default recording options for the specified target. These options will be applied to any
         recordings started on this target if no override values are specified when the recording is
         created.
       parameters:
         - in: path
@@ -3717,20 +4257,26 @@ paths:
               type: object
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 additionalProperties: {}
                 type: object
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Update the recording options for the specified target
       tags:
         - Recording Options
   /api/v4/targets/{targetId}/recordings:
     get:
       description: |
         Retrieve a list of active recordings currently present on the specified target. This may initiate
         a new remote connection to the target to update Cryostat's model of available recordings.
       parameters:
         - in: path
@@ -3741,20 +4287,26 @@ paths:
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 items:
                   $ref: '#/components/schemas/LinkedRecordingDescriptor'
                 type: array
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: List active recordings on the specified target
       tags:
         - Active Recordings
     post:
       description: |
         Create a new Flight Recording on the specified target. The recording will be immediately started
         and begin capturing Flight Recording data.
         The recording must be given a name (unique within the
         target). An event specifier string must be included, which follows the format
         "template={name},(type={type})". The type parameter is optional and the template name is required.
@@ -3815,20 +4367,26 @@ paths:
         required: true
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/LinkedRecordingDescriptor'
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Start a new recording on the specified target
       tags:
         - Active Recordings
   /api/v4/targets/{targetId}/recordings/{remoteId}:
     delete:
       description: |
         Delete a recording from the specified target. This will remove it both from Cryostat's database
         as well as remove the recording and release all resources in the remote target JVM.
       parameters:
         - in: path
@@ -3839,20 +4397,26 @@ paths:
             type: integer
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "204":
           description: No Content
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Delete a recording from the specified target
       tags:
         - Active Recordings
     get:
       description: |
         Given a recording ID and a remote recording ID within that target, Cryostat will open a remote
         connection to the target and pipe back a data stream containing the Flight Recording binary file
         format for that recording. The client can feed this data to other tooling which ingests the JFR
         binary file format.
       parameters:
@@ -3869,20 +4433,26 @@ paths:
             format: int64
             type: integer
       responses:
         "200":
           content:
             application/octet-stream:
               schema:
                 format: binary
                 type: string
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Download a Flight Recording binary file
       tags:
         - Active Recordings
     patch:
       description: |
         Remote recordings can be stopped by sending the request body "stop", or copied to archives by
         sending the request body "save". The body is case-insensitive.
       parameters:
         - in: path
           name: remoteId
@@ -3902,20 +4472,26 @@ paths:
             schema:
               $ref: '#/components/schemas/HttpServerResponse'
         required: true
       responses:
         "200":
           content:
             text/plain:
               schema:
                 type: string
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Update a remote recording on the specified target
       tags:
         - Active Recordings
   /api/v4/targets/{targetId}/recordings/{remoteId}/upload:
     post:
       description: |
         Upload the current data stream of the specified recording to the jfr-datasource for online analysis
         in the associated Grafana dashboard.
       parameters:
         - in: path
@@ -3938,20 +4514,26 @@ paths:
         required: true
       responses:
         "200":
           content:
             text/plain:
               schema:
                 type: string
           description: OK
         "400":
           description: Bad Request
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Upload a recording for analysis in Grafana dashboard
       tags:
         - Active Recordings
   /api/v4/targets/{targetId}/reports/{recordingId}:
     get:
       description: |
         Request an automated analysis report for a particular active recording on the specified target. If
         such a report already exists it will be returned directly as a JSON response body. If the report
         does not yet exist then an asynchronous task for it will be started, and a Job UUID will be
         sent as the plaintext response body.
@@ -3978,39 +4560,51 @@ paths:
           application/json:
             schema:
               $ref: '#/components/schemas/HttpServerResponse'
         required: true
       responses:
         "200":
           content:
             application/json:
               schema: {}
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Get an automated analysis report for a particular recording on the specified target
       tags:
         - Reports
   /api/v4/targets/{targetId}/snapshot:
     post:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "200":
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/LinkedRecordingDescriptor'
           description: OK
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Create a JFR Snapshot on the specified target
       tags:
         - Snapshots
   /api/v4/tls/certs:
     get:
       description: |
         In addition to the standard system/OpenJDK certificate trust store, Cryostat can be configured to
         trust additional certificates which may be presented by target JVM JMX servers or by Cryostat Agent
         HTTPS servers. This endpoint returns a list of local file paths to additional certificate files,
         which Cryostat will have loaded into an additional trust store at startup.

No GraphQL schema changes detected.

WebSocket notifications schema change detected:

diff --git a/schema/notifications.yaml b/schema/notifications.yaml
index b263685..bce6681 100644
--- a/schema/notifications.yaml
+++ b/schema/notifications.yaml
@@ -1081,43 +1081,39 @@ components:
             properties:
               event:
                 type: object
                 properties:
                   kind:
                     type: object
                     description: Payload of type EventKind
                   serviceRef:
                     type: object
                     properties:
-                      id:
-                        type: integer
                       connectUrl:
                         type: object
                         description: Payload of type URI
                       alias:
                         type: string
                       jvmId:
                         type: string
                       labels:
                         type: object
                         additionalProperties: true
                       annotations:
                         type: object
                         properties:
                           platform:
                             type: object
                             additionalProperties: true
                           cryostat:
                             type: object
                             additionalProperties: true
-                      agent:
-                        type: boolean
                   jvmId:
                     type: string
         required:
         - meta
         - message
     TemplateDeleted:
       name: TemplateDeleted
       title: Template Deleted
       summary: 'Notification: TemplateDeleted'
       description: WebSocket notification for Template Deleted events

@github-actions

Copy link
Copy Markdown

CI build:
Integration tests pass ✅
Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
https://github.com/cryostatio/cryostat/actions/runs/31534025918

@andrewazores

Copy link
Copy Markdown
Member Author

/build_test

@github-actions

Copy link
Copy Markdown

Workflow started at 8/11/2026, 4:57:25 PM. View Actions Run.

@github-actions

Copy link
Copy Markdown

CI build:
Unit tests pass with flaky tests ⚠️
Tests run: 740, Failures: 0, Errors: 0, Skipped: 108, Flakes: 1

Flaky tests:

@github-actions

Copy link
Copy Markdown

No OpenAPI schema changes detected.

No GraphQL schema changes detected.

No WebSocket notifications schema changes detected.

@github-actions

Copy link
Copy Markdown

CI build:
Integration tests pass ✅
Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
https://github.com/cryostatio/cryostat/actions/runs/31535526255

@andrewazores

Copy link
Copy Markdown
Member Author

/build_test

@github-actions

Copy link
Copy Markdown

Workflow started at 8/11/2026, 5:10:45 PM. View Actions Run.

@github-actions

Copy link
Copy Markdown

No OpenAPI schema changes detected.

No GraphQL schema changes detected.

No WebSocket notifications schema changes detected.

@github-actions

Copy link
Copy Markdown

CI build:
Integration tests pass ✅
Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
https://github.com/cryostatio/cryostat/actions/runs/31536622782

@github-actions

Copy link
Copy Markdown

CI build:
Unit tests pass with flaky tests ⚠️
Tests run: 742, Failures: 0, Errors: 0, Skipped: 108, Flakes: 1

Flaky tests:

@andrewazores

Copy link
Copy Markdown
Member Author

/build_test

@github-actions

Copy link
Copy Markdown

Workflow started at 8/11/2026, 5:51:21 PM. View Actions Run.

@github-actions

Copy link
Copy Markdown

No OpenAPI schema changes detected.

No GraphQL schema changes detected.

No WebSocket notifications schema changes detected.

@github-actions

Copy link
Copy Markdown

CI build:
Integration tests pass ✅
Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
https://github.com/cryostatio/cryostat/actions/runs/31539793674

@andrewazores
andrewazores marked this pull request as ready for review August 11, 2026 21:58
@andrewazores
andrewazores requested a review from a team as a code owner August 11, 2026 21:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (4)
src/main/java/io/cryostat/security/rbac/graphql/GraphQLSecurityInterceptor.java (1)

70-84: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache authorization decisions per request and permission. SsarClientCache only caches Kubernetes clients; repeated checks still create separate SelfSubjectAccessReview requests. Add request-scoped memoization in buildOpenshiftIdentity, including false results, while preserving fail-closed error handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/io/cryostat/security/rbac/graphql/GraphQLSecurityInterceptor.java`
around lines 70 - 84, Update buildOpenshiftIdentity to memoize each
authorization decision by request and permission, including denied (false)
results, so repeated checks reuse the cached outcome instead of issuing new
SelfSubjectAccessReview requests. Preserve the existing fail-closed behavior for
authorization errors and keep the per-permission validation semantics unchanged.

Source: Path instructions

src/main/java/io/cryostat/security/rbac/SsarClientCache.java (1)

47-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use Cache instead of LoadingCache with a throwing loader.

The declared loader always throws UnsupportedOperationException, and getOrCreate supplies its own mapping function. A plain Cache<String, KubernetesClient> built with Caffeine.build() expresses the same behavior and removes the unreachable loader.

♻️ Proposed refactor
-    private final LoadingCache<String, KubernetesClient> clientCache;
+    private final Cache<String, KubernetesClient> clientCache;
-                        .build(
-                                tokenHash -> {
-                                    throw new UnsupportedOperationException(
-                                            "Use getOrCreate(rawToken)");
-                                });
+                        .build();

Update the imports to com.github.benmanes.caffeine.cache.Cache.

Also applies to: 82-86

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/io/cryostat/security/rbac/SsarClientCache.java` at line 47,
Replace the LoadingCache field in SsarClientCache with Cache<String,
KubernetesClient>, update the Caffeine import, and build it with build() rather
than a throwing loader. Remove the unreachable loader configuration while
preserving getOrCreate’s existing mapping function behavior.
src/test/java/io/cryostat/security/rbac/SsarClientCacheTest.java (1)

114-124: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

testHashingConsistency duplicates testGetOrCreateReturnsCachedClientForSameToken.

Both tests call getOrCreate twice with the same token and assert one factory call. Either remove this test or change it to assert the property its name states, for example that two distinct tokens produce two distinct cache entries and that a token containing non-ASCII characters is hashed without error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/io/cryostat/security/rbac/SsarClientCacheTest.java` around
lines 114 - 124, Remove the duplicate testHashingConsistency coverage or rewrite
it to validate hashing-specific behavior rather than repeating
testGetOrCreateReturnsCachedClientForSameToken. Prefer asserting that distinct
tokens create distinct cached clients and that a non-ASCII token is processed
successfully, while retaining the relevant factory-call and identity assertions.
src/main/java/io/cryostat/security/rbac/RbacHttpAuthenticationMechanism.java (1)

189-248: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Cache SSAR decisions, not only clients.

Each permission check performs one SelfSubjectAccessReview HTTP call to the API server. Endpoints declare multiple permissions (for example {"targets:read", "activerecordings:write", "reports:write"} in Reports.analyze), so a single request produces several serial API-server round trips on the request path. SsarClientCache caches clients only, so no decision reuse occurs.

Add a short-TTL cache keyed by token hash plus resource/subresource/verb. A TTL of a few seconds bounds staleness and removes most API-server traffic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/io/cryostat/security/rbac/RbacHttpAuthenticationMechanism.java`
around lines 189 - 248, The checkSsarPermission flow currently caches only the
client, so repeated permission checks still issue SSAR requests. Add a short-TTL
decision cache keyed by a hash of rawToken plus the resolved
resource/subresource/verb, consult it before creating the SSAR request, and
store both allowed and denied results with a TTL of a few seconds. Preserve the
existing denial behavior for unmapped permissions and SSAR failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@schema/openapi.yaml`:
- Around line 1296-1297: Define the referenced
components.securitySchemes.SecurityScheme in the OpenAPI document, ensuring its
type and required authentication details match the existing security usage. Keep
the existing security reference unchanged and place the definition under
components.securitySchemes.

In `@src/main/java/io/cryostat/asyncprofiler/AsyncProfiler.java`:
- Line 101: Require all permissions in every multi-permission
`@PermissionsAllowed` declaration by setting inclusive = true, preserving each
declaration’s existing permission names. Apply this to AsyncProfiler.java at
lines 101, 113, 132, 142, and 153; UnifiedLogs.java at lines 89, 130, 182, 207,
217, 251, 278, 295, and 349; RecordingOptions.java at lines 56 and 75; and
SmartTriggers.java at lines 69, 89, 123, and 156, as well as the remaining
multi-permission declarations in these affected classes.

In `@src/main/java/io/cryostat/credentials/Credentials.java`:
- Line 105: Add the targets:read permission requirement to the target-scoped
operation in Credentials.java at lines 105-105, before testing supplied
credentials against targetId. Also add targets:read to the operation in
Events.java at lines 46-46, before retrieving event types from id; retain the
existing feature permissions.

In `@src/main/java/io/cryostat/diagnostic/Diagnostics.java`:
- Line 141: Require all permissions in every multi-value `@PermissionsAllowed`
declaration by setting inclusive = true. Update the declarations at
src/main/java/io/cryostat/diagnostic/Diagnostics.java:141-141, 162-162, 299-299,
362-362, and 423-423;
src/main/java/io/cryostat/events/TargetEventTemplates.java:60-60 and 87-87; and
src/main/java/io/cryostat/recordings/ActiveRecordings.java:82-82, 127-127, and
176-176. Apply the same change to all multi-value declarations in Reports.java,
SmartTriggers.java, UnifiedLogs.java, AsyncProfiler.java, and
RecordingOptions.java, preserving single-permission declarations.

In `@src/main/java/io/cryostat/discovery/CustomDiscovery.java`:
- Line 94: Update both CustomDiscovery handlers at
src/main/java/io/cryostat/discovery/CustomDiscovery.java lines 94-94 and 116-116
to require credentials:write when supplied credentials will be persisted, while
retaining targets:write for target creation. Apply the authorization
consistently to both doCreate and the form-request handler.

In `@src/main/java/io/cryostat/graphql/TargetNodes.java`:
- Line 227: Update the resolver governed by the `@RequiresPermission` annotation
in TargetNodes so authorization is selection-aware: require
archivedrecordings:read only when archived data is selected, and
activerecordings:read only when active data is selected. Ensure clients
selecting only recordings { active } can proceed with targets:read and
activerecordings:read without requiring archived access, while preserving
authorization for each selected resource before loading its data.

In `@src/main/java/io/cryostat/recordings/ActiveRecordings.java`:
- Line 127: Update the authorization on the handler around the save-processing
logic in ActiveRecordings to require archivedrecordings:write before creating an
archived recording, while preserving targets:read and the existing
stop-operation authorization. If the handler combines stop and save, split their
authorization or enforce the archive permission specifically for the save
branch.

In `@src/main/java/io/cryostat/recordings/Snapshots.java`:
- Line 50: Update the multi-permission authorization declarations so all listed
permissions are required together: add inclusive = true to Snapshots.java lines
50-50 and Reports.java lines 164-164, and apply the same AND semantics to other
multi-permission declarations in UnifiedLogs.java, AsyncProfiler.java, and the
`@RequiresPermission` GraphQL resolvers in TargetNodes.java. Verify
GraphQLSecurityInterceptor enforces AND semantics for permission arrays as well.

In
`@src/main/java/io/cryostat/security/rbac/RbacHttpAuthenticationMechanism.java`:
- Around line 148-172: Update the permission checker lambda in
RbacHttpAuthenticationMechanism to build the filtered action checks before
calling Uni.combine().all(). If the resulting list is empty, fall back to
checkSsarPermission(rawToken, permission.getName()); otherwise combine the
checks and require all results to be true.

In `@src/main/resources/application.properties`:
- Around line 308-321: Remove the shipped
cryostat.security.rbac.default-permission property from
src/main/resources/application.properties while leaving the property available
for explicit deployment overrides. In
src/test/java/io/cryostat/security/rbac/PermissionMapperNoDefaultTest.java,
explicitly clear the inherited default and assert that
PermissionMapper.resolve("unknown:action") returns empty.

In `@src/test/java/io/cryostat/security/rbac/SsarClientCacheTest.java`:
- Around line 73-83: Remove the cache.shutdown() call from
testInvalidateClosesClient so the subsequent verify(mockClient).close()
assertion isolates and validates the behavior of cache.invalidate("token-close")
alone.

---

Nitpick comments:
In
`@src/main/java/io/cryostat/security/rbac/graphql/GraphQLSecurityInterceptor.java`:
- Around line 70-84: Update buildOpenshiftIdentity to memoize each authorization
decision by request and permission, including denied (false) results, so
repeated checks reuse the cached outcome instead of issuing new
SelfSubjectAccessReview requests. Preserve the existing fail-closed behavior for
authorization errors and keep the per-permission validation semantics unchanged.

In
`@src/main/java/io/cryostat/security/rbac/RbacHttpAuthenticationMechanism.java`:
- Around line 189-248: The checkSsarPermission flow currently caches only the
client, so repeated permission checks still issue SSAR requests. Add a short-TTL
decision cache keyed by a hash of rawToken plus the resolved
resource/subresource/verb, consult it before creating the SSAR request, and
store both allowed and denied results with a TTL of a few seconds. Preserve the
existing denial behavior for unmapped permissions and SSAR failures.

In `@src/main/java/io/cryostat/security/rbac/SsarClientCache.java`:
- Line 47: Replace the LoadingCache field in SsarClientCache with Cache<String,
KubernetesClient>, update the Caffeine import, and build it with build() rather
than a throwing loader. Remove the unreachable loader configuration while
preserving getOrCreate’s existing mapping function behavior.

In `@src/test/java/io/cryostat/security/rbac/SsarClientCacheTest.java`:
- Around line 114-124: Remove the duplicate testHashingConsistency coverage or
rewrite it to validate hashing-specific behavior rather than repeating
testGetOrCreateReturnsCachedClientForSameToken. Prefer asserting that distinct
tokens create distinct cached clients and that a non-ASCII token is processed
successfully, while retaining the relevant factory-call and identity assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: b8def103-b0e4-4378-bd2d-e1d3dae26b24

📥 Commits

Reviewing files that changed from the base of the PR and between 17ca85e and 67566bd.

📒 Files selected for processing (46)
  • pom.xml
  • schema/openapi.yaml
  • src/main/java/io/cryostat/asyncprofiler/AsyncProfiler.java
  • src/main/java/io/cryostat/credentials/Credentials.java
  • src/main/java/io/cryostat/diagnostic/Diagnostics.java
  • src/main/java/io/cryostat/diagnostic/UnifiedLogs.java
  • src/main/java/io/cryostat/discovery/CustomDiscovery.java
  • src/main/java/io/cryostat/discovery/Discovery.java
  • src/main/java/io/cryostat/events/EventTemplates.java
  • src/main/java/io/cryostat/events/Events.java
  • src/main/java/io/cryostat/events/TargetEventTemplates.java
  • src/main/java/io/cryostat/expressions/MatchExpressions.java
  • src/main/java/io/cryostat/graphql/ActiveRecordings.java
  • src/main/java/io/cryostat/graphql/ArchivedRecordings.java
  • src/main/java/io/cryostat/graphql/AsyncProfilerGraphQL.java
  • src/main/java/io/cryostat/graphql/EnvironmentNodes.java
  • src/main/java/io/cryostat/graphql/HeapDumpGraphQL.java
  • src/main/java/io/cryostat/graphql/RecordingLinks.java
  • src/main/java/io/cryostat/graphql/RootNode.java
  • src/main/java/io/cryostat/graphql/TargetNodes.java
  • src/main/java/io/cryostat/graphql/ThreadDumpGraphQL.java
  • src/main/java/io/cryostat/recordings/ActiveRecordings.java
  • src/main/java/io/cryostat/recordings/ActiveRecordingsDownload.java
  • src/main/java/io/cryostat/recordings/ArchivedRecordings.java
  • src/main/java/io/cryostat/recordings/RecordingOptions.java
  • src/main/java/io/cryostat/recordings/Snapshots.java
  • src/main/java/io/cryostat/recordings/analysis/JfrAnalytics.java
  • src/main/java/io/cryostat/reports/AnalysisReportAggregator.java
  • src/main/java/io/cryostat/reports/Reports.java
  • src/main/java/io/cryostat/rules/Rules.java
  • src/main/java/io/cryostat/security/SecurityContextFilter.java
  • src/main/java/io/cryostat/security/rbac/PermissionMapper.java
  • src/main/java/io/cryostat/security/rbac/RbacConfig.java
  • src/main/java/io/cryostat/security/rbac/RbacHttpAuthenticationMechanism.java
  • src/main/java/io/cryostat/security/rbac/RbacMode.java
  • src/main/java/io/cryostat/security/rbac/SsarClientCache.java
  • src/main/java/io/cryostat/security/rbac/SsarClientFactory.java
  • src/main/java/io/cryostat/security/rbac/graphql/GraphQLSecurityInterceptor.java
  • src/main/java/io/cryostat/security/rbac/graphql/RequiresPermission.java
  • src/main/java/io/cryostat/targets/Targets.java
  • src/main/java/io/cryostat/triggers/SmartTriggers.java
  • src/main/resources/application.properties
  • src/test/java/io/cryostat/security/rbac/PermissionMapperNoDefaultTest.java
  • src/test/java/io/cryostat/security/rbac/PermissionMapperTest.java
  • src/test/java/io/cryostat/security/rbac/RbacHttpAuthenticationMechanismTest.java
  • src/test/java/io/cryostat/security/rbac/SsarClientCacheTest.java

Comment thread schema/openapi.yaml
Comment thread src/main/java/io/cryostat/asyncprofiler/AsyncProfiler.java Outdated
Comment thread src/main/java/io/cryostat/credentials/Credentials.java Outdated
Comment thread src/main/java/io/cryostat/diagnostic/Diagnostics.java Outdated
Comment thread src/main/java/io/cryostat/discovery/CustomDiscovery.java Outdated
Comment thread src/main/java/io/cryostat/recordings/ActiveRecordings.java Outdated
Comment thread src/main/java/io/cryostat/recordings/Snapshots.java Outdated
Comment thread src/main/resources/application.properties
Comment thread src/test/java/io/cryostat/security/rbac/SsarClientCacheTest.java
@github-actions

Copy link
Copy Markdown

CI build:
Unit tests pass with flaky tests ⚠️
Tests run: 742, Failures: 0, Errors: 0, Skipped: 108, Flakes: 1

Flaky tests:

@andrewazores
andrewazores force-pushed the fine-grained-permissions branch from 664637f to ed24546 Compare September 14, 2026 20:08
@andrewazores

Copy link
Copy Markdown
Member Author

/build_test

@github-actions

Copy link
Copy Markdown

CI build — Schema check results

OpenAPI schema change detected:

diff --git a/schema/openapi.yaml b/schema/openapi.yaml
index b31c9382..42aa6fdd 100644
--- a/schema/openapi.yaml
+++ b/schema/openapi.yaml
@@ -2883,20 +2883,26 @@ paths:
       parameters:
         - in: path
           name: targetId
           required: true
           schema:
             format: int64
             type: integer
       responses:
         "204":
           description: Active recordings synchronized
+        "401":
+          description: Not Authorized
+        "403":
+          description: Not Allowed
+      security:
+        - SecurityScheme: []
       summary: Resynchronize active recordings on the specified target
       tags:
         - Recording Sync
   /api/v4/activedownload/{id}:
     get:
       description: |
         Given a recording ID and a remote recording ID within that target, Cryostat will open a remote
         connection to the target and pipe back a data stream containing the Flight Recording binary file
         format for that recording. The client can feed this data to other tooling which ingests the JFR
         binary file format.

No GraphQL schema changes detected.

WebSocket notifications schema change detected:

diff --git a/schema/notifications.yaml b/schema/notifications.yaml
index 2a482aaa..3d54e68d 100644
--- a/schema/notifications.yaml
+++ b/schema/notifications.yaml
@@ -1158,43 +1158,39 @@ components:
             properties:
               event:
                 type: object
                 properties:
                   kind:
                     type: object
                     description: Payload of type EventKind
                   serviceRef:
                     type: object
                     properties:
-                      id:
-                        type: integer
                       connectUrl:
                         type: object
                         description: Payload of type URI
                       alias:
                         type: string
                       jvmId:
                         type: string
                       labels:
                         type: object
                         additionalProperties: true
                       annotations:
                         type: object
                         properties:
                           platform:
                             type: object
                             additionalProperties: true
                           cryostat:
                             type: object
                             additionalProperties: true
-                      agent:
-                        type: boolean
                   jvmId:
                     type: string
         required:
         - meta
         - message
     TemplateDeleted:
       name: TemplateDeleted
       title: Template Deleted
       summary: 'Notification: TemplateDeleted'
       description: WebSocket notification for Template Deleted events

@github-actions

Copy link
Copy Markdown

CI build:
Integration tests pass ✅
Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
https://github.com/cryostatio/cryostat/actions/runs/34891100686

@andrewazores

Copy link
Copy Markdown
Member Author

/build_test

@github-actions

Copy link
Copy Markdown

CI build — Schema check results

No OpenAPI schema changes detected.

No GraphQL schema changes detected.

WebSocket notifications schema change detected:

diff --git a/schema/notifications.yaml b/schema/notifications.yaml
index 2a482aaa..3d54e68d 100644
--- a/schema/notifications.yaml
+++ b/schema/notifications.yaml
@@ -1158,43 +1158,39 @@ components:
             properties:
               event:
                 type: object
                 properties:
                   kind:
                     type: object
                     description: Payload of type EventKind
                   serviceRef:
                     type: object
                     properties:
-                      id:
-                        type: integer
                       connectUrl:
                         type: object
                         description: Payload of type URI
                       alias:
                         type: string
                       jvmId:
                         type: string
                       labels:
                         type: object
                         additionalProperties: true
                       annotations:
                         type: object
                         properties:
                           platform:
                             type: object
                             additionalProperties: true
                           cryostat:
                             type: object
                             additionalProperties: true
-                      agent:
-                        type: boolean
                   jvmId:
                     type: string
         required:
         - meta
         - message
     TemplateDeleted:
       name: TemplateDeleted
       title: Template Deleted
       summary: 'Notification: TemplateDeleted'
       description: WebSocket notification for Template Deleted events

@github-actions

Copy link
Copy Markdown

CI build:
Integration tests pass ✅
Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
https://github.com/cryostatio/cryostat/actions/runs/34892395305

@github-actions

Copy link
Copy Markdown

CI build:
Unit tests pass with flaky tests ⚠️
Tests run: 803, Failures: 0, Errors: 0, Skipped: 4, Flakes: 1

Flaky tests:

@github-actions

Copy link
Copy Markdown

CI build:
Unit tests pass with flaky tests ⚠️
Tests run: 803, Failures: 0, Errors: 0, Skipped: 4, Flakes: 1

Flaky tests:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Agent Autoconfig on OpenShift via nginx mTLS proxy fails since #1725 [Request] Fine-grained RBAC

3 participants