|
8 | 8 | import urllib.error |
9 | 9 | import urllib.request |
10 | 10 | from pathlib import Path |
11 | | -from urllib.parse import urlencode |
| 11 | +from urllib.parse import quote, urlencode |
12 | 12 | from urllib.request import Request, urlopen |
13 | 13 |
|
14 | 14 | from z2jh import get_config |
15 | 15 |
|
16 | 16 | log = logging.getLogger(__name__) |
17 | 17 |
|
| 18 | +external_auth_enabled = "__EXTERNAL_AUTH_ENABLED__".lower() == "true" |
| 19 | +external_auth_broker_url = "__EXTERNAL_AUTH_BROKER_URL__".strip() |
| 20 | +external_auth_providers = json.loads(r'''__EXTERNAL_AUTH_PROVIDERS_JSON__''') |
| 21 | +external_auth_env_var_map = json.loads(r'''__EXTERNAL_AUTH_ENV_VAR_MAP_JSON__''') |
| 22 | +external_auth_timeout_seconds = int("__EXTERNAL_AUTH_TIMEOUT_SECONDS__" or "10") |
| 23 | + |
18 | 24 |
|
19 | 25 | # --------------------------------------------------------------------------- |
20 | 26 | # Storage |
@@ -726,6 +732,54 @@ async def _setup_nss_wrapper(spawner, username, groups): |
726 | 732 | log.info("nss-wrapper: postStart lifecycle hook registered for %s", username) |
727 | 733 |
|
728 | 734 |
|
| 735 | +# --------------------------------------------------------------------------- |
| 736 | +# External auth pack integration |
| 737 | +# --------------------------------------------------------------------------- |
| 738 | + |
| 739 | +def _external_auth_provider_token(broker_url, provider, bearer_token): |
| 740 | + """Fetch one provider token from the external-auth broker.""" |
| 741 | + req = Request( |
| 742 | + f"{broker_url.rstrip('/')}/external-auth/{quote(provider)}/token", |
| 743 | + headers={"Authorization": f"Bearer {bearer_token}", "Accept": "application/json"}, |
| 744 | + method="GET", |
| 745 | + ) |
| 746 | + with urlopen(req, timeout=external_auth_timeout_seconds) as resp: |
| 747 | + return json.loads(resp.read().decode("utf-8")) |
| 748 | + |
| 749 | + |
| 750 | +async def _external_auth_pre_spawn_hook(spawner, auth_state): |
| 751 | + """Deliver external-auth provider tokens from configured contract fields.""" |
| 752 | + if not external_auth_enabled or not external_auth_broker_url: |
| 753 | + return |
| 754 | + access_token = (auth_state or {}).get("access_token") |
| 755 | + if not access_token: |
| 756 | + log.debug("external-auth: no access_token for %s; skipping", spawner.user.name) |
| 757 | + return |
| 758 | + |
| 759 | + providers = external_auth_providers or list(external_auth_env_var_map) |
| 760 | + for provider in providers: |
| 761 | + try: |
| 762 | + payload = await asyncio.to_thread( |
| 763 | + _external_auth_provider_token, |
| 764 | + external_auth_broker_url, |
| 765 | + provider, |
| 766 | + access_token, |
| 767 | + ) |
| 768 | + except urllib.error.HTTPError as exc: |
| 769 | + log.debug("external-auth: token request failed provider=%s status=%s", provider, exc.code) |
| 770 | + continue |
| 771 | + except Exception as exc: |
| 772 | + log.debug("external-auth: token request failed provider=%s error=%s", provider, exc) |
| 773 | + continue |
| 774 | + |
| 775 | + token = str(payload.get("access_token", "")).strip() |
| 776 | + if payload.get("status") != "token_valid" or not token: |
| 777 | + continue |
| 778 | + |
| 779 | + env_var = str(external_auth_env_var_map.get(provider) or f"{provider.upper()}_TOKEN").strip() |
| 780 | + spawner.environment = {**spawner.environment, env_var: token} |
| 781 | + |
| 782 | + |
729 | 783 | # --------------------------------------------------------------------------- |
730 | 784 | # Pre-spawn hook orchestrator |
731 | 785 | # --------------------------------------------------------------------------- |
@@ -791,6 +845,12 @@ async def _pre_spawn_hook(spawner): |
791 | 845 | except Exception: |
792 | 846 | log.exception("pre-spawn: NSS wrapper setup FAILED for %s", username) |
793 | 847 |
|
| 848 | + # 5. External auth provider tokens (non-fatal) |
| 849 | + try: |
| 850 | + await _external_auth_pre_spawn_hook(spawner, auth_state) |
| 851 | + except Exception: |
| 852 | + log.exception("pre-spawn: external-auth setup FAILED for %s", username) |
| 853 | + |
794 | 854 | log.info("pre-spawn: hook complete for user %s", username) |
795 | 855 |
|
796 | 856 |
|
|
0 commit comments