feat(api): publish validated OpenAPI contract
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Admin API authentication
|
||||
|
||||
Implemented for `GET /api/admin/whoami` and the read-only [suggestions API](admin-suggestions-api.md). The shared guard is `apps/web/src/lib/auth/admin-api-auth.ts`. This is not browser token login, a general admin mutation API, or the Velocity admission credential mechanism. Browser pages, privileged server actions, RCON, and Velocity authentication are unchanged. OpenAPI publication is separate work (US-026).
|
||||
Implemented for `GET /api/admin/whoami` and the read-only [suggestions API](admin-suggestions-api.md). The shared guard is `apps/web/src/lib/auth/admin-api-auth.ts`. This is not browser token login, a general admin mutation API, or the Velocity admission credential mechanism. Browser pages, privileged server actions, RCON, and Velocity authentication are unchanged. The canonical [OpenAPI 3.1 contract](../openapi.yaml) is public at `/openapi.yaml`; see [contract maintenance](openapi.md).
|
||||
|
||||
## Credential selection
|
||||
|
||||
@@ -23,6 +23,74 @@ One bounded, process-local remote JWKS resolver caches keys for ten minutes, coa
|
||||
|
||||
`KEYCLOAK_CLIENT_SECRET` is not needed for machine verification. This implementation does not obtain tokens or alter identity-provider clients, role/audience mappers, credentials, or deployments. See [OIDC setup](admin-oidc-keycloak-setup.md) for the distinct browser configuration.
|
||||
|
||||
## Obtain and use a machine token safely
|
||||
|
||||
The Keycloak token endpoint is `<KEYCLOAK_ISSUER_URL-without-final-slash>/protocol/openid-connect/token`. Use `grant_type=client_credentials` with a separately provisioned confidential service-account client. Its issued access token must include the **portal audience** and the **portal client's administrator role** described above; the machine client's own ID or `azp` is not a substitute. Client provisioning, credential retrieval and role/audience changes require separate operational approval. Production uses issuer `https://auth.20faces.games/realms/infra`, token endpoint `https://auth.20faces.games/realms/infra/protocol/openid-connect/token`, and portal `https://portal.somc.club`. Confirm these against current approved configuration before use. The token endpoint is owned by Keycloak, not a portal route.
|
||||
|
||||
No helper is installed. This optional, one-shot Python 3 standard-library example prompts on the controlling terminal, keeps the client secret and access token in process memory, and prints only the HTTP status of whoami. It does not save or print the token or identity response. Use only on an approved trusted workstation. Do not enable shell tracing, HTTP debug logging, terminal recording, or request-body/header capture. Never paste a secret into a command, `curl -d`, an Authorization argument, environment export, chat, or a log. For automation, use an approved secret-manager/protected-file input and pass credentials directly to an HTTP library in memory rather than command arguments.
|
||||
|
||||
```sh
|
||||
python3 - <<'PY'
|
||||
import getpass
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
return None
|
||||
|
||||
# HTTPS certificate verification remains enabled; never follow credential redirects.
|
||||
def https_url(value):
|
||||
value = value.strip().rstrip("/")
|
||||
url = urllib.parse.urlsplit(value)
|
||||
if (url.scheme != "https" or not url.hostname or url.username is not None
|
||||
or url.password is not None or url.query or url.fragment):
|
||||
raise ValueError("An approved HTTPS URL is required")
|
||||
return value
|
||||
|
||||
try:
|
||||
with open("/dev/tty", "r") as terminal:
|
||||
def prompt(label):
|
||||
print(label, end="", flush=True)
|
||||
return terminal.readline().strip()
|
||||
issuer = https_url(prompt("Approved Keycloak issuer URL: "))
|
||||
portal = https_url(prompt("Approved portal URL: "))
|
||||
client_id = prompt("Machine client ID: ")
|
||||
client_secret = getpass.getpass("Machine client secret: ")
|
||||
opener = urllib.request.build_opener(NoRedirect)
|
||||
form = urllib.parse.urlencode({
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
}).encode()
|
||||
token_request = urllib.request.Request(
|
||||
issuer + "/protocol/openid-connect/token", data=form,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
with opener.open(token_request, timeout=10) as response:
|
||||
access_token = json.load(response)["access_token"]
|
||||
identity_request = urllib.request.Request(
|
||||
portal + "/api/admin/whoami",
|
||||
headers={"Authorization": "Bearer " + access_token},
|
||||
)
|
||||
with opener.open(identity_request, timeout=10) as response:
|
||||
print("whoami HTTP", response.status)
|
||||
# To read suggestions, use the same in-memory header with /api/suggestions.
|
||||
# Process exit releases memory; this is not a secure-memory erasure guarantee.
|
||||
except urllib.error.HTTPError as error:
|
||||
print("Request failed; HTTP", error.code, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception:
|
||||
print("Request failed; verify configuration and connectivity securely.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
```
|
||||
|
||||
Token acquisition is not part of this application's implementation or offline tests. The example performs real network requests only when an operator explicitly runs it; it is not run by the source checks. A 401/403/503 from whoami has the semantics below. Do not print upstream error bodies while diagnosing issuance failures. Token requests can appear in identity-provider access logs: confirm that request bodies and Authorization headers are redacted before use.
|
||||
|
||||
## Safe identity endpoint
|
||||
|
||||
`GET /api/admin/whoami` authenticates and checks administrator permission before returning JSON with `Cache-Control: no-store`:
|
||||
|
||||
Reference in New Issue
Block a user