kaos-cli 0.7.7__tar.gz → 0.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/PKG-INFO +1 -1
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/__init__.py +1 -1
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/install/__init__.py +40 -5
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/install/auth.py +306 -33
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/install/infra.py +26 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli.egg-info/PKG-INFO +1 -1
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/pyproject.toml +1 -1
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/tests/test_cli_integration.py +164 -22
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/README.md +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/agent/__init__.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/agent/a2a.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/agent/build.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/agent/deploy.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/agent/init.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/agent/invoke.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/agent/memory.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/agent/run.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/agent/status.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/agent/tools.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/auth/__init__.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/auth/consent.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/auth/grant.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/auth/login.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/cluster_http.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/config.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/config_cli.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/install/observability.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/main.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/mcp/__init__.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/mcp/build.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/mcp/crud.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/mcp/deploy.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/mcp/init.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/mcp/invoke.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/mcp/run.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/memory/__init__.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/memorystore/__init__.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/memorystore/create.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/modelapi/__init__.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/modelapi/deploy.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/modelapi/invoke.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/proxy.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/samples/__init__.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/samples/data/1-simple-echo-agent.yaml +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/samples/data/2-multi-agent-mcp.yaml +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/samples/data/3-hierarchical-agents.yaml +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/samples/data/4-dev-ollama-proxy-agent.yaml +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/samples/data/5-proxy-external-api.yaml +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/samples/data/6-autonomous-monitor.yaml +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/samples/data/7-memory-agent.yaml +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/samples/data/8-authorization-walkthrough.yaml +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/system/__init__.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/system/create_rbac.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/system/install.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/system/runtimes.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/system/status.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/ui.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/utils/__init__.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/utils/crud.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/utils/port_forward.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli/utils/wait.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli.egg-info/SOURCES.txt +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli.egg-info/dependency_links.txt +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli.egg-info/entry_points.txt +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli.egg-info/requires.txt +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/kaos_cli.egg-info/top_level.txt +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/setup.cfg +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/tests/test_agent_invoke.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/tests/test_agent_tools.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/tests/test_auth_cli.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/tests/test_config_cli.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/tests/test_deploy_wait.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/tests/test_install_surface.py +0 -0
- {kaos_cli-0.7.7 → kaos_cli-0.8.1}/tests/test_memory_cli.py +0 -0
|
@@ -38,6 +38,9 @@ DEFAULT_CREDENTIAL_SECRET_PREFIX = "kaos-aib"
|
|
|
38
38
|
AUTH_ENDUSER_PORT = 8000
|
|
39
39
|
AUTH_ADMIN_PORT = 14000
|
|
40
40
|
AUTH_EXTPROC_PORT = 50051
|
|
41
|
+
DEFAULT_AIB_EXTPROC_IMAGE = (
|
|
42
|
+
"ghcr.io/zalando-incubator/agentic-identity-broker-extproc:v0.1.8"
|
|
43
|
+
)
|
|
41
44
|
|
|
42
45
|
|
|
43
46
|
# Independent agent and user identity modes selected by the install flags.
|
|
@@ -121,8 +124,8 @@ from .auth import (
|
|
|
121
124
|
_build_aib_broker_public_url_args, _build_auth_operator_args,
|
|
122
125
|
_build_token_exchange_aib_args, _default_auth_admin_url,
|
|
123
126
|
_default_auth_issuer, _default_user_auth_issuer, _install_aib,
|
|
124
|
-
_install_keycloak, _keycloak_dev_manifests,
|
|
125
|
-
_keycloak_realm_json,
|
|
127
|
+
_install_aib_extproc, _install_keycloak, _keycloak_dev_manifests,
|
|
128
|
+
_keycloak_realm_configmap_name, _keycloak_realm_json,
|
|
126
129
|
)
|
|
127
130
|
|
|
128
131
|
|
|
@@ -248,8 +251,23 @@ def install_command(
|
|
|
248
251
|
keycloak_namespace, keycloak_release
|
|
249
252
|
)
|
|
250
253
|
|
|
251
|
-
#
|
|
252
|
-
#
|
|
254
|
+
# In exchange mode the public broker and extProc both contact Keycloak
|
|
255
|
+
# during startup, so apply Keycloak first.
|
|
256
|
+
if token_exchange_enabled:
|
|
257
|
+
if not _install_keycloak(
|
|
258
|
+
keycloak_namespace,
|
|
259
|
+
keycloak_release,
|
|
260
|
+
DEFAULT_USER_AUTH_REALM,
|
|
261
|
+
user_auth_audience,
|
|
262
|
+
keycloak_chart_path,
|
|
263
|
+
wait,
|
|
264
|
+
token_exchange_enabled,
|
|
265
|
+
):
|
|
266
|
+
typer.echo(
|
|
267
|
+
"Warning: Keycloak installation failed, continuing...", err=True
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Install the broker from the public chart checkout when provided.
|
|
253
271
|
if (identity_provider == "aib" or token_exchange_enabled) and aib_chart_path:
|
|
254
272
|
if token_exchange_enabled:
|
|
255
273
|
keycloak_issuer = user_auth_issuer or _default_user_auth_issuer(
|
|
@@ -269,6 +287,7 @@ def install_command(
|
|
|
269
287
|
aib_values_path,
|
|
270
288
|
wait,
|
|
271
289
|
extra_set=aib_extra_set,
|
|
290
|
+
seed_identity_bootstrap=identity_provider == "aib",
|
|
272
291
|
):
|
|
273
292
|
typer.echo(
|
|
274
293
|
"Warning: identity broker installation failed, continuing...",
|
|
@@ -285,7 +304,7 @@ def install_command(
|
|
|
285
304
|
|
|
286
305
|
# Keycloak backs the user plane and the OIDC DCR agent mode. Install it
|
|
287
306
|
# when either plane selects it.
|
|
288
|
-
if user_auth or identity_provider == "oidc":
|
|
307
|
+
if (user_auth or identity_provider == "oidc") and not token_exchange_enabled:
|
|
289
308
|
if not _install_keycloak(
|
|
290
309
|
keycloak_namespace,
|
|
291
310
|
keycloak_release,
|
|
@@ -299,6 +318,17 @@ def install_command(
|
|
|
299
318
|
"Warning: Keycloak installation failed, continuing...", err=True
|
|
300
319
|
)
|
|
301
320
|
|
|
321
|
+
if token_exchange_enabled and not _install_aib_extproc(
|
|
322
|
+
auth_namespace,
|
|
323
|
+
auth_release,
|
|
324
|
+
user_auth_issuer
|
|
325
|
+
or _default_user_auth_issuer(keycloak_namespace, keycloak_release),
|
|
326
|
+
wait,
|
|
327
|
+
):
|
|
328
|
+
typer.echo(
|
|
329
|
+
"Warning: AIB extProc installation failed, continuing...", err=True
|
|
330
|
+
)
|
|
331
|
+
|
|
302
332
|
# Phase 2: Wait for infra that the operator depends on
|
|
303
333
|
if gateway_enabled:
|
|
304
334
|
_wait_for_gateway_class()
|
|
@@ -432,6 +462,11 @@ def install_command(
|
|
|
432
462
|
policy_rego_override=policy_rego_override,
|
|
433
463
|
policy_configmap_name=policy_configmap_name or "",
|
|
434
464
|
policy_configmap_namespace=policy_configmap_namespace or "",
|
|
465
|
+
default_permission_set=(
|
|
466
|
+
"kaos-identity"
|
|
467
|
+
if identity_provider == "aib" and aib_chart_path
|
|
468
|
+
else ""
|
|
469
|
+
),
|
|
435
470
|
)
|
|
436
471
|
)
|
|
437
472
|
if token_exchange_enabled:
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"""Authentication provider installation and Helm argument helpers."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
from urllib.parse import urlsplit
|
|
4
5
|
|
|
6
|
+
import httpx
|
|
5
7
|
import typer
|
|
6
8
|
|
|
9
|
+
from kaos_cli.cluster_http import local_service_url
|
|
10
|
+
|
|
7
11
|
from . import (
|
|
8
|
-
AUTH_ADMIN_PORT, AUTH_ENDUSER_PORT,
|
|
12
|
+
AUTH_ADMIN_PORT, AUTH_ENDUSER_PORT, AUTH_EXTPROC_PORT,
|
|
13
|
+
DEFAULT_AGENT_AUTH_AUDIENCE, DEFAULT_AIB_EXTPROC_IMAGE,
|
|
9
14
|
DEFAULT_KEYCLOAK_ADMIN_PASSWORD, DEFAULT_KEYCLOAK_ADMIN_USER,
|
|
10
15
|
DEFAULT_KEYCLOAK_IMAGE, DEFAULT_OIDC_REGISTRATION_SECRET_KEY,
|
|
11
16
|
DEFAULT_OIDC_REGISTRATION_SECRET_NAME, DEFAULT_TOKEN_EXCHANGE_AUDIENCE,
|
|
@@ -64,6 +69,7 @@ def _build_auth_operator_args(
|
|
|
64
69
|
policy_rego_override: bool = False,
|
|
65
70
|
policy_configmap_name: str = "",
|
|
66
71
|
policy_configmap_namespace: str = "",
|
|
72
|
+
default_permission_set: str = "",
|
|
67
73
|
) -> list[str]:
|
|
68
74
|
"""Build the operator Helm --set arguments that enable agent-auth wiring.
|
|
69
75
|
|
|
@@ -101,6 +107,14 @@ def _build_auth_operator_args(
|
|
|
101
107
|
)
|
|
102
108
|
if identity_provider == "aib" and admin_url:
|
|
103
109
|
args.extend(["--set", f"security.agentAuth.adminUrl={admin_url}"])
|
|
110
|
+
if default_permission_set:
|
|
111
|
+
args.extend(
|
|
112
|
+
[
|
|
113
|
+
"--set",
|
|
114
|
+
"security.agentAuth.defaultPermissionSet="
|
|
115
|
+
f"{default_permission_set}",
|
|
116
|
+
]
|
|
117
|
+
)
|
|
104
118
|
if identity_provider == "oidc" and oidc_registration_secret_name:
|
|
105
119
|
args.extend(
|
|
106
120
|
[
|
|
@@ -179,8 +193,9 @@ def _install_aib(
|
|
|
179
193
|
values_path: str | None,
|
|
180
194
|
wait: bool,
|
|
181
195
|
extra_set: list[str] | None = None,
|
|
196
|
+
seed_identity_bootstrap: bool = True,
|
|
182
197
|
) -> bool:
|
|
183
|
-
"""Install the identity
|
|
198
|
+
"""Install the public identity-broker chart from a local v0.1.8 checkout."""
|
|
184
199
|
typer.echo("Installing identity broker...")
|
|
185
200
|
helm_args = [
|
|
186
201
|
"upgrade",
|
|
@@ -203,9 +218,111 @@ def _install_aib(
|
|
|
203
218
|
typer.echo(f"Error installing identity broker: {result.stderr}", err=True)
|
|
204
219
|
return False
|
|
205
220
|
|
|
221
|
+
if seed_identity_bootstrap and not _seed_aib_identity_bootstrap(
|
|
222
|
+
namespace, release
|
|
223
|
+
):
|
|
224
|
+
return False
|
|
225
|
+
|
|
206
226
|
typer.echo(f"✅ Identity broker installed in '{namespace}' namespace")
|
|
207
227
|
return True
|
|
208
228
|
|
|
229
|
+
|
|
230
|
+
AIB_BOOTSTRAP_SERVICE = "kaos-identity-placeholder"
|
|
231
|
+
AIB_BOOTSTRAP_PERMISSION_SET = "kaos-identity"
|
|
232
|
+
AIB_BOOTSTRAP_DESCRIPTION = (
|
|
233
|
+
"KAOS identity-only bootstrap; remove once AIB allows agents without permission sets"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _items(response: httpx.Response) -> list[dict]:
|
|
238
|
+
response.raise_for_status()
|
|
239
|
+
payload = response.json()
|
|
240
|
+
return payload if isinstance(payload, list) else payload.get("items", [])
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _seed_aib_identity_bootstrap(namespace: str, release: str) -> bool:
|
|
244
|
+
"""Idempotently seed the public AIB permission-set registration bridge."""
|
|
245
|
+
rollout = _root()._run_kubectl(
|
|
246
|
+
[
|
|
247
|
+
"rollout",
|
|
248
|
+
"status",
|
|
249
|
+
f"deployment/{_auth_broker_fullname(release)}",
|
|
250
|
+
"-n",
|
|
251
|
+
namespace,
|
|
252
|
+
"--timeout=120s",
|
|
253
|
+
],
|
|
254
|
+
check=False,
|
|
255
|
+
)
|
|
256
|
+
if rollout.returncode != 0:
|
|
257
|
+
typer.echo(f"Error waiting for identity broker: {rollout.stderr}", err=True)
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
admin_url = _default_auth_admin_url(namespace, release)
|
|
261
|
+
headers = {
|
|
262
|
+
"Host": urlsplit(admin_url).netloc,
|
|
263
|
+
"X-Remote-User": "kaos-operator",
|
|
264
|
+
}
|
|
265
|
+
try:
|
|
266
|
+
with local_service_url(admin_url) as local_url:
|
|
267
|
+
base = local_url.rstrip("/")
|
|
268
|
+
with httpx.Client(headers=headers, timeout=30.0) as client:
|
|
269
|
+
services = _items(client.get(f"{base}/services"))
|
|
270
|
+
service = next(
|
|
271
|
+
(
|
|
272
|
+
item
|
|
273
|
+
for item in services
|
|
274
|
+
if item.get("canonical_id") == AIB_BOOTSTRAP_SERVICE
|
|
275
|
+
or item.get("display_name") == AIB_BOOTSTRAP_SERVICE
|
|
276
|
+
),
|
|
277
|
+
None,
|
|
278
|
+
)
|
|
279
|
+
if service is None:
|
|
280
|
+
response = client.post(
|
|
281
|
+
f"{base}/services",
|
|
282
|
+
json={
|
|
283
|
+
"canonical_id": AIB_BOOTSTRAP_SERVICE,
|
|
284
|
+
# AIB services have no description field, so the
|
|
285
|
+
# descriptive marker lives in display_name.
|
|
286
|
+
"display_name": AIB_BOOTSTRAP_DESCRIPTION,
|
|
287
|
+
"client_id": AIB_BOOTSTRAP_SERVICE,
|
|
288
|
+
"token_endpoint_auth_method": "none",
|
|
289
|
+
"issuer_uri": "https://kaos-identity-placeholder.invalid",
|
|
290
|
+
"discovery": {"enable_discovery": False},
|
|
291
|
+
"endpoints": {
|
|
292
|
+
"authorize_endpoint": "https://kaos-identity-placeholder.invalid/oauth2/authorize",
|
|
293
|
+
"token_endpoint": "https://kaos-identity-placeholder.invalid/oauth2/token",
|
|
294
|
+
},
|
|
295
|
+
"scopes": [],
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
response.raise_for_status()
|
|
299
|
+
service = response.json()
|
|
300
|
+
|
|
301
|
+
permission_sets = _items(client.get(f"{base}/permission-sets"))
|
|
302
|
+
if not any(
|
|
303
|
+
item.get("name") == AIB_BOOTSTRAP_PERMISSION_SET
|
|
304
|
+
for item in permission_sets
|
|
305
|
+
):
|
|
306
|
+
response = client.post(
|
|
307
|
+
f"{base}/permission-sets",
|
|
308
|
+
json={
|
|
309
|
+
"canonical_id": AIB_BOOTSTRAP_PERMISSION_SET,
|
|
310
|
+
"name": AIB_BOOTSTRAP_PERMISSION_SET,
|
|
311
|
+
"description": AIB_BOOTSTRAP_DESCRIPTION,
|
|
312
|
+
"service_scopes": [
|
|
313
|
+
{"service_id": service["id"], "scopes": []}
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
response.raise_for_status()
|
|
318
|
+
except (httpx.HTTPError, KeyError, RuntimeError, ValueError) as exc:
|
|
319
|
+
typer.echo(f"Error seeding identity broker: {exc}", err=True)
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
typer.echo("✅ Seeded AIB permission set 'kaos-identity'")
|
|
323
|
+
return True
|
|
324
|
+
|
|
325
|
+
|
|
209
326
|
def _build_aib_broker_public_url_args(public_url: str) -> list[str]:
|
|
210
327
|
"""Set the broker enduser ``public_url`` to its in-cluster service URL.
|
|
211
328
|
|
|
@@ -219,25 +336,17 @@ def _build_aib_broker_public_url_args(public_url: str) -> list[str]:
|
|
|
219
336
|
"""
|
|
220
337
|
return ["--set", f"broker.server.enduser.publicUrl={public_url}"]
|
|
221
338
|
|
|
339
|
+
|
|
222
340
|
def _build_token_exchange_aib_args(
|
|
223
341
|
auth_namespace: str,
|
|
224
342
|
auth_release: str,
|
|
225
343
|
keycloak_issuer: str,
|
|
226
344
|
) -> list[str]:
|
|
227
|
-
"""Configure
|
|
345
|
+
"""Configure public AIB v0.1.8 for Keycloak-backed token exchange."""
|
|
228
346
|
aib_issuer = _default_auth_issuer(auth_namespace, auth_release)
|
|
229
347
|
keycloak_token_endpoint = f"{keycloak_issuer}/protocol/openid-connect/token"
|
|
230
348
|
keycloak_authorize_endpoint = f"{keycloak_issuer}/protocol/openid-connect/auth"
|
|
231
|
-
|
|
232
|
-
{"name": "EXTPROC_OAUTH2_ISSUER", "value": keycloak_issuer},
|
|
233
|
-
{"name": "EXTPROC_OAUTH2_CLIENT_ID", "value": DEFAULT_USER_AUTH_CLIENT_ID},
|
|
234
|
-
{
|
|
235
|
-
"name": "EXTPROC_OAUTH2_CLIENT_SECRET",
|
|
236
|
-
"value": DEFAULT_USER_AUTH_CLIENT_SECRET,
|
|
237
|
-
},
|
|
238
|
-
{"name": "EXTPROC_OAUTH2_CLIENT_ASSERTION_TYPE", "value": "access_token"},
|
|
239
|
-
]
|
|
240
|
-
return [
|
|
349
|
+
args = [
|
|
241
350
|
"--set",
|
|
242
351
|
f"broker.server.enduser.publicUrl={aib_issuer}",
|
|
243
352
|
"--set",
|
|
@@ -248,7 +357,8 @@ def _build_token_exchange_aib_args(
|
|
|
248
357
|
"broker.oauth2AuthorizationServer.proxy.upstreamAuthorizeEndpoint="
|
|
249
358
|
f"{keycloak_authorize_endpoint}",
|
|
250
359
|
"--set",
|
|
251
|
-
|
|
360
|
+
"broker.oauth2AuthorizationServer.proxy.upstreamTokenEndpoint="
|
|
361
|
+
f"{keycloak_token_endpoint}",
|
|
252
362
|
"--set",
|
|
253
363
|
f"broker.tokenExchange.expectedAudience={DEFAULT_TOKEN_EXCHANGE_AUDIENCE}",
|
|
254
364
|
"--set",
|
|
@@ -261,13 +371,153 @@ def _build_token_exchange_aib_args(
|
|
|
261
371
|
"--set",
|
|
262
372
|
"broker.tokenExchange.authorization.cel.expression="
|
|
263
373
|
f'client_assertion.azp == "{DEFAULT_USER_AUTH_CLIENT_ID}"',
|
|
264
|
-
"--set",
|
|
265
|
-
"extProc.enabled=true",
|
|
266
|
-
"--set",
|
|
267
|
-
f"extProc.oauth2.clientCredentialsEndpoint={keycloak_token_endpoint}",
|
|
268
|
-
"--set-json",
|
|
269
|
-
f"extProc.extraEnv={json.dumps(extra_env, separators=(',', ':'))}",
|
|
270
374
|
]
|
|
375
|
+
if keycloak_issuer.startswith("http://"):
|
|
376
|
+
args.extend(
|
|
377
|
+
["--set", "broker.security.skipThirdpartyHttpsValidation=true"]
|
|
378
|
+
)
|
|
379
|
+
return args
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _auth_extproc_name(auth_release: str) -> str:
|
|
383
|
+
return f"{auth_release}-agentic-identity-broker-extproc"
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _aib_extproc_manifests(
|
|
387
|
+
namespace: str, release: str, keycloak_issuer: str
|
|
388
|
+
) -> list[dict]:
|
|
389
|
+
"""Build the public extProc workload using its v0.1.8 env contract."""
|
|
390
|
+
name = _auth_extproc_name(release)
|
|
391
|
+
labels = {"app.kubernetes.io/name": name}
|
|
392
|
+
secret_name = f"{name}-credentials"
|
|
393
|
+
token_endpoint = f"{_default_auth_issuer(namespace, release)}/oauth2/token"
|
|
394
|
+
client_credentials_endpoint = (
|
|
395
|
+
f"{keycloak_issuer}/protocol/openid-connect/token"
|
|
396
|
+
)
|
|
397
|
+
secret = {
|
|
398
|
+
"apiVersion": "v1",
|
|
399
|
+
"kind": "Secret",
|
|
400
|
+
"metadata": {"name": secret_name, "namespace": namespace},
|
|
401
|
+
"type": "Opaque",
|
|
402
|
+
"stringData": {"client-secret": DEFAULT_USER_AUTH_CLIENT_SECRET},
|
|
403
|
+
}
|
|
404
|
+
deployment = {
|
|
405
|
+
"apiVersion": "apps/v1",
|
|
406
|
+
"kind": "Deployment",
|
|
407
|
+
"metadata": {"name": name, "namespace": namespace, "labels": labels},
|
|
408
|
+
"spec": {
|
|
409
|
+
"replicas": 1,
|
|
410
|
+
"selector": {"matchLabels": labels},
|
|
411
|
+
"template": {
|
|
412
|
+
"metadata": {"labels": labels},
|
|
413
|
+
"spec": {
|
|
414
|
+
"containers": [
|
|
415
|
+
{
|
|
416
|
+
"name": "extproc",
|
|
417
|
+
"image": DEFAULT_AIB_EXTPROC_IMAGE,
|
|
418
|
+
"imagePullPolicy": "IfNotPresent",
|
|
419
|
+
"ports": [
|
|
420
|
+
{"name": "grpc", "containerPort": AUTH_EXTPROC_PORT}
|
|
421
|
+
],
|
|
422
|
+
"env": [
|
|
423
|
+
{
|
|
424
|
+
"name": "EXTPROC_OAUTH2_TOKEN_ENDPOINT",
|
|
425
|
+
"value": token_endpoint,
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"name": "EXTPROC_OAUTH2_ISSUER",
|
|
429
|
+
"value": keycloak_issuer,
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
"name": "EXTPROC_OAUTH2_CLIENT_ID",
|
|
433
|
+
"value": DEFAULT_USER_AUTH_CLIENT_ID,
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"name": "EXTPROC_OAUTH2_CLIENT_SECRET",
|
|
437
|
+
"valueFrom": {
|
|
438
|
+
"secretKeyRef": {
|
|
439
|
+
"name": secret_name,
|
|
440
|
+
"key": "client-secret",
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
"name": (
|
|
446
|
+
"EXTPROC_OAUTH2_CLIENT_CREDENTIALS_ENDPOINT"
|
|
447
|
+
),
|
|
448
|
+
"value": client_credentials_endpoint,
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
"name": "EXTPROC_OAUTH2_CLIENT_ASSERTION_TYPE",
|
|
452
|
+
"value": "access_token",
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
"name": "EXTPROC_OAUTH2_TLS_ALLOW_HTTP",
|
|
456
|
+
"value": "true",
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
"name": "EXTPROC_GRPC_PORT",
|
|
460
|
+
"value": str(AUTH_EXTPROC_PORT),
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
"readinessProbe": {
|
|
464
|
+
"tcpSocket": {"port": "grpc"},
|
|
465
|
+
"periodSeconds": 2,
|
|
466
|
+
},
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
}
|
|
473
|
+
service = {
|
|
474
|
+
"apiVersion": "v1",
|
|
475
|
+
"kind": "Service",
|
|
476
|
+
"metadata": {"name": name, "namespace": namespace, "labels": labels},
|
|
477
|
+
"spec": {
|
|
478
|
+
"selector": labels,
|
|
479
|
+
"ports": [
|
|
480
|
+
{
|
|
481
|
+
"name": "grpc",
|
|
482
|
+
"port": AUTH_EXTPROC_PORT,
|
|
483
|
+
"targetPort": "grpc",
|
|
484
|
+
}
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
}
|
|
488
|
+
return [secret, deployment, service]
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _install_aib_extproc(
|
|
492
|
+
namespace: str, release: str, keycloak_issuer: str, wait: bool
|
|
493
|
+
) -> bool:
|
|
494
|
+
"""Apply KAOS-owned extProc resources for the public AIB release."""
|
|
495
|
+
typer.echo("Installing AIB token-exchange extProc...")
|
|
496
|
+
for manifest in _aib_extproc_manifests(namespace, release, keycloak_issuer):
|
|
497
|
+
result = _root()._run_kubectl(
|
|
498
|
+
["apply", "-f", "-"], check=False, input=json.dumps(manifest)
|
|
499
|
+
)
|
|
500
|
+
if result.returncode != 0:
|
|
501
|
+
typer.echo(f"Error installing AIB extProc: {result.stderr}", err=True)
|
|
502
|
+
return False
|
|
503
|
+
if wait:
|
|
504
|
+
result = _root()._run_kubectl(
|
|
505
|
+
[
|
|
506
|
+
"rollout",
|
|
507
|
+
"status",
|
|
508
|
+
f"deployment/{_auth_extproc_name(release)}",
|
|
509
|
+
"-n",
|
|
510
|
+
namespace,
|
|
511
|
+
"--timeout=120s",
|
|
512
|
+
],
|
|
513
|
+
check=False,
|
|
514
|
+
)
|
|
515
|
+
if result.returncode != 0:
|
|
516
|
+
typer.echo(f"Error waiting for AIB extProc: {result.stderr}", err=True)
|
|
517
|
+
return False
|
|
518
|
+
typer.echo(f"✅ AIB extProc installed in '{namespace}' namespace")
|
|
519
|
+
return True
|
|
520
|
+
|
|
271
521
|
|
|
272
522
|
def _keycloak_realm_json(
|
|
273
523
|
realm: str,
|
|
@@ -276,6 +526,7 @@ def _keycloak_realm_json(
|
|
|
276
526
|
audience: str,
|
|
277
527
|
username: str,
|
|
278
528
|
password: str,
|
|
529
|
+
token_exchange_enabled: bool = False,
|
|
279
530
|
) -> dict:
|
|
280
531
|
"""Build a minimal Keycloak realm definition for non-interactive validation.
|
|
281
532
|
|
|
@@ -285,6 +536,32 @@ def _keycloak_realm_json(
|
|
|
285
536
|
and group-based AccessGrants require.
|
|
286
537
|
"""
|
|
287
538
|
|
|
539
|
+
agent_audience_mappers = [
|
|
540
|
+
{
|
|
541
|
+
"name": "kaos-agent-audience",
|
|
542
|
+
"protocol": "openid-connect",
|
|
543
|
+
"protocolMapper": "oidc-audience-mapper",
|
|
544
|
+
"config": {
|
|
545
|
+
"included.custom.audience": DEFAULT_AGENT_AUTH_AUDIENCE,
|
|
546
|
+
"id.token.claim": "false",
|
|
547
|
+
"access.token.claim": "true",
|
|
548
|
+
},
|
|
549
|
+
}
|
|
550
|
+
]
|
|
551
|
+
if token_exchange_enabled:
|
|
552
|
+
agent_audience_mappers.append(
|
|
553
|
+
{
|
|
554
|
+
"name": "token-exchange-audience",
|
|
555
|
+
"protocol": "openid-connect",
|
|
556
|
+
"protocolMapper": "oidc-audience-mapper",
|
|
557
|
+
"config": {
|
|
558
|
+
"included.custom.audience": DEFAULT_TOKEN_EXCHANGE_AUDIENCE,
|
|
559
|
+
"id.token.claim": "false",
|
|
560
|
+
"access.token.claim": "true",
|
|
561
|
+
},
|
|
562
|
+
}
|
|
563
|
+
)
|
|
564
|
+
|
|
288
565
|
return {
|
|
289
566
|
"realm": realm,
|
|
290
567
|
"enabled": True,
|
|
@@ -293,18 +570,7 @@ def _keycloak_realm_json(
|
|
|
293
570
|
"name": "kaos-agent-audience",
|
|
294
571
|
"protocol": "openid-connect",
|
|
295
572
|
"attributes": {"include.in.token.scope": "false"},
|
|
296
|
-
"protocolMappers":
|
|
297
|
-
{
|
|
298
|
-
"name": "kaos-agent-audience",
|
|
299
|
-
"protocol": "openid-connect",
|
|
300
|
-
"protocolMapper": "oidc-audience-mapper",
|
|
301
|
-
"config": {
|
|
302
|
-
"included.custom.audience": DEFAULT_AGENT_AUTH_AUDIENCE,
|
|
303
|
-
"id.token.claim": "false",
|
|
304
|
-
"access.token.claim": "true",
|
|
305
|
-
},
|
|
306
|
-
}
|
|
307
|
-
],
|
|
573
|
+
"protocolMappers": agent_audience_mappers,
|
|
308
574
|
}
|
|
309
575
|
],
|
|
310
576
|
"defaultDefaultClientScopes": ["kaos-agent-audience"],
|
|
@@ -385,15 +651,18 @@ def _keycloak_realm_json(
|
|
|
385
651
|
],
|
|
386
652
|
}
|
|
387
653
|
|
|
654
|
+
|
|
388
655
|
def _keycloak_realm_configmap_name(release: str) -> str:
|
|
389
656
|
"""Name of the ConfigMap holding the imported realm definition."""
|
|
390
657
|
return f"{release}-realm-import"
|
|
391
658
|
|
|
659
|
+
|
|
392
660
|
def _bootstrap_keycloak_realm(
|
|
393
661
|
namespace: str,
|
|
394
662
|
release: str,
|
|
395
663
|
realm: str,
|
|
396
664
|
audience: str,
|
|
665
|
+
token_exchange_enabled: bool = False,
|
|
397
666
|
) -> bool:
|
|
398
667
|
"""Create the realm-import ConfigMap consumed by Keycloak's --import-realm.
|
|
399
668
|
|
|
@@ -409,6 +678,7 @@ def _bootstrap_keycloak_realm(
|
|
|
409
678
|
audience,
|
|
410
679
|
DEFAULT_USER_AUTH_TEST_USER,
|
|
411
680
|
DEFAULT_USER_AUTH_TEST_PASSWORD,
|
|
681
|
+
token_exchange_enabled,
|
|
412
682
|
),
|
|
413
683
|
indent=2,
|
|
414
684
|
)
|
|
@@ -521,6 +791,7 @@ def _keycloak_dev_manifests(
|
|
|
521
791
|
}
|
|
522
792
|
return [deployment, service]
|
|
523
793
|
|
|
794
|
+
|
|
524
795
|
def _install_keycloak(
|
|
525
796
|
namespace: str,
|
|
526
797
|
release: str,
|
|
@@ -537,7 +808,9 @@ def _install_keycloak(
|
|
|
537
808
|
dev path); otherwise a self-contained dev deployment is applied via kubectl.
|
|
538
809
|
"""
|
|
539
810
|
typer.echo("Installing user identity provider (Keycloak)...")
|
|
540
|
-
if not _bootstrap_keycloak_realm(
|
|
811
|
+
if not _bootstrap_keycloak_realm(
|
|
812
|
+
namespace, release, realm, audience, token_exchange_enabled
|
|
813
|
+
):
|
|
541
814
|
return False
|
|
542
815
|
|
|
543
816
|
if chart_path:
|
|
@@ -90,6 +90,32 @@ def _install_gateway_api(enable_backend: bool = False) -> bool:
|
|
|
90
90
|
typer.echo(f"Error installing Envoy Gateway: {result.stderr}", err=True)
|
|
91
91
|
return False
|
|
92
92
|
|
|
93
|
+
if enable_backend:
|
|
94
|
+
for args in (
|
|
95
|
+
[
|
|
96
|
+
"rollout",
|
|
97
|
+
"restart",
|
|
98
|
+
"deployment/envoy-gateway",
|
|
99
|
+
"-n",
|
|
100
|
+
"envoy-gateway-system",
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
"rollout",
|
|
104
|
+
"status",
|
|
105
|
+
"deployment/envoy-gateway",
|
|
106
|
+
"-n",
|
|
107
|
+
"envoy-gateway-system",
|
|
108
|
+
"--timeout=180s",
|
|
109
|
+
],
|
|
110
|
+
):
|
|
111
|
+
rollout = _root()._run_kubectl(args, check=False)
|
|
112
|
+
if rollout.returncode != 0:
|
|
113
|
+
typer.echo(
|
|
114
|
+
f"Error reloading Envoy Gateway Backend support: {rollout.stderr}",
|
|
115
|
+
err=True,
|
|
116
|
+
)
|
|
117
|
+
return False
|
|
118
|
+
|
|
93
119
|
typer.echo("✅ Envoy Gateway installed")
|
|
94
120
|
return True
|
|
95
121
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""CLI integration tests using --dry-run to validate YAML generation."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import re
|
|
4
5
|
import yaml
|
|
5
6
|
import pytest
|
|
@@ -1137,16 +1138,40 @@ def test_token_exchange_gateway_install_enables_backend_api():
|
|
|
1137
1138
|
from kaos_cli.install import _install_gateway_api
|
|
1138
1139
|
|
|
1139
1140
|
calls = []
|
|
1141
|
+
kubectl_calls = []
|
|
1140
1142
|
|
|
1141
1143
|
def fake_helm(args, check=False):
|
|
1142
1144
|
calls.append(args)
|
|
1143
1145
|
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
1144
1146
|
|
|
1145
|
-
|
|
1147
|
+
def fake_kubectl(args, check=False, input=None):
|
|
1148
|
+
kubectl_calls.append(args)
|
|
1149
|
+
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
1150
|
+
|
|
1151
|
+
with patch("kaos_cli.install.run_helm_command", side_effect=fake_helm), patch(
|
|
1152
|
+
"kaos_cli.install._run_kubectl", side_effect=fake_kubectl
|
|
1153
|
+
):
|
|
1146
1154
|
assert _install_gateway_api(enable_backend=True)
|
|
1147
1155
|
|
|
1148
1156
|
install_args = calls[-1]
|
|
1149
1157
|
assert "config.envoyGateway.extensionApis.enableBackend=true" in install_args
|
|
1158
|
+
assert kubectl_calls[-2:] == [
|
|
1159
|
+
[
|
|
1160
|
+
"rollout",
|
|
1161
|
+
"restart",
|
|
1162
|
+
"deployment/envoy-gateway",
|
|
1163
|
+
"-n",
|
|
1164
|
+
"envoy-gateway-system",
|
|
1165
|
+
],
|
|
1166
|
+
[
|
|
1167
|
+
"rollout",
|
|
1168
|
+
"status",
|
|
1169
|
+
"deployment/envoy-gateway",
|
|
1170
|
+
"-n",
|
|
1171
|
+
"envoy-gateway-system",
|
|
1172
|
+
"--timeout=180s",
|
|
1173
|
+
],
|
|
1174
|
+
]
|
|
1150
1175
|
|
|
1151
1176
|
|
|
1152
1177
|
class TestAuthWiring:
|
|
@@ -1183,6 +1208,105 @@ class TestAuthWiring:
|
|
|
1183
1208
|
# Each value is preceded by a --set flag.
|
|
1184
1209
|
assert args.count("--set") == 4
|
|
1185
1210
|
|
|
1211
|
+
def test_seed_aib_identity_bootstrap(self):
|
|
1212
|
+
from contextlib import nullcontext
|
|
1213
|
+
from types import SimpleNamespace
|
|
1214
|
+
from unittest.mock import MagicMock, patch
|
|
1215
|
+
|
|
1216
|
+
from kaos_cli.install.auth import _seed_aib_identity_bootstrap
|
|
1217
|
+
|
|
1218
|
+
empty = MagicMock()
|
|
1219
|
+
empty.json.side_effect = [[], {"items": []}]
|
|
1220
|
+
created_service = MagicMock()
|
|
1221
|
+
created_service.json.return_value = {"id": "service-1"}
|
|
1222
|
+
created_permission_set = MagicMock()
|
|
1223
|
+
created_permission_set.json.return_value = {"id": "permission-set-1"}
|
|
1224
|
+
client = MagicMock()
|
|
1225
|
+
client.__enter__.return_value = client
|
|
1226
|
+
client.get.side_effect = [empty, empty]
|
|
1227
|
+
client.post.side_effect = [created_service, created_permission_set]
|
|
1228
|
+
root = SimpleNamespace(
|
|
1229
|
+
_run_kubectl=lambda *args, **kwargs: SimpleNamespace(
|
|
1230
|
+
returncode=0, stdout="", stderr=""
|
|
1231
|
+
)
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
with patch("kaos_cli.install.auth._root", return_value=root), patch(
|
|
1235
|
+
"kaos_cli.install.auth.local_service_url",
|
|
1236
|
+
return_value=nullcontext("http://127.0.0.1:14000/api"),
|
|
1237
|
+
), patch("kaos_cli.install.auth.httpx.Client", return_value=client):
|
|
1238
|
+
assert _seed_aib_identity_bootstrap("aib-system", "aib") is True
|
|
1239
|
+
|
|
1240
|
+
service_body = client.post.call_args_list[0].kwargs["json"]
|
|
1241
|
+
assert service_body["canonical_id"] == "kaos-identity-placeholder"
|
|
1242
|
+
assert service_body["display_name"].startswith("KAOS identity-only bootstrap")
|
|
1243
|
+
permission_body = client.post.call_args_list[1].kwargs["json"]
|
|
1244
|
+
assert permission_body["name"] == "kaos-identity"
|
|
1245
|
+
assert permission_body["service_scopes"] == [
|
|
1246
|
+
{"service_id": "service-1", "scopes": []}
|
|
1247
|
+
]
|
|
1248
|
+
|
|
1249
|
+
def test_aib_extproc_manifest_uses_public_v018_contract(self):
|
|
1250
|
+
from kaos_cli.install.auth import _aib_extproc_manifests
|
|
1251
|
+
|
|
1252
|
+
secret, deployment, service = _aib_extproc_manifests(
|
|
1253
|
+
"aib-system",
|
|
1254
|
+
"aib",
|
|
1255
|
+
"http://keycloak.keycloak.svc.cluster.local:8080/realms/kaos",
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1258
|
+
assert secret["kind"] == "Secret"
|
|
1259
|
+
container = deployment["spec"]["template"]["spec"]["containers"][0]
|
|
1260
|
+
assert container["image"].endswith(
|
|
1261
|
+
"agentic-identity-broker-extproc:v0.1.8"
|
|
1262
|
+
)
|
|
1263
|
+
env = {item["name"]: item for item in container["env"]}
|
|
1264
|
+
assert env["EXTPROC_OAUTH2_TOKEN_ENDPOINT"]["value"] == (
|
|
1265
|
+
"http://aib-agentic-identity-broker.aib-system.svc.cluster.local:8000"
|
|
1266
|
+
"/oauth2/token"
|
|
1267
|
+
)
|
|
1268
|
+
assert env["EXTPROC_OAUTH2_CLIENT_CREDENTIALS_ENDPOINT"]["value"].endswith(
|
|
1269
|
+
"/realms/kaos/protocol/openid-connect/token"
|
|
1270
|
+
)
|
|
1271
|
+
assert env["EXTPROC_OAUTH2_CLIENT_ASSERTION_TYPE"]["value"] == "access_token"
|
|
1272
|
+
assert env["EXTPROC_OAUTH2_TLS_ALLOW_HTTP"]["value"] == "true"
|
|
1273
|
+
assert "valueFrom" in env["EXTPROC_OAUTH2_CLIENT_SECRET"]
|
|
1274
|
+
assert service["metadata"]["name"] == (
|
|
1275
|
+
"aib-agentic-identity-broker-extproc"
|
|
1276
|
+
)
|
|
1277
|
+
assert service["spec"]["ports"][0]["port"] == 50051
|
|
1278
|
+
|
|
1279
|
+
def test_install_aib_extproc_applies_and_waits(self):
|
|
1280
|
+
from types import SimpleNamespace
|
|
1281
|
+
from unittest.mock import patch
|
|
1282
|
+
|
|
1283
|
+
from kaos_cli.install.auth import _install_aib_extproc
|
|
1284
|
+
|
|
1285
|
+
calls = []
|
|
1286
|
+
|
|
1287
|
+
def fake_kubectl(args, check=False, input=None):
|
|
1288
|
+
calls.append((args, input))
|
|
1289
|
+
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
|
1290
|
+
|
|
1291
|
+
with patch("kaos_cli.install._run_kubectl", side_effect=fake_kubectl):
|
|
1292
|
+
assert _install_aib_extproc(
|
|
1293
|
+
"aib-system",
|
|
1294
|
+
"aib",
|
|
1295
|
+
"http://keycloak:8080/realms/kaos",
|
|
1296
|
+
wait=True,
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
assert [json.loads(body)["kind"] for _, body in calls[:3]] == [
|
|
1300
|
+
"Secret",
|
|
1301
|
+
"Deployment",
|
|
1302
|
+
"Service",
|
|
1303
|
+
]
|
|
1304
|
+
assert calls[3][0][:3] == [
|
|
1305
|
+
"rollout",
|
|
1306
|
+
"status",
|
|
1307
|
+
"deployment/aib-agentic-identity-broker-extproc",
|
|
1308
|
+
]
|
|
1309
|
+
|
|
1186
1310
|
def test_install_uses_one_aib_issuer_for_broker_and_operator(self):
|
|
1187
1311
|
from types import SimpleNamespace
|
|
1188
1312
|
from unittest.mock import patch
|
|
@@ -1644,6 +1768,24 @@ class TestAuthWiring:
|
|
|
1644
1768
|
"--features=token-exchange,admin-fine-grained-authz",
|
|
1645
1769
|
]
|
|
1646
1770
|
|
|
1771
|
+
def test_keycloak_te_realm_adds_exchange_audience_to_dynamic_clients(self):
|
|
1772
|
+
from kaos_cli.install.auth import _keycloak_realm_json
|
|
1773
|
+
|
|
1774
|
+
realm = _keycloak_realm_json(
|
|
1775
|
+
"kaos",
|
|
1776
|
+
"kaos",
|
|
1777
|
+
"secret",
|
|
1778
|
+
"kaos",
|
|
1779
|
+
"user",
|
|
1780
|
+
"password",
|
|
1781
|
+
token_exchange_enabled=True,
|
|
1782
|
+
)
|
|
1783
|
+
audiences = {
|
|
1784
|
+
mapper["config"]["included.custom.audience"]
|
|
1785
|
+
for mapper in realm["clientScopes"][0]["protocolMappers"]
|
|
1786
|
+
}
|
|
1787
|
+
assert audiences == {"kaos-gateway", "token-exchange-broker"}
|
|
1788
|
+
|
|
1647
1789
|
def test_auth_enabled_wires_user_auth_values(self):
|
|
1648
1790
|
"""aib-keycloak (user-auth on) adds security.userAuth.* args."""
|
|
1649
1791
|
from unittest.mock import patch
|
|
@@ -1934,6 +2076,7 @@ class TestAuthWiring:
|
|
|
1934
2076
|
"security.agentAuth.issuer=http://aib-agentic-identity-broker.aib-system.svc.cluster.local:8000",
|
|
1935
2077
|
"security.agentAuth.credentialSecretPrefix=kaos-aib",
|
|
1936
2078
|
"security.agentAuth.adminUrl=http://aib-agentic-identity-broker.aib-system.svc.cluster.local:14000/api",
|
|
2079
|
+
"security.agentAuth.defaultPermissionSet=kaos-identity",
|
|
1937
2080
|
"security.agentAuth.authorization.policyDataSource=automated",
|
|
1938
2081
|
"security.agentAuth.projection.policyConfigMap.name=kaos-authz-policy",
|
|
1939
2082
|
"security.agentAuth.projection.policyConfigMap.namespace=kaos-system",
|
|
@@ -1954,6 +2097,7 @@ class TestAuthWiring:
|
|
|
1954
2097
|
"security.agentAuth.issuer=http://aib-agentic-identity-broker.aib-system.svc.cluster.local:8000",
|
|
1955
2098
|
"security.agentAuth.credentialSecretPrefix=kaos-aib",
|
|
1956
2099
|
"security.agentAuth.adminUrl=http://aib-agentic-identity-broker.aib-system.svc.cluster.local:14000/api",
|
|
2100
|
+
"security.agentAuth.defaultPermissionSet=kaos-identity",
|
|
1957
2101
|
"security.agentAuth.authorization.policyDataSource=automated",
|
|
1958
2102
|
"security.agentAuth.projection.policyConfigMap.name=kaos-authz-policy",
|
|
1959
2103
|
"security.agentAuth.projection.policyConfigMap.namespace=kaos-system",
|
|
@@ -2142,7 +2286,7 @@ class TestAuthWiring:
|
|
|
2142
2286
|
assert result.exit_code != 0
|
|
2143
2287
|
assert "No such option: --auth-enabled" in strip_ansi(result.output)
|
|
2144
2288
|
|
|
2145
|
-
def
|
|
2289
|
+
def test_token_exchange_deploys_public_extproc_and_wires_operator(self):
|
|
2146
2290
|
from types import SimpleNamespace
|
|
2147
2291
|
from unittest.mock import patch
|
|
2148
2292
|
|
|
@@ -2156,7 +2300,9 @@ class TestAuthWiring:
|
|
|
2156
2300
|
"kaos_cli.install.run_helm_command", side_effect=fake_helm
|
|
2157
2301
|
), patch("kaos_cli.install._install_aib", return_value=True) as mock_aib, patch(
|
|
2158
2302
|
"kaos_cli.install._install_keycloak", return_value=True
|
|
2159
|
-
) as mock_keycloak
|
|
2303
|
+
) as mock_keycloak, patch(
|
|
2304
|
+
"kaos_cli.install._install_aib_extproc", return_value=True
|
|
2305
|
+
) as mock_extproc:
|
|
2160
2306
|
result = runner.invoke(
|
|
2161
2307
|
app,
|
|
2162
2308
|
[
|
|
@@ -2173,27 +2319,23 @@ class TestAuthWiring:
|
|
|
2173
2319
|
assert result.exit_code == 0, result.output
|
|
2174
2320
|
mock_keycloak.assert_called_once()
|
|
2175
2321
|
mock_aib.assert_called_once()
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
assert "
|
|
2184
|
-
assert "
|
|
2185
|
-
assert "
|
|
2186
|
-
assert "extProc.oauth2.clientCredentialsEndpoint=" in joined_aib
|
|
2322
|
+
mock_extproc.assert_called_once_with(
|
|
2323
|
+
"aib-system",
|
|
2324
|
+
"aib",
|
|
2325
|
+
"http://keycloak.keycloak.svc.cluster.local:8080/realms/kaos",
|
|
2326
|
+
False,
|
|
2327
|
+
)
|
|
2328
|
+
joined_aib = " ".join(mock_aib.call_args.kwargs["extra_set"])
|
|
2329
|
+
assert mock_aib.call_args.kwargs["seed_identity_bootstrap"] is False
|
|
2330
|
+
assert "broker.oauth2AuthorizationServer.mode=proxy" in joined_aib
|
|
2331
|
+
assert "resolveAgentIdByClientId(subject_token.azp)" in joined_aib
|
|
2187
2332
|
assert 'client_assertion.azp == "kaos"' in joined_aib
|
|
2333
|
+
assert "broker.security.skipThirdpartyHttpsValidation=true" in joined_aib
|
|
2334
|
+
assert "extProc." not in joined_aib
|
|
2188
2335
|
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
assert "security.agentAuth.identity.provider=oidc" in joined
|
|
2193
|
-
assert "security.userAuth.audience=kaos" in joined
|
|
2194
|
-
assert "security.tokenExchange.enabled=true" in joined
|
|
2195
|
-
assert "security.tokenExchange.aib.adminUrl=" in joined
|
|
2196
|
-
assert "security.tokenExchange.extProc.port=50051" in joined
|
|
2336
|
+
joined_operator = " ".join(captured["operator"])
|
|
2337
|
+
assert "security.tokenExchange.enabled=true" in joined_operator
|
|
2338
|
+
assert "security.tokenExchange.extProc.port=50051" in joined_operator
|
|
2197
2339
|
|
|
2198
2340
|
@pytest.mark.parametrize(
|
|
2199
2341
|
"args,message",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|