Skip to content

BYOIDC: unprivileged user login using secrets from cluster#892

Merged
dbasunag merged 5 commits intoopendatahub-io:mainfrom
asmigala:byoidc-unprivileged-user-login
Dec 1, 2025
Merged

BYOIDC: unprivileged user login using secrets from cluster#892
dbasunag merged 5 commits intoopendatahub-io:mainfrom
asmigala:byoidc-unprivileged-user-login

Conversation

@asmigala
Copy link
Copy Markdown
Contributor

@asmigala asmigala commented Nov 28, 2025

Use secrets on cluster to log in as unprivileged user for BYOIDC

Description

Previously, there was a workaround for BYOIDC clusters that required that a context with an unprivileged user already exists in the kubeconfig.

Changed to reuse the same logic as with regular clusters, reading the user credentials from a secret on the cluster. Also rewrote the login to use native oidc support in kubernetes client.

How Has This Been Tested?

Locally ran workbenches, llama_stack and rag tests

Merge criteria:

  • The commits are squashed in a cohesive manner and have meaningful messages.
  • Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious).
  • The developer has manually tested the changes and verified that the changes work

Summary by CodeRabbit

  • New Features

    • BYOIDC token sign-in with in-memory kubeconfig for unprivileged clients; new utilities to obtain OIDC tokens and issuer URL.
  • Bug Fixes

    • Clearer "user credentials secret not found" error.
    • Secrets lookup now chooses the correct credential source by authentication type.
  • Refactor

    • Authentication flow reorganized to cleanly support OpenLDAP and BYOIDC paths.
  • Tests

    • Test fixtures updated to expose kubeconfig path; tests import OIDC issuer utility from shared utilities.

✏️ Tip: You can customize this high-level summary in your review settings.

@asmigala asmigala requested a review from a team as a code owner November 28, 2025 10:17
@github-actions
Copy link
Copy Markdown

The following are automatically added/executed:

  • PR size label.
  • Run pre-commit
  • Run tox
  • Add PR author as the PR assignee
  • Build image based on the PR

Available user actions:

  • To mark a PR as WIP, add /wip in a comment. To remove it from the PR comment /wip cancel to the PR.
  • To block merging of a PR, add /hold in a comment. To un-block merging of PR comment /hold cancel.
  • To mark a PR as approved, add /lgtm in a comment. To remove, add /lgtm cancel.
    lgtm label removed on each new commit push.
  • To mark PR as verified comment /verified to the PR, to un-verify comment /verified cancel to the PR.
    verified label removed on each new commit push.
  • To Cherry-pick a merged PR /cherry-pick <target_branch_name> to the PR. If <target_branch_name> is valid,
    and the current PR is merged, a cherry-picked PR would be created and linked to the current PR.
  • To build and push image to quay, add /build-push-pr-image in a comment. This would create an image with tag
    pr-<pr_number> to quay repository. This image tag, however would be deleted on PR merge or close action.
Supported labels

{'/build-push-pr-image', '/lgtm', '/wip', '/hold', '/cherry-pick', '/verified'}

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Nov 28, 2025

📝 Walkthrough

Walkthrough

Add BYOIDC support: new kubeconfig fixture; non_admin_user_password gains is_byoidc and selects BYOIDC or OpenLDAP secrets; tests can build an in-memory OIDC user kubeconfig using tokens and issuer. Add get_oidc_tokens and get_byoidc_issuer_url; remove get_unprivileged_context.

Changes

