mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 12:47:04 +00:00
Merge pull request #1283 from infinito-nexus/fix/web-app-fider/tenant-without-sso
fix(web-app-fider): bootstrap the tenant without SSO
This commit is contained in:
6 files changed
+124
-16
No files matched your search
@@ -8,7 +8,7 @@
|
||||
|
||||
This role deploys Fider as part of the Infinito.Nexus stack using a single Docker Compose container backed by PostgreSQL. It configures SSO via Keycloak automatically on first deploy, optional email notifications via Mailu, and HTTPS via NGINX reverse proxy.
|
||||
|
||||
The setup tasks handle the full first-deploy bootstrap automatically:
|
||||
The setup tasks handle the full first-deploy bootstrap automatically. Steps 1 to 3 run on every deploy, with or without SSO; step 4 runs only when `services.sso.enabled` is `true`:
|
||||
|
||||
1. **Tenant bootstrap:** calls Fider's `POST /_api/tenants` API to create the initial tenant.
|
||||
2. **Admin user creation:** inserts the admin user directly into the `users` table, bypassing email verification. Idempotent via `WHERE NOT EXISTS`.
|
||||
|
||||
@@ -20,8 +20,12 @@
|
||||
- name: "🚿 Flush compose handlers for {{ application_id }}"
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: "🥾 Fider | Tenant bootstrap"
|
||||
include_tasks: 01_tenant.yml
|
||||
when: IS_STACK_HOST | bool
|
||||
|
||||
- name: "🔧 Fider | OIDC setup"
|
||||
include_tasks: 01_setup_oidc.yml
|
||||
include_tasks: 02_setup_oidc.yml
|
||||
when:
|
||||
- FIDER_OIDC_ENABLED | bool
|
||||
- IS_STACK_HOST | bool
|
||||
@@ -85,17 +85,3 @@
|
||||
query_file: "{{ role_path }}/files/sql/activate-tenants.sql"
|
||||
mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
changed_when: true
|
||||
|
||||
- name: "🔧 Fider | Configure Keycloak as OIDC provider in database"
|
||||
database_query:
|
||||
config: "{{ lookup('database', application_id) }}"
|
||||
query_file: "{{ role_path }}/files/sql/configure-oidc-provider.sql"
|
||||
named_args:
|
||||
display_name: "{{ SOFTWARE_NAME }} SSO"
|
||||
client_id: "{{ OIDC.CLIENT.ID }}"
|
||||
client_secret: "{{ OIDC.CLIENT.SECRET }}"
|
||||
authorize_url: "{{ OIDC.CLIENT.AUTHORIZE_URL }}"
|
||||
token_url: "{{ OIDC.CLIENT.TOKEN_URL }}"
|
||||
profile_url: "{{ OIDC.CLIENT.USER_INFO_URL }}"
|
||||
mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
changed_when: true
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
- name: "🔧 Fider | Configure Keycloak as OIDC provider in database"
|
||||
database_query:
|
||||
config: "{{ lookup('database', application_id) }}"
|
||||
query_file: "{{ role_path }}/files/sql/configure-oidc-provider.sql"
|
||||
named_args:
|
||||
display_name: "{{ SOFTWARE_NAME }} SSO"
|
||||
client_id: "{{ OIDC.CLIENT.ID }}"
|
||||
client_secret: "{{ OIDC.CLIENT.SECRET }}"
|
||||
authorize_url: "{{ OIDC.CLIENT.AUTHORIZE_URL }}"
|
||||
token_url: "{{ OIDC.CLIENT.TOKEN_URL }}"
|
||||
profile_url: "{{ OIDC.CLIENT.USER_INFO_URL }}"
|
||||
mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
changed_when: true
|
||||
@@ -0,0 +1,3 @@
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT: Path = Path(__file__).resolve().parents[5]
|
||||
@@ -0,0 +1,101 @@
|
||||
import unittest
|
||||
|
||||
from jinja2 import Environment
|
||||
|
||||
from utils.cache.yaml import load_yaml_any
|
||||
|
||||
from . import PROJECT_ROOT
|
||||
|
||||
TASKS = PROJECT_ROOT / "roles" / "web-app-fider" / "tasks"
|
||||
MODES = ("compose", "swarm")
|
||||
|
||||
_TRUTHY = frozenset({"1", "true", "yes", "y", "on"})
|
||||
|
||||
|
||||
def _to_bool(value):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return str(value).strip().lower() in _TRUTHY
|
||||
|
||||
|
||||
def _guard_holds(task, context) -> bool:
|
||||
when = task.get("when", [])
|
||||
guards = when if isinstance(when, list) else [when]
|
||||
env = Environment(autoescape=True)
|
||||
env.filters["bool"] = _to_bool
|
||||
return all(bool(env.compile_expression(expr)(**context)) for expr in guards)
|
||||
|
||||
|
||||
def _run_tasks(*, sso: bool, stack_host: bool, mode: str, tenants: int) -> list:
|
||||
context = {
|
||||
"FIDER_OIDC_ENABLED": sso,
|
||||
"FIDER_MCP_ENABLED": False,
|
||||
"IS_STACK_HOST": stack_host,
|
||||
"DEPLOYMENT_MODE": mode,
|
||||
"fider_tenant_count": {"rows": [[tenants]]},
|
||||
}
|
||||
executed: list = []
|
||||
for include in load_yaml_any(str(TASKS / "00_core.yml")):
|
||||
target = include.get("include_tasks")
|
||||
if not isinstance(target, str) or not (TASKS / target).is_file():
|
||||
continue
|
||||
if not _guard_holds(include, context):
|
||||
continue
|
||||
executed.extend(
|
||||
task
|
||||
for task in load_yaml_any(str(TASKS / target))
|
||||
if _guard_holds(task, context)
|
||||
)
|
||||
return executed
|
||||
|
||||
|
||||
def _tenant_posts(tasks: list) -> list:
|
||||
return [
|
||||
task
|
||||
for task in tasks
|
||||
if str(task.get("uri_retry", {}).get("url", "")).endswith("/_api/tenants")
|
||||
or "POST /_api/tenants" in str(task.get("ansible.builtin.shell", ""))
|
||||
]
|
||||
|
||||
|
||||
def _configures_keycloak(tasks: list) -> bool:
|
||||
return any(
|
||||
str(task.get("database_query", {}).get("query_file", "")).endswith(
|
||||
"configure-oidc-provider.sql"
|
||||
)
|
||||
for task in tasks
|
||||
)
|
||||
|
||||
|
||||
class TestTenantBootstrapGate(unittest.TestCase):
|
||||
def test_fresh_stack_host_posts_one_tenant_with_and_without_sso(self):
|
||||
for sso in (True, False):
|
||||
for mode in MODES:
|
||||
with self.subTest(sso=sso, mode=mode):
|
||||
tasks = _run_tasks(sso=sso, stack_host=True, mode=mode, tenants=0)
|
||||
self.assertEqual(len(_tenant_posts(tasks)), 1)
|
||||
|
||||
def test_existing_tenant_is_not_posted_again(self):
|
||||
for sso in (True, False):
|
||||
for mode in MODES:
|
||||
with self.subTest(sso=sso, mode=mode):
|
||||
tasks = _run_tasks(sso=sso, stack_host=True, mode=mode, tenants=1)
|
||||
self.assertEqual(_tenant_posts(tasks), [])
|
||||
|
||||
def test_keycloak_provider_is_configured_only_with_sso(self):
|
||||
for sso in (True, False):
|
||||
for mode in MODES:
|
||||
with self.subTest(sso=sso, mode=mode):
|
||||
tasks = _run_tasks(sso=sso, stack_host=True, mode=mode, tenants=1)
|
||||
self.assertEqual(_configures_keycloak(tasks), sso)
|
||||
|
||||
def test_non_stack_host_runs_no_bootstrap(self):
|
||||
for sso in (True, False):
|
||||
for mode in MODES:
|
||||
with self.subTest(sso=sso, mode=mode):
|
||||
tasks = _run_tasks(sso=sso, stack_host=False, mode=mode, tenants=0)
|
||||
self.assertEqual(tasks, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user