Skip to content

[Fix-2] RHCLOUD-47253: Extract credential type from request path/headers#125

Open
Adam0Brien wants to merge 22 commits into
mainfrom
RHCLOUD-47253-Fix-2
Open

[Fix-2] RHCLOUD-47253: Extract credential type from request path/headers#125
Adam0Brien wants to merge 22 commits into
mainfrom
RHCLOUD-47253-Fix-2

Conversation

@Adam0Brien
Copy link
Copy Markdown
Member

@Adam0Brien Adam0Brien commented May 18, 2026

PR Template:

Describe your changes

  • Tested using Stub-validator (where any non empty string is a valid token)

Config

# Trust store configuration
trust_store:
  type: stub_store
  validators:
    - type: stub_validator
      claims:
        preferred_username: "dev-user"
        email: "dev@example.com"
        scope: "openid api.console"
        sub: "dev-user-id"
        given_name: "Dev"
        family_name: "User"
        organization:
          id: "12345"
          account_number: "54321"
    - name: redhat-sso
      type: jwt_validator
      issuer: "https://sso.redhat.com/auth/realms/redhat-external"
      jwks_url: "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/certs"
      trust_domain: "https://sso.redhat.com/auth/realms/redhat-external"
      refresh_interval: "15m"
export TOKEN='test-token-123'

Testing

Bearer

grpcurl -plaintext -d "{
  \"attributes\": {
    \"request\": {
      \"http\": {
        \"method\": \"GET\",
        \"path\": \"/api/test\",
        \"headers\": {
          \"authorization\": \"Bearer ${TOKEN}\"
        }
      }
    },
    \"source\": {
      \"address\": { \"socketAddress\": { \"address\": \"127.0.0.1\" } }
    }
  }
}" localhost:9090 envoy.service.auth.v3.Authorization/Check

Result

{
  "status": {},
  "okResponse": {
    "headers": [
      {
        "header": {
          "key": "Transaction-Token",
          "value": "stub-txn-token.test-subject.txn-1779107786145237.{\"host\":\"\",\"ip_address\":\"127.0.0.1\",\"method\":\"GET\",\"path\":\"/api/test\"}"
        }
      },
      {
        "header": {
          "key": "x-rh-identity",
          "value": "eyJlbnRpdGxlbWVudHMiOnt9LCJpZGVudGl0eSI6eyJhY2NvdW50X251bWJlciI6IjU0MzIxIiwiYXV0aF90eXBlIjoiand0LWF1dGgiLCJpbnRlcm5hbCI6eyJhdXRoX3RpbWUiOjE3NzkxMDc3ODYxNDUsImNyb3NzX2FjY2VzcyI6ZmFsc2UsIm9yZ19pZCI6IjEyMzQ1In0sIm9yZ19pZCI6IjEyMzQ1IiwidHlwZSI6IlVzZXIiLCJ1c2VyIjp7ImVtYWlsIjoiZGV2QGV4YW1wbGUuY29tIiwiZmlyc3RfbmFtZSI6IkRldiIsImlzX2FjdGl2ZSI6dHJ1ZSwiaXNfaW50ZXJuYWwiOmZhbHNlLCJpc19vcmdfYWRtaW4iOmZhbHNlLCJsYXN0X25hbWUiOiJVc2VyIiwibG9jYWxlIjoiIiwidXNlcl9pZCI6IiIsInVzZXJuYW1lIjoiZGV2LXVzZXIifX19"
        }
      }
    ],
    "headersToRemove": [
      "authorization"
    ]
  }
}

Cookie

grpcurl -plaintext -d "{
  \"attributes\": {
    \"request\": {
      \"http\": {
        \"method\": \"GET\",
        \"path\": \"/api/test\",
        \"headers\": {
          \"cookie\": \"session=abc; cs_jwt=${TOKEN}\"
        }
      }
    },
    \"source\": {
      \"address\": { \"socketAddress\": { \"address\": \"127.0.0.1\" } }
    }
  }
}" localhost:9090 envoy.service.auth.v3.Authorization/Check

Result