Cohort / File(s) Summary
Test configuration & fixtures
tests/conftest.py
Added exported fixture kubconfig_filepath; changed non_admin_user_password(...) signature to accept is_byoidc; replaced get_unprivileged_context import with get_oidc_tokens and get_byoidc_issuer_url; secret lookup now branches on is_byoidc (uses byoidc-credentials in oidc namespace or openldap in openldap namespace); BYOIDC branch obtains tokens and issuer, constructs an in-memory OIDC user entry, updates current-context, and creates an unprivileged DynamicClient from the in-memory kubeconfig (no file persisted); error messages and None checks updated.
OIDC utilities
utilities/user_utils.py
Removed get_unprivileged_context; added get_oidc_tokens(admin_client, username, password) which performs an OAuth2 password-grant POST to the BYOIDC token endpoint and returns (id_token, refresh_token) (validates presence and propagates HTTP errors); added get_byoidc_issuer_url(admin_client) which reads the BYOIDC issuer URL from cluster authentication data and asserts presence; added requests and DynamicClient imports.
Test helper import change
tests/model_registry/utils.py
Removed local get_byoidc_issuer_url implementation and replaced it with an import: from utilities.user_utils import get_byoidc_issuer_url.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Verify OAuth2 password-grant implementation in get_oidc_tokens: discovery of token endpoint, POST payload and headers, response error handling, and JSON parsing.
  • Confirm get_byoidc_issuer_url reads the correct cluster authentication field and properly handles missing values.
  • Review in-memory kubeconfig manipulation in tests/conftest.py: correct user entry keys, context update, and DynamicClient creation from the in-memory config.
  • Check secret lookup branching and updated error messages for correctness.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing BYOIDC unprivileged user login by reading credentials from cluster secrets instead of requiring pre-existing kubeconfig.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
Copy Markdown
Contributor

@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: 0

🧹 Nitpick comments (4)
utilities/user_utils.py (2)

117-147: Simplify error handling in get_oidc_tokens

The core flow looks good, but the error handling can be tightened:

  • except Exception as e: raise e is redundant; it adds no behavior over letting the exception propagate, and static analysis (TRY203/TRY201) rightly flags it.
  • The AssertionError for missing tokens is fine for test infra, but you don’t need a try around the whole block just for that.

You can simplify to:

 def get_oidc_tokens(admin_client: DynamicClient, username: str, password: str) -> tuple[str, str]:
@@
-    try:
-        LOGGER.info(f"Requesting token for user {username} in byoidc environment")
-        response = requests.post(
-            url=url,
-            headers=headers,
-            data=data,
-            allow_redirects=True,
-            timeout=30,
-            verify=True,  # Set to False if you need to skip SSL verification
-        )
-        response.raise_for_status()
-        json_response = response.json()
-
-        # Validate that we got an access token
-        if "id_token" not in json_response or "refresh_token" not in json_response:
-            LOGGER.error("Warning: No id_token or refresh_token in response")
-            raise AssertionError(f"No id_token or refresh_token in response: {json_response}")
-        return json_response["id_token"], json_response["refresh_token"]
-    except Exception as e:
-        raise e
+    LOGGER.info(f"Requesting token for user {username} in byoidc environment")
+    response = requests.post(
+        url=url,
+        headers=headers,
+        data=data,
+        allow_redirects=True,
+        timeout=30,
+        verify=True,  # Set to False if you need to skip SSL verification
+    )
+    response.raise_for_status()
+    json_response = response.json()
+
+    # Validate that we got an access token
+    if "id_token" not in json_response or "refresh_token" not in json_response:
+        LOGGER.error("Warning: No id_token or refresh_token in response")
+        raise AssertionError(f"No id_token or refresh_token in response: {json_response}")
+    return json_response["id_token"], json_response["refresh_token"]

This keeps behavior identical while removing unnecessary noise.


151-156: Guardrails around get_byoidc_issuer_url and duplication

The helper correctly mirrors the pattern used elsewhere, but it mixes a few strong assumptions:

  • It assumes authentication.instance.spec.oidcProviders is a non-empty list and indexes [0].
  • It uses bare assert statements, which give limited context if misconfigured.

For slightly clearer failures in misconfigured clusters and easier reuse, consider:

 def get_byoidc_issuer_url(admin_client: DynamicClient) -> str:
-    authentication = get_cluster_authentication(admin_client=admin_client)
-    assert authentication is not None
-    url = authentication.instance.spec.oidcProviders[0].issuer.issuerURL
-    assert url is not None
-    return url
+    authentication = get_cluster_authentication(admin_client=admin_client)
+    if authentication is None:
+        raise RuntimeError("Cluster Authentication resource 'cluster' not found; cannot resolve BYOIDC issuer URL")
+
+    providers = getattr(authentication.instance.spec, "oidcProviders", None) or []
+    if not providers:
+        raise RuntimeError("No OIDC providers configured on Authentication resource; cannot resolve BYOIDC issuer URL")
+
+    url = providers[0].issuer.issuerURL
+    if not url:
+        raise RuntimeError("OIDC provider issuerURL is empty; cannot resolve BYOIDC issuer URL")
+    return url

