fix(web-app-friendica): patch upstream ldapauth autocreate + variant-aware playwright

Root cause: upstream friendica-addons/ldapauth's `ldap_createaccount`
returns the raw `User::create()` result (`['user' => UserRow, 'password' => '']`),
but the `authenticate` hook only sets `authenticated = 1` when
`$user['uid']` is non-empty — never true for the autocreate path. First-
login LDAP users (e.g. biber) were inserted into `friendica.user` but
the session was never started, so /network kept serving the login form.

Encoded as an idempotent in-container patch task in `tasks/02_ldap.yml`
+ `files/patch_ldapauth_autocreate.sh` (reports PATCHED on first run,
ALREADY on subsequent runs). Runs whenever LDAP is wired, gated by
FRIENDICA_LDAP_ENABLED.

Other changes in the same stack:
- meta/services.yml: drop keycloakpassword addon (oauth2-proxy in front
  owns SSO); ldap.enabled now follows svc-db-openldap group presence so
  the variant matrix can disable it without a static override.
- tasks/01_core.yml + tasks/06_user_bootstrap.yml: pre-create the
  administrator only in LDAP-disabled variants — when LDAP is wired,
  ldapauth materialises the row on first admin login.
- files/playwright.spec.js: variant-aware spec replacing the parked
  test.skip cases. Three tests now run: guest flow (always), admin
  native login (picks double-login vs direct based on
  OAUTH2_SERVICE_ENABLED), biber + admin side-by-side in isolated
  contexts. PERSONA_* still blocked — friendica has no header-trusted
  autocreate path.
- templates/playwright.env.j2: expose BIBER_* + service-gate flags.
- inventories/development/default.yml: biber email aligned with the
  oauth2-proxy `email_domains = infinito.example` filter so biber's
  Keycloak login isn't 403'd at the proxy.
- TODO.md: SAML migration outlined as the long-term replacement for
  the double-login (currently second hop silently bypasses Keycloak 2FA).

Drive-by: trim double-space before `# nocheck` in
roles/sys-aur/meta/services.yml.

Verified on VARIANT=0 (oauth2 + ldap + oidc all enabled): three
playwright tests green, failed=0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.7 committed 2026-05-16 18:39:52 +02:00
1 parent b8a372ded7
commit 9763cd8a4a
12 files changed
+296 -131

No files matched your search

