#!/usr/bin/env python3 """Bind the installed programmatic OAuth client for prod server-to-server jobs. Prod jobs that authenticate server-to-server cannot use the browser client (`nexuscrm-ui`): the target admits it on the password grant only for requests carrying browser session-control evidence, so such a login is refused with an opaque HTTP 400. This resolves the programmatic Connected App that ships in the final merged pack, validates it is actually usable for a password grant, then appends the binding to $GITHUB_ENV. The mask directive goes to stdout so the runner registers it as a workflow command; the credentials themselves are written only to $GITHUB_ENV, never to the step log. """ from __future__ import annotations import json import os import sys from pathlib import Path from typing import Any, Mapping BROWSER_SESSION_CLIENT_ID = "nexuscrm-ui" CLIENT_FILE = Path( "packs/NOT-EDIT-final-merged-packs/connected_apps/salesforce_compat_client.json" ) def resolve_client(path: Path) -> tuple[str, str]: if not path.is_file(): raise SystemExit(f"installed Connected App metadata is missing: {path}") try: client = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise SystemExit(f"installed Connected App metadata is not valid JSON: {exc}") from exc return validate_client(client) def validate_client(client: Mapping[str, Any]) -> tuple[str, str]: """Validate one programmatic Connected App for password-grant consumers.""" if client.get("sessionIssuerClass") != "salesforce_compat": raise SystemExit( "Connected App is not a Salesforce-compatible session issuer: " f"sessionIssuerClass={client.get('sessionIssuerClass')!r}" ) if client.get("isActive") is not True: raise SystemExit("Connected App is not active") grants = client.get("allowedGrantTypes") if not isinstance(grants, list) or "password" not in grants: raise SystemExit(f"Connected App does not allow the password grant: {grants!r}") client_id = str(client.get("consumerKey", "")).strip() client_secret = str(client.get("consumerSecret", "")).strip() if not client_id or not client_secret: raise SystemExit("Connected App is missing consumerKey or consumerSecret") if client_id == BROWSER_SESSION_CLIENT_ID: raise SystemExit( f"Connected App consumerKey is {BROWSER_SESSION_CLIENT_ID!r}, the browser " "client that browser session-control admission refuses" ) # A line break would let a crafted value forge extra GITHUB_ENV assignments. if any(ch in value for value in (client_id, client_secret) for ch in "\r\n"): raise SystemExit("Connected App credentials contain a line break") return client_id, client_secret def main() -> int: client_id, client_secret = resolve_client(CLIENT_FILE) github_env = os.environ.get("GITHUB_ENV", "") if not github_env: raise SystemExit("GITHUB_ENV is not set; this script binds a workflow environment") # Mask first: any later step that echoes the value is redacted from the log. print(f"::add-mask::{client_secret}", flush=True) with open(github_env, "a", encoding="utf-8") as handle: handle.write(f"NEXUSCRM_OAUTH_CLIENT_ID={client_id}\n") handle.write(f"NEXUSCRM_OAUTH_CLIENT_SECRET={client_secret}\n") print(f"bound programmatic OAuth client (client_id length {len(client_id)})") return 0 if __name__ == "__main__": raise SystemExit(main())