feat(services): refine split-horizon OIDC routing and harden nextcloud patch
- authentik: address the rewrite service by compose service name instead of a network alias on the public FQDN, which shadowed extra_hosts pins and broke OIDC discovery for c-ares-based (Node) resolvers - homarr: add homarr_extra_hosts to pin the IdP FQDN to a LAN IP so OIDC discovery stays in-network while the issuer matches the browser-facing URL - opnform: add opnform_oidc_sso_redirect_root to 302 the root URL to the SSO path (deep-links untouched, /login?bypass=1 break-glass); restart ingress via container restart so envsubst re-renders nginx.conf - nextcloud: make the UserConfig sed workaround fail loud on upstream drift instead of silently skipping (nextcloud/server#59629) - gitignore: exclude the local .ansible/ collection cache
This commit is contained in:
parent
3236ca332f
commit
3ace667b6c
12 changed files with 264 additions and 49 deletions
|
|
@ -75,22 +75,97 @@
|
|||
src: nginx.conf.j2
|
||||
dest: "{{ opnform_docker_compose_dir }}/nginx.conf"
|
||||
mode: '0644'
|
||||
notify: restart opnform
|
||||
notify: restart opnform ingress
|
||||
|
||||
# OIDC_FORCE_LOGIN disables OpnForm's password login — including the
|
||||
# password-based admin/OIDC bootstrap this role performs below. So the
|
||||
# first compose render always keeps force-login OFF; it is switched on
|
||||
# only after the bootstrap completes (see step 7). This keeps a first
|
||||
# deploy on a fresh host working even when opnform_oidc_force_login=true.
|
||||
- name: Render compose with force-login disabled during bootstrap
|
||||
# password-based admin/OIDC bootstrap this role performs below. The
|
||||
# bootstrap must therefore run with force-login OFF. To stay idempotent
|
||||
# on re-runs (avoid recreating api containers on every apply), we only
|
||||
# turn force-login OFF when the bootstrap is actually needed (first run
|
||||
# on a fresh host, no OIDC connection yet). Once the connection exists
|
||||
# we render the final force-login value straight away, so the compose
|
||||
# file is byte-identical across re-runs.
|
||||
- name: Probe whether OpnForm is already bootstrapped
|
||||
block:
|
||||
- name: Check if opnform-api container exists and is healthy
|
||||
ansible.builtin.command:
|
||||
cmd: docker inspect --format='{% raw %}{{.State.Health.Status}}{% endraw %}' opnform-api
|
||||
register: _opnform_api_health_probe
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Attempt admin login (only when api is healthy)
|
||||
ansible.builtin.uri:
|
||||
url: "https://127.0.0.1/api/login"
|
||||
method: POST
|
||||
headers:
|
||||
Host: "{{ opnform_domain }}"
|
||||
body_format: json
|
||||
body:
|
||||
email: "{{ opnform_admin_email }}"
|
||||
password: "{{ opnform_admin_password }}"
|
||||
status_code: [200, 401, 422]
|
||||
validate_certs: false
|
||||
register: _opnform_probe_login
|
||||
no_log: true
|
||||
when:
|
||||
- _opnform_api_health_probe.rc == 0
|
||||
- _opnform_api_health_probe.stdout == "healthy"
|
||||
- opnform_admin_email | length > 0
|
||||
- opnform_admin_password | length > 0
|
||||
|
||||
- name: Probe for existing OIDC connection
|
||||
ansible.builtin.uri:
|
||||
url: "https://127.0.0.1/api/open/workspaces"
|
||||
method: GET
|
||||
headers:
|
||||
Host: "{{ opnform_domain }}"
|
||||
Authorization: "Bearer {{ _opnform_probe_login.json.token }}"
|
||||
status_code: 200
|
||||
validate_certs: false
|
||||
register: _opnform_probe_workspaces
|
||||
no_log: true
|
||||
when:
|
||||
- opnform_oidc_enabled | bool
|
||||
- _opnform_probe_login is defined
|
||||
- _opnform_probe_login.status | default(0) == 200
|
||||
|
||||
- name: Probe OIDC connections on default workspace
|
||||
ansible.builtin.uri:
|
||||
url: "https://127.0.0.1/api/open/workspaces/{{ _opnform_probe_workspaces.json[0].id }}/oidc-connections"
|
||||
method: GET
|
||||
headers:
|
||||
Host: "{{ opnform_domain }}"
|
||||
Authorization: "Bearer {{ _opnform_probe_login.json.token }}"
|
||||
status_code: 200
|
||||
validate_certs: false
|
||||
register: _opnform_probe_oidc
|
||||
no_log: true
|
||||
when:
|
||||
- opnform_oidc_enabled | bool
|
||||
- _opnform_probe_workspaces is defined
|
||||
- _opnform_probe_workspaces.json | default([]) | length > 0
|
||||
|
||||
- name: Decide whether force-login can render in its final state
|
||||
ansible.builtin.set_fact:
|
||||
_opnform_force_login_effective: false
|
||||
# True when force-login is desired AND admin+OIDC bootstrap has
|
||||
# already completed (admin user exists with the configured password,
|
||||
# OIDC connection is present). On a fresh host both checks fail and
|
||||
# we fall back to false so the bootstrap below can run.
|
||||
_opnform_force_login_effective: >-
|
||||
{{
|
||||
(opnform_oidc_enabled | bool)
|
||||
and (opnform_oidc_force_login | bool)
|
||||
and (_opnform_probe_login.status | default(0) == 200)
|
||||
and ((_opnform_probe_oidc.json | default([])) | length > 0)
|
||||
}}
|
||||
|
||||
- name: Deploy docker-compose file
|
||||
ansible.builtin.template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ opnform_docker_compose_dir }}/docker-compose.yml"
|
||||
mode: '0644'
|
||||
register: _opnform_compose_rendered
|
||||
notify: restart opnform
|
||||
|
||||
# =====================================================================
|
||||
|
|
@ -123,6 +198,12 @@
|
|||
# Skips the self-hosted setup page by registering the first user via
|
||||
# OpnForm's /api/register endpoint. Idempotent: a successful login
|
||||
# attempt with the same credentials means the user already exists.
|
||||
#
|
||||
# Skipped entirely when force-login already rendered in its final state
|
||||
# (probe in step 2 confirmed admin + connection exist). Re-running the
|
||||
# /api/login probe on a force-login-enabled api would 401 and 422, so
|
||||
# avoid the noise — and avoid spurious "changed" status from a register
|
||||
# call that won't help anyway.
|
||||
|
||||
- name: Check if OpnForm admin user already exists
|
||||
ansible.builtin.uri:
|
||||
|
|
@ -140,6 +221,7 @@
|
|||
when:
|
||||
- opnform_admin_email | length > 0
|
||||
- opnform_admin_password | length > 0
|
||||
- not (_opnform_force_login_effective | bool)
|
||||
|
||||
- name: Create OpnForm admin user via /api/register
|
||||
ansible.builtin.uri:
|
||||
|
|
@ -160,7 +242,8 @@
|
|||
when:
|
||||
- opnform_admin_email | length > 0
|
||||
- opnform_admin_password | length > 0
|
||||
- opnform_admin_login.status != 200
|
||||
- not (_opnform_force_login_effective | bool)
|
||||
- opnform_admin_login.status | default(0) != 200
|
||||
|
||||
# =====================================================================
|
||||
# 6. OIDC IDENTITY CONNECTION (optional)
|
||||
|
|
@ -171,6 +254,13 @@
|
|||
# existing one to the desired state. PATCHing (rather than skipping when
|
||||
# one exists) keeps inventory changes — e.g. a corrected issuer — applied
|
||||
# on re-runs instead of leaving stale values in the DB forever.
|
||||
#
|
||||
# Skipped on re-applies when force-login is already enabled — the API
|
||||
# password login required for these calls is disabled, and the connection
|
||||
# is known to exist (otherwise force-login wouldn't have rendered in its
|
||||
# final state in step 2). To intentionally re-provision the connection
|
||||
# from inventory changes on such a host: temporarily set
|
||||
# opnform_oidc_force_login=false, re-apply, then set it back to true.
|
||||
|
||||
- name: Log in as admin to obtain OIDC API token
|
||||
ansible.builtin.uri:
|
||||
|
|
@ -186,7 +276,9 @@
|
|||
validate_certs: false
|
||||
register: opnform_oidc_token
|
||||
no_log: true
|
||||
when: opnform_oidc_enabled | bool
|
||||
when:
|
||||
- opnform_oidc_enabled | bool
|
||||
- not (_opnform_force_login_effective | bool)
|
||||
|
||||
- name: Fetch admin's workspaces
|
||||
ansible.builtin.uri:
|
||||
|
|
@ -199,7 +291,9 @@
|
|||
validate_certs: false
|
||||
register: opnform_workspaces
|
||||
no_log: true
|
||||
when: opnform_oidc_enabled | bool
|
||||
when:
|
||||
- opnform_oidc_enabled | bool
|
||||
- not (_opnform_force_login_effective | bool)
|
||||
|
||||
- name: Fetch existing OIDC connections for the default workspace
|
||||
ansible.builtin.uri:
|
||||
|
|
@ -212,7 +306,9 @@
|
|||
validate_certs: false
|
||||
register: opnform_existing_oidc
|
||||
no_log: true
|
||||
when: opnform_oidc_enabled | bool
|
||||
when:
|
||||
- opnform_oidc_enabled | bool
|
||||
- not (_opnform_force_login_effective | bool)
|
||||
|
||||
- name: Resolve OIDC group-role mappings
|
||||
ansible.builtin.set_fact:
|
||||
|
|
@ -224,7 +320,9 @@
|
|||
([{'idp_group': opnform_oidc_admin_group, 'role': 'admin'}]
|
||||
if (opnform_oidc_admin_group | length > 0) else [])
|
||||
}}
|
||||
when: opnform_oidc_enabled | bool
|
||||
when:
|
||||
- opnform_oidc_enabled | bool
|
||||
- not (_opnform_force_login_effective | bool)
|
||||
|
||||
# Desired connection state shared by both the create (POST) and update
|
||||
# (PATCH) calls below. client_secret is always sent: OpnForm's update
|
||||
|
|
@ -244,7 +342,9 @@
|
|||
require_state: true
|
||||
group_role_mappings: "{{ _opnform_oidc_group_role_mappings }}"
|
||||
no_log: true
|
||||
when: opnform_oidc_enabled | bool
|
||||
when:
|
||||
- opnform_oidc_enabled | bool
|
||||
- not (_opnform_force_login_effective | bool)
|
||||
|
||||
- name: Create OIDC identity connection
|
||||
ansible.builtin.uri:
|
||||
|
|
@ -260,6 +360,7 @@
|
|||
no_log: true
|
||||
when:
|
||||
- opnform_oidc_enabled | bool
|
||||
- not (_opnform_force_login_effective | bool)
|
||||
- opnform_existing_oidc.json | length == 0
|
||||
|
||||
# An OIDC connection already exists: PATCH it to the desired state so
|
||||
|
|
@ -280,20 +381,26 @@
|
|||
no_log: true
|
||||
when:
|
||||
- opnform_oidc_enabled | bool
|
||||
- not (_opnform_force_login_effective | bool)
|
||||
- opnform_existing_oidc.json | length > 0
|
||||
|
||||
# =====================================================================
|
||||
# 7. ENABLE FORCE LOGIN (optional, must run last)
|
||||
# 7. ENABLE FORCE LOGIN (first-run only)
|
||||
# =====================================================================
|
||||
# OIDC_FORCE_LOGIN disables password login — including the password-based
|
||||
# admin/OIDC bootstrap above — so it is switched on only now, after the
|
||||
# connection is provisioned. OpnForm itself only enforces force-login when
|
||||
# an enabled OIDC connection exists, so the order matters: connection
|
||||
# first, force-login second.
|
||||
- name: Enable force login now that the OIDC connection exists
|
||||
# On the very first apply, step 2 rendered the compose file with
|
||||
# force-login disabled (so the bootstrap above could use the password
|
||||
# login). Now that the OIDC connection exists, re-render the compose
|
||||
# file with force-login in its final state and recreate the api
|
||||
# containers once.
|
||||
#
|
||||
# On all subsequent applies the probe in step 2 already rendered the
|
||||
# final value, the compose file is byte-identical here, and this block
|
||||
# is a no-op (the template task reports "ok", no recreate).
|
||||
- name: Enable force login (first run, after OIDC bootstrap)
|
||||
when:
|
||||
- opnform_oidc_enabled | bool
|
||||
- opnform_oidc_force_login | bool
|
||||
- not (_opnform_force_login_effective | bool)
|
||||
block:
|
||||
- name: Re-render compose with force-login enabled
|
||||
ansible.builtin.set_fact:
|
||||
|
|
@ -314,6 +421,13 @@
|
|||
wait_timeout: 180
|
||||
when: _opnform_force_login_compose is changed
|
||||
|
||||
- name: Restart ingress so nginx picks up the new api container IPs
|
||||
community.docker.docker_container:
|
||||
name: opnform-ingress
|
||||
state: started
|
||||
restart: true
|
||||
when: _opnform_force_login_compose is changed
|
||||
|
||||
- name: Display deployment info
|
||||
ansible.builtin.debug:
|
||||
msg: |-
|
||||
|
|
@ -335,8 +449,9 @@
|
|||
Users with @{{ opnform_oidc_domain }} addresses will be
|
||||
redirected to {{ opnform_oidc_issuer }} on login.
|
||||
{% if opnform_oidc_sso_entrypoint %}
|
||||
Direct-SSO entrypoint: {{ opnform_base_url }}{{ opnform_oidc_sso_path }}
|
||||
(link users here to skip the email login form)
|
||||
Login intercept active: {{ opnform_base_url }}/login forwards
|
||||
directly to the IdP. Use {{ opnform_base_url }}/login?bypass=1
|
||||
as a break-glass path for the email form when the IdP is down.
|
||||
{% endif %}
|
||||
{% else %}
|
||||
OIDC: disabled (set opnform_oidc_enabled=true to auto-configure)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue