Skip to content

feat(egress): refactored credential injection test to use Vault#951

Open
averevki wants to merge 1 commit intoKuadrant:mainfrom
averevki:egress-vault-tests
Open

feat(egress): refactored credential injection test to use Vault#951
averevki wants to merge 1 commit intoKuadrant:mainfrom
averevki:egress-vault-tests

Conversation

@averevki
Copy link
Copy Markdown
Contributor

@averevki averevki commented Apr 24, 2026

Note

Requires Kuadrant/helm-charts-olm#86 to be merged first (Vault deployment in tools-vault namespace).

Closes #949

Description

  • Rewrites the egress credential injection test to use HashiCorp Vault with Kubernetes auth, replicating the credential-injection user guide
  • Adds hvac Python client for Vault management instead of kubectl exec into the Vault pod
  • Introduces ValueOrSelector and NamedValueOrSelector dataclasses matching Authorino's API spec for typed metadata HTTP configuration
  • Extends fetch_service_ip to support cross-namespace service discovery for Vault in tools-vault namespace

Changes

New Features

  • testsuite/kubernetes/vault.py — New Vault class using hvac library for managing policies, Kubernetes auth roles, and KV v2 secrets
  • ValueOrSelector dataclass wrapping Value | ValueFrom | CelExpression to match Authorino's ValueOrSelector spec
  • NamedValueOrSelector extending ValueOrSelector with a name field, serializing as {name: {value/selector/expression}}
  • CelPredicate added to the Rule type union for CEL-based pattern matching
  • PlainResponse.plain now accepts CelExpression in addition to ABCValue

Refactoring

  • MetadataSection.add_http() — Extended with keyword-only parameters: url_expression: str, content_type: Literal[...], body: ValueOrSelector, headers: list[NamedValueOrSelector]; endpoint and method are now optional
  • fetch_service_ip() — Added optional namespace parameter for cross-namespace LoadBalancer lookups
  • Shared fixtures (mockserver_expectation, service1_api_secret, sa_token_secret, authorization) moved from conftest.py into their respective test files to
    decouple the Vault-based and K8s-Secret-based credential injection tests

Tests

  • test_credential_injection.py — Rewritten to use Vault Kubernetes auth flow: creates a ServiceAccount, configures Vault policy/role/secret, builds AuthPolicy with
    vault_login + vault_secret metadata steps, and validates credential injection via MockServer
  • Three test cases: successful injection with valid SA token, rejection with invalid token, rejection without token
  • test_credential_injection_by_destination.py — Now self-contained with its own fixtures (previously shared from conftest)

Configuration

  • config/settings.local.yaml.tpl — Added vault.url and vault.token settings template
  • testsuite/config/__init__.py — Added DefaultValueValidator entries for vault.url (auto-fetched from tools-vault namespace) and vault.token (defaults to
    "root")
  • pyproject.toml — Added hvac to main dependencies, types-hvac to dev dependencies

Verification steps

poetry run pytest -vv -n2 testsuite/tests/singlecluster/egress/credentials_injection/

Summary by CodeRabbit

Release Notes

  • New Features

    • Authorization rules now support CEL predicates for advanced policy conditions
    • HTTP metadata configuration expanded with URL expressions, custom headers, request bodies, and content-type options
    • Vault integration for credential management and secret injection
  • Tests

    • Updated credential injection test suite with Vault-backed authentication

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Warning

Rate limit exceeded

@averevki has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 42 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 46 minutes and 42 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 74385557-8560-4e28-a3d8-33b255bc1c30

📥 Commits

Reviewing files that changed from the base of the PR and between efb9dc6 and af684de.

📒 Files selected for processing (10)
  • config/settings.local.yaml.tpl
  • pyproject.toml
  • testsuite/config/__init__.py
  • testsuite/config/tools.py
  • testsuite/kuadrant/policy/authorization/__init__.py
  • testsuite/kuadrant/policy/authorization/sections.py
  • testsuite/kubernetes/vault.py
  • testsuite/tests/singlecluster/egress/credentials_injection/conftest.py
  • testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection.py
  • testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection_by_destination.py
