-
Notifications
You must be signed in to change notification settings - Fork 613
[FEATURE][AUTH]: LDAP / Active Directory integrationΒ #284
Copy link
Copy link
Labels
MUSTP1: Non-negotiable, critical requirements without which the product is non-functional or unsafeP1: Non-negotiable, critical requirements without which the product is non-functional or unsafedatabaseenhancementNew feature or requestNew feature or requestpythonPython / backend development (FastAPI)Python / backend development (FastAPI)readyValidated, ready-to-work-on itemsValidated, ready-to-work-on itemssecurityImproves securityImproves security
Milestone
Description
π§ Epic β LDAP / Active-Directory Integration
| Field | Value |
|---|---|
| Title | Enterprise Directory Sync & LDAP Bind Support |
| Goal | Import users + groups from on-prem AD/LDAP and (optionally) allow direct LDAP-bind login when the IdP is unavailable. |
| Why now | Our SSO rollout (SAML / OIDC) depends on clean group data. Many customers still need just-in-time sync from AD and a fail-safe auth path that bypasses the IdP during outages. |
| Depends on | βAdd SSO & IdP-Issued Tokensβ epic (for base user / table schema). |
| Related | Feature #220 β SSO + IdP Integration |
π§ Type of Feature
- Security hardening
- New capability
- Operations / reliability
πββοΈ User Story 1 β Directory Sync
As a: Tenant admin
I want: the Gateway to import users, groups and memberOf relations from our AD every hour
So that: RBAC policies always reflect the source-of-truth directory.
β Acceptance Criteria
Scenario: Hourly sync detects new user
Given "eve" exists in LDAP under "ou=Users,dc=corp,dc=com"
And "eve" is a member of "cn=ml-team,ou=Groups,dc=corp,dc=com"
When the scheduled sync job runs
Then "eve" is present in gateway.users
And "ml-team" is present in gateway.groups
And a link row exists in gateway.group_members(user_id=eve, group=ml-team)πββοΈ User Story 2 β LDAP Bind Fallback Login (optional flag)
As a: On-call engineer
I want: users to sign-in with AD credentials when the IdP is down
So that: critical dashboards stay reachable during SAML/OIDC outages.
β Acceptance Criteria
Scenario: LDAP bind succeeds
Given the IdP health-check is failing
And user "alice" enters AD username/password
When gateway performs an LDAP simple bind with those creds
Then it issues a session cookie scoped to alice
And maps aliceβs AD groups to gateway rolesπββοΈ User Story 3 β Group-Derived RBAC Refresh
As a: Security auditor
I want: group membership changes in AD to propagate within < 1 hour
So that: removed users lose access promptly.
β Acceptance Criteria
Scenario: User removed from AD group
Given "bob" was a member of "finance-analyst"
And an admin deletes bob from that group in AD
When the next sync runs
Then bobβs gateway session is invalidated
And subsequent API calls return 403 "forbidden_scope"π Design Sketch
flowchart TD
subgraph "Active Directory / LDAP"
LDAP[(ldaps://ad.corp.com)]
end
SyncJob["LDAP Sync Cron"] -->|hourly search| LDAP
LDAP --> SyncJob
SyncJob --> DB[(gateway.users / groups)]
Browser --> LoginUI
LoginUI --> AuthService
AuthService -->|if IdP down| LDAP
AuthService -->|else| IdP[(SAML / OIDC)]
π Roll-out Plan
| Phase | Feature Flag | Key Tasks |
|---|---|---|
| 0 | EXPERIMENTAL_LDAP_SYNC off |
Implement read-only sync job targeting staging AD replica. |
| 1 | on (shadow) | Sync to prod; no bind login yet; verify RBAC mapping accuracy. |
| 2 | EXPERIMENTAL_LDAP_BIND off |
Add bind-login endpoint behind separate flag; security review & rate-limiting. |
| 3 | on (optional) | Enable bind login for DR run-books; document rollback. |
π§ Tasks
-
Connector & Schema
- Add
ldap_connector.py(usesldap3, paged searches). - Extend
users,groups,group_memberswithexternal_source='ldap'. - Store
last_sync_at,ad_dnfields.
- Add
-
Sync Scheduler
- Reusable cron-style job (default 60 min, configurable).
- Full import β diff β upsert; delete-orphan flag.
-
Bind Auth Endpoint
-
POST /auth/ldap(username, password). - Simple bind over LDAPS, follow referrals, 5 s timeout.
- Issue same JWT/session-cookie pipeline.
-
-
RBAC Cache Invalidation
- Push βgroup-changeβ events β session store.
- Middleware denies stale tokens.
-
DevOps / Secrets
- Vault path for
LDAP_BIND_DN/LDAP_BIND_PW. - TLS pinning for LDAPS certificates.
- Vault path for
-
Monitoring & Alerts
- Prometheus:
ldap_sync_success,ldap_bind_latency. - Alert if sync fails > 3 Γ or bind error rate > 5 %.
- Prometheus:
-
Docs & Run-books
- βHow to map OUsβ guide.
- Disaster-recovery login procedure.
-
Test Infrastructure
- Docker Compose file with OpenLDAP + phpLDAPadmin for local smoke-tests.
- Optional Helm chart (
osixia/openldap-stack-ha) for CI/kind clusters. - CI script that seeds users/groups, runs sync, and asserts DB state.
π§ͺ Testing with OpenLDAP (no IdP required)
1. Spin-up
# docker-compose.yml (drop in repo root)
version: "3"
services:
ldap:
image: osixia/openldap:1.5.0
container_name: ldap
environment:
LDAP_ORGANISATION: "Example Corp"
LDAP_DOMAIN: "example.org"
LDAP_ADMIN_PASSWORD: "admin"
ports:
- "389:389" # LDAP
- "636:636" # LDAPS (self-signed cert)
phpldapadmin:
image: osixia/phpldapadmin:0.9.0
environment:
PHPLDAPADMIN_LDAP_HOSTS: "ldap"
ports:
- "8080:80"
depends_on: [ ldap ]docker compose up -d
# LDAP β ldap://localhost:389 , admin DN "cn=admin,dc=example,dc=org" / pw "admin"
# GUI β http://localhost:8080 (login with same DN)2. Seed demo data
# seed.ldif
dn: ou=Users,dc=example,dc=org
objectClass: organizationalUnit
ou: Users
dn: uid=alice,ou=Users,dc=example,dc=org
objectClass: inetOrgPerson
uid: alice
sn: Liddell
cn: Alice Liddell
userPassword: password
dn: uid=bob,ou=Users,dc=example,dc=org
objectClass: inetOrgPerson
uid: bob
sn: Builder
cn: Bob Builder
userPassword: password
dn: cn=data-science,ou=Users,dc=example,dc=org
objectClass: groupOfNames
cn: data-science
member: uid=alice,ou=Users,dc=example,dc=org
member: uid=bob,ou=Users,dc=example,dc=org
docker cp seed.ldif ldap:/seed.ldif
docker exec ldap \
ldapadd -x -D "cn=admin,dc=example,dc=org" -w admin -f /seed.ldif3. Smoke-check
ldapsearch -x -H ldap://localhost:389 \
-D "cn=admin,dc=example,dc=org" -w admin \
-b "dc=example,dc=org" "(uid=alice)" uid cn memberOf
# β verify alice appears and is in data-science4. Wire to Gateway
LDAP_URI=ldap://localhost:389
LDAP_BASE_DN=dc=example,dc=org
LDAP_BIND_DN=cn=admin,dc=example,dc=org
LDAP_BIND_PW=admin
Run the sync job β database now holds alice, bob, data-science.
Call /auth/ldap with alice/password β expect JWT & gateway access.
π Alternatives Considered
| Approach | Pros | Cons | Decision |
|---|---|---|---|
| IdP-only (no LDAP) | Simpler, fewer ports | IdP outage = total lockout | β |
| One-time CSV import | No LDAP secrets | No automatic updates | β |
| SCIM 2.0 | Standard provisioning | AD needs extra tooling | π Future |
π Additional Context
- Security β Bind DN is least-privilege, read-only; LDAPS :636 only.
- Performance β Sync of 10 k users / 1 k groups β 15 s.
- Compliance β Only import
sAMAccountName,mail,memberOf. - Backwards-compat β If IdP + LDAP both fail, legacy local admin remains.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
MUSTP1: Non-negotiable, critical requirements without which the product is non-functional or unsafeP1: Non-negotiable, critical requirements without which the product is non-functional or unsafedatabaseenhancementNew feature or requestNew feature or requestpythonPython / backend development (FastAPI)Python / backend development (FastAPI)readyValidated, ready-to-work-on itemsValidated, ready-to-work-on itemssecurityImproves securityImproves security