Back openDesk Edu for a sovereign, open-source education — every vote counts.
Vote nowSave products you love by clicking the heart icon.
Eine gründliche Evaluierung von SOGo 6, der Next.js/Flask-Groupware-Suite, betrieben mit Stalwart Mailserver und OpenLDAP via Docker Compose. Behandelt Architektur, Feature-Implementierung, SSO, MFA und >1.800 bestandene Tests.

Nachdem ein Benutzer, der via Keycloak OIDC automatisch provisioniert wurde, erfolgreich in LDAP erstellt wurde, kann der Proxy von OpenCloud kein Session-Token ausstellen. Der Benutzer sieht „Sie werden eingeloggt“, gefolgt von „Nicht angemeldet“. Jedes. Einzelne. Mal.
Dies ist die Geschichte darüber, wie drei unabhängige Bugs aufeinandertrafen, um diesen Fehler zu verursachen – und was es brauchte, um sie zu finden.
OpenCloud 4.0.3, bereitgestellt via Helm auf Kubernetes, konfiguriert mit einem externen UMS LDAP (OpenLDAP) als Identity-Backend. Benutzer authentifizieren sich über Keycloak/Shibboleth via OIDC. Auto-Provisioning ist aktiviert: Wenn sich ein Benutzer zum ersten Mal anmeldet, erstellt OpenCloud dessen LDAP-Eintrag on-the-fly.
Der Fehler war konsistent und reproduzierbar:
https://opencloud...Die OpenCloud-Logs zeigten ein klares Muster:
graph: failed to add user → LDAP Result Code 68 "Entry Already Exists"
graph: could not create user: backend error → nameAlreadyExists
proxy: Error Response → OData Error: a user with that name already exists
proxy: Error getting token for autoprovisioned user → user not found
Der Auto-Provision-Flow sah so aus:
Jeder Login-Versuch stieß an eine Wand. Und der Benutzer existierte TATSÄCHLICH in LDAP. Drei unabhängige Bugs waren dafür verantwortlich.
EQUALITY auf openCloudUUIDDer Auto-Provision-Code durchsucht LDAP nach einem existierenden Benutzer per UUID, bevor ein neuer erstellt wird. Der Benutzer existierte – aber die Suche lieferte null Ergebnisse.
Test der LDAP-Suche direkt:
$ ldapsearch ... "(openCloudUUID=*)"
→ returns the entry (presence check works)
$ ldapsearch ... "(openCloudUUID=b7ada882-...)"
→ returns 0 entries (equality check FAILS)
Das Attribut existierte. Der Wert war korrekt. Aber der Equality-Matching-Vergleich funktionierte nicht.
Der Attributtyp openCloudUUID im OpenLDAP-Schema wurde ohne eine EQUALITY-Regel geladen:
olcAttributeTypes: ( 1.3.6.1.4.1.99999.1.1
NAME 'openCloudUUID'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
# Configmap definition (correct):
olcAttributeTypes: ( 1.3.6.1.4.1.99999.1.1
NAME 'openCloudUUID'
EQUALITY caseIgnoreMatch ← MISSING from loaded schema!
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
Ohne EQUALITY caseIgnoreMatch kann OpenLDAP keinen Equality-Matching-Vergleich auf dem Attribut durchführen. Der LDAP-Schema-Job prüfte lediglich auf das Vorhandensein neuer Attribut-OIDs – er hat nie verifiziert, ob bestehende Attribute die korrekten Matching-Rules hatten. Wenn also ein altes Schema geladen wurde (aus einer früheren Chart-Version, der EQUALITY fehlte), haben nachfolgende Upgrades dies nie korrigiert.
EQUALITY-Regel zum laufenden OpenLDAP via ldapmodify:ldapmodify -Y EXTERNAL -H ldapi:/// <<'EOF'
dn: cn={53}opencloud,cn=schema,cn=config
changetype: modify
replace: olcAttributeTypes
olcAttributeTypes: ( 1.3.6.1.4.1.99999.1.1 NAME 'openCloudUUID'
DESC 'OpenCloud user UUID'
EQUALITY caseIgnoreMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
olcAttributeTypes: ( 1.3.6.1.4.1.99999.1.2 ... )
olcAttributeTypes: ( 1.3.6.1.4.1.99999.1.3 ... )
olcAttributeTypes: ( 1.3.6.1.4.1.99999.1.4 ... )
EOF
EQUALITY bei openCloudUUID zu prüfen, nicht nur auf das Vorhandensein von Attribut-OIDs.Nachdem die UUID-Suche behoben war, lieferte der LDAP-Lookup immer noch null Ergebnisse – aber diesmal lag der Grund tief im Suchfilter vergraben.
Der reva LDAP-User-Provider baut einen Suchfilter für GetUserByClaim("userid", uuid) auf. Durch das Tracing im OpenCloud-Quellcode ergab sich folgender Filteraufbau:
filter = fmt.Sprintf("(&%s(objectclass=%s)(%s=%s)%s%s)",
i.User.Filter,
i.User.Objectclass, // "openCloudUser"
attribute, // "openCloudUUID"
value, // "b7ada882-..."
i.tenantFilter(tenantID), // ""
i.disabledFilter(), // "(!(openCloudUserEnabled=FALSE))" )
The resulting filter:
(&(objectclass=openCloudUser)(openCloudUUID=b7ada882-...)(!(openCloudUserEnabled=FALSE)))
Testing it directly:
$ ldapsearch ... "(&(objectclass=openCloudUser)(openCloudUUID=b7ada882-...))" → 1 Eintrag gefunden
$ ldapsearch ... "(&(objectclass=openCloudUser)(openCloudUUID=b7ada882-...)(!(openCloudUserEnabled=FALSE)))" → 0 Einträge gefunden
### The Root Cause
LDAP uses three-valued logic: TRUE, FALSE, and **UNDEFINED**. When an attribute doesn't exist on an entry:
- `(attr=FALSE)` → UNDEFINED (the attribute isn't present, so the comparison can't be evaluated)
- `(!(attr=FALSE))` → NOT(UNDEFINED) → **UNDEFINED**
- `(TRUE AND TRUE AND UNDEFINED)` → **UNDEFINED** → entry is **not returned**
The user entry in the external UMS LDAP didn't have an `openCloudUserEnabled` attribute. This is an OpenCloud-internal attribute that doesn't exist in the external LDAP. The `disabledFilter()` was designed for OpenCloud's internal IDM LDAP (which has this attribute), but when pointed at an external LDAP, it silently filtered out **every user**.
The `DisableUserMechanism` was set to `"attribute"` by default, which adds the `(!(openCloudUserEnabled=FALSE))` filter. In OpenCloud's internal IDM, every user has this attribute set to `TRUE`. In an external LDAP, nobody does.
### The Fix
```yaml
# values.yaml
oidc:
roleAssignmentDriver: "default"
# → setzt Umgebungsvariable OC_LDAP_DISABLE_USER_MECHANISM=none
Setting OC_LDAP_DISABLE_USER_MECHANISM=none tells the users service to skip the disabled filter entirely. This is the correct setting when using an external LDAP that doesn't manage OpenCloud-specific attributes.
After fixing the LDAP search, the login flow progressed further — but failed with a new error:
proxy: no roles in user claims
proxy: Error mapping role names to role ids → oidcroles.go:84
proxy: Could not get user roles → account_resolver.go:192
The proxy was configured with PROXY_ROLE_ASSIGNMENT_DRIVER=oidc, which reads role information from OIDC claims and maps them to OpenCloud roles. Our Keycloak instance doesn't send roles in the OIDC token — it's a simple authentication-only setup.
The OIDC role mapper iterates over the claims, looks for roles, finds none, and returns an error. This error propagates up through the account resolver, which aborts the login.
I initially tried GRAPH_ASSIGN_DEFAULT_USER_ROLE=true, which controls whether the Graph API assigns a default role when creating users. But the error was coming from the Proxy after user creation, during token issuance. Two different code paths, two different env vars.
The PROXY_ROLE_ASSIGNMENT_DRIVER supports two values:
| Driver | Behaviour |
|---|---|
oidc | Reads roles from OIDC claims. Fails if claims have no roles. |
default | Assigns the role "user" to any user without a role at login time. |
The oidc driver is designed for setups where Keycloak sends roles via an OIDC claim (e.g., roles, groups, or a custom mapper). When used with a Keycloak that doesn't send roles, it's a hard blocker.
# values.yaml
oidc:
roleAssignmentDriver: "default"
# → setzt Umgebungsvariable PROXY_ROLE_ASSIGNMENT_DRIVER=default
The default driver checks if the user already has a role assigned. If not, it assigns the built-in "user" role. This is the correct choice for most simple OIDC setups.
Here's how the three bugs interacted:
Benutzer authentifiziert sich via OIDC
↓
Proxy ruft GetUserByClaims("userid", uuid) auf
↓
Gateway delegiert an users service (LDAP backend)
↓
Bug #1: LDAP-Schema → UUID-Equality-Suche liefert 0 Einträge
Bug #2: disabledFilter → bestehender Benutzer wird stillschweigend aus den Ergebnissen ausgeschlossen
↓
GetUserByClaims → ErrAccountNotFound
↓
Proxy ruft CreateUserFromClaims auf → Graph API → LDAP add → "Entry Already Exists"
↓
Cloud gibt nameAlreadyExists zurück → CreateUserFromClaims liest Benutzer erneut aus → gibt Benutzer zurück
↓
Proxy ruft GetUserByClaims erneut auf → immer noch ErrAccountNotFound (Bugs 1 & 2 erneut)
↓
Proxy läuft weiter → versucht Rollenzuweisung
Bug #3: OIDC-Driver → keine Rollen in den Claims → Fehler
↓
"No roles in user claims" → 401 → "Nicht angemeldet"
Jeder dieser Bugs wäre in einer anderen Konfiguration einzeln verkraftbar gewesen:
Aber zusammen bildeten sie eine absolut undurchdringliche Mauer.
LDAP-Schema-Gleichheitsregeln verifizieren. Präsenzprüfungen (attr=*) können einwandfrei funktionieren, während Gleichheitsprüfungen (attr=value) stillschweigend fehlschlagen. Testen Sie immer beides.
Die dreiwertige Logik von LDAP ist nicht intuitiv. (!(attr=FALSE)) ist KEIN No-Op für fehlende Attribute – es ist UNDEFINED, was Einträge aus den Suchergebnissen ausschließt. Wenn Sie einen „disabled“-Filter hinzufügen, stellen Sie sicher, dass jeder Benutzer das Attribut tatsächlich besitzt.
Wissen, welcher Dienst welche Umgebungsvariable besitzt. GRAPH_ASSIGN_DEFAULT_USER_ROLE (Graph API, während der Benutzererstellung) und PROXY_ROLE_ASSIGNMENT_DRIVER (Proxy, während des Logins/der Token-Ausstellung) steuern unterschiedliche Phasen desselben Flows. Die Behebung der falschen Variable bewirkt nichts.
Immer nach jedem Fix erneut testen. Das Debugging von drei gestapelten Bugs ist nur machbar, wenn jeder Fix unabhängig verifiziert wird, bevor man zum nächsten übergeht. Die Fehlermeldungen änderten sich bei jedem Schritt – was die unabhängige Bestätigung jedes Fixes ermöglichte.
Alle drei Fixes wurden in die OpenCloud-Revision 50 eingespielt, wobei die Chart-Templates aktualisiert wurden, um ein erneutes Auftreten bei zukünftigen Deployments zu verhindern.