📝 Walkthrough

Walkthrough

This pull request introduces Vault-based credential injection for egress gateway testing, adding configuration templates, hvac dependencies, a Vault client wrapper class, authorization model extensions to support CEL expressions, and comprehensive test implementations validating Vault-backed credential flows using Kubernetes authentication.

Changes

Cohort / File(s) Summary
Configuration and Dependencies
config/settings.local.yaml.tpl, pyproject.toml
Added Vault configuration template with URL and token settings; added hvac and types-hvac dependencies for Vault client library support.
Test Infrastructure
testsuite/config/__init__.py, testsuite/config/tools.py
Extended dynaconf configuration to register Vault URL and token settings with namespace-aware service discovery; added optional namespace parameter to fetch_service_ip() to support multi-namespace service lookups.
Vault Client Wrapper
testsuite/kubernetes/vault.py
Introduced new Vault class wrapping hvac client with methods for policy management (create/delete), Kubernetes role binding (create/delete), and KV v2 secret operations (store/delete).
Authorization Model Extensions
testsuite/kuadrant/policy/authorization/__init__.py, testsuite/kuadrant/policy/authorization/sections.py
Extended Rule union to include CelPredicate; added ValueOrSelector and NamedValueOrSelector abstractions supporting static values, dynamic selectors, or CEL expressions; broadened MetadataSection.add_http() to accept optional endpoint and expression-driven url_expression, plus configurable content_type, body, and custom headers.
Credential Injection Tests
testsuite/tests/singlecluster/egress/credentials_injection/conftest.py, testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection.py, testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection_by_destination.py
Refactored conftest to isolate Vault-specific setup; implemented Vault-backed credential injection test with Kubernetes SA isolation, Vault policy/role bindings, JWT token generation, and three-scenario validation (successful injection, invalid token rejection, missing token rejection); added complementary Kubernetes Secret-based injection test for route-specific credential injection.

Sequence Diagram(s)

sequenceDiagram
    participant Workload as Workload<br/>(with SA)
    participant Authorino as Authorino<br/>(AuthPolicy)
    participant Vault as Vault<br/>(Server)
    participant Egress as Egress<br/>Gateway
    participant Backend as MockServer<br/>Backend

    Workload->>Authorino: 1. Request (with K8s SA token)
    Authorino->>Vault: 2. POST /auth/kubernetes/login<br/>(SA token) [metadata.http p0]
    Vault-->>Authorino: 3. Client token + auth data
    Authorino->>Vault: 4. GET /secret/data/egress/.../<sa-name><br/>(Client token) [metadata.http p1]
    Vault-->>Authorino: 5. Secret data {api_key: "..."}
    Authorino->>Authorino: 6. Authorize (pattern match success)
    Authorino->>Egress: 7. Request + response directives
    Egress->>Backend: 8. Outbound request<br/>(Authorization: Bearer <api_key>)
    Backend-->>Egress: 9. 200 OK
    Egress-->>Workload: 10. Response
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly Related PRs

Suggested Labels

Test case, Service Protection

Suggested Reviewers

  • trepel
  • zkraus
  • silvi-t

Poem

🐰 A Vault of secrets, now we've found,
With tokens bound and policies sound,
The egress gateway trusts and knows,
Where every credential safely flows! 🗝️✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: refactoring the egress credential injection test to use Vault instead of Kubernetes Secrets.
Description check ✅ Passed The PR description includes all required sections: Description, Changes, and Verification, following the template structure with detailed information about the modifications.
Linked Issues check ✅ Passed The pull request comprehensively addresses all coding-related requirements from issue #949: implements Vault client wrapper, refactors MetadataSection.add_http, introduces ValueOrSelector/NamedValueOrSelector dataclasses, and rewrites the credential injection test with AuthPolicy using Vault authentication flow.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing Vault-based credential injection testing. Configuration files, dependencies, and test utilities are all directly related to the primary objective of replacing K8s Secret-based injection with Vault.
Docstring Coverage ✅ Passed Docstring coverage is 96.55% which is sufficient. The required threshold is 80.00%.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@averevki averevki self-assigned this Apr 24, 2026
@averevki averevki added Test case New test case Service Protection Issues for Service Protection tests labels Apr 24, 2026
@averevki averevki moved this to Ready For Review in Kuadrant Apr 24, 2026
@averevki averevki requested review from a team and maksymvavilov April 24, 2026 11:25
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: 2

Caution

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

⚠️ Outside diff range comments (1)
testsuite/kuadrant/policy/authorization/sections.py (1)

202-234: ⚠️ Potential issue | 🟡 Minor

Consider validating that at least one of endpoint or url_expression is provided.

With both endpoint and url_expression now optional and no mutual-exclusivity/presence check, callers can construct an http metadata entry with neither url nor urlExpression, or with both set simultaneously. Authorino will reject it server-side, but surfacing the error at construction time makes failures easier to localise.

🛡️ Suggested validation
         """Set metadata http external auth feature"""
+        if not endpoint and not url_expression:
+            raise ValueError("add_http requires either `endpoint` or `url_expression`")
+        if endpoint and url_expression:
+            raise ValueError("add_http accepts only one of `endpoint` or `url_expression`")
         http_config: dict = {"method": method, "headers": {"Accept": {"value": "application/json"}}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@testsuite/kuadrant/policy/authorization/sections.py` around lines 202 - 234,
In add_http, ensure callers supply exactly one of endpoint or url_expression:
add a validation at the start of the add_http method (before building
http_config) that raises a ValueError if neither endpoint nor url_expression is
provided, and optionally also raise if both are provided (to enforce mutual
exclusivity) so you don't construct an invalid "http" metadata object; reference
the add_http function and the endpoint and url_expression parameters and perform
this check before calling self.add_item.
🧹 Nitpick comments (5)
testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection.py (4)

115-127: Fragile manual JSON assembly in CEL; consider a readable alternative.

The double-escaped JSON literal ('"{\\"jwt\\": \\"" + ... + "\\", \\"role\\": \\"..."}}"') is hard to read and fragile — any future token whose value contains a double quote or backslash would produce invalid JSON. Since Authorino supports JSON body types via content_type="application/json" with a structured body, prefer a dict-shaped payload if the API allows it, or at minimum extract the CEL into a named helper/constant for readability.

Also, substring(7) implicitly assumes the incoming Authorization header starts with exactly Bearer (7 chars). That matches the Kubernetes identity credentials convention used here, but a brief comment would save future readers a lookup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection.py`
around lines 115 - 127, Replace the fragile manually-escaped JSON CEL expression
passed to auth.metadata.add_http with a structured body using
ValueOrSelector/CelExpression that builds a map/object (e.g., {"jwt": <token>,
"role": <role>}) instead of concatenating JSON strings; locate the call to
auth.metadata.add_http and change the body argument from the current
CelExpression string concat to a map-shaped CEL expression (using
ValueOrSelector with CelExpression that returns a map) or move the long CEL into
a named constant for readability, and add a short inline comment next to the
substring(7) usage explaining it assumes a "Bearer " prefix on
request.headers.authorization to make the intent explicit.

20-20: Consider adding an issue marker linking to #949.

The PR closes issue #949; adding pytest.mark.issue("https://github.com/Kuadrant/testsuite/issues/949") (or the Kuadrant repo URL) to pytestmark helps with traceability in Report Portal.

As per coding guidelines: "Common markers include: @pytest.mark.authorino, ... @pytest.mark.issue('https://github.com/...'), ...".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection.py`
at line 20, Add the issue marker to the existing pytestmark declaration so the
test is traceable to issue `#949`; update the pytestmark list (the pytestmark
variable in this file) to include
pytest.mark.issue("https://github.com/Kuadrant/testsuite/issues/949") alongside
pytest.mark.kuadrant_only and pytest.mark.egress_gateway.