Also, if tests/model_registry/utils.py still defines a duplicate get_byoidc_issuer_url, consider switching it to import this shared helper to avoid divergence over time.

tests/conftest.py (2)

400-416: Clarify type hints and error path for missing unprivileged user

In unprivileged_client, you now correctly raise when an unprivileged client is requested but non_admin_user_password is None. One small polish:

  • The fixture non_admin_user_password returns tuple[str, str] | None, but the parameter here is annotated as just tuple[str, str]. Aligning the type hint improves static checking and communicates the None-guard more clearly.

For example:

-def unprivileged_client(
+def unprivileged_client(
     admin_client: DynamicClient,
     use_unprivileged_client: bool,
     kubconfig_filepath: str,
-    non_admin_user_password: tuple[str, str],
+    non_admin_user_password: tuple[str, str] | None,
     is_byoidc: bool,
 ) -> Generator[DynamicClient, Any, Any]:
@@
-    elif non_admin_user_password is None:
-        raise ValueError("Unprivileged user not provisioned")
+    elif non_admin_user_password is None:
+        raise ValueError("Unprivileged user not provisioned (user credentials secret missing?)")

Behavior stays the same; the interface and error message become clearer.


417-453: BYOIDC unprivileged client construction is sound; consider a small robustness tweak

The BYOIDC branch correctly:

  • Fetches OIDC tokens and issuer using the new helpers.
  • Builds an in-memory kubeconfig with a single OIDC user.
  • Updates the current context’s user and uses get_client(config_dict=..., context=..., persist_config=False) to avoid touching on-disk kubeconfig.

Two small optional improvements:

  1. Context lookup robustness
    Using a list comprehension and [0] will raise an unhelpful IndexError if the current context name is missing. You can both satisfy the static hint and improve the error with next(...):
-        current_context_name = kubeconfig_content["current-context"]
-        current_context = [c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name][0]
+        current_context_name = kubeconfig_content["current-context"]
+        try:
+            current_context = next(
+                c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name
+            )
+        except StopIteration:
+            raise RuntimeError(f"Context '{current_context_name}' not found in kubeconfig") from None
  1. Minor comment nit
    The comment # replace the users - we only need this one users could be clarified to “user”, but that’s purely cosmetic.

Overall, the OIDC-based unprivileged client wiring looks good.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8041322 and d219c00.

📒 Files selected for processing (2)
  • tests/conftest.py (3 hunks)
  • utilities/user_utils.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/conftest.py (2)
utilities/user_utils.py (2)
  • get_oidc_tokens (117-148)
  • get_byoidc_issuer_url (151-156)
tests/model_registry/utils.py (1)
  • get_byoidc_issuer_url (838-843)
utilities/user_utils.py (1)
utilities/infra.py (1)
  • get_cluster_authentication (1215-1219)
🪛 Ruff (0.14.6)
tests/conftest.py

415-415: Avoid specifying long messages outside the exception class

(TRY003)


446-446: Prefer next(...) over single element slice

Replace with next(...)

(RUF015)

utilities/user_utils.py

145-145: Abstract raise to an inner function

(TRY301)


145-145: Avoid specifying long messages outside the exception class

(TRY003)


147-148: Remove exception handler; error is immediately re-raised

(TRY203)


148-148: Use raise without specifying exception name

Remove exception name

(TRY201)

🔇 Additional comments (3)
utilities/user_utils.py (1)

6-7: Imports for BYOIDC helpers look consistent

The added imports (requests, DynamicClient, get_cluster_authentication) align with the new helpers and are used correctly; no issues from a dependency/usage standpoint.

Also applies to: 13-13

tests/conftest.py (2)

65-65: Wiring new BYOIDC helpers into tests

Importing get_oidc_tokens and get_byoidc_issuer_url here is the right place for centralizing BYOIDC behavior in fixtures; the usage below matches their intended purpose.


345-347: Secret selection for BYOIDC vs non-BYOIDC looks correct

The updated non_admin_user_password fixture:

  • Properly switches between byoidc-credentials/oidc and openldap/openldap based on is_byoidc.
  • Keeps the base64 decode/split logic unchanged and targets the first username containing "user", preserving existing behavior.
  • Logs a clear error and returns None when the secret isn’t found, allowing the caller to surface a more explicit failure.

This is a clean, localized change to make the unprivileged-user credentials source mode-aware.

Also applies to: 354-356, 357-365, 371-372

jiripetrlik
jiripetrlik previously approved these changes Nov 28, 2025
Copy link
Copy Markdown
Contributor

@jiripetrlik jiripetrlik left a comment

Choose a reason for hiding this comment

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

Looks good to me. Thank you for making the unprivileged client work.

jgarciao
jgarciao previously approved these changes Nov 28, 2025
Copy link
Copy Markdown
Contributor

@jgarciao jgarciao left a comment

Choose a reason for hiding this comment

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

LGTM. Did you run it also on clusters with regular authentication to verify automation kept working?

Copy link
Copy Markdown
Contributor

@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: 0

🧹 Nitpick comments (4)
tests/conftest.py (4)

345-372: Consider using explicit retrieval for consistency.

The function correctly selects secrets based on the is_byoidc flag. However, Line 364 uses direct list indexing users_Secret[0], whereas Line 367 uses next() with a generator. For consistency and clarity, consider using the same pattern.

Apply this diff for consistency:

-    data = users_Secret[0].instance.data
+    secret = next(iter(users_Secret))
+    data = secret.instance.data

414-415: Consider extracting exception message to a constant (optional).

Static analysis suggests avoiding long exception messages specified directly in the raise statement. While this is a minor style issue, you could extract the message to improve maintainability.

Apply this diff:

+    UNPRIVILEGED_USER_NOT_PROVISIONED = "Unprivileged user not provisioned"
+
     elif non_admin_user_password is None:
-        raise ValueError("Unprivileged user not provisioned")
+        raise ValueError(UNPRIVILEGED_USER_NOT_PROVISIONED)

421-422: Add error handling for kubeconfig file operations.

The code opens the kubeconfig file without handling potential FileNotFoundError. If the file doesn't exist at the specified path, the fixture will fail with an unclear error.

Apply this diff to add defensive error handling:

-        with open(kubconfig_filepath) as fd:
-            kubeconfig_content = yaml.safe_load(fd)
+        try:
+            with open(kubconfig_filepath) as fd:
+                kubeconfig_content = yaml.safe_load(fd)
+        except FileNotFoundError as e:
+            raise ValueError(f"Kubeconfig file not found at {kubconfig_filepath}") from e

445-447: Use next() for safer context lookup.

The code uses a list comprehension followed by indexing [0], which could raise an IndexError if the context is not found. Using next() with a default or explicit error message would be more robust and Pythonic.

Apply this diff:

-        current_context = [c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name][0]
+        current_context = next(
+            (c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name),
+            None
+        )
+        if current_context is None:
+            raise ValueError(f"Context '{current_context_name}' not found in kubeconfig")
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d219c00 and 4524655.

📒 Files selected for processing (1)
  • tests/conftest.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/conftest.py (2)
utilities/user_utils.py (2)
  • get_oidc_tokens (117-148)
  • get_byoidc_issuer_url (151-156)
tests/model_registry/utils.py (1)
  • get_byoidc_issuer_url (838-843)
🪛 Ruff (0.14.6)
tests/conftest.py

415-415: Avoid specifying long messages outside the exception class

(TRY003)


446-446: Prefer next(...) over single element slice

Replace with next(...)

(RUF015)

🔇 Additional comments (4)
tests/conftest.py (4)

65-65: LGTM!

The new imports are necessary for BYOIDC authentication and are used appropriately in the unprivileged_client fixture.


376-383: LGTM!

The fixture correctly prioritizes the KUBECONFIG environment variable when it points to a valid file, falling back to the default ~/.kube/config path.


457-481: LGTM!

The existing non-BYOIDC authentication flow is correctly preserved and continues to work as before, with the admin account properly restored after creating the unprivileged client.


417-453: Unable to complete verification due to repository access restrictions.

I attempted to verify the BYOIDC flow review comment but encountered limitations:

  1. Repository Clone Failed: The codebase could not be accessed in the sandbox environment
  2. Web Search Unsuccessful: The get_oidc_tokens and get_byoidc_issuer_url functions are not publicly documented or indexed

What the review comment requires verification of:

  • Error handling in BYOIDC token retrieval functions
  • Kubeconfig structure compatibility with Kubernetes client expectations
  • Edge case coverage in the BYOIDC flow

To proceed, you would need to:

  • Provide direct access to the repository or the specific function implementations
  • Clarify if this is a private repository and confirm the correct repository URL
  • Share the implementation code for get_oidc_tokens and get_byoidc_issuer_url directly

The original review comment's concerns are reasonable—validating error handling in authentication flows and client compatibility are important—but I cannot confirm whether the issues actually exist without examining the implementations.

lugi0
lugi0 previously approved these changes Dec 1, 2025
@mwaykole
Copy link
Copy Markdown
Member

mwaykole commented Dec 1, 2025

/lgtm

@asmigala, could u please fix pre-commit

rhods-ci-bot
rhods-ci-bot previously approved these changes Dec 1, 2025
Copy link
Copy Markdown
Contributor

@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: 0

🧹 Nitpick comments (3)
utilities/user_utils.py (1)

125-130: Remove redundant exception handler and fix log message inconsistency.

  1. The except Exception as e: raise e block is redundant and should be removed entirely.
  2. Line 126 logs "Warning:" but uses LOGGER.error - this is inconsistent.

Apply this diff:

         # Validate that we got an access token
         if "id_token" not in json_response or "refresh_token" not in json_response:
-            LOGGER.error("Warning: No id_token or refresh_token in response")
-            raise AssertionError(f"No id_token or refresh_token in response: {json_response}")
+            LOGGER.error("No id_token or refresh_token in response")
+            raise AssertionError("Missing id_token or refresh_token in OIDC response")
         return json_response["id_token"], json_response["refresh_token"]
-    except Exception as e:
-        raise e
tests/conftest.py (2)

417-419: Redundant call to get_byoidc_issuer_url.

get_byoidc_issuer_url(admin_client) is called here on line 419, but it's already called internally within get_oidc_tokens (line 100 in user_utils.py). This results in a duplicate fetch of the cluster authentication resource.

Consider refactoring get_oidc_tokens to also return the issuer URL, or call get_byoidc_issuer_url once before get_oidc_tokens and pass it as a parameter:

     elif is_byoidc:
-        tokens = get_oidc_tokens(admin_client, non_admin_user_password[0], non_admin_user_password[1])
         issuer = get_byoidc_issuer_url(admin_client)
+        tokens = get_oidc_tokens(admin_client, non_admin_user_password[0], non_admin_user_password[1])

Then update get_oidc_tokens to accept the issuer URL as a parameter instead of calling get_byoidc_issuer_url internally.


445-447: Use next() instead of list comprehension with index access.

The list comprehension with [0] index access is less idiomatic and can raise IndexError if no matching context is found.

Apply this diff:

         # get the current context and modify the referenced user in place
         current_context_name = kubeconfig_content["current-context"]
-        current_context = [c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name][0]
+        current_context = next(c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name)
         current_context["context"]["user"] = non_admin_user_password[0]

Note: next() raises StopIteration if no match is found, which provides a clearer error than IndexError. If you want a more descriptive error, you can use:

current_context = next(
    (c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name),
    None
)
if current_context is None:
    raise ValueError(f"Context '{current_context_name}' not found in kubeconfig")
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4524655 and d66e310.

📒 Files selected for processing (2)
  • tests/conftest.py (3 hunks)
  • utilities/user_utils.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/conftest.py (1)
utilities/user_utils.py (2)
  • get_oidc_tokens (99-130)
  • get_byoidc_issuer_url (133-138)
utilities/user_utils.py (3)
utilities/exceptions.py (1)
  • ExceptionUserLogin (137-138)
utilities/infra.py (2)
  • login_with_user_password (449-473)
  • get_cluster_authentication (1215-1219)
tests/model_registry/utils.py (1)
  • get_byoidc_issuer_url (838-843)
🪛 Ruff (0.14.6)
tests/conftest.py

415-415: Avoid specifying long messages outside the exception class

(TRY003)


446-446: Prefer next(...) over single element slice

Replace with next(...)

(RUF015)

utilities/user_utils.py

127-127: Abstract raise to an inner function

(TRY301)


127-127: Avoid specifying long messages outside the exception class

(TRY003)


129-130: Remove exception handler; error is immediately re-raised

(TRY203)


130-130: Use raise without specifying exception name

Remove exception name

(TRY201)

🔇 Additional comments (6)
utilities/user_utils.py (2)

6-14: LGTM on imports.

The new imports for requests, DynamicClient, and get_cluster_authentication are properly organized and necessary for the new BYOIDC functionality.


133-138: Verify duplicate function in tests/model_registry/utils.py.

This review comment claims an identical get_byoidc_issuer_url function exists at lines 837-842 in tests/model_registry/utils.py. If confirmed, consider consolidating by importing from the utility module instead.

tests/conftest.py (4)

65-65: LGTM on import.

The new imports for BYOIDC utilities are correctly added.


354-372: LGTM on BYOIDC credential lookup logic.

The conditional logic for selecting between BYOIDC (byoidc-credentials in oidc namespace) and OpenLDAP credentials is correct. The updated error message is more generic and appropriate.

Minor: Variable users_Secret (line 357, 364) uses mixed case - consider users_secret for consistency with Python naming conventions.


375-383: LGTM on kubeconfig path resolution.

The fixture correctly prioritizes the KUBECONFIG environment variable over the default ~/.kube/config path. The file existence check on the env var path is appropriate.


449-455: LGTM on in-memory client creation.

Using config_dict with persist_config=False correctly creates the client without modifying the kubeconfig file on disk. This is the appropriate approach for the BYOIDC flow.

@asmigala asmigala force-pushed the byoidc-unprivileged-user-login branch from d66e310 to e102ec6 Compare December 1, 2025 10:42
Copy link
Copy Markdown
Contributor

@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

🧹 Nitpick comments (1)
tests/conftest.py (1)

417-455: LGTM! Consider minor optimization.

The BYOIDC authentication flow is well-implemented. It correctly:

  • Retrieves tokens and issuer URL
  • Builds an in-memory kubeconfig with OIDC auth-provider configuration
  • Sets persist_config=False to avoid modifying the original kubeconfig file

Optional optimization: Line 446 could use next() instead of list comprehension for better idiomaticity:

-        current_context = [c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name][0]
+        current_context = next(c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name)

Based on static analysis hints.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d66e310 and e102ec6.

📒 Files selected for processing (2)
  • tests/conftest.py (3 hunks)
  • utilities/user_utils.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/conftest.py (2)
utilities/user_utils.py (2)
  • get_oidc_tokens (99-130)
  • get_byoidc_issuer_url (133-138)
tests/model_registry/utils.py (1)
  • get_byoidc_issuer_url (838-843)
utilities/user_utils.py (3)
utilities/exceptions.py (1)
  • ExceptionUserLogin (137-138)
utilities/infra.py (2)
  • login_with_user_password (449-473)
  • get_cluster_authentication (1215-1219)
tests/model_registry/utils.py (1)
  • get_byoidc_issuer_url (838-843)
🪛 Ruff (0.14.6)
tests/conftest.py

415-415: Avoid specifying long messages outside the exception class

(TRY003)


446-446: Prefer next(...) over single element slice

Replace with next(...)

(RUF015)

utilities/user_utils.py

127-127: Abstract raise to an inner function

(TRY301)


127-127: Avoid specifying long messages outside the exception class

(TRY003)


129-130: Remove exception handler; error is immediately re-raised

(TRY203)


130-130: Use raise without specifying exception name

Remove exception name

(TRY201)

🔇 Additional comments (7)
utilities/user_utils.py (2)

6-7: LGTM!

The new imports are necessary for the BYOIDC token retrieval functionality.


133-138: Address code duplication and add boundary check.

This function is duplicated in tests/model_registry/utils.py (lines 837-842). Additionally, line 136 directly accesses oidcProviders[0] without verifying the list is non-empty, which could raise an IndexError.

Recommendations:

  1. Consolidate the duplicate implementations into this single location and update imports in tests/model_registry/utils.py to use this version.
  2. Add a boundary check before accessing oidcProviders[0].
tests/conftest.py (5)

65-65: LGTM!

The import change correctly reflects the new BYOIDC authentication utilities, replacing the removed get_unprivileged_context function.


345-372: LGTM!

The fixture correctly branches between BYOIDC and OpenLDAP credential sources. The secret selection logic appropriately chooses byoidc-credentials from the oidc namespace for BYOIDC environments and openldap from the openldap namespace otherwise.


376-384: LGTM!

The fixture correctly resolves the kubeconfig path by prioritizing the KUBECONFIG environment variable when it points to an existing file, with a sensible fallback to the default location.


392-396: LGTM!

The fixture correctly determines BYOIDC mode by checking if the cluster authentication type is "OIDC", with proper handling of the None case.


457-481: LGTM!

The non-BYOIDC authentication flow correctly handles the OpenLDAP-based login, context switching, and cleanup by restoring the original user context.

mwaykole
mwaykole previously approved these changes Dec 1, 2025
@asmigala asmigala dismissed stale reviews from jgarciao and mwaykole via a0b8a15 December 1, 2025 13:26
@asmigala asmigala force-pushed the byoidc-unprivileged-user-login branch from 719c188 to a0b8a15 Compare December 1, 2025 13:26
@asmigala asmigala requested a review from fege as a code owner December 1, 2025 13:26
@asmigala asmigala requested a review from dbasunag December 1, 2025 13:26
Copy link
Copy Markdown
Contributor

@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: 0

♻️ Duplicate comments (1)
utilities/user_utils.py (1)

99-131: Remove the redundant try/except wrapper in get_oidc_tokens

The try/except Exception as e: raise e block adds no value, just re-raises the same exception and triggers Ruff warnings. You can simplify by letting exceptions bubble up directly.

Suggested refactor:

 def get_oidc_tokens(admin_client: DynamicClient, username: str, password: str) -> tuple[str, str]:
@@
-    try:
-        LOGGER.info(f"Requesting token for user {username} in byoidc environment")
-        response = requests.post(
-            url=url,
-            headers=headers,
-            data=data,
-            allow_redirects=True,
-            timeout=30,
-            verify=True,  # Set to False if you need to skip SSL verification
-        )
-        response.raise_for_status()
-        json_response = response.json()
-
-        # Validate that we got an access token
-        if "id_token" not in json_response or "refresh_token" not in json_response:
-            LOGGER.error("Warning: No id_token or refresh_token in response")
-            raise AssertionError(f"No id_token or refresh_token in response: {json_response}")
-        return json_response["id_token"], json_response["refresh_token"]
-    except Exception as e:
-        raise e
+    LOGGER.info(f"Requesting token for user {username} in byoidc environment")
+    response = requests.post(
+        url=url,
+        headers=headers,
+        data=data,
+        allow_redirects=True,
+        timeout=30,
+        verify=True,  # Set to False if you need to skip SSL verification
+    )
+    response.raise_for_status()
+    json_response = response.json()
+
+    # Validate that we got an access token
+    if "id_token" not in json_response or "refresh_token" not in json_response:
+        LOGGER.error("Warning: No id_token or refresh_token in response")
+        raise AssertionError(f"No id_token or refresh_token in response: {json_response}")
+    return json_response["id_token"], json_response["refresh_token"]

This keeps behavior identical while satisfying the linter and simplifying the code.

🧹 Nitpick comments (2)
tests/conftest.py (2)

345-347: non_admin_user_password secret selection logic is sound; consider a clearer failure for malformed secrets

The fixture correctly:

  • Short-circuits when use_unprivileged_client is False.
  • Chooses byoidc-credentials/oidc vs openldap/openldap based on is_byoidc.
  • Decodes users/passwords from the secret and picks the first entry containing "user".

The only minor edge case is:

first_user_index = next(index for index, user in enumerate(users) if "user" in user)

If the secret content changes and no username contains "user", this will raise StopIteration, which can be a bit opaque when debugging CI failures. If you care about a more explicit error, you could wrap this in a small helper that raises ValueError with a clearer message, but it's not required for correctness.

Also applies to: 354-365, 371-372


375-383: BYOIDC unprivileged_client flow looks correct; minor readability tweak possible for context lookup

The BYOIDC branch does the right things:

  • Fails fast if non_admin_user_password is None.
  • Uses get_oidc_tokens and get_byoidc_issuer_url to build an OIDC user entry.
  • Replaces kubeconfig_content["users"] with a single OIDC user and rewires the current context to that username.
  • Creates the client from config_dict with persist_config=False, so the on-disk kubeconfig is left untouched.
  • Keeps the existing LDAP-based login flow unchanged for non-BYOIDC clusters.

One optional readability improvement is in the current-context lookup:

current_context_name = kubeconfig_content["current-context"]
current_context = [c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name][0]

You could make this slightly clearer (and align with Ruff’s RUF015 suggestion) by using next(...):

 current_context_name = kubeconfig_content["current-context"]
- current_context = [c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name][0]
+ current_context = next(
+     c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name
+ )

Functionally it’s equivalent, so this is purely a style/nitpick change.

Also applies to: 400-406, 414-453

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e102ec6 and a0b8a15.

📒 Files selected for processing (3)
  • tests/conftest.py (3 hunks)
  • tests/model_registry/utils.py (1 hunks)
  • utilities/user_utils.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/conftest.py (1)
utilities/user_utils.py (2)
  • get_oidc_tokens (99-130)
  • get_byoidc_issuer_url (133-138)
tests/model_registry/utils.py (1)
utilities/user_utils.py (1)
  • get_byoidc_issuer_url (133-138)
utilities/user_utils.py (2)
utilities/exceptions.py (1)
  • ExceptionUserLogin (137-138)
utilities/infra.py (2)
  • login_with_user_password (449-473)
  • get_cluster_authentication (1215-1219)
🪛 Ruff (0.14.6)
tests/conftest.py

415-415: Avoid specifying long messages outside the exception class

(TRY003)


446-446: Prefer next(...) over single element slice

Replace with next(...)

(RUF015)

utilities/user_utils.py

127-127: Abstract raise to an inner function

(TRY301)


127-127: Avoid specifying long messages outside the exception class

(TRY003)


129-130: Remove exception handler; error is immediately re-raised

(TRY203)


130-130: Use raise without specifying exception name

Remove exception name

(TRY201)

🔇 Additional comments (3)
tests/model_registry/utils.py (1)

37-37: Centralizing get_byoidc_issuer_url import looks good

Importing get_byoidc_issuer_url from utilities.user_utils removes duplication in this module and aligns with the earlier request to centralize issuer URL retrieval. No issues from this change alone.

utilities/user_utils.py (1)

133-138: get_byoidc_issuer_url implementation is consistent and reusable

Fetching the issuer URL via get_cluster_authentication and asserting non-None values matches how other codepaths treat cluster auth; centralizing this helper here is a good move and aligns with its new usage in tests and model registry utils.

tests/conftest.py (1)

65-65: BYOIDC detection and helper imports are wired cleanly

Importing get_oidc_tokens/get_byoidc_issuer_url here and deriving is_byoidc from the Authentication resource gives a single, clear switch for BYOIDC vs non-BYOIDC behavior across fixtures. The wiring looks consistent and low-risk.

Also applies to: 386-397

@dbasunag dbasunag enabled auto-merge (squash) December 1, 2025 15:29
@dbasunag dbasunag merged commit ae20c4f into opendatahub-io:main Dec 1, 2025
9 checks passed
@github-actions
Copy link
Copy Markdown

github-actions bot commented Dec 1, 2025

Status of building tag latest: success.
Status of pushing tag latest to image registry: success.

mwaykole pushed a commit to mwaykole/opendatahub-tests that referenced this pull request Jan 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants