Skip to content

Commit 82feb83

Browse files
asmigaladbasunag
authored andcommitted
BYOIDC: unprivileged user login using secrets from cluster (opendatahub-io#892)
Co-authored-by: Debarati Basu-Nag <dbasunag@redhat.com>
1 parent 9a94c89 commit 82feb83

File tree

3 files changed

+95
-47
lines changed

3 files changed

+95
-47
lines changed

tests/conftest.py

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
from utilities.minio import create_minio_data_connection_secret
6363
from utilities.operator_utils import get_csv_related_images, get_cluster_service_version
6464
from ocp_resources.authentication_config_openshift_io import Authentication
65-
from utilities.user_utils import get_unprivileged_context
65+
from utilities.user_utils import get_oidc_tokens, get_byoidc_issuer_url
6666

6767
LOGGER = get_logger(name=__name__)
6868

@@ -342,28 +342,33 @@ def use_unprivileged_client(pytestconfig: pytest.Config) -> bool:
342342

343343

344344
@pytest.fixture(scope="session")
345-
def non_admin_user_password(admin_client: DynamicClient, use_unprivileged_client: bool) -> tuple[str, str] | None:
345+
def non_admin_user_password(
346+
admin_client: DynamicClient, use_unprivileged_client: bool, is_byoidc: bool
347+
) -> tuple[str, str] | None:
346348
def _decode_split_data(_data: str) -> list[str]:
347349
return base64.b64decode(_data).decode().split(",")
348350

349351
if not use_unprivileged_client:
350352
return None
351353

352-
if ldap_Secret := list(
354+
secret_name = "byoidc-credentials" if is_byoidc else "openldap" # pragma: allowlist secret
355+
secret_ns = "oidc" if is_byoidc else "openldap" # pragma: allowlist secret
356+
357+
if users_Secret := list(
353358
Secret.get(
354359
dyn_client=admin_client,
355-
name="openldap",
356-
namespace="openldap",
360+
name=secret_name,
361+
namespace=secret_ns,
357362
)
358363
):
359-
data = ldap_Secret[0].instance.data
364+
data = users_Secret[0].instance.data
360365
users = _decode_split_data(_data=data.users)
361366
passwords = _decode_split_data(_data=data.passwords)
362367
first_user_index = next(index for index, user in enumerate(users) if "user" in user)
363368

364369
return users[first_user_index], passwords[first_user_index]
365370

366-
LOGGER.error("ldap secret not found")
371+
LOGGER.error("user credentials secret not found")
367372
return None
368373

369374

@@ -406,24 +411,49 @@ def unprivileged_client(
406411
LOGGER.warning("Unprivileged client is not enabled, using admin client")
407412
yield admin_client
408413

409-
elif is_byoidc:
410-
# this requires a pre-existing context in $KUBECONFIG with a unprivileged user
411-
try:
412-
unprivileged_context, _ = get_unprivileged_context()
413-
except ValueError as e:
414-
raise ValueError(
415-
f"Failed to get unprivileged context for BYOIDC mode. "
416-
f"Ensure the context naming follows the convention: <context>-unprivileged. "
417-
f"Error: {e}"
418-
) from e
414+
elif non_admin_user_password is None:
415+
raise ValueError("Unprivileged user not provisioned")
419416

420-
unprivileged_client = get_client(config_file=kubconfig_filepath, context=unprivileged_context)
417+
elif is_byoidc:
418+
tokens = get_oidc_tokens(admin_client, non_admin_user_password[0], non_admin_user_password[1])
419+
issuer = get_byoidc_issuer_url(admin_client)
420+
421+
with open(kubconfig_filepath) as fd:
422+
kubeconfig_content = yaml.safe_load(fd)
423+
424+
# create the oidc user config
425+
user = {
426+
"name": non_admin_user_password[0],
427+
"user": {
428+
"auth-provider": {
429+
"name": "oidc",
430+
"config": {
431+
"client-id": "oc-cli",
432+
"client-secret": "",
433+
"idp-issuer-url": issuer,
434+
"id-token": tokens[0],
435+
"refresh-token": tokens[1],
436+
},
437+
}
438+
},
439+
}
440+
441+
# replace the users - we only need this one user
442+
kubeconfig_content["users"] = [user]
443+
444+
# get the current context and modify the referenced user in place
445+
current_context_name = kubeconfig_content["current-context"]
446+
current_context = [c for c in kubeconfig_content["contexts"] if c["name"] == current_context_name][0]
447+
current_context["context"]["user"] = non_admin_user_password[0]
448+
449+
unprivileged_client = get_client(
450+
config_dict=kubeconfig_content,
451+
context=current_context_name,
452+
persist_config=False, # keep the kubeconfig intact
453+
)
421454

422455
yield unprivileged_client
423456

424-
elif non_admin_user_password is None:
425-
raise ValueError("Unprivileged user not provisioned")
426-
427457
else:
428458
current_user = run_command(command=["oc", "whoami"])[1].strip()
429459
non_admin_user_name = non_admin_user_password[0]

tests/model_registry/utils.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from model_registry.types import RegisteredModel
3535

3636
from utilities.general import wait_for_pods_running
37-
from utilities.infra import get_cluster_authentication
37+
from utilities.user_utils import get_byoidc_issuer_url
3838

3939
ADDRESS_ANNOTATION_PREFIX: str = "routing.opendatahub.io/external-address-"
4040
MARIA_DB_IMAGE = (
@@ -835,14 +835,6 @@ def get_mr_user_token(admin_client: DynamicClient, user_credentials_rbac: dict[s
835835
raise e
836836

837837

838-
def get_byoidc_issuer_url(admin_client: DynamicClient) -> str:
839-
authentication = get_cluster_authentication(admin_client=admin_client)
840-
assert authentication is not None
841-
url = authentication.instance.spec.oidcProviders[0].issuer.issuerURL
842-
assert url is not None
843-
return url
844-
845-
846838
def get_byoidc_user_credentials(username: str = None) -> Dict[str, str]:
847839
"""
848840
Get user credentials from byoidc-credentials secret.

utilities/user_utils.py

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import tempfile
44
from dataclasses import dataclass
55

6+
import requests
7+
from kubernetes.dynamic import DynamicClient
68
from ocp_resources.user import User
79
from pyhelper_utils.shell import run_command
810
from timeout_sampler import retry
911

1012
from utilities.exceptions import ExceptionUserLogin
11-
from utilities.infra import login_with_user_password
13+
from utilities.infra import login_with_user_password, get_cluster_authentication
1214
import base64
1315
from pathlib import Path
1416

@@ -94,19 +96,43 @@ def wait_for_user_creation(username: str, password: str, cluster_url: str) -> bo
9496
raise ExceptionUserLogin(f"Could not login as user {username}.")
9597

9698

97-
def get_unprivileged_context() -> tuple[str, str]:
98-
"""
99-
Get the unprivileged context from the current context.
100-
This only works in BYOIDC mode, and it assumes that the unprivileged context is already created
101-
with a precise naming convention: <current_context>-unprivileged.
102-
103-
Returns:
104-
Tuple of (unprivileged context, current context).
105-
"""
106-
status, current_context, _ = run_command(command=["oc", "config", "current-context"])
107-
current_context = current_context.strip()
108-
if not status or not current_context:
109-
raise ValueError("Could not get current context from oc config current-context")
110-
if current_context.endswith("-unprivileged"):
111-
raise ValueError("Current context is already called [...]-unprivileged")
112-
return current_context + "-unprivileged", current_context
99+
def get_oidc_tokens(admin_client: DynamicClient, username: str, password: str) -> tuple[str, str]:
100+
url = f"{get_byoidc_issuer_url(admin_client=admin_client)}/protocol/openid-connect/token"
101+
headers = {"Content-Type": "application/x-www-form-urlencoded", "User-Agent": "python-requests"}
102+
103+
data = {
104+
"username": username,
105+
"password": password,
106+
"grant_type": "password",
107+
"client_id": "oc-cli",
108+
"scope": "openid",
109+
}
110+
111+
try:
112+
LOGGER.info(f"Requesting token for user {username} in byoidc environment")
113+
response = requests.post(
114+
url=url,
115+
headers=headers,
116+
data=data,
117+
allow_redirects=True,
118+
timeout=30,
119+
verify=True, # Set to False if you need to skip SSL verification
120+
)
121+
response.raise_for_status()
122+
json_response = response.json()
123+
124+
# Validate that we got an access token
125+
if "id_token" not in json_response or "refresh_token" not in json_response:
126+
LOGGER.error("Warning: No id_token or refresh_token in response")
127+
raise AssertionError(f"No id_token or refresh_token in response: {json_response}")
128+
return json_response["id_token"], json_response["refresh_token"]
129+
except Exception as e:
130+
raise e
131+
132+
133+
def get_byoidc_issuer_url(admin_client: DynamicClient) -> str:
134+
authentication = get_cluster_authentication(admin_client=admin_client)
135+
assert authentication is not None
136+
url = authentication.instance.spec.oidcProviders[0].issuer.issuerURL
137+
assert url is not None
138+
return url

0 commit comments

Comments
 (0)