154-170: Consider also covering the "authenticated SA but no Vault role binding" case.

The linked issue explicitly lists a 403 scenario for a workload whose ServiceAccount token is valid at the K8s TokenReview stage but has no Vault role binding. The current three tests cover valid/invalid/missing SA tokens (all rejected at Kubernetes identity stage for the negative paths), but none exercise the Vault-side authorization failure. A fourth test using a SA that is not bound to the Vault role would close that gap.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection.py`
around lines 154 - 170, Add a new test that exercises the Vault-side
authorization failure: create a test function (e.g.,
test_egress_authenticated_no_vault_binding_returns_403) that sends a request
using a ServiceAccount token that is valid for Kubernetes TokenReview but
intentionally has no Vault role binding (use or add a fixture like
sa_token_unbound_to_vault or construct such a token), call
client.get(mockserver_expectation, headers={"Authorization": f"Bearer
{sa_token_unbound_to_vault}"}), and assert the response.status_code == 403 and
that the backend Authorization header was not overwritten; mirror assertions and
structure from test_egress_credential_injection_via_vault to keep consistency.

22-23: S105 is a false positive here.

K8S_TOKEN_AUDIENCE holds an audience URL, not a credential. If Ruff noise becomes an issue, suppress with # noqa: S105 or rename; otherwise safe to ignore.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection.py`
around lines 22 - 23, S105 is a false positive for K8S_TOKEN_AUDIENCE because it
stores an audience URL, not a secret; fix by either appending a Ruff suppression
or renaming the identifier: add "# noqa: S105" to the K8S_TOKEN_AUDIENCE
definition or rename the constant to a clearly non-secret name such as
K8S_TOKEN_AUDIENCE_URL (update all references) so the linter no longer flags it.
testsuite/kubernetes/vault.py (1)

28-28: Consider making ttl configurable.

The ttl="1h" is hardcoded inside the wrapper, which prevents callers from overriding it for longer-running tests or negative scenarios. Exposing it as a keyword argument (with the current value as default) would keep the call sites concise while preserving flexibility.

