feat(opnform)!: add admin and OIDC bootstrap, rename role to lowercase
Rename roles/OpnForm → roles/opnform so the role resolves as
digitalboard.core.opnform (Ansible collection convention is
lowercase). Update tests/test.yml reference accordingly.
Add automated admin user creation via POST /api/register, gated on
opnform_admin_email + opnform_admin_password. Idempotent through a
prior login probe. Without these vars the manual setup page flow is
preserved.
Add automated OIDC IdentityConnection setup via the per-workspace
/api/open/workspaces/{id}/oidc-connections endpoint, gated on
opnform_oidc_enabled. Hard-coupled to the admin bootstrap (the API
requires an authenticated admin token); validation block fails fast
if OIDC is enabled without admin credentials. Supports both an
explicit opnform_oidc_group_role_mappings list and a fallback
opnform_oidc_admin_group convenience var.
Convert opnform_oidc_scopes from space-separated string to YAML list
to match OpnForm's API expectation. Rewrite README "First login" and
"OIDC setup" sections to reflect that self-hosted OpnForm does not
ship a pre-seeded admin and to document the new bootstrap paths.
BREAKING CHANGE: opnform_oidc_scopes changed from space-separated
string to YAML list. Inventories that override it must update from
"openid profile email" to [openid, profile, email].
This commit is contained in:
parent
3f90843f97
commit
2341815daf
11 changed files with 366 additions and 145 deletions
169
roles/opnform/README.md
Normal file
169
roles/opnform/README.md
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# opnform
|
||||
|
||||
Deploy [OpnForm](https://github.com/OpnForm/OpnForm) as a self-contained
|
||||
Docker Compose stack behind Traefik.
|
||||
|
||||
## What this role does
|
||||
|
||||
- Deploys the full official OpnForm stack: `api`, `api-worker`, `api-scheduler`,
|
||||
`ui`, `db` (Postgres), `redis`, and `ingress` (nginx)
|
||||
- Configures all environment variables for self-hosted production use
|
||||
- Integrates the ingress container with an existing Traefik proxy network
|
||||
- Waits for the API container to become healthy before returning
|
||||
|
||||
## What this role does NOT do (stage 1)
|
||||
|
||||
- Does not pre-configure OIDC / identity_connections — set up via Admin UI
|
||||
|
||||
## Architecture note: why two reverse proxies?
|
||||
|
||||
```
|
||||
Browser → Traefik (TLS, host routing) → ingress-nginx → api (PHP-FPM) / ui (Nuxt)
|
||||
```
|
||||
|
||||
The `ingress` container looks like a redundant proxy next to Traefik but
|
||||
does a different job. OpnForm's `api` image is **PHP-FPM only** — it
|
||||
speaks the FastCGI protocol on port 9000, not HTTP. Traefik cannot
|
||||
translate FastCGI, so the ingress nginx is required to:
|
||||
|
||||
- Translate HTTP `/api/*` requests into FastCGI calls to `api:9000`
|
||||
- Rewrite request URIs via the `$api_uri` map
|
||||
- Set Laravel-specific FastCGI params (`SCRIPT_FILENAME`, `REQUEST_URI`)
|
||||
- Reverse-proxy `/` to the Nuxt UI container on port 3000
|
||||
|
||||
Both containers run on the same Docker network on the same host, so the
|
||||
performance overhead of the extra hop is negligible (in-kernel memory
|
||||
copy, not a real network round-trip). Removing the ingress would require
|
||||
a custom OpnForm image with a built-in HTTP server, which is out of
|
||||
scope for this role.
|
||||
|
||||
## Required variables
|
||||
|
||||
Provide via OpenBao, Ansible Vault, or extra-vars. **Never commit real
|
||||
secrets to version control.**
|
||||
|
||||
| Variable | Format | Generate with |
|
||||
|---|---|---|
|
||||
| `opnform_app_key` | `base64:<32 bytes base64>` | `echo "base64:$(openssl rand -base64 32)"` |
|
||||
| `opnform_jwt_secret` | 32 bytes base64 | `openssl rand -base64 32` |
|
||||
| `opnform_front_api_secret` | 32 bytes base64 | `openssl rand -base64 32` |
|
||||
| `opnform_db_password` | strong password | `openssl rand -base64 24` |
|
||||
|
||||
When `opnform_oidc_enabled` is `true`:
|
||||
|
||||
| Variable | Source |
|
||||
|---|---|
|
||||
| `opnform_oidc_client_secret` | from your Keycloak/Authentik client |
|
||||
|
||||
The `assert` task at the top of the role will fail fast if any secret is
|
||||
missing or malformed.
|
||||
|
||||
## First login
|
||||
|
||||
OpnForm in self-hosted mode does **not** ship a pre-seeded admin user.
|
||||
The first user to register becomes the owner of the default workspace,
|
||||
and further public registration is disabled afterwards (additional
|
||||
users must be invited via the Admin UI).
|
||||
|
||||
This role supports two ways to create that first user:
|
||||
|
||||
### Option A — automated bootstrap (recommended)
|
||||
|
||||
Set `opnform_admin_email` and `opnform_admin_password` (ideally from
|
||||
Vault / OpenBao). The role then POSTs to `/api/register` after the
|
||||
API container is healthy, skipping the setup page entirely. The task
|
||||
is idempotent: it does a login check first and only registers if the
|
||||
user does not already exist.
|
||||
|
||||
```yaml
|
||||
opnform_admin_name: "Administrator" # default
|
||||
opnform_admin_email: "admin@example.com"
|
||||
opnform_admin_password: "{{ vault_opnform_admin_password }}"
|
||||
```
|
||||
|
||||
Password rules enforced by OpnForm: minimum 8 characters, at least one
|
||||
letter, one digit, and one of `@$!%*#?&-_+=.,:;<>^()[]{}|~`.
|
||||
|
||||
### Option B — manual setup page
|
||||
|
||||
Leave `opnform_admin_email` / `opnform_admin_password` empty. Visit
|
||||
`opnform_base_url` and complete the setup page in the browser.
|
||||
|
||||
## OIDC setup
|
||||
|
||||
Set `opnform_oidc_enabled: true` and the role creates an
|
||||
IdentityConnection on the admin's default workspace via
|
||||
`POST /api/open/workspaces/{id}/oidc-connections`. OpnForm enforces a
|
||||
single OIDC connection per workspace, so the task is idempotent (GETs
|
||||
existing connections first and skips if any exist).
|
||||
|
||||
**Prerequisite**: the admin bootstrap must be configured
|
||||
(`opnform_admin_email` + `opnform_admin_password`). The OIDC API
|
||||
requires an authenticated admin token; the role logs in with those
|
||||
credentials to make the call. The validation block fails fast if OIDC
|
||||
is enabled without admin credentials.
|
||||
|
||||
### Required when `opnform_oidc_enabled: true`
|
||||
|
||||
| Variable | Notes |
|
||||
|---|---|
|
||||
| `opnform_oidc_client_secret` | from your IdP, never commit |
|
||||
| `opnform_oidc_domain` | email domain that triggers OIDC (e.g. `example.com`) |
|
||||
|
||||
### Tunables (defaults shown)
|
||||
|
||||
```yaml
|
||||
opnform_oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard"
|
||||
opnform_oidc_client_id: "opnform-digitalboard"
|
||||
opnform_oidc_client_name: "Digitalboard" # display name in UI
|
||||
opnform_oidc_slug: "oidc" # used in /auth/{slug}/callback
|
||||
opnform_oidc_scopes: [openid, profile, email, groups]
|
||||
```
|
||||
|
||||
### Group → role mapping
|
||||
|
||||
Two ways, the list takes precedence:
|
||||
|
||||
```yaml
|
||||
# Option 1: full list (any number of mappings)
|
||||
opnform_oidc_group_role_mappings:
|
||||
- idp_group: "opnform-admins"
|
||||
role: admin
|
||||
- idp_group: "opnform-editors"
|
||||
role: editor
|
||||
|
||||
# Option 2: convenience — single admin group
|
||||
opnform_oidc_admin_group: "opnform-admins" # mapped to role=admin
|
||||
```
|
||||
|
||||
Valid roles: `owner`, `admin`, `editor`, `member`.
|
||||
|
||||
## Example playbook
|
||||
|
||||
```yaml
|
||||
- name: Deploy OpnForm service
|
||||
hosts: opnform_servers
|
||||
become: true
|
||||
roles:
|
||||
- digitalboard.core.opnform
|
||||
```
|
||||
|
||||
With inventory variables:
|
||||
|
||||
```yaml
|
||||
# group_vars/opnform_servers.yml
|
||||
opnform_domain: forms.digitalboard.ch
|
||||
opnform_base_url: "https://forms.digitalboard.ch"
|
||||
opnform_app_key: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
||||
'digitalboard/opnform',
|
||||
mount_point='kv').data.data.app_key }}"
|
||||
opnform_jwt_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
||||
'digitalboard/opnform',
|
||||
mount_point='kv').data.data.jwt_secret }}"
|
||||
opnform_front_api_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
||||
'digitalboard/opnform',
|
||||
mount_point='kv').data.data.front_api_secret }}"
|
||||
opnform_db_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
||||
'digitalboard/opnform',
|
||||
mount_point='kv').data.data.db_password }}"
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue