Files

131 lines
8.7 KiB
YAML

# Lux IAM provision document — POLICY / DATA (this is Lux's app graph).
#
# The MECHANISM is brand-neutral and lives in the OSS server (hanzoai/iam,
# package internal/provision, exposed as `iam2 provision`). It derives
# client_id (<org>-<app>), redirect URIs and grant types from each app's
# `type`, then idempotently upserts every client through
# POST /v1/iam/admin/applications/upsert. One line per app; the OSS ships ZERO
# brands, Lux owns its own app data HERE.
#
# APPLY IT (deterministic, re-runnable — this is the ONLY way Lux's IAM state
# is changed; never hand-edit applications in a console):
#
# kubectl --context <ctx> diff -k infra/k8s/iam # always, first
# kubectl --context <ctx> apply -k infra/k8s/iam
#
# Re-running is a no-op by construction: the upsert keys on <org>-<app> and a
# request that omits clientSecret PRESERVES the secret IAM already issued, so
# convergence never rotates a live credential. Secrets stay in KMS
# (kms.hanzo.ai, path lux/iam/clients/<org>-<app>) — never in this file.
#
# app `type` → derived shape (authoritative table: internal/provision/provision.go):
# spa public browser client; https://<host>/auth/callback per host
# confidential server-side web app; same redirects + an IAM-issued client_secret
# cli loopback http://127.0.0.1/callback + http://localhost/callback
# desktop custom-scheme deep link <deepLinkScheme>://oauth/<app>
# service machine-to-machine, client_credentials only, no redirects
#
# /auth/callback is the ONE browser callback path, verified against what the
# deployed apps actually send (lux.cloud, zoo.cloud and platform.hanzo.ai all
# drive redirect_uri=https://<host>/auth/callback). It is NOT /api/... — the
# /v1-only rule holds, and the browser callback is unversioned.
#
# client_id is ALWAYS <org>-<app>. HIP-0111.
orgs:
- name: lux
displayName: Lux
homepage: https://lux.network
deepLinkScheme: lux
# The org's ONE human superuser, declared as data so it is provisioned
# deterministically like every client below. There is NO built-in admin:
# the seeded superuser IS the admin, and it belongs to the ORG, never to an
# app. Convention is z@<org primary domain>.
#
# The password is NOT here and never will be. It lives in KMS at
# `kms.hanzo.ai` path `lux/iam/owner`, the same rule the client secrets
# follow, and IAM stores it only as an argon2id/bcrypt hash — never
# plaintext, in this file, in a ConfigMap, or in the database.
#
# Re-running provision is a no-op for an existing owner: it upserts the
# account and its SuperAdmin binding, and a request that omits the password
# PRESERVES the credential already set, so convergence never rotates a live
# login.
owner:
email: [email protected]
displayName: Z
passwordRef: kms://lux/iam/owner
apps:
- { app: wallet, type: spa, hosts: [lux.network, wallet.lux.network, bridge.lux.network] } # clientId lux-wallet (was Casdoor app `app-lux`)
- { app: explore, type: spa, hosts: [explore.lux.network] } # clientId lux-explore; the live shared explorer also answers brand-themed *.lux.network and cross-brand domains — preserved via idempotent reconcile, declared per-brand when decomplected
- { app: vote, type: spa, hosts: [lux.vote, www.lux.vote] } # clientId lux-vote — luxfi/vote (vote2). spa/PKCE, never confidential (see note below)
# lux.cloud is the LIVE console (observed: client_id=lux-cloud,
# redirect_uri=https://lux.cloud/auth/callback). cloud.lux.network is the
# older name, kept registered so it does not break if still linked.
# `spa`, NOT confidential: the page does the code->token exchange IN THE
# BROWSER with PKCE (the authorize request carries code_challenge). A
# browser cannot hold a secret, and IAM demands client auth whenever one
# is stored — registering it confidential is what made every login die on
# `invalid_client` at the callback.
# nodes.lux.cloud is the same Lux console on another hostname (the
# bootnode web app white-labels by host), so it is a HOST of `cloud`, not
# a second client: one console, one audience, one registration. It shipped
# the literal string `lux-app-client-id` — a client that has never existed
# — so every sign-in died on "organization, username and password are
# required" before a password was ever checked.
# console.lux.cloud and cloud.lux.cloud are the same console on two more
# hostnames (bootnode's web app white-labels by host), so they are HOSTS of
# `cloud`, exactly as nodes.lux.cloud is. The page sends client_id=lux-cloud
# with PKCE from every one of them; until console.lux.cloud was listed here
# lux.id answered its /oauth/authorize with "invalid redirect_uri" and NO
# ONE COULD SIGN IN TO THE CONSOLE AT ALL — which is why nothing was
# watching the metrics/traces/logs that already flow to it.
# https://lux.id/callback is on the live registration and is not derived
# from these hosts. The upsert REPLACES the URI list, so it is declared
# here to keep a converge from deleting it; dropping it is its own change.
- { app: cloud, type: spa, hosts: [lux.cloud, www.lux.cloud, cloud.lux.network, nodes.lux.cloud, console.lux.cloud, cloud.lux.cloud],
redirects: ["https://lux.id/callback"] }
# Its own client, not a host of `cloud`: the live api.lux.cloud backend
# already lists lux-console as a distinct audience
# (GATEWAY_ALLOWED_AUDIENCES=lux-cloud,lux-console,lux-id,…).
- { app: console, type: confidential, hosts: [console.lux.cloud] }
# The MPC custody dashboard. mpc.lux.cloud was the one host missing from a
# registration that already carried the other four, so `lux-mpc` answered
# /v1/iam/oauth/authorize with 400 invalid_redirect_uri there while
# mpc.lux.network 302'd to the login UI. Declaring the host set here is
# what stops that class of drift: the URIs are derived, never hand-kept.
# The cross-brand hosts are the live registration's, preserved verbatim —
# they are declared per-brand when the dashboard is decomplected, exactly
# as `explore` above. localhost is the dev loopback the dashboard uses.
# `spa` deliberately drops the two grants the live client had accumulated:
# `token` (implicit — no deployed page can reach it, they all navigate to
# /oauth/authorize, which IAM does not route) and `client_credentials`
# (unusable — this client holds no secret, so it can never authenticate).
- { app: mpc, type: spa, hosts: [mpc.lux.cloud, mpc.lux.network, mpc.pars.network, mpc.zoo.network, mpc.hanzo.ai],
redirects: ["http://localhost:3000/auth/callback", "http://localhost:3001/auth/callback"] }
- { app: build, type: spa, hosts: [lux.build] }
- { app: billing, type: spa, hosts: [billing.lux.network] }
- { app: id, type: spa, hosts: [lux.id, id.lux.network] }
- { app: market, type: spa, hosts: [lux.market] }
- { app: exchange, type: spa, hosts: [lux.exchange, www.lux.exchange] }
# lux.vote. The app is `dao` and the host is `vote` — one bundle serves
# lux.vote, zoo.vote and pars.vote, resolving issuer and client from the
# hostname, so the app is named for what it is rather than for one of the
# three places it answers. The other two are declared in their own org's
# universe, which is why only lux-dao appears here.
- { app: dao, type: spa, hosts: [lux.vote] }
# Lux Financial's browser surfaces: the customer app and the admin console,
# one public PKCE client. lux.financial itself is static files with no
# sign-in. `lux-bank` is bankd's service identity and never a browser's.
# Tokens live an hour and refresh for a day: these consoles move money.
- { app: financial, type: spa, hosts: [app.lux.financial, admin.lux.financial], expireInHours: 1, refreshExpireInHours: 24 }
- { app: loan, type: spa, hosts: [lux.loan] }
- { app: finance, type: spa, hosts: [lux.finance] }
# Lux's CI identity (client_id lux-ci): client_credentials only. A KMS login
# with it yields a token whose org is lux, so `bin/kms HANZO_DEPLOY_TOKEN`
# reads /orgs/lux and a site publishes under lux/<slug>. Repos hold it as
# the repo-level KMS_CLIENT_ID/KMS_CLIENT_SECRET, which override the lux
# forge org's secrets. IAM mints the secret on the first upsert and a
# re-run preserves it.
- { app: ci, type: service }