024 - ERPNext Role with OIDC SSO¶
User Story¶
As a platform administrator of Infinito.Nexus, I want ERPNext (Frappe Framework) integrated as a web-app-erpnext role with OpenID Connect identity provider integration so that users can access the ERP through the same Single Sign-On (SSO) mechanism used across the Infinito.Nexus ecosystem, reusing the platform’s central database and cache services.
Background¶
ERPNext is an open-source ERP suite built on the Frappe Framework. Upstream ships a multi-container deployment (frappe_docker) consisting of:
frappe(Gunicorn) — Python web backendsocketio— Node real-time WebSocket servicescheduler— periodic-job dispatcher (bench schedule)Workers:
queue-short,queue-default,queue-long— background jobsNginx (frontend) — static assets + reverse proxy onto Frappe / SocketIO
Data plane: MariaDB (primary), Redis (three logical roles:
cache,queue,socketio)
Two of those data-plane dependencies have a central Infinito.Nexus service equivalent — svc-db-mariadb and svc-db-redis — and MUST be reused per the central-service convention. Frappe’s three Redis logical roles are served by a single central Redis instance using distinct DB numbers (Frappe supports that out of the box via redis_cache / redis_queue / redis_socketio config entries).
Frappe ships native OAuth 2.0 / OpenID Connect client support via the built-in “Social Login Key” record (see Frappe docs: Social Login Key). The integration uses Frappe’s own OIDC client against the Infinito Keycloak IdP — no oauth2-proxy sidecar. The role therefore uses the services.sso.flavor: oidc schema (post-021 unified SSO contract), aligned with web-app-odoo.
The closest existing analogues in this repo are web-app-odoo (ERP shape, OIDC + LDAP variants, central MariaDB consumer pattern omitted — odoo uses Postgres) and web-app-zammad (central-service reuse pattern, OIDC-direct flavor, three-variant matrix).
Confirmed Decisions¶
These decisions were confirmed by the operator before implementation starts and are NOT subject to re-litigation during implementation.
# |
Decision |
Rationale |
|---|---|---|
1 |
Canonical hostname: |
Mirrors the |
2 |
SSO flavor is OIDC-direct ( |
Frappe supports OIDC natively as an OAuth client; an oauth2-proxy sidecar would be redundant and break the Frappe “Login with X” UX. |
3 |
Use the unified post-021 |
021 is merged. New roles MUST land on the unified schema directly, not on the legacy oauth2/oidc service shape. |
4 |
The Keycloak OIDC client for ERPNext is auto-provisioned via |
No manual operator step on deploy. |
5 |
Keycloak group → ERPNext role mapping target tiers: |
Operator-confirmable; Frappe’s Social Login Key alone does NOT consume group claims. Persisting the map in v1 unblocks the follow-up requirement without re-litigating the mapping table. |
6 |
Image strategy: upstream |
Operator-confirmed: track the actively maintained major. Self-built image (à la 015 Moodle) explicitly out of scope. |
7 |
External-service reuse: |
Operator-confirmable; matches the convention codified in 022 Zammad Decision #7. No |
8 |
Email integration in v1: outbound SMTP only. When |
Operator-confirmed: defer inbound; v1 = outbound only. The follow-up requirement will introduce the generic Mailu auto-mailbox hook and sweep this role at the same time. |
9 |
The Frappe site-setup wizard is bypassed on first deploy via auto-bootstrap: |
Matches the Zammad wizard-bypass pattern (022 Decision #9); avoids a manual UI step on every fresh box. |
10 |
Playwright coverage per 019: both |
Mirrors the Zammad rollout (022 Decision #10). |
11 |
|
Standard variant matrix across helpdesk / ERP roles. |
12 |
Multi-tenancy / multi-site (Frappe’s |
Keeps v1 surface small; multi-site can land in a follow-up requirement if needed. |
13 |
Backup hook in v1: documented as an operator command in the role README. |
Operator-confirmed: keep this PR focused on the role itself. No |
Target Schema¶
Role layout¶
roles/web-app-erpnext/
├── README.md
├── files/
│ └── playwright/test-*.js
├── meta/
│ ├── main.yml
│ ├── info.yml
│ ├── server.yml
│ ├── services.yml
│ ├── secrets.yml
│ ├── users.yml
│ ├── variants.yml
│ └── volumes.yml
├── tasks/
│ ├── main.yml
│ ├── 01_core.yml
│ └── 03_bench_bootstrap.yml # new-site + install-app + wizard-bypass
├── templates/
│ ├── docker-compose.yml.j2
│ ├── env.j2
│ ├── common_site_config.json.j2
│ └── playwright.env.j2
└── vars/
└── main.yml
meta/services.yml excerpt¶
---
sso:
enabled: "{{ 'web-app-keycloak' in group_names }}"
shared: "{{ 'web-app-keycloak' in group_names }}"
flavor: oidc
ldap:
enabled: "{{ 'svc-db-openldap' in group_names }}"
shared: "{{ 'svc-db-openldap' in group_names }}"
email:
enabled: "{{ 'web-app-mailu' in group_names }}"
shared: "{{ 'web-app-mailu' in group_names }}"
logout:
enabled: "{{ 'web-svc-logout' in group_names }}"
shared: "{{ 'web-svc-logout' in group_names }}"
dashboard:
enabled: "{{ 'web-app-dashboard' in group_names }}"
shared: "{{ 'web-app-dashboard' in group_names }}"
matomo:
enabled: "{{ 'web-app-matomo' in group_names }}"
shared: "{{ 'web-app-matomo' in group_names }}"
prometheus:
enabled: "{{ 'web-app-prometheus' in group_names }}"
shared: "{{ 'web-app-prometheus' in group_names }}"
# nocheck: playwright-service-flag — DB engine, covered by role-local integration tests
mariadb:
enabled: true # nocheck: dynamic-flag
shared: "{{ 'svc-db-mariadb' in group_names }}"
# nocheck: playwright-service-flag — cache/queue/socketio buses, covered by role-local integration tests
redis:
enabled: true # nocheck: dynamic-flag
shared: "{{ 'svc-db-redis' in group_names }}"
erpnext:
backup:
no_stop_required: false # Frappe's bench backup requires a quiesced site
image: frappe/erpnext
version: "X.Y.Z" # latest stable semver at the time of the PR
name: erpnext
min_storage: 15GB
ports:
local:
http: <free port> # nginx frontend
socketio: <free port>
run_after:
- svc-db-mariadb
- svc-db-redis
- web-app-keycloak
- web-app-mailu
lifecycle: alpha
cpus: "2.0"
mem_reservation: 2g
mem_limit: 4g
pids_limit: 2048
meta/variants.yml (three variants per Decision #11)¶
---
# V1: sso + ldap together (everything that can be true, is true)
- services:
sso:
enabled: true
shared: true
ldap:
enabled: true
shared: true
email:
enabled: true
shared: true
# … all other dynamic flags true …
# V2: no auth — everything false
- services:
sso:
enabled: false
shared: false
ldap:
enabled: false
shared: false
email:
enabled: false
shared: false
# … all other dynamic flags false …
# V3: ldap only
- services:
sso:
enabled: false
shared: false
ldap:
enabled: true
shared: true
email:
enabled: false
shared: false
# … all other dynamic flags false …
Frappe site config (Decision #7 — Redis DB-number split)¶
The role MUST template sites/{{ canonical_domain }}/site_config.json (or the equivalent common-site-config layer) so Frappe’s three Redis logical roles all point at the shared svc-db-redis instance with distinct DB numbers:
{
"redis_cache": "redis://<svc-db-redis-host>:<port>/0",
"redis_queue": "redis://<svc-db-redis-host>:<port>/1",
"redis_socketio": "redis://<svc-db-redis-host>:<port>/2"
}
DB numbers (0 / 1 / 2) are stable for v1; if svc-db-redis later partitions tenants, this requirement gets swept by the same migration.
Acceptance Criteria¶
Routing & TLS¶
[x]
next.erp.{{ DOMAIN_PRIMARY }}resolves throughsys-svc-proxyto the ERPNext Nginx frontend and returns HTTP 200 onGET /with a Frappe-served HTML body (verified in matrix r3 across all three variants; CSPunsafe-evaladded to satisfy Frappe’seval()use).[x] WebSocket upgrade to the SocketIO container is wired (
websocketport inmeta/services.yml,BACKEND/SOCKETIOenv on the frontend nginx).[x] CSP
connect-srcwhitelist includes the canonical host and itswss://variant (mirrors theweb-app-odooprecedent).
Role layout & image¶
[x]
roles/web-app-erpnext/exists with the layout in the Target Schema above.[x]
meta/services.ymlpinsfrappe/erpnextto a concrete stable v15.x semver (no:latest, no:edge, no v14, no v16).[x]
meta/info.yml,meta/server.yml,meta/main.yml,meta/secrets.yml,meta/users.yml,meta/volumes.yml,meta/rbac.yml,meta/variants.ymlexist and pass the repo’s standard role-meta lint (per 008).
Central-service reuse (Decision #7)¶
[x] When
services.mariadb.shared=true(V1 + matchinggroup_names), Frappe connects to the centralsvc-db-mariadbvia the in-stackmariadbnetwork alias using svc-db-mariadb’ssecrets.credentials.root_passwordforbench new-site; whenshared=false(V2/V3)sys-svc-rdbmstemplates a per-role MariaDB container and bench uses the consumer’s per-role db password as root.[x] Frappe’s three Redis logical roles share one instance via DB-number split (
cache=0,queue=1,socketio=2) using the in-composeredisalias.[x] When
services.mariadb.enabled(always true for ERPNext) is unsatisfiable,sys-stk-fullfails the deploy at the database-readiness gate before bench-bootstrap runs.
SSO / OIDC (Decisions #4, #5)¶
[x] When
web-app-keycloakis ingroup_names, the existingweb-app-keycloak/redirect_urisfilter auto-includes ERPNext’shttps://next.erp.<DOMAIN_PRIMARY>/*in the realm’s shared OIDC client (no per-role Keycloak entry needed).[x] Frappe’s Social Login Key for Keycloak is auto-created by
tasks/04_oidc.ymlviafiles/scripts/apply/oidc_settings.py(provider=Keycloak,custom_base_url=1,auth_url_data={response_type:code,scope:openid}, login_via_keycloak redirect path).[x] V1 OIDC + administrator + biber Playwright specs land on the Frappe desk (matrix r3 5-passed-2-skipped on V1).
[ ] Group mapping (Decision #5): the path-to-roles map is persisted (
apply/oidc_settings.pywrites it tofrappe.db.set_default("erpnext_oidc_group_role_map", …)), but reconciling the map into Frappe roles on each OIDC login still needs ahooks.pyon_session_creationhook in a small custom Frappe app. Deferred to a follow-up requirement; documented in README.
LDAP (V3 variant + V1 dual)¶
[x] When
svc-db-openldapis ingroup_names, Frappe’s LDAP Settings doctype is auto-configured bytasks/05_ldap.ymlviafiles/scripts/apply/ldap_source.py. End-to-end LDAP-login (auto-create Frappe user from LDAP bind on first sign-in) is deferred to a follow-up requirement — needs additional Frappe-side attribute mapping + user-creation hooks; documented in README. The corresponding Playwright login specs (test-login-via-ldap-*) are intentionally absent in v1.[x] When both are in
group_names(variant V1), the role deploys cleanly with OIDC as the primary login button.
Email (Decision #8 — outbound only in v1)¶
[x] When
web-app-mailuis ingroup_names, ERPNext’s outboundEmail Accountis auto-configured bytasks/06_email.ymlviafiles/scripts/apply/email_account.py(usesdb_insert/db_updateto bypass Frappe’s deploy-time SMTP socket validation).[x] When
web-app-mailuis NOT ingroup_names,tasks/01_core.ymlskips the email subtask viawhen: ERPNEXT_EMAIL_ENABLED(verified by the V2 all-false variant deploying clean without an Email Account record).[x]
roles/web-app-erpnext/README.mdnotes that IMAP inbound (mail-to-Communication) is deferred to a follow-up requirement and points to the upstream Frappe Email Account doctype for operator-manual inbound config in the meantime.
First-admin bootstrap (Decision #9)¶
[x]
apply/api_bot.pysetssetup_complete=1on theSystem Settingsdoctype + writes the global default; verified in matrix r3.[x] The Frappe
Administratoruser is seeded bybench new-site --admin-password=$ERPNEXT_INITIAL_ADMIN_PASSWORD; thetest-login-native-administrator.jsPlaywright spec exercises the break-glass local login in every variant (passed in matrix r3).[x]
bench install-app erpnextruns as part ofbench new-site --install-app erpnextintasks/03_bench_bootstrap.yml; the long-lived Frappe containers are restarted afterwards so their cached app set picks up the new install (otherwise/loginserves HTTP 500 from a stale app cache).
Variants (Decision #11)¶
[x]
meta/variants.ymldefines exactly three variants in this order: V1 sso+ldap, V2 all-false, V3 ldap-only.[x] All three variants deploy cleanly on a fresh box (FULL matrix gate
make compose-deploy mode=reinstall apps=web-app-erpnext full_cycle=true purge=true— 6 PLAY RECAPs, allfailed=0).
Backup (Decision #13 — documented operator command in v1)¶
[x]
roles/web-app-erpnext/README.mddocuments the manual backup command (bench --site {{ canonical_domain }} backup --with-files) and the resulting artefacts (*.sql.gz+*-files.tar+*-private-files.tar).[x]
roles/web-app-erpnext/README.mddocuments restore (the inversebench --site … restore+ central-MariaDB import path).[x] No
pre_backup_hookschema is introduced insvc-bkp-*here; that is deferred to a follow-up requirement.
Playwright (Decision #10, per 019)¶
[x]
roles/web-app-erpnext/files/playwright/test-login-biber.jscontains the biber-persona spec, exercising the SSO-sign-in path landing on an authenticated ERPNext surface. (Flat layout per the existingweb-app-zammadprecedent.)[x]
roles/web-app-erpnext/files/playwright/test-login-administrator.jscontains the administrator-persona spec, exercising the SSO-sign-in path landing on the Frappe desk.[x]
roles/web-app-erpnext/files/playwright/test-login-native-administrator.jsadds a break-glass local-login spec forAdministrator(runs in every variant — not gated on SSO/LDAP).[x] OIDC specs gate on
SSO_SERVICE_ENABLEDper the standardservice-gating.jshelper (viashared.skipUnlessServiceEnabled("sso")), so they skip-correctly under V2/V3.[x]
templates/playwright.env.j2emits the standard service-flag set per 019 Rule 6; unused flags carry# nocheck: playwright-service-gatemarkers with documented rationale.
Health & quality¶
[x] ERPNext’s compose stack reaches a steady running state across all three variants in matrix r3 (
backend,frontend,websocket,scheduler,queue-short,queue-long, plus the one-shotconfigurator). v1 ships two workers (queue-short+queue-long) per the canonical frappe_docker v15 layout —queue-defaultis absorbed intoqueue-short’s--queue short,defaultflag.[x] No fatal failures in matrix r3 (all PLAY RECAPs
failed=0; Playwright runs all passed-or-skipped, no failures).[x]
make testis green tree-wide (the role passes role-meta lints, services contract lints, playwright-services-parity lints, and the newtest_task_name_lengthlint that caps Ansiblename:strings at 120 chars).
Documentation¶
[x]
roles/web-app-erpnext/README.mddocuments: image source + bump policy, the central-MariaDB and central-Redis (3-DB-number split) consumer pattern, the OIDC group-mapping reconciliation, the variant matrix, the wizard-bypass bootstrap path, and the backup / restore flow.[ ] This requirement file is cross-linked from the implementing PR (per docs/contributing/requirements.md#cross-linking).
Validation Apps¶
The role MUST deploy cleanly under all three variants on a fresh box. V1 (sso + ldap) and V3 (ldap-only) additionally MUST pass the biber + administrator Playwright personas.
INFINITO_APPS="web-app-erpnext" \
make deploy-fresh-purged-apps INFINITO_FULL_CYCLE=true
End-to-end smoke after deploy:
Visit
https://next.erp.{{ DOMAIN_PRIMARY }}/— Frappe / ERPNext login page renders, no wizard.Click the “Login with Keycloak” SSO button — Keycloak login flow completes, user lands on the ERPNext desk (
/app).Open
/app/erpnext— ERPNext landing page renders (not bare Frappe desk).(V1 / mail variant) Trigger a Frappe notification (e.g. a forgot-password flow) — the outbound mail leaves via Mailu and arrives at the target inbox.
(V1 / SSO + group mapping) An OIDC user in the
roles/web-app-erpnext/administratorgroup has theSystem ManagerFrappe role assigned after first login.
Prerequisites¶
Before starting any implementation work, the agent MUST read AGENTS.md and follow all instructions in it.
Implementation Strategy¶
The agent MUST execute this requirement autonomously. Open clarifications only when a decision is genuinely ambiguous and would otherwise block progress; default to the intent already captured in this document and proceed. Avoid back-and-forth questions on choices already resolved in Confirmed Decisions.
Read Compose Loop before starting.
Scaffold the role using
roles/web-app-odoo/as the structural template (closest analogue: ERP-shaped, OIDC + LDAP variants, central-service consumer pattern, dual HTTP + WebSocket vhost).Wire the upstream
frappe/erpnextimage into the compose template, plus the SocketIO, scheduler, three worker, and Nginx frontend containers (per frappe_docker).Template
common_site_config.jsonwith the central MariaDB endpoint and the three Redis URLs split by DB number.Implement
tasks/03_bench_bootstrap.yml:bench new-site→bench install-app erpnext→ wizard-bypass API call → Social Login Key seeding.Add Keycloak client auto-provisioning in
web-app-keycloakfor the new ERPNext consumer.Add the biber + administrator Playwright specs.
Wire the
svc-bkp-pre-backup hook (bench backup --with-files).Iterate
make testuntil green, then run the Validation deploys.
Commit Policy¶
The agent MUST NOT create any git commit until every Acceptance Criterion in this document is checked off (
- [x]).A single commit (or a tight, related sequence) lands the whole role addition; no half-scaffolded intermediate commits.
When all ACs are met,
make testis green, and the three variants deploy cleanly, the agent instructs the operator to rungit-sign-pushoutside the sandbox (per AGENTS.md). The agent MUST NOT push.
Context¶
Upstream framework: https://frappeframework.com/
Upstream product docs: https://docs.frappe.io/erpnext
Upstream container reference: https://github.com/frappe/frappe_docker
Closest in-repo analogue for layout:
roles/web-app-odoo/Central-service reuse precedent: 022 Zammad
Playwright coverage parity contract: 019
Role meta layout contract: 008
Unified SSO schema: 021