Local test directory
deploy/dev is an OpenLDAP for development and manual testing. It holds enough data to make listings, paging and search behave the way they do against a real domain, and it is shaped like Active Directory where that matters to the application.
It is not the quick start’s directory. The quick start runs everything in Docker and reaches its directory over LDAPS inside the Docker network. This one is published on your machine so that an API run from the IDE or dotnet run can reach it.
Starting, stopping, resetting
docker compose -f deploy/dev/docker-compose.ldap.yml up -d --wait # start (first start loads data, ~40 s)
docker compose -f deploy/dev/docker-compose.ldap.yml down # stop, keep data
docker compose -f deploy/dev/docker-compose.ldap.yml down -v # stop and delete everything
On Linux without membership in the docker group, prefix each with sudo.
The data is loaded only on the first start of an empty directory. To pick up a change to any of the LDIF files, down -v and start again.
| Address | ldap://127.0.0.1:389, plain LDAP, no TLS |
| Base DN | dc=example,dc=org |
| Directory admin | cn=admin,dc=example,dc=org / adminpassword |
Config admin (cn=config) | cn=admin,cn=config / config (the image’s default) |
| Every account’s password | Axia-Demo-1 |
TLS is off on purpose. The quick start’s certificate names the host openldap, which a process outside Docker does not reach it as, and certificate verification in the API cannot be relaxed. Configure the API with Ldap:Security=None for this directory; see Installation, path 3.
The same file starts a PostgreSQL for the API’s own state, which the Development example configuration already points at:
| Address | 127.0.0.1:5432 |
| Database, user | nelsar_axia / nelsar_axia |
| Password | nelsar-axia-dev |
up -d --wait postgres starts it alone, for an API pointed at a directory of your own. down -v deletes its data along with the directory’s.
What is in it
The files are loaded in name order:
| File | Loaded as | Contents |
|---|---|---|
ad-schema.ldif | 40-ad-schema | AD’s user, computer and group classes and the attributes the API uses (sAMAccountName, userPrincipalName, userAccountControl, dNSHostName, operatingSystem, operatingSystemVersion, groupType, managedBy, adminCount, lastLogonTimestamp, …), with AD’s own OIDs. |
ad-memberof.ldif | 45-ad-memberof | Points the memberOf overlay at group/member, as AD maintains it. |
../quickstart/bootstrap.ldif | 50-demo | The quick start’s demo accounts and role groups. |
test-data.ldif | 60-test | A UserDeleter account and group, more units, and the group shapes the API reads: groupOfNames, groupOfUniqueNames, posixGroup, a nested group. |
bulk-users.ldif | 70-bulk | 300 OpenLDAP-style (inetOrgPerson) accounts in ou=staff, with a group per department. |
ad-data.ldif | 80-ad-data | An AD-shaped domain under ou=Corp. See below. |
Totals after a fresh start: about 1,525 accounts, 2,056 computer accounts, 20 printers, 110 groups and 74 organizational units.
Accounts that sign in with a role
| Login | Role | Group |
|---|---|---|
admin.demo | Admin | cn=axia-admins,ou=groups,dc=example,dc=org (protected) |
creator.demo | UserCreator | cn=axia-creators,ou=groups,… (groupOfUniqueNames) |
editor.demo | UserEditor | cn=axia-editors,ou=groups,… |
deleter.demo | UserDeleter | cn=axia-deleters,ou=groups,… |
user.demo | User | none |
Every other account signs in as User.
The AD-shaped domain (ou=Corp)
ou=Corp,dc=example,dc=org
├── ou=Users
│ └── ou=Almaty | Astana | Shymkent | Karaganda
│ └── ou=IT | Finance | Sales | HR | Marketing | Support | Logistics | Legal | Management
├── ou=Disabled Users leavers, userAccountControl 514
├── ou=Service Accounts adm.* (adminCount=1) and svc.* accounts
├── ou=Computers
│ ├── ou=Workstations
│ │ └── ou=Almaty | Astana | Shymkent | Karaganda ALA-WS-00001, ALA-NB-00002, …
│ ├── ou=Servers
│ │ └── ou=Domain Controllers | File | Database | Web | Application | Remote Desktop
│ └── ou=Printers device entries (RFC 4519), not AD computers
└── ou=Groups
├── ou=Security DL-VPN-Users, DL-Share-*-RW/RO, GG-Workstations-*, GG-Servers, …
├── ou=Distribution All Staff, Managers, Sales Team, … (with mail)
├── ou=Departments GG-<dept>-<office>, GG-<dept> (nested), GG-Office-*
├── cn=Domain Admins adminCount=1
├── cn=Enterprise Admins adminCount=1
├── cn=Server Operators adminCount=1
├── cn=Domain Users every enabled account
└── cn=Domain Computers every computer account
Accounts carry cn as the display name (as AD does), sAMAccountName (first.last, at most 20 characters), userPrincipalName and mail (first.last@example.org), title, department, company, employeeID, l, telephoneNumber, mobile, manager and lastLogonTimestamp. About 4% are disabled (userAccountControl 514) and sit in ou=Disabled Users.
Computers carry sAMAccountName with a trailing $, dNSHostName, operatingSystem and operatingSystemVersion (Windows 10/11, Windows Server 2019/2022, Ubuntu, RHEL, macOS), userAccountControl (4096 workstation, 8192 domain controller, +2 when disabled, about 6% are), managedBy (the owner), location and lastLogonTimestamp.
Groups are AD group objects with sAMAccountName and groupType (global, domain-local or universal; security or distribution), some with managedBy or mail. They include nested groups (GG-<dept> contains the office groups; All Staff contains department groups) and one empty group, DL-Empty-Legacy.
Any of these accounts can sign in by sAMAccountName, userPrincipalName or mail, provided Ldap:UserSearchFilter includes those attributes (the default filter does).
Where it differs from real Active Directory
The point is to exercise the application, not to emulate AD. Things that behave differently:
userAccountControlis not enforced by the directory. OpenLDAP stores the flag but does not act on it, so a disabled account still binds. The application reads the flag itself, so the account is still refused (401) and loses any session it has; Active Directory refuses the bind before that.- Computers carry
sn. OpenLDAP’spersonclass requires it, AD’s does not. Because of this, creating a computer through the API fails against this directory (502, “The directory rejected the creation of the entry”): the API writes AD’s attribute set, which has nosn. Listing, editing, enabling, disabling, moving and deleting computers all work. memberOffollows onlygroup/member. The overlay can follow one class.memberOfis therefore missing for thegroupOfUniqueNamesgroups (axia-creators,it-staff). The API also reads membership from the groups themselves, so roles are unaffected.- Passwords are
userPassword, set through the LDAP Password Modify operation. AD usesunicodePwd. - No
objectGUID/objectSid, nowhenCreated, no Global Catalog. - Size limits do not apply: the API binds as the database’s root DN, which OpenLDAP exempts from limits.
Regenerating the data
Both generators are seeded, so they produce the same file every time unless asked otherwise.
python3 deploy/dev/generate-ad-data.py # 1,200 users, 2,000 computers
python3 deploy/dev/generate-ad-data.py --users 5000 --computers 10000 # bigger
python3 deploy/dev/generate-bulk-users.py --count 500 # the ou=staff accounts
Then down -v and up to load it. To add a generated file to a running directory instead:
ldapadd -x -H ldap://127.0.0.1 -D cn=admin,dc=example,dc=org -w adminpassword -c -f deploy/dev/ad-data.ldif
The compose healthcheck waits for cn=Domain Computers,ou=Groups,ou=Corp,dc=example,dc=org, the last entry the generator writes, so --wait returns only once everything is loaded.
Useful queries
# everything disabled, users and computers (AD's bitwise-AND rule works in OpenLDAP too)
ldapsearch -x -LLL -H ldap://127.0.0.1 -D cn=admin,dc=example,dc=org -w adminpassword \
-b ou=Corp,dc=example,dc=org -E pr=500/noprompt '(userAccountControl:1.2.840.113556.1.4.803:=2)' dn
# one account's groups
ldapsearch -x -LLL -H ldap://127.0.0.1 -D cn=admin,dc=example,dc=org -w adminpassword \
-b dc=example,dc=org '(sAMAccountName=alexei.rakhimov)' memberOf
# count computers (paged, so it is not cut at the server's size limit)
ldapsearch -x -LLL -H ldap://127.0.0.1 -D cn=admin,dc=example,dc=org -w adminpassword \
-b dc=example,dc=org -E pr=500/noprompt '(objectClass=computer)' dn | grep -c '^dn:'