♻️ Suggested change
-    def create_role(self, name: str, sa_names: list[str], sa_namespaces: list[str], policies: list[str]):
+    def create_role(
+        self, name: str, sa_names: list[str], sa_namespaces: list[str], policies: list[str], ttl: str = "1h"
+    ):
         """Create a Kubernetes auth role binding service accounts to policies"""
         self._client.auth.kubernetes.create_role(
             name,
             bound_service_account_names=sa_names,
             bound_service_account_namespaces=sa_namespaces,
             policies=policies,
-            ttl="1h",
+            ttl=ttl,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@testsuite/kubernetes/vault.py` at line 28, The hardcoded ttl="1h" in
testsuite/kubernetes/vault.py prevents callers from overriding token lifetime;
add a ttl keyword argument to the enclosing function (default="1h") and replace
the literal ttl="1h" with the parameter so callers can pass a custom ttl; update
any internal calls/tests to keep behavior unchanged by default but allow
overriding.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@testsuite/kubernetes/vault.py`:
- Around line 21-29: The method create_role in testsuite/kubernetes/vault.py
calls a non-existent helper self._client.auth.kubernetes.createq_role; change
that call to the correct hvac method name create_role so the function body uses
self._client.auth.kubernetes.create_role(...). Keep the existing argument names
(bound_service_account_names, bound_service_account_namespaces, policies, ttl)
and ensure the method name in the call matches the enclosing method create_role.

In
`@testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection.py`:
- Around line 115-140: The test fixture fails because of a typo: replace the
incorrect symbol createq_role with the correct Vault.create_role call in
testsuite/kubernetes/vault.py (fix the function name where the vault fixture
invokes createq_role), ensure the module imports or class Vault exposes
create_role and update any callers to use create_role so the vault fixture and
downstream authorization fixture can initialize properly.

---

Outside diff comments:
In `@testsuite/kuadrant/policy/authorization/sections.py`:
- Around line 202-234: In add_http, ensure callers supply exactly one of
endpoint or url_expression: add a validation at the start of the add_http method
(before building http_config) that raises a ValueError if neither endpoint nor
url_expression is provided, and optionally also raise if both are provided (to
enforce mutual exclusivity) so you don't construct an invalid "http" metadata
object; reference the add_http function and the endpoint and url_expression
parameters and perform this check before calling self.add_item.

---

Nitpick comments:
In `@testsuite/kubernetes/vault.py`:
- Line 28: The hardcoded ttl="1h" in testsuite/kubernetes/vault.py prevents
callers from overriding token lifetime; add a ttl keyword argument to the
enclosing function (default="1h") and replace the literal ttl="1h" with the
parameter so callers can pass a custom ttl; update any internal calls/tests to
keep behavior unchanged by default but allow overriding.

In
`@testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection.py`:
- Around line 115-127: Replace the fragile manually-escaped JSON CEL expression
passed to auth.metadata.add_http with a structured body using
ValueOrSelector/CelExpression that builds a map/object (e.g., {"jwt": <token>,
"role": <role>}) instead of concatenating JSON strings; locate the call to
auth.metadata.add_http and change the body argument from the current
CelExpression string concat to a map-shaped CEL expression (using
ValueOrSelector with CelExpression that returns a map) or move the long CEL into
a named constant for readability, and add a short inline comment next to the
substring(7) usage explaining it assumes a "Bearer " prefix on
request.headers.authorization to make the intent explicit.
- Line 20: Add the issue marker to the existing pytestmark declaration so the
test is traceable to issue `#949`; update the pytestmark list (the pytestmark
variable in this file) to include
pytest.mark.issue("https://github.com/Kuadrant/testsuite/issues/949") alongside
pytest.mark.kuadrant_only and pytest.mark.egress_gateway.
- Around line 154-170: Add a new test that exercises the Vault-side
authorization failure: create a test function (e.g.,
test_egress_authenticated_no_vault_binding_returns_403) that sends a request
using a ServiceAccount token that is valid for Kubernetes TokenReview but
intentionally has no Vault role binding (use or add a fixture like
sa_token_unbound_to_vault or construct such a token), call
client.get(mockserver_expectation, headers={"Authorization": f"Bearer
{sa_token_unbound_to_vault}"}), and assert the response.status_code == 403 and
that the backend Authorization header was not overwritten; mirror assertions and
structure from test_egress_credential_injection_via_vault to keep consistency.
- Around line 22-23: S105 is a false positive for K8S_TOKEN_AUDIENCE because it
stores an audience URL, not a secret; fix by either appending a Ruff suppression
or renaming the identifier: add "# noqa: S105" to the K8S_TOKEN_AUDIENCE
definition or rename the constant to a clearly non-secret name such as
K8S_TOKEN_AUDIENCE_URL (update all references) so the linter no longer flags it.
🪄 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: Pro

Run ID: e89ad610-0254-4e07-936f-67f5e647a4bc

📥 Commits

Reviewing files that changed from the base of the PR and between cd427e1 and efb9dc6.

📒 Files selected for processing (10)
  • config/settings.local.yaml.tpl
  • pyproject.toml
  • testsuite/config/__init__.py
  • testsuite/config/tools.py
  • testsuite/kuadrant/policy/authorization/__init__.py
  • testsuite/kuadrant/policy/authorization/sections.py
  • testsuite/kubernetes/vault.py
  • testsuite/tests/singlecluster/egress/credentials_injection/conftest.py
  • testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection.py
  • testsuite/tests/singlecluster/egress/credentials_injection/test_credential_injection_by_destination.py

Comment thread testsuite/kubernetes/vault.py
Signed-off-by: averevki <sandyverevkin@gmail.com>
@averevki averevki force-pushed the egress-vault-tests branch from efb9dc6 to af684de Compare April 24, 2026 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Service Protection Issues for Service Protection tests Test case New test case

Projects

Status: Ready For Review

Development

Successfully merging this pull request may close these issues.

[Egress] Credential Injection with Vault

1 participant