{
  "status": {},
  "okResponse": {
    "headers": [
      {
        "header": {
          "key": "Transaction-Token",
          "value": "stub-txn-token.test-subject.txn-1779114197327907.{\"host\":\"\",\"ip_address\":\"127.0.0.1\",\"method\":\"GET\",\"path\":\"/api/test\"}"
        }
      },
      {
        "header": {
          "key": "x-rh-identity",
          "value": "eyJlbnRpdGxlbWVudHMiOnt9LCJpZGVudGl0eSI6eyJhY2NvdW50X251bWJlciI6IjU0MzIxIiwiYXV0aF90eXBlIjoiand0LWF1dGgiLCJpbnRlcm5hbCI6eyJhdXRoX3RpbWUiOjE3NzkxMTQxOTczMjgsImNyb3NzX2FjY2VzcyI6ZmFsc2UsIm9yZ19pZCI6IjEyMzQ1In0sIm9yZ19pZCI6IjEyMzQ1IiwidHlwZSI6IlVzZXIiLCJ1c2VyIjp7ImVtYWlsIjoiZGV2QGV4YW1wbGUuY29tIiwiZmlyc3RfbmFtZSI6IkRldiIsImlzX2FjdGl2ZSI6dHJ1ZSwiaXNfaW50ZXJuYWwiOmZhbHNlLCJpc19vcmdfYWRtaW4iOmZhbHNlLCJsYXN0X25hbWUiOiJVc2VyIiwibG9jYWxlIjoiIiwidXNlcl9pZCI6IiIsInVzZXJuYW1lIjoiZGV2LXVzZXIifX19"
        }
      },
      {
        "header": {
          "key": "cookie",
          "value": "session=abc"
        }
      }
    ]
  }
}

Query

grpcurl -plaintext -d "{
  \"attributes\": {
    \"request\": {
      \"http\": {
        \"method\": \"GET\",
        \"path\": \"/api/test?token=${TOKEN}\",
        \"headers\": {}
      }
    },
    \"source\": {
      \"address\": { \"socketAddress\": { \"address\": \"127.0.0.1\" } }
    }
  }
}" localhost:9090 envoy.service.auth.v3.Authorization/Check

Result

{
  "status": {},
  "okResponse": {
    "headers": [
      {
        "header": {
          "key": "Transaction-Token",
          "value": "stub-txn-token.test-subject.txn-1779114832725989.{\"host\":\"\",\"ip_address\":\"127.0.0.1\",\"method\":\"GET\",\"path\":\"/api/test?token=eyJlbnRpdGxlbWVudHMiOnt9LCJpZGVudGl0eSI6eyJhY2NvdW50X251bWJlciI6IjU0MzIxIiwiYXV0aF90eXBlIjoiand0LWF1dGgiLCJpbnRlcm5hbCI6eyJhdXRoX3RpbWUiOjE3NzkxMDMzMDcyODgsImNyb3NzX2FjY2VzcyI6ZmFsc2UsIm9yZ19pZCI6IjEyMzQ1In0sIm9yZ19pZCI6IjEyMzQ1IiwidHlwZSI6IlVzZXIiLCJ1c2VyIjp7ImVtYWlsIjoiZGV2QGV4YW1wbGUuY29tIiwiZmlyc3RfbmFtZSI6IkRldiIsImlzX2FjdGl2ZSI6dHJ1ZSwiaXNfaW50ZXJuYWwiOmZhbHNlLCJpc19vcmdfYWRtaW4iOmZhbHNlLCJsYXN0X25hbWUiOiJVc2VyIiwibG9jYWxlIjoiIiwidXNlcl9pZCI6IiIsInVzZXJuYW1lIjoiZGV2LXVzZXIifX19\"}"
        }
      },
      {
        "header": {
          "key": "x-rh-identity",
          "value": "eyJlbnRpdGxlbWVudHMiOnt9LCJpZGVudGl0eSI6eyJhY2NvdW50X251bWJlciI6IjU0MzIxIiwiYXV0aF90eXBlIjoiand0LWF1dGgiLCJpbnRlcm5hbCI6eyJhdXRoX3RpbWUiOjE3NzkxMTQ4MzI3MjYsImNyb3NzX2FjY2VzcyI6ZmFsc2UsIm9yZ19pZCI6IjEyMzQ1In0sIm9yZ19pZCI6IjEyMzQ1IiwidHlwZSI6IlVzZXIiLCJ1c2VyIjp7ImVtYWlsIjoiZGV2QGV4YW1wbGUuY29tIiwiZmlyc3RfbmFtZSI6IkRldiIsImlzX2FjdGl2ZSI6dHJ1ZSwiaXNfaW50ZXJuYWwiOmZhbHNlLCJpc19vcmdfYWRtaW4iOmZhbHNlLCJsYXN0X25hbWUiOiJVc2VyIiwibG9jYWxlIjoiIiwidXNlcl9pZCI6IiIsInVzZXJuYW1lIjoiZGV2LXVzZXIifX19"
        }
      }
    ],
    "queryParametersToRemove": [
      "token"
    ]
  }
}

Cert

grpcurl -plaintext -d '{
  "attributes": {
    "request": {
      "http": {
        "method": "GET",
        "path": "/api/test",
        "headers": {
          "x-forwarded-client-cert": "By=spiffe://example/ns/default/sa/app;Hash=abc"
        }
      }
    },
    "source": {
      "address": { "socketAddress": { "address": "127.0.0.1" } }
    }
  }
}' localhost:9090 envoy.service.auth.v3.Authorization/Check

