Merge pull request #17 from Coffey-Labs/feat/local-login-compose-and-docs
Make local login reachable, and write down how it works
This commit is contained in:
@@ -111,6 +111,25 @@ cluster. Override per invocation:
|
|||||||
COMPOSE_PROFILES=enterprise docker compose up
|
COMPOSE_PROFILES=enterprise docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Signing in
|
||||||
|
|
||||||
|
A plain `docker compose up` has **no authentication** — every runbook in
|
||||||
|
`docs/` verifies the pipeline with bare `curl` against `/query`, and those
|
||||||
|
steps depend on that. For a login screen without an identity provider behind
|
||||||
|
it, add the local-login overlay:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.local-auth.yml up -d --build
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.local-auth.yml run --rm api -seed-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
The second command prints a generated password once. Full detail, including
|
||||||
|
what each setting does and how each one fails on its own, is in
|
||||||
|
[`docs/local-login.md`](docs/local-login.md).
|
||||||
|
|
||||||
|
SSO (OIDC/SAML) is the other option and takes precedence over local login
|
||||||
|
where both are configured — see [`docs/phase-4-runbook.md`](docs/phase-4-runbook.md).
|
||||||
|
|
||||||
Kubernetes deployment via the Helm chart in [`deploy/`](deploy/README.md).
|
Kubernetes deployment via the Helm chart in [`deploy/`](deploy/README.md).
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Turns on local username/password login, which docker-compose.yml
|
||||||
|
# deliberately leaves off: a plain `docker compose up` has no
|
||||||
|
# authentication at all, and the Phase 0-3 runbooks' bare curls against
|
||||||
|
# /query depend on that staying true.
|
||||||
|
#
|
||||||
|
# Usage -- both commands need both -f flags:
|
||||||
|
#
|
||||||
|
# docker compose -f docker-compose.yml -f docker-compose.local-auth.yml up -d --build
|
||||||
|
# docker compose -f docker-compose.yml -f docker-compose.local-auth.yml run --rm api -seed-admin
|
||||||
|
#
|
||||||
|
# The second prints a generated password once. See /docs/local-login.md.
|
||||||
|
#
|
||||||
|
# The four settings below have to agree with each other, and three of
|
||||||
|
# the four are not obviously about login at all:
|
||||||
|
#
|
||||||
|
# LOCAL_AUTH_ENABLED registers /auth/* on api, and makes both
|
||||||
|
# api and alerting swap WithCORS for
|
||||||
|
# WithCredentialedCORS
|
||||||
|
# CORS_ALLOWED_ORIGIN must be a literal origin. The default is
|
||||||
|
# "*", and a browser categorically refuses
|
||||||
|
# to combine a credentialed fetch with a
|
||||||
|
# wildcard origin -- so leaving the default
|
||||||
|
# in place fails every request the moment
|
||||||
|
# the cookie starts being sent
|
||||||
|
# LOCAL_AUTH_COOKIE_SECURE the session cookie is Secure by default
|
||||||
|
# and would not be stored over
|
||||||
|
# http://localhost. Set it back to true for
|
||||||
|
# anything served over HTTPS
|
||||||
|
# VITE_LOCAL_AUTH_ENABLED a BUILD arg, so this needs --build, not a
|
||||||
|
# restart. Without it the bundle never
|
||||||
|
# sends credentials: 'include' and no
|
||||||
|
# request ever carries the session
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
environment:
|
||||||
|
LOCAL_AUTH_ENABLED: "true"
|
||||||
|
LOCAL_AUTH_COOKIE_SECURE: "false"
|
||||||
|
CORS_ALLOWED_ORIGIN: "http://localhost:3000"
|
||||||
|
|
||||||
|
alerting:
|
||||||
|
environment:
|
||||||
|
LOCAL_AUTH_ENABLED: "true"
|
||||||
|
CORS_ALLOWED_ORIGIN: "http://localhost:3000"
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
args:
|
||||||
|
VITE_LOCAL_AUTH_ENABLED: "true"
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Local login
|
||||||
|
|
||||||
|
Username and password authentication, served by `api` itself against the
|
||||||
|
control-plane Postgres. It is the alternative to Phase 4's SSO
|
||||||
|
(`enterprise-auth`, OIDC/SAML) for a deployment that wants a login screen
|
||||||
|
without an identity provider behind it.
|
||||||
|
|
||||||
|
The two are mutually exclusive by construction. `cmd/api/main.go` picks
|
||||||
|
one authorizer: `ENTERPRISE_AUTH_URL` wins if it is set, and only
|
||||||
|
otherwise does `LOCAL_AUTH_ENABLED` take effect. With SSO configured,
|
||||||
|
`/auth/*` is never registered and local login does not exist.
|
||||||
|
|
||||||
|
## Off by default, and why
|
||||||
|
|
||||||
|
A plain `docker compose up` has no authentication at all. That is
|
||||||
|
deliberate rather than an oversight: every Phase 0-3 runbook verifies the
|
||||||
|
pipeline with bare `curl` against `/query`, and those steps only work
|
||||||
|
against an unauthenticated API. Turning login on globally would break the
|
||||||
|
project's own documented verification procedure.
|
||||||
|
|
||||||
|
So it is an opt-in overlay:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.local-auth.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
`--build` is not optional. One of the four settings is a Vite build arg
|
||||||
|
baked into the static bundle, so a restart alone leaves the frontend
|
||||||
|
disagreeing with the backend.
|
||||||
|
|
||||||
|
## Creating the first account
|
||||||
|
|
||||||
|
There is no sign-up. The first account is an operator action:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.local-auth.yml \
|
||||||
|
run --rm api -seed-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
created default admin user:
|
||||||
|
username: admin
|
||||||
|
password: <generated>
|
||||||
|
this password will not be shown again -- save it now.
|
||||||
|
```
|
||||||
|
|
||||||
|
It creates `admin` as an **owner** with a random password, prints it once,
|
||||||
|
and stores only a bcrypt hash. Losing it means resetting it, not
|
||||||
|
recovering it. The command is idempotent: if the `admin` account already
|
||||||
|
exists it says so and does nothing.
|
||||||
|
|
||||||
|
Then sign in at <http://localhost:3000/login>. Change the password from
|
||||||
|
the account page.
|
||||||
|
|
||||||
|
## The four settings, and why they must agree
|
||||||
|
|
||||||
|
| Setting | Service | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| `LOCAL_AUTH_ENABLED` | `api`, `alerting` | Registers `/auth/*`, and swaps `WithCORS` for `WithCredentialedCORS` |
|
||||||
|
| `CORS_ALLOWED_ORIGIN` | `api`, `alerting` | Must be a literal origin — the default `*` is refused by browsers for credentialed requests |
|
||||||
|
| `LOCAL_AUTH_COOKIE_SECURE` | `api` | Defaults to `true`; the cookie is not stored over plain `http://localhost` unless this is `false` |
|
||||||
|
| `VITE_LOCAL_AUTH_ENABLED` | `web` (build arg) | Without it the bundle never sends `credentials: 'include'` |
|
||||||
|
|
||||||
|
Only the first is obviously about login, and every one of them fails
|
||||||
|
differently:
|
||||||
|
|
||||||
|
- `LOCAL_AUTH_ENABLED` unset — the login page posts to `/auth/login` and
|
||||||
|
gets a **404**. The route is not registered at all, deliberately, so a
|
||||||
|
deployment with the feature off answers as though it does not exist
|
||||||
|
rather than advertising a disabled feature.
|
||||||
|
- `CORS_ALLOWED_ORIGIN` left at `*` — the browser refuses the request
|
||||||
|
before it is sent, and the page reports a network failure rather than
|
||||||
|
an HTTP status.
|
||||||
|
- `LOCAL_AUTH_COOKIE_SECURE` left at `true` over HTTP — login returns
|
||||||
|
`200` and appears to work, then every subsequent request is
|
||||||
|
unauthenticated, because the cookie was never stored.
|
||||||
|
- `VITE_LOCAL_AUTH_ENABLED` unset — same symptom as the previous one, and
|
||||||
|
from a different cause: the cookie exists but is never attached.
|
||||||
|
|
||||||
|
Set `LOCAL_AUTH_COOKIE_SECURE` back to `true` for anything served over
|
||||||
|
HTTPS, and set `CORS_ALLOWED_ORIGIN` to the real web origin. The values in
|
||||||
|
the overlay assume `http://localhost:3000`.
|
||||||
|
|
||||||
|
## Roles
|
||||||
|
|
||||||
|
`owner`, `admin`, `editor`, `viewer`, from `api/authz`. Owners manage
|
||||||
|
everyone; admins may create and delete `viewer`/`editor` accounts only,
|
||||||
|
and cannot reset an owner's password. Two guards prevent a deployment
|
||||||
|
from becoming unadministrable: the last owner can be neither deleted nor
|
||||||
|
demoted, and no account can delete itself while signed in.
|
||||||
|
|
||||||
|
## Not covered by the Helm chart
|
||||||
|
|
||||||
|
`deploy/helm/cairnobs` has no local-login support — it sets none of these
|
||||||
|
variables. A Kubernetes deployment authenticates via `enterprise-auth`
|
||||||
|
SSO or not at all. This is a gap, not a decision.
|
||||||
Reference in New Issue
Block a user