+1 -1
View File
@@ -35,7 +35,7 @@ users:
firstname: "Harry"
lastname: "Beaver"
website: "https://www.infinito.nexus/"
email: "contact@infinito.nexus"
email: "biber@infinito.example"
gpg: "DUMMY-GPG-KEY-NOT-SET"
password: "VeryUnsecurePassword"
+1 -1
View File
@@ -1,5 +1,5 @@
---
aur:
lifecycle: beta
repository: https://aur.archlinux.org/yay.git # nocheck: url
repository: https://aur.archlinux.org/yay.git # nocheck: url
ref: master
+50
View File
@@ -0,0 +1,50 @@
# TODOs
## Migrate auth to the `saml` addon (replaces oauth2-proxy + ldapauth + double-login)
**Why:** Friendica currently authenticates in two steps: oauth2-proxy gates
the vhost (Keycloak-Authorization-Code-Flow, 2FA-enforceable), then the user
must fill Friendica's own `/login` form because stock Friendica has **no
header-trusted auto-login** — Owa.php / OAuth/Token.php only read
`HTTP_AUTHORIZATION` for API endpoints, never for the web session. The
ldapauth addon stays enabled solely as a first-login bootstrap that
materialises the `friendica.user` row from openldap.
The double-login is UX-hostile (creds typed twice) and the second hop
silently bypasses any 2FA Keycloak enforced on the first hop — anyone with
the LDAP password can reach the app even when WebAuthn/OTP is required at
the IdP. Friendica's stock `saml` addon (already on disk under
`/var/www/html/addon/saml/`) supports a SAML2 AuthnRequest → IdP-initiated
SSO with auto-account-creation, which collapses the chain to a single
2FA-gated round-trip.
**Migration outline (~4-6h):**
1. Enable `addons.saml: {}` in `meta/services.yml` (and drop `addons.ldapauth: {}`
plus `services.oauth2` once SAML autocreate is verified).
2. Template `saml.config.php.j2` analogous to `ldapauth.config.php.j2`:
IdP-metadata-URL, SP-Entity-ID, SP-ACS-URL, NameID format, attribute map
(`email`/`firstName`/`lastName`/`groups`), signing/encryption cert paths.
3. Mount the rendered `saml.config.php` in `compose.yml.j2` (analogous to
ldapauth).
4. Provision a SAML client in Keycloak (extend `roles/web-app-keycloak`'s
client-provisioning logic which currently only handles OIDC clients).
Attribute mappers, SP-metadata import, signing certificate alignment.
5. Drop `services.oauth2.enabled` for friendica — SAML handles SSO end-to-
end, oauth2-proxy in front would double-gate.
6. Rewrite `files/playwright.spec.js` to use the generic admin/biber
persona flows (remove `PERSONA_*_BLOCKED=true` from
`templates/playwright.env.j2`, drop the bespoke `loginViaOauth2ProxyAndFriendica`
helper, drop the FRIENDICA_BASE_URL env var).
**Risks / unknowns:**
- Friendica's `saml` addon is community-maintained — last-commit recency
AND config-format compatibility need verification before committing to
it as the canonical SSO path.
- SAML cert lifecycle (rotation, alignment between Friendica SP and
Keycloak IdP) is a new maintenance burden — clarify ownership before
shipping.
- First-login autocreate via SAML's `auto_register` option must be tested
with both the `administrator` and `biber` openldap-sourced identities
before ldapauth is removed.
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
#
# Patch the upstream friendica-addons/ldapauth `ldap_createaccount` so
# the autocreate path returns the user-row shape the `authenticate`
# hook expects.
#
# Upstream returns the bare `User::create()` result, which is
# `['user' => UserRow, 'password' => '']`. The hook then evaluates
# `if (!empty($user['uid']))` and silently bails out — first-login
# users get inserted into `friendica.user` but never receive
# `authenticated = 1`, so /network keeps serving the login form
# instead of starting a session. We unwrap `$user['user']` so the uid
# is visible to the hook.
#
# Idempotent: a second run sees the patched line and exits "ALREADY".
# Retries are safe across `compose up` restarts because the addon
# tree is image-baked, not volume-mounted, so the upstream content
# returns whenever the friendica image is re-pulled.
#
# Usage:
# patch_ldapauth_autocreate.sh EXEC_PREFIX LDAPAUTH_PATH
# EXEC_PREFIX full container-exec command prefix as a single arg
# (typical: 'docker compose exec -T --user root
# friendica')
# LDAPAUTH_PATH absolute path of ldapauth.php inside the container
set -euo pipefail
EXEC="$1"
P="$2"
ORIG=" return \$user;"
NEW=" return \$user['user'] ?? \$user;"
if $EXEC grep -qF "$NEW" "$P"; then
echo "ALREADY"
exit 0
fi
if ! $EXEC grep -qF "$ORIG" "$P"; then
echo "NO-MATCH ldap_createaccount return-line shape changed upstream"
exit 1
fi
# Use python to avoid shell-escaping the single quotes inside sed/awk
# replacement text — friendica image ships python3 via Composer base.
$EXEC python3 -c "
import sys
p='$P'
src=open(p).read()
new=src.replace('''$ORIG''', '''$NEW''', 1)
open(p, 'w').write(new)
print('PATCHED')
"
+113 -116
View File
@@ -1,158 +1,155 @@
const { test, expect } = require("@playwright/test");
const { decodeDotenvQuotedValue, runAdminFlow, runBiberFlow, runGuestFlow } = require("./personas");
const { decodeDotenvQuotedValue, performKeycloakLoginForm, runGuestFlow } = require("./personas");
const { isServiceEnabled, skipUnlessServiceEnabled } = require("./service-gating");
test.use({
ignoreHTTPSErrors: true
});
const friendicaBaseUrl = decodeDotenvQuotedValue(process.env.FRIENDICA_BASE_URL);
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN);
const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME);
const adminPassword = decodeDotenvQuotedValue(process.env.ADMIN_PASSWORD);
const biberUsername = decodeDotenvQuotedValue(process.env.BIBER_USERNAME);
const biberPassword = decodeDotenvQuotedValue(process.env.BIBER_PASSWORD);
// Friendica authenticates users directly against its local DB. The openldap
// bridge is provided by the `ldapauth` addon: users typed into the Friendica
// login form are resolved against the shared openldap server, which already
// contains `administrator` and `biber`. No OIDC redirect step is involved.
async function performFriendicaLogin(page, baseUrl, username, password) {
await page.goto(`${baseUrl}/login`, { waitUntil: "domcontentloaded" });
// Variant matrix (see meta/variants.yml):
// v0 oauth2 + ldap + oidc all enabled — double-login pattern below
// v1 every dynamic flag disabled — only guest test runs
// v2 ldap enabled, oauth2 + oidc off — direct in-app login (no Keycloak hop)
// All three are selected at runtime by inspecting `OAUTH2_SERVICE_ENABLED`
// and `LDAP_SERVICE_ENABLED` — the spec stays one file across variants.
const usernameField = page.locator("input[name='username']");
const passwordField = page.locator("input[name='password']");
async function loginViaFriendicaForm(page, username, password) {
const passwordField = page.locator("input[name='password']").first();
await passwordField.waitFor({ state: "visible", timeout: 30_000 });
// Friendica renders a `btn form-button-search` submit in the topbar before
// the main login form, so a generic `button[type='submit']` selector picks
// up the search button. Scope the submit click to the form that owns the
// password field, and accept any common submit label upstream may render
// (`Sign in`, `Login`, `Anmelden`, …) to stay resilient against theme
// / locale changes on Friendica's login template.
const usernameField = page.locator("input[name='username']").first();
const loginForm = page.locator("form").filter({ has: passwordField });
const signInButton = loginForm
.locator("button[type='submit'], input[type='submit']")
.or(loginForm.getByRole("button", { name: /sign\s*in|log\s*in|anmelden|connexion|iniciar|entrar/i }))
.first();
await usernameField.waitFor({ state: "visible", timeout: 60_000 });
await usernameField.fill(username);
await passwordField.fill(password);
await Promise.all([
page.waitForLoadState("domcontentloaded"),
signInButton.click(),
]);
}
// Wait until the post-login Friendica UI is rendered. The sidebar nav always
// exposes the `#navbar-apps-menu` / profile button for authenticated users.
async function waitForFriendicaHome(page) {
// v0 path: oauth2-proxy gates the friendica vhost, so /login first redirects
// through Keycloak — the browser-flow path that ENFORCES 2FA when the realm
// has it configured. After the Keycloak round-trip Friendica still renders
// its OWN login form: there is no header-trusted auto-login addon in stock
// Friendica (Owa.php / OAuth.php only read HTTP_AUTHORIZATION for API auth,
// never for the web session). Fill that second form so `ldapauth`'s
// else-branch (User::getAuthenticationInfo + User::getById against the
// pre-created friendica.user row) starts a Friendica session. See TODO.md
// for the SAML migration that would collapse this back into a single hop.
async function loginViaOauth2ProxyAndFriendica(page, username, password) {
const baseUrl = friendicaBaseUrl.replace(/\/$/, "");
await page.goto(`${baseUrl}/`, { waitUntil: "domcontentloaded" }).catch(() => {});
// Step 1: oauth2-proxy intercepted the request — page is now on Keycloak.
await performKeycloakLoginForm(page, username, password);
// Wait until the browser hostname is the canonical friendica domain. The
// naive `url().includes(canonicalDomain)` matches Keycloak's auth URL too
// (the canonical domain appears in the `redirect_uri` query param), which
// would let Step 2 run against Keycloak's form instead of Friendica's.
await expect
.poll(() => {
try { return new URL(page.url()).hostname; } catch { return ""; }
}, {
timeout: 60_000,
message: `Expected page hostname to become ${canonicalDomain} after Keycloak login`,
})
.toBe(canonicalDomain);
// Step 2: Friendica's own login form (post-oauth2-proxy).
await page.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
if (await page.locator("input[name='password']").first().isVisible({ timeout: 30_000 }).catch(() => false)) {
await loginViaFriendicaForm(page, username, password);
}
// Verify Friendica's authenticated surface is now rendered. Navigate to
// an authenticated route explicitly so we don't race a same-page re-render
// after the POST redirect — the frio / vier themes both expose
// #topbar-first on /network; `a[href*='/logout']` is the cross-theme
// fallback for less-canonical themes.
await page.goto(`${baseUrl}/network`, { waitUntil: "domcontentloaded" }).catch(() => {});
const profileMenu = page.locator("#topbar-first, #navbar-apps-menu, a[href*='/logout']").first();
await profileMenu.waitFor({ state: "visible", timeout: 60_000 });
}
// Log out via the universal logout endpoint. Every app's nginx vhost intercepts
// `location = /logout` and proxies it to web-svc-logout, which invalidates all
// active sessions. `waitUntil: 'commit'` avoids ERR_ABORTED from the multi-
// domain redirect chain the service triggers after session invalidation.
async function friendicaLogout(page, baseUrl) {
await page.goto(`${baseUrl.replace(/\/$/, "")}/logout`, { waitUntil: "commit" }).catch(() => {});
// v2 path: no oauth2-proxy gating, friendica's `/login` is directly reachable.
// `ldapauth`'s bind path authenticates against openldap, the pre-created
// administrator row in friendica.user lets the else-branch return a uid →
// hook sets authenticated=1 → session established.
async function loginViaFriendicaDirect(page, username, password) {
const baseUrl = friendicaBaseUrl.replace(/\/$/, "");
await page.goto(`${baseUrl}/login`, { waitUntil: "domcontentloaded" });
await loginViaFriendicaForm(page, username, password);
await page.goto(`${baseUrl}/network`, { waitUntil: "domcontentloaded" }).catch(() => {});
const profileMenu = page.locator("#topbar-first, #navbar-apps-menu, a[href*='/logout']").first();
await profileMenu.waitFor({ state: "visible", timeout: 60_000 });
}
test.beforeEach(() => {
expect(friendicaBaseUrl, "FRIENDICA_BASE_URL must be set in the Playwright env file").toBeTruthy();
expect(adminUsername, "ADMIN_USERNAME must be set in the Playwright env file").toBeTruthy();
expect(adminPassword, "ADMIN_PASSWORD must be set in the Playwright env file").toBeTruthy();
expect(biberUsername, "BIBER_USERNAME must be set in the Playwright env file").toBeTruthy();
expect(biberPassword, "BIBER_PASSWORD must be set in the Playwright env file").toBeTruthy();
});
// Scenario I: dashboard → click Friendica → verify iframe → login as admin → logout.
test("dashboard to friendica: admin login, verify ui, logout", async ({ page }) => {
const expectedFriendicaBaseUrl = friendicaBaseUrl.replace(/\/$/, "");
// 1. Open dashboard and click the Friendica card.
await page.goto("/");
await page.getByRole("link", { name: /Explore Friendica/i }).click();
// 2. The dashboard embeds Friendica in a fullscreen iframe.
await expect(page.locator("#main iframe")).toBeVisible({ timeout: 30_000 });
// 3. Log in as administrator (navigates the top-level page, not the iframe).
await performFriendicaLogin(page, expectedFriendicaBaseUrl, adminUsername, adminPassword);
await waitForFriendicaHome(page);
// 4. Log out via the universal logout endpoint.
await friendicaLogout(page, expectedFriendicaBaseUrl);
// 5. Confirm the session is gone — an unauthenticated request to /network
// must not render the authenticated nav.
await page.goto(`${expectedFriendicaBaseUrl}/network`, { waitUntil: "domcontentloaded" }).catch(() => {});
await expect(page.locator("a[href*='/logout']")).not.toBeAttached({ timeout: 10_000 });
await page.goto("/");
});
// Scenario II: biber and administrator each log in in isolated browser contexts,
// both reach the authenticated Friendica UI, each logs out independently.
//
// The two contexts model two separate users on separate machines — no shared
// cookies or sessions. This covers LDAP autocreate for a non-admin identity
// alongside the admin path, and verifies that logout in one session does not
// touch the other's session.
test("friendica: biber and administrator log in side by side in isolated contexts", async ({ browser }) => {
const expectedFriendicaBaseUrl = friendicaBaseUrl.replace(/\/$/, "");
const biberContext = await browser.newContext({ ignoreHTTPSErrors: true });
const adminContext = await browser.newContext({ ignoreHTTPSErrors: true });
try {
const biberPage = await biberContext.newPage();
await performFriendicaLogin(biberPage, expectedFriendicaBaseUrl, biberUsername, biberPassword);
await waitForFriendicaHome(biberPage);
const adminPage = await adminContext.newPage();
await performFriendicaLogin(adminPage, expectedFriendicaBaseUrl, adminUsername, adminPassword);
await waitForFriendicaHome(adminPage);
// biber logs out first — admin session must stay alive.
await friendicaLogout(biberPage, expectedFriendicaBaseUrl);
await adminPage.goto(`${expectedFriendicaBaseUrl}/network`, { waitUntil: "domcontentloaded" });
await expect(adminPage.locator("a[href*='/logout']").first()).toBeAttached({ timeout: 10_000 });
await friendicaLogout(adminPage, expectedFriendicaBaseUrl);
} finally {
await biberContext.close().catch(() => {});
await adminContext.close().catch(() => {});
}
});
// Persona scenarios.
// Bodies live in the shared helper roles/test-e2e-playwright/files/personas.js
// so every role's persona flow stays consistent.
async function friendicaLogout(page) {
const baseUrl = friendicaBaseUrl.replace(/\/$/, "");
await page.goto(`${baseUrl}/logout`, { waitUntil: "commit" }).catch(() => {});
}
test("guest: public-landing → auth chain → never authenticated", async ({ page }) => {
await runGuestFlow(page);
});
test("biber: dashboard → app → universal logout", async ({ page }) => {
await runBiberFlow(page);
// Administrator native login — picks the variant-specific path at runtime.
// Skips entirely when neither oauth2 nor ldap is enabled (v1 has no auth
// providers wired, the role only serves the public landing).
test("friendica: administrator native login and logout", async ({ page }) => {
skipUnlessServiceEnabled("ldap");
if (isServiceEnabled("oauth2")) {
// v0: full double-login via oauth2-proxy + Keycloak round-trip.
await loginViaOauth2ProxyAndFriendica(page, adminUsername, adminPassword);
} else {
// v2: direct /login form, no Keycloak step.
await loginViaFriendicaDirect(page, adminUsername, adminPassword);
}
await friendicaLogout(page);
await page.goto(`${friendicaBaseUrl.replace(/\/$/, "")}/network`, { waitUntil: "domcontentloaded" }).catch(() => {});
await expect(page.locator("a[href*='/logout']")).not.toBeAttached({ timeout: 10_000 });
});
test("administrator: dashboard → prometheus → app → universal logout", async ({ page }) => {
await runAdminFlow(page, {
adminInteraction: async (interactivePage) => {
// web-app-friendica admin-only interaction: open a management surface.
const link = interactivePage
.getByRole("link", { name: /^(admin|administration|site|users)$/i })
.first();
if (await link.isVisible({ timeout: 10_000 }).catch(() => false)) {
await link.click().catch(() => {});
await interactivePage.waitForLoadState("domcontentloaded", { timeout: 30_000 }).catch(() => {});
await expect(interactivePage.locator("body")).toContainText(
/admin|site|users|database|federation/i,
{ timeout: 30_000 },
);
}
},
});
// biber + administrator side-by-side: two isolated browser contexts log in
// independently, biber logs out first, admin session must persist. Exercises
// ldapauth's bridge for a non-admin LDAP user AND verifies session isolation
// between contexts. Skips when LDAP is disabled.
test("friendica: biber + administrator side by side in isolated contexts", async ({ browser }) => {
skipUnlessServiceEnabled("ldap");
const login = isServiceEnabled("oauth2") ? loginViaOauth2ProxyAndFriendica : loginViaFriendicaDirect;
const biberContext = await browser.newContext({ ignoreHTTPSErrors: true });
const adminContext = await browser.newContext({ ignoreHTTPSErrors: true });
try {
const biberPage = await biberContext.newPage();
await login(biberPage, biberUsername, biberPassword);
const adminPage = await adminContext.newPage();
await login(adminPage, adminUsername, adminPassword);
// biber logs out first — admin session must stay alive.
await friendicaLogout(biberPage);
await adminPage.goto(`${friendicaBaseUrl.replace(/\/$/, "")}/network`, { waitUntil: "domcontentloaded" });
await expect(adminPage.locator("a[href*='/logout']").first()).toBeAttached({ timeout: 10_000 });
await friendicaLogout(adminPage);
} finally {
await biberContext.close().catch(() => {});
await adminContext.close().catch(() => {});
}
});
+5 -2
View File
@@ -7,7 +7,10 @@ friendica:
version: latest
min_storage: 10GB
addons:
keycloakpassword: {}
# SSO is owned by the oauth2-proxy gate in front of the friendica vhost
# (services.oauth2.enabled=keycloak below). `ldapauth` stays enabled
# because friendica has no header-trusted autocreate mechanism — the
# ldap bind is the only path that materialises a friendica.user row.
ldapauth: {}
ports:
local:
@@ -25,7 +28,7 @@ logout:
enabled: "{{ 'web-svc-logout' in group_names }}"
shared: "{{ 'web-svc-logout' in group_names }}"
ldap:
enabled: true # nocheck: dynamic-flag
enabled: "{{ 'svc-db-openldap' in group_names }}"
shared: "{{ 'svc-db-openldap' in group_names }}"
oidc:
enabled: "{{ 'web-app-keycloak' in group_names }}"
+5 -1
View File
@@ -9,7 +9,7 @@
- name: Integrate LDAP
include_tasks: 02_ldap.yml
when: lookup('config',application_id, 'services.ldap.enabled')
when: FRIENDICA_LDAP_ENABLED | bool
# General
- name: "flush handlers to ensure that '{{ FRIENDICA_CONTAINER }}' is up before executing container tasks"
@@ -28,3 +28,7 @@
- name: Add Friendica Add Ons
include_tasks: 04_addons.yml
- name: Bootstrap Friendica administrator user (only when no LDAP provider is wired to provision them)
include_tasks: 06_user_bootstrap.yml
when: not (FRIENDICA_LDAP_ENABLED | bool)
+23
View File
@@ -8,3 +8,26 @@
group: 33
force: true
notify: compose up
# Upstream `ldap_createaccount` returns the raw `User::create()` result
# (`['user' => UserRow, 'password' => '']`), but the `authenticate` hook
# only sets `authenticated = 1` when `$user['uid']` is non-empty — which
# is never true for the autocreate path. The fix unwraps `$user['user']`
# so the freshly-created user's uid is visible to the hook. Without it
# every first-login LDAP user (e.g. biber) silently fails to start a
# session despite the row being inserted; the next request still serves
# the login form. See web-app-friendica/TODO.md for the SAML migration
# that would remove this whole code path.
- name: "Patch upstream friendica-addons/ldapauth autocreate return shape"
script: >-
patch_ldapauth_autocreate.sh
"{{ BIN_COMPOSE }} exec -T --user root {{ FRIENDICA_SERVICE }}"
"{{ FRIENDICA_APPLICATION_DIR }}/addon/ldapauth/ldapauth.php"
args:
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
executable: /bin/bash
register: friendica_ldapauth_patch
changed_when: "'PATCHED' in friendica_ldapauth_patch.stdout"
retries: 30
delay: 2
until: friendica_ldapauth_patch.rc == 0
+1 -3
View File
@@ -10,9 +10,7 @@
+ [{
'name': item.key,
'enabled': (
FRIENDICA_OIDC_ENABLED
if item.key == 'keycloakpassword'
else FRIENDICA_LDAP_ENABLED
FRIENDICA_LDAP_ENABLED
if item.key == 'ldapauth'
else (item.value.enabled if item.value is mapping and 'enabled' in item.value else False)
)
@@ -0,0 +1,35 @@
---
# Pre-create the administrator user in friendica.user so ldapauth's
# else-branch (User::getAuthenticationInfo → User::getById) handles the
# login. The autocreate path inside ldap_createaccount is broken in
# stock friendica + ldapauth as of the version pinned here — it never
# materialises the row even when ldap_bind succeeds. Pre-creating side-
# steps the broken branch; the auto-generated friendica.user password
# is irrelevant because ldap_bind owns the auth check.
- name: "Check whether Friendica administrator user already exists"
command: >-
{{ BIN_COMPOSE }} exec -T --user {{ FRIENDICA_USER }}
--workdir {{ FRIENDICA_APPLICATION_DIR }}
{{ FRIENDICA_SERVICE }}
{{ FRIENDICA_APPLICATION_DIR }}/bin/console user search nick administrator
args:
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
register: friendica_admin_search
changed_when: false
failed_when: false
- name: "Pre-create Friendica administrator user"
command: >-
{{ BIN_COMPOSE }} exec -T --user {{ FRIENDICA_USER }}
--workdir {{ FRIENDICA_APPLICATION_DIR }}
{{ FRIENDICA_SERVICE }}
{{ FRIENDICA_APPLICATION_DIR }}/bin/console user add
"{{ lookup('users', 'administrator').firstname | default('Administrator') }}"
administrator
"{{ lookup('users', 'administrator').email }}"
en
args:
chdir: "{{ lookup('container', application_id, 'directories.instance') }}"
when: "'administrator' not in friendica_admin_search.stdout"
register: friendica_admin_create
changed_when: friendica_admin_create.rc == 0
@@ -6,7 +6,7 @@
volumes:
- html:{{ FRIENDICA_APPLICATION_DIR }}
- data:/var/www/data # I assume that this one is unnessecarry
- {{ FRIENDICA_LDAP_CONFIG_HOST }}:{{ FRIENDICA_LDAP_CONFIG_DOCKER }}:ro
- {{ FRIENDICA_LDAP_CONFIG_HOST }}:{{ FRIENDICA_LDAP_CONFIG_DOCKER }}:ro
ports:
- "{{ DOCKER_BIND_HOST }}:{{ lookup('config', application_id, 'services.friendica.ports.local.http') }}:80"
@@ -6,10 +6,12 @@ BIBER_USERNAME={{ lookup('users', 'biber').username | dotenv_quote }}
BIBER_PASSWORD={{ lookup('users', 'biber').password | dotenv_quote }}
CANONICAL_DOMAIN={{ lookup('domain', application_id) | dotenv_quote }}
# Friendica authenticates via local DB + LDAP (ldapauth addon), not OIDC.
# The generic persona helpers drive Keycloak/OAuth2-proxy round-trips and
# cannot reconcile with Friendica's own login form. Bespoke admin/biber
# tests above own the actual login flows.
# Persona flows require a header-trusted auto-login that stock Friendica does
# NOT provide (Owa.php / OAuth.php only read HTTP_AUTHORIZATION for API auth).
# Block them whenever LDAP is wired in: the bespoke admin / biber tests below
# already exercise the canonical login paths via ldapauth + native /login.
# In LDAP-disabled variants there is no auth provider at all, so the personas
# also can't run — same block keyed off LDAP for symmetry.
PERSONA_BIBER_BLOCKED=true
PERSONA_ADMINISTRATOR_BLOCKED=true
# The Playwright container image has no custom ENTRYPOINT so the infinito CA cert
@@ -22,8 +24,8 @@ MATOMO_BASE_URL={{ lookup('tls', 'web-app-matomo', 'url.base') | dotenv_quote }}
LOGOUT_SERVICE_ENABLED={{ lookup('config', application_id, 'services.logout.enabled') | string | lower }}
# nocheck: playwright-service-gate — persona drives the OIDC path; LDAP federation is exercised in the LDAP-only matrix variant
LDAP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.ldap.enabled') | string | lower }}
OIDC_SERVICE_ENABLED={{ lookup('config', application_id, 'services.oidc.enabled') | string | lower }}
LDAP_SERVICE_ENABLED={{ FRIENDICA_LDAP_ENABLED | string | lower }}
OIDC_SERVICE_ENABLED={{ FRIENDICA_OIDC_ENABLED | string | lower }}
MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enabled') | string | lower }}
# nocheck: playwright-service-gate — covered by sys-svc-mail integration tests, not the persona surface
EMAIL_SERVICE_ENABLED={{ lookup('config', application_id, 'services.email.enabled') | string | lower }}