Result

{
  "status": {},
  "okResponse": {
    "headers": [
      {
        "header": {
          "key": "Transaction-Token",
          "value": "stub-txn-token.test-subject.txn-1779114864917438.{\"host\":\"\",\"ip_address\":\"127.0.0.1\",\"method\":\"GET\",\"path\":\"/api/test\"}"
        }
      },
      {
        "header": {
          "key": "x-rh-identity",
          "value": "eyJlbnRpdGxlbWVudHMiOnt9LCJpZGVudGl0eSI6eyJhY2NvdW50X251bWJlciI6IjU0MzIxIiwiYXV0aF90eXBlIjoiand0LWF1dGgiLCJpbnRlcm5hbCI6eyJhdXRoX3RpbWUiOjE3NzkxMTQ4NjQ5MTcsImNyb3NzX2FjY2VzcyI6ZmFsc2UsIm9yZ19pZCI6IjEyMzQ1In0sIm9yZ19pZCI6IjEyMzQ1IiwidHlwZSI6IlVzZXIiLCJ1c2VyIjp7ImVtYWlsIjoiZGV2QGV4YW1wbGUuY29tIiwiZmlyc3RfbmFtZSI6IkRldiIsImlzX2FjdGl2ZSI6dHJ1ZSwiaXNfaW50ZXJuYWwiOmZhbHNlLCJpc19vcmdfYWRtaW4iOmZhbHNlLCJsYXN0X25hbWUiOiJVc2VyIiwibG9jYWxlIjoiIiwidXNlcl9pZCI6IiIsInVzZXJuYW1lIjoiZGV2LXVzZXIifX19"
        }
      }
    ],
    "headersToRemove": [
      "x-forwarded-client-cert"
    ]
  }
}

Deny

grpcurl -plaintext -d '{
  "attributes": {
    "request": {
      "http": {
        "method": "GET",
        "path": "/api/test",
        "headers": {}
      }
    },
    "source": {
      "address": { "socketAddress": { "address": "127.0.0.1" } }
    }
  }
}' localhost:9090 envoy.service.auth.v3.Authorization/Check

Result

{
  "status": {
    "code": 16,
    "message": "failed to extract credentials: query parameter \"token\" not found"
  },
  "deniedResponse": {
    "body": "failed to extract credentials: query parameter \"token\" not found"
  }
}

Ticket reference (if applicable)

Fixes #

Checklist

  • Are the agreed upon acceptance criteria fulfilled?

  • Was the 4-eye-principle applied? (async PR review, pairing, ensembling)

  • Do your changes have passing automated tests and sufficient observability?

  • Are the work steps you introduced repeatable by others, either through automation or documentation?

    • If automation is possible but not done due to other constraints, a ticket to the tech debt sprint is added
    • An SOP (Standard Operating Procedure) was created
  • The Changes were automatically built, tested, and - if needed, behind a feature flag - deployed to our production environment. (Please check this when the new deployment is done and you could verify it.)

  • Are the agreed upon coding/architectural practices applied?

  • Are security needs fullfilled? (e.g. no internal URL)

  • Is the corresponding Ticket in the right state? (should be on "review" now, put to done when this change made it to production)

  • For changes to the public API / code dependencies: Was the whole team (or a sufficient amount of ppl) able to review?

Summary by CodeRabbit

  • New Features

    • Configurable credential sources for authorization: support bearer tokens, cookies, certificates, and query parameters with configurable precedence and removal/sanitization of used headers/params.
    • Authorization server accepts and wires credential-source configuration; issuance and mapping flows surface the credential source type.
    • Stub validator can return configured JWT-like claims.
    • CEL mapper scripts gain a variable exposing the credential source type.
  • Tests

    • Added tests validating credential-source extraction, precedence, sanitization, error cases, and default behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

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
📝 Walkthrough

Walkthrough

Adds configurable ordered credential sources for ext_authz (bearer, cookie, cert, query), implements extractors and tests, validates and maps config to runtime sources, refactors AuthzServer to use them and propagate extraction metadata, and surfaces the source as cred_type to CEL and mappers.

Changes

Multi-Source Credential Extraction

Layer / File(s) Summary
Configuration schema
configs/parsec.yaml, internal/config/config.go
Adds authz_server.credential_sources and ValidatorConfig.Claims to the public config model.
CLI wiring
internal/cli/serve.go
runServe() loads credential sources from provider and conditionally passes them to NewAuthzServer via WithCredentialSources().
Provider mapping & tests
internal/config/provider.go, internal/config/provider_test.go
Adds Provider.AuthzServerCredentialSources() to validate and convert configured sources into server.CredentialSource; tests cover success and validation failures.
Stub validator claims
internal/config/validators.go
Clones configured claims and attaches a populated trust.Result to the stub validator when claims are provided.
CredentialSource core
internal/server/credential_sources.go
Defines CredentialSource interface, CredentialExtraction struct, NewCredentialSource factory, default fallback, and extractCredentialFromSources orchestrator with HTTP attribute helper.
Source implementations
internal/server/*.go
Implements BearerCredentialSource, CookieCredentialSource (with parsing/sanitization), QueryCredentialSource, CertCredentialSource.
Source unit tests
internal/server/credential_sources_test.go
Comprehensive tests for extraction, precedence, sanitization, factory mapping, and default fallback.
AuthzServer refactor & integration
internal/server/authz.go
Adds AuthzServerOption/WithCredentialSources, accepts options in NewAuthzServer, uses extractCredentialFromSources() in Check(), includes CredentialSourceType in issuance, and applies extraction metadata to response headers and query removals.
Service & mapper propagation
internal/service/*.go
Adds CredentialSourceType to IssueRequest, IssueContext, and MapperInput; IssueTokens() populates the context for issuers and mappers.
CEL mapper variable and tests
internal/cel/mapper_input.go, internal/mapper/cel_mapper.go, internal/mapper/cel_mapper_test.go
Adds cred_type CEL variable and sets it from MapperInput.CredentialSourceType; test verifies CEL sees the configured source.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AuthzServer
  participant Extractor
  participant TokenService
  participant CELMapper

  Client->>AuthzServer: ext_authz Check (request with bearer/cookie/query/cert)
  AuthzServer->>Extractor: extractCredentialFromSources(request, configuredSources)
  Extractor->>Extractor: try configured sources in order
  Extractor-->>AuthzServer: credential + sourceType + headers/query removals + headerSets
  AuthzServer->>TokenService: IssueRequest (with CredentialSourceType)
  TokenService->>CELMapper: Map claims (MapperInput.CredentialSourceType)
  CELMapper->>CELMapper: evaluate CEL using cred_type
  CELMapper-->>TokenService: transformed claims
  TokenService-->>AuthzServer: issued token
  AuthzServer->>Client: ext_authz response (with dynamic headers/query removals)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • jharting
  • coderbydesign
  • Rajagopalan-Ranganathan
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: extracting credential type from request path and headers via a configurable credential sources layer.
Description check ✅ Passed The PR description provides detailed testing results with examples for Bearer, Cookie, Query, and Cert extraction flows, configuration examples, and gRPC test cases. However, the checklist is incomplete with all items unchecked and no ticket reference provided.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch RHCLOUD-47253-Fix-2

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 18, 2026

The latest Buf updates on your PR. Results from workflow buf-pull-request / build (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed⏩ skipped✅ passed⏩ skippedJun 8, 2026, 11:08 AM

@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

Codecov Report

❌ Patch coverage is 55.36232% with 154 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/server/authz.go 45.94% 17 Missing and 3 partials ⚠️
internal/probe/zlog/logging.go 0.00% 18 Missing ⚠️
internal/config/validators.go 0.00% 17 Missing ⚠️
internal/server/actor_credential.go 46.42% 14 Missing and 1 partial ⚠️
internal/observer/composite.go 0.00% 12 Missing ⚠️
internal/cli/serve.go 0.00% 11 Missing ⚠️
internal/datasource/lua_datasource.go 54.16% 8 Missing and 3 partials ⚠️
internal/service/observer.go 0.00% 11 Missing ⚠️
internal/server/cookie_credential_source.go 75.60% 6 Missing and 4 partials ⚠️
internal/server/query_credential_source.go 57.14% 5 Missing and 4 partials ⚠️
... and 4 more
Flag Coverage Δ
main 48.59% <55.36%> (+1.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
internal/config/credential_sources.go 100.00% <100.00%> (ø)
internal/config/provider.go 32.12% <100.00%> (+1.43%) ⬆️
internal/mapper/cel_mapper.go 85.39% <100.00%> (+0.33%) ⬆️
internal/server/credential_context.go 100.00% <100.00%> (ø)
internal/trust/validator.go 83.33% <ø> (ø)
internal/server/bearer_credential_source.go 81.25% <81.25%> (ø)
internal/service/fake.go 37.97% <0.00%> (-0.99%) ⬇️
internal/server/exchange.go 73.46% <64.70%> (-3.28%) ⬇️
internal/server/credential_sources.go 68.18% <68.18%> (ø)
internal/server/query_credential_source.go 57.14% <57.14%> (ø)
... and 9 more

... and 7 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Adam0Brien Adam0Brien changed the title RHCLOUD-47253: Extract credential type from request path/headers [Fix-2] RHCLOUD-47253: Extract credential type from request path/headers May 18, 2026
@Adam0Brien
Copy link
Copy Markdown
Member Author

Adam0Brien commented May 18, 2026

@coderabbitai full-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

🧩 Analysis chain

I'll start a full review of the PR. Let me gather all the necessary information first.


🏁 Script executed:

#!/bin/bash
# Fetch PR details
gh pr view 125 --repo project-kessel/parsec --json title,body,files,additions,deletions,changedFiles

# Get the list of changed files
gh pr diff 125 --repo project-kessel/parsec --name-only

Length of output: 11861


🏁 Script executed:

#!/bin/bash
# Get the full diff for this PR
gh pr diff 125 --repo project-kessel/parsec

Length of output: 26402


Let me check the Go version requirement and a few more things before writing the full review.


🏁 Script executed:

#!/bin/bash
# Check go.mod for minimum Go version
cat go.mod | head -20

# Check for any existing use of strings.SplitSeq (Go 1.24+)
rg -n "SplitSeq" --type go

# Check how trust.NewStubValidator and WithResult are defined
rg -n "WithResult\|NewStubValidator\|StubValidator" --type go -A3

# Check what trust.BearerCredential looks like, and if there's a CertCredential type
rg -n "BearerCredential\|CertCredential\|Credential\b" --type go -B1 -A3

# Check cookieValue usage and the net/http cookiejar or stdlib approach
rg -n "cookieValue\|http.ReadCookies\|net/http" internal/server/ --type go

# Check what x-rh-certauth-cn is about - any existing references
rg -rn "x-rh-certauth-cn\|certauth" --type go

# Check if there's any test for the cert fallback
rg -n "certauth\|x-rh-certauth" --type go

Length of output: 842


@Adam0Brien — I've completed a full review of the PR. Here are my findings:


Summary

This PR introduces a clean, extensible credential extraction layer for ext_authz with support for Bearer, cookie, query-parameter, and certificate-based credentials. The functional-options pattern for AuthzServer and the cred_type CEL variable are well-designed. A few security and correctness issues need attention before merging.


internal/server/credential_sources.go

🔴 Security — Query token is forwarded to the upstream

extractBearerFromQuery sets headers: nil, meaning no entry is added to headersToRemove. The full path including ?token=<jwt> is forwarded to the upstream service:

return &credentialExtraction{
    credential: &trust.BearerCredential{Token: token},
    headers:    nil, // token-bearing URL is passed upstream
    sourceType: "query",
}, nil

This is confirmed by the PR's own test output — the okResponse for the query case has no headersToRemove field. The upstream receives GET /api/test?token=<jwt>. Consider stripping the query parameter from the forwarded path, or at minimum documenting this behaviour explicitly.


🔴 Security — Cookie source strips all downstream cookies

extractBearerFromCookie adds the entire cookie header to headersToRemove:

headers: []string{"cookie"},

This removes all cookies from the downstream request, not just cs_jwt. Services downstream that need session, CSRF, or other cookies will have them stripped silently. Consider extracting only the target cookie and reconstructing a cookie header without it, or documenting the all-or-nothing removal behaviour as an explicit design decision.


🟡 Bug — Undocumented, untested fallback in extractCertCredential

certHeader := headers[header]
if certHeader == "" && header != "x-rh-certauth-cn" {
    certHeader = headers["x-rh-certauth-cn"]
    if certHeader != "" {
        header = "x-rh-certauth-cn"
    }
}

When the configured header (e.g. x-forwarded-client-cert) is absent, the code silently falls back to x-rh-certauth-cn. This behaviour is:

  • Not documented anywhere (config, comments, or tests)
  • Not reflected in configs/parsec.yaml
  • Potentially surprising when an explicit header: is configured and it silently shadows to a different header

If this is an intentional Red Hat platform convenience, it should be documented and have test coverage. If not, it should be removed.


🟡 Debugging — Only the last error is returned when all sources fail

for _, src := range sources {
    ext, err := tryCredentialSource(...)
    if err != nil {
        lastErr = err
        continue
    }
    ...
}
if lastErr != nil {
    return nil, lastErr
}

With e.g. [bearer, cookie, query] configured and all failing, only the query error is reported. Errors from earlier sources are silently discarded. Consider aggregating errors or at least returning the error from the first source (which is likely the one the caller intended):

var errs []string
for _, src := range sources {
    ...
    if err != nil {
        errs = append(errs, fmt.Sprintf("%s: %v", src.Type, err))
        continue
    }
    ...
}
return nil, fmt.Errorf("no credentials found: [%s]", strings.Join(errs, "; "))

🟡 Test gaps

Missing test File
extractCertCredential fallback to x-rh-certauth-cn credential_sources_test.go
Unsupported source type (default branch in tryCredentialSource) credential_sources_test.go
cookieValue with URL-encoded values or empty cookie value (cs_jwt=) credential_sources_test.go
Cert extraction with a custom header: value (e.g. header: x-ssl-client-cert) credential_sources_test.go

internal/config/provider.go

🟡 Missing early config validation

AuthzServerCredentialSources() only validates that type is non-empty. Source-type-specific requirements (e.g. that name is meaningful for cookie/query, or that header is a valid HTTP header name for cert) are only checked at extraction time. Consider failing fast at startup:

switch srcCfg.Type {
case "cookie", "query":
    // Name defaults are applied at extraction time, OK
case "cert":
    // Optionally validate header is a valid header name
case "bearer":
    // No extra fields needed
default:
    return nil, fmt.Errorf("credential_sources[%d]: unknown type %q", i, srcCfg.Type)
}

internal/config/validators.go

🟢 Minor — Stub validator result fields are hardcoded

result := &trust.Result{
    Subject:     "test-subject",
    Issuer:      "https://test-issuer.example.com",
    ...
}

For a stub/dev validator this is reasonable, but subject / issuer should ideally be overridable from config (e.g. cfg.Claims["sub"]) to produce more realistic test tokens. Not blocking.


internal/server/credential_sources.go — Minor

Bearer scheme is case-sensitive

token, ok := strings.CutPrefix(authHeader, "Bearer ")

RFC 7235 specifies auth-scheme names are case-insensitive (bearer, BEARER, Bearer are all valid). This is pre-existing behaviour but now more visible. Consider strings.EqualFold on the prefix or strings.ToLower on the header before splitting.


Checklist of Positives ✅

  • Clean functional options pattern for AuthzServer with backward-compatible default (bearer fallback when no sources configured)
  • cred_type CEL variable is well-plumbed through the entire pipeline (IssueRequestIssueContextMapperInput → CEL activation)
  • Good test coverage for the happy path and fallthrough scenarios (first matching source wins, falls through to second source)
  • Config YAML example is clear and well-commented
  • extractCredentialFromSources properly guards against nil HTTP attributes
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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 `@internal/config/provider.go`:
- Around line 394-402: The loop over p.config.AuthzServer.CredentialSources
currently only checks that Type is non-empty and defers other schema errors to
runtime; update the validation in the loop that builds sources (iterating
p.config.AuthzServer.CredentialSources) to fail-fast by: validating that Type is
one of the allowed values (e.g., "header","cookie","query", etc.), and for each
type enforce required fields (e.g., if Type == "header" require Header
non-empty; if Type == "cookie" or "query" require Name non-empty); return a
descriptive fmt.Errorf for any invalid or unknown type instead of appending an
invalid server.CredentialSource so startup rejects malformed configs.

In `@internal/server/authz.go`:
- Around line 42-49: Refactor the options to target a package-private config
struct (e.g., authzServerConfig) instead of mutating AuthzServer directly:
change the type AuthzServerOption to func(*authzServerConfig), update
WithCredentialSources to set config.CredentialSources on that config type, and
modify NewAuthzServer to create a default authzServerConfig, apply all
AuthzServerOption functions to it, then construct and return the public
AuthzServer instance using fields from the populated config; ensure any other
option functions in the same block follow the same pattern and that AuthzServer
remains immutable from options.

In `@internal/server/credential_sources.go`:
- Around line 148-152: The code silently falls back to
headers["x-rh-certauth-cn"] when certHeader is empty (the certHeader/header
assignment block); remove that implicit fallback and require explicit
configuration instead: either delete the fallback branch so certHeader/header
remain unchanged when not configured, or gate the assignment behind a clearly
named config flag (e.g., AllowCertHeaderFallback) and log a warning when using
the fallback; update any tests that assumed the implicit behavior and reference
the variables certHeader, header and the headers map when making the change.
- Around line 81-84: The Bearer scheme check using strings.CutPrefix(authHeader,
"Bearer ") is case-sensitive and will reject "bearer ..."; instead normalize the
scheme portion to lowercase before checking: compute a lowercased prefix (e.g.,
strings.ToLower(authHeader)) or split on whitespace to inspect the scheme, then
extract the token into the existing token variable and preserve the original
token casing; update the logic around authHeader, token, and the CutPrefix usage
so it accepts any case variation of "bearer" while still returning the token or
the same error when the scheme is not Bearer.
- Around line 108-112: The code currently returns credentialExtraction with
headers set to []string{"cookie"}, which removes the entire Cookie header;
instead parse the original Cookie header, filter out only the credential cookie
(e.g. "cs_jwt" or the token cookie used to build trust.BearerCredential),
rebuild a sanitized Cookie string containing the remaining cookies, and set the
credentialExtraction to include that sanitized cookie header (not a blanket
"cookie" removal). Update the return in the function that constructs
credentialExtraction so it computes sanitizedCookie by splitting/parsing the
original cookie string, omitting the credential name, and using that sanitized
value in headers (or the appropriate header-to-value form your codebase uses)
while leaving other cookies intact.
- Around line 115-138: extractBearerFromQuery currently returns a
credentialExtraction with sourceType "query" while leaving the token in the URL,
which Check() won't remove (it only strips headers) and thus leaks secrets; fix
by refusing query-based extraction: update extractBearerFromQuery to return an
error (e.g., "query token extraction not allowed") instead of a
credentialExtraction, or alternatively ensure the extraction sanitizes the path
by removing/overwriting the token before returning (so upstream never sees it);
reference extractBearerFromQuery, credentialExtraction and Check() when making
the change.
🪄 Autofix (Beta)

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: a5271951-ad7e-4de6-8ee0-ccc932f1805b

📥 Commits

Reviewing files that changed from the base of the PR and between 1bbd298 and a73cf63.

📒 Files selected for processing (14)
  • configs/parsec.yaml
  • internal/cel/mapper_input.go
  • internal/cli/serve.go
  • internal/config/config.go
  • internal/config/provider.go
  • internal/config/validators.go
  • internal/mapper/cel_mapper.go
  • internal/mapper/cel_mapper_test.go
  • internal/server/authz.go
  • internal/server/credential_sources.go
  • internal/server/credential_sources_test.go
  • internal/service/issuer.go
  • internal/service/mapper.go
  • internal/service/service.go

Comment thread internal/config/provider.go Outdated
Comment thread internal/server/authz.go
Comment thread internal/server/credential_sources.go Outdated
Comment thread internal/server/credential_sources.go Outdated
Comment thread internal/server/credential_sources.go Outdated
Comment thread internal/server/credential_sources.go Outdated
…stead of at request time: enforce allowed types and required fields per type.
Apply AuthzServerOption against authzServerConfig and embed it in
AuthzServer so credential sources are configured once at construction
instead of mutating the server via options.
…wercase bearer tokens are accepted; token value casing is unchanged.
…eader on ext_authz

Sanitize the Cookie header by removing the JWT cookie while keeping
other cookies, using header override when cookies remain and full
removal only when it was the sole cookie.
Set QueryParametersToRemove on ext_authz OK responses so tokens
extracted from the URL are stripped and do not leak to backends.
Require the configured cert header only so cert credentials are not
read from an undocumented alternate header.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@internal/server/authz.go`:
- Around line 179-186: The current loop that builds responseHeaders from
ext.headerSets creates corev3.HeaderValueOption entries without setting
AppendAction, causing Envoy to append sanitized headers instead of replacing
originals; update the construction of each corev3.HeaderValueOption (in the loop
over ext.headerSets where responseHeaders is appended) to set AppendAction to
corev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD so the sanitized header
overwrites any existing header rather than appending.
🪄 Autofix (Beta)

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: 900ff651-c99d-47b3-845b-169d6e3777ea

📥 Commits

Reviewing files that changed from the base of the PR and between a73cf63 and f44a0ae.

📒 Files selected for processing (5)
  • internal/config/provider.go
  • internal/config/provider_test.go
  • internal/server/authz.go
  • internal/server/credential_sources.go
  • internal/server/credential_sources_test.go

Comment thread internal/server/authz.go Outdated
…ders

Cookie credential sanitization returns a rewritten Cookie via HeaderSets;
without AppendAction Envoy appends the value and the original credential
header can still reach upstream.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
internal/server/authz.go (1)

179-186: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sanitized headers must explicitly overwrite existing request headers.

Line 180 creates corev3.HeaderValueOption without AppendAction. In Envoy, default behavior may append, which can keep original credential-bearing headers alongside sanitized values (notably cookie), breaking the intended security boundary.

Proposed fix
 	for name, value := range ext.HeaderSets {
 		responseHeaders = append(responseHeaders, &corev3.HeaderValueOption{
 			Header: &corev3.HeaderValue{
 				Key:   name,
 				Value: value,
 			},
+			AppendAction: corev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD,
 		})
 	}
🤖 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 `@internal/server/authz.go` around lines 179 - 186, The loop that builds
responseHeaders from ext.HeaderSets uses corev3.HeaderValueOption without
setting AppendAction, so Envoy may append instead of replacing headers; update
the construction in the ext.HeaderSets loop (where responseHeaders is appended)
to set AppendAction to corev3.HeaderValueOption_OVERWRITE on each
corev3.HeaderValueOption so sanitized headers (e.g., cookie, authorization)
explicitly overwrite any existing request headers.
🤖 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.

Duplicate comments:
In `@internal/server/authz.go`:
- Around line 179-186: The loop that builds responseHeaders from ext.HeaderSets
uses corev3.HeaderValueOption without setting AppendAction, so Envoy may append
instead of replacing headers; update the construction in the ext.HeaderSets loop
(where responseHeaders is appended) to set AppendAction to
corev3.HeaderValueOption_OVERWRITE on each corev3.HeaderValueOption so sanitized
headers (e.g., cookie, authorization) explicitly overwrite any existing request
headers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: c3eba967-0d30-44f8-b7bf-e488f3b78542

📥 Commits

Reviewing files that changed from the base of the PR and between f44a0ae and a064aeb.

📒 Files selected for processing (8)
  • internal/config/provider.go
  • internal/server/authz.go
  • internal/server/bearer_credential_source.go
  • internal/server/cert_credential_source.go
  • internal/server/cookie_credential_source.go
  • internal/server/credential_sources.go
  • internal/server/credential_sources_test.go
  • internal/server/query_credential_source.go

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/server/authz.go (1)

171-177: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Issued token headers should overwrite to prevent header spoofing.

These headers lack AppendAction, defaulting to APPEND_IF_EXISTS_OR_ADD. If an attacker sends a pre-existing Transaction-Token header, the real token is appended rather than replacing it. The backend then receives both headers, potentially processing the attacker-controlled value first. This undermines the security boundary the ext_authz is meant to create.

Proposed fix
 	for _, spec := range s.TokenTypesToIssue {
 		if token, ok := issuedTokens[spec.Type]; ok {
 			responseHeaders = append(responseHeaders, &corev3.HeaderValueOption{
 				Header: &corev3.HeaderValue{
 					Key:   spec.HeaderName,
 					Value: token.Value,
 				},
+				AppendAction: corev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD,
 			})
 		}
 	}
🤖 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 `@internal/server/authz.go` around lines 171 - 177, The header being added via
corev3.HeaderValueOption should explicitly set the AppendAction to overwrite
existing headers to prevent header spoofing; update the code that constructs the
HeaderValueOption (where responseHeaders is appended and spec.HeaderName /
token.Value are used) to set AppendAction to the enum value that overwrites
existing headers (HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD) so the ext_authz
header replaces any attacker-supplied header instead of appending.
🤖 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.

Outside diff comments:
In `@internal/server/authz.go`:
- Around line 171-177: The header being added via corev3.HeaderValueOption
should explicitly set the AppendAction to overwrite existing headers to prevent
header spoofing; update the code that constructs the HeaderValueOption (where
responseHeaders is appended and spec.HeaderName / token.Value are used) to set
AppendAction to the enum value that overwrites existing headers
(HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD) so the ext_authz header replaces
any attacker-supplied header instead of appending.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: 93b17335-7b3e-461c-a058-680c88c24300

📥 Commits

Reviewing files that changed from the base of the PR and between a064aeb and b2b9e08.

📒 Files selected for processing (1)
  • internal/server/authz.go

…oofing

Set AppendAction to OVERWRITE_IF_EXISTS_OR_ADD on token HeaderValueOption
entries so parsec-issued headers (e.g. Transaction-Token) replace any
client-supplied value instead of appending alongside it.
Comment thread configs/parsec.yaml Outdated
Comment thread internal/cel/mapper_input.go Outdated
Comment thread internal/config/provider.go Outdated
Use name to identify each credential source (like validators and data
sources), cookie_name and parameter_name for type-specific settings,
and validate names are unique at load time. cred_type in CEL now
reports the configured source name so multiple sources of the same
type can be distinguished.
Rename cred_type to cred_source_name and CredentialSourceType to
CredentialSourceName so mappers can identify which configured source
matched, avoiding confusion when multiple sources share the same type.
Build server credential sources from CredentialSourceConfig in config,
matching data source and observer patterns. Remove the duplicate
CredentialSourceSpec and keep server types focused on extraction only.
Comment thread internal/server/cert_credential_source.go Outdated
Remove the placeholder cert credential source from this PR. It
incorrectly wrapped XFCC header material in BearerCredential; proper
MTLSCredential parsing and validation will land in a dedicated
[Fix-3] sub-task under RHCLOUD-46385. This change keeps bearer, cookie,
and query extraction only.
Rename TransportContext to CredentialContext, encapsulate credential
source stamping in validateCredential, and move credential_sources to
top-level config so authz and exchange share the same sources.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants