Features
Everything the application does, by area. Each section names the permission it needs, the API endpoints behind it, and the console screen. Endpoint details are in the REST API reference.
- Signing in
- Users
- Bulk operations
- Login generator
- Groups
- Organizational units
- Computers
- Directories (providers)
- Role mappings and role permissions
- Rate limits
- Audit log
- Console
- Languages
Signing in
POST /api/v1/auth/login · no permission · rate-limited (auth-login, 5 per minute per address; auth-login-account, 5 failed per 15 minutes per account)
- Field
emailtakes any login:uid,sAMAccountName,userPrincipalNameor e-mail. How it is looked up: Directories → Accounts. - The password is checked by binding to the directory as that account. The application never stores or compares passwords.
- With several directories, the console offers a picker; the API takes
X-LDAP-Provider-Id. - The response carries the token, the user (name, e-mail, DN, groups, role) and the current permissions.
- Every attempt, successful or not, is audited.
Self-registration: POST /api/v1/auth/register creates a directory account for an unauthenticated caller. Off unless Auth:AllowSelfRegistration=true. Leave it off.
Users
| Action | Permission | Endpoint |
|---|---|---|
| Search | users.read | GET /api/v1/users/search?filter=&pageSize=&pageToken= |
| Read one | users.read | GET /api/v1/users/{id} |
| Read all raw attributes | users.read | GET /api/v1/users/{id}/attributes |
| List everyone | users.list | GET /api/v1/users |
| Create | users.create | POST /api/v1/users |
| Edit name, e-mail, password | users.update | PUT /api/v1/users/{id} |
| Delete | users.delete | DELETE /api/v1/users/{id} |
Search splits the text on spaces, commas and semicolons and matches each term as a substring of mail, displayName, cn, uid, sAMAccountName, userPrincipalName, givenName or sn. The search runs in the directory, then the result is paged.
List everyone reads the whole directory in one response (no paging) and says isComplete: false if the directory stopped it partway.
Raw attributes shows every attribute of the entry under the directory’s own names. Binary values are flagged; values of credential attributes (passwords, LAPS) are withheld and marked isRedacted.
Create takes name, e-mail and password and writes an inetOrgPerson at uid=<local part>,<BaseDn>; see what is written, including the Active Directory caveat. An e-mail already in use is a 409, and so is a login that another entry already has or that a posixGroup still lists as a member (see group membership).
Edit changes displayName, mail and/or the password. The login (RDN, uid) does not change with the e-mail. A new e-mail must not belong to another account.
Protected targets: editing, resetting the password of, and deleting an account are refused when it holds more than you do, is privileged in the directory, or is the bind account. See Access control.
Computers and contacts never appear here, even though they are person entries in AD.
Bulk operations
users.bulk, plus users.create for create rows and users.update for update rows.
| Mode | Endpoint | Answer |
|---|---|---|
| Synchronous | POST /api/v1/users/bulk/sync | 207 Multi-Status with one result per row |
| Queued | POST /api/v1/users/bulk | 202 Accepted with { "jobId": "…" } |
| Queued job status | GET /api/v1/users/bulk/status/{jobId} | state, counts, per-row results |
A batch is a JSON array of rows:
[
{ "operation": "create", "email": "ivan.petrov@example.org", "name": "Ivan Petrov", "password": "S3cure-Pass!" },
{ "operation": "update", "email": "olga.sidorova@example.org", "name": "Olga Sidorova", "newEmail": "o.sidorova@example.org" },
{ "operation": "update", "email": "john.smith@example.org", "name": "John Smith", "password": "N3w-Pass!" }
]
| Field | create | update |
|---|---|---|
operation | "create" (default) | "update" |
email | the new account’s address | finds the account to change |
name | required | required; the new display name |
password | required | optional; sets a new password |
newEmail | — | optional; changes the address |
Behaviour:
- Rows are independent. A bad row fails on its own with a message; the others go ahead. Results are
completedorfailedper row. - Permission is checked per batch at submission (and again when a queued job runs, as the person who queued it): a batch containing create rows needs
users.create, update rowsusers.update. Protected-target checks apply per row. - Every row is audited, under the submitter’s name, including password resets.
- Queued jobs live in memory. They are lost if the API restarts, and they are not shared between instances. Job states:
pending,inprogress,completed,failed. - Sizes. A synchronous batch holds at most 100 rows, since the caller waits on it; a queued one at most 10,000, as many logins as one generator preview. Larger, or empty, is
400. - The queue holds 10 jobs besides the one running. A full queue answers
503withRetry-After; nothing is queued, so the same batch can simply be sent again. - A job’s status is its owner’s. Only the account that queued it, signed in to the same directory, can read it; anyone else gets
404, as for a job that does not exist.
In the console: Users → Bulk operations, which accepts pasted rows and shows per-row results, polling queued jobs.
Login generator
users.generate · POST /api/v1/users/generate
Produces candidate logins for a bulk create, and optionally checks which already exist. It writes nothing.
From a mask
{a..b} ranges are expanded and combined:
| Pattern | Produces |
|---|---|
node-{01..03} | node-01, node-02, node-03 (zero padding follows the wider bound) |
srv-{a..c} | srv-a, srv-b, srv-c |
lab{1..2}-pc{01..02} | lab1-pc01, lab1-pc02, lab2-pc01, lab2-pc02 |
A malformed range ({01-03}, a descending {03..01}) is kept literally. Nested braces produce nothing.
From pasted text
Paste one record per line in rawText, give a regular expression with named groups in regexPattern, and build each login with usernameTemplate:
{
"rawText": "Иванов Игорь\nҚанат Әсел",
"regexPattern": "^(?<lastname>\\S+)\\s+(?<firstname>\\S+)$",
"usernameTemplate": "{firstname}.{lastname}",
"transliterate": true,
"checkConflicts": true,
"maxItems": 100
}
→ igor.ivanov, asel.qanat, each with exists: true/false in details.
- Without a template, the whole match is used.
transliteratemaps Cyrillic, including Kazakh letters, to Latin (Щербак→Shcherbak,Қанат→Qanat).- Results are trimmed, lower-cased and de-duplicated.
- The regex has a 500 ms timeout per line; lines that time out or do not match are skipped. Over the whole text it has 2 seconds, after which the request is refused with
400. maxItems: 1 to 10,000 (default 50).- Lengths:
patternandusernameTemplateup to 256 characters,regexPatternup to 512,rawTextup to 1,000,000. checkConflictslooks each login up in the directory. If the directory is unreachable, the request fails rather than answering “no conflicts”.
Groups
| Action | Permission | Endpoint |
|---|---|---|
| List / search | groups.read | GET /api/v1/groups?filter=&pageSize=&pageToken= |
| Read one | groups.read | GET /api/v1/groups/{id} |
| Create | groups.manage | POST /api/v1/groups |
| Rename, move, describe, set members | groups.manage | PUT /api/v1/groups/{id} |
| Delete | groups.manage | DELETE /api/v1/groups/{id} |
- Lists AD
group,groupOfNamesandgroupOfUniqueNamesunder the base DN.posixGroupentries are used for role membership but not listed here. - Search matches every term against the name, DN or description (all terms must match). Member DNs are not searched.
- Members are DNs. Saving replaces the whole membership with the list sent. A
groupOfNamescannot be left empty (schema rule), so the API refuses an empty list for one. - Create writes a
groupOfNameswith at least one member. In AD that is not a security group; see Directories. - Changing or deleting a group mapped to a role you do not fully hold, or a protected group, is refused. So is renaming a group to a name that a more powerful mapping uses.
- Against the Windows
ActiveDirectoryprovider, every group endpoint answers501.
Organizational units
| Action | Permission | Endpoint |
|---|---|---|
| List / search | orgunits.read | GET /api/v1/organizational-units?filter=&pageSize=&pageToken= |
| Read one | orgunits.read | GET /api/v1/organizational-units/{id} |
| Create | orgunits.manage | POST /api/v1/organizational-units |
| Rename, move, describe | orgunits.manage | PUT /api/v1/organizational-units/{id} |
| Delete | orgunits.manage | DELETE /api/v1/organizational-units/{id} |
- A flat list (name, DN, parent DN, description), not a tree.
- The parent can be any container inside the base DN, including ones not in the list (the base DN itself, or AD’s
CN=Userswhen the base DN is the domain). A parent outside it is400; the same goes for groups and computers, created or moved. - Deleting a unit that still contains entries is refused by the directory:
409. Nothing is deleted recursively. - Renaming or moving a unit is judged by what it holds, since everything beneath moves with it: a group mapped to a role you do not hold, or anything privileged in the directory, makes it
403unless you hold every permission, and the unit holding the bind account is not renamed or moved at all. See protected targets. - The
idis derived from the DN, so it changes on rename or move.
Computers
| Action | Permission | Endpoint |
|---|---|---|
| List / search | computers.read | GET /api/v1/computers?filter=&pageSize=&pageToken= |
| Read one | computers.read | GET /api/v1/computers/{id} |
| Pre-stage an account | computers.manage | POST /api/v1/computers |
| Rename, move, describe, set DNS name, enable/disable | computers.manage | PUT /api/v1/computers/{id} |
| Delete | computers.manage | DELETE /api/v1/computers/{id} |
- Lists AD
computerand RFC 4519deviceentries. Shows name, DN, DNS name, operating system and version, description, and enabled state (nullfordeviceentries, which have none). - Search matches name, DN, description, DNS name and operating system.
- These manage directory entries, not machines. Creating pre-stages an account a machine can claim when it joins; renaming does not rename the machine; deleting revokes a joined machine’s trust and it must rejoin.
- Enable/disable flips the
ACCOUNTDISABLEbit ofuserAccountControlonly. - Domain controllers (writable or read-only), critical system objects and computers with
adminCountare changed and deleted only by someone holding every permission:403otherwise. - Creating fails against OpenLDAP; see Directories.
Directories (providers)
providers.manage · /api/v1/providers · console: LDAP providers
Add, edit, deactivate and delete directories stored in the database. Fields: name, host, port, security (Ldaps/StartTls/None), provider type, failover hosts, base DN, bind account, bind password (write-only; never returned), user search filter, page size, active.
- The bind password is encrypted with
Encryption:Keybefore it is stored. - Changing host, port or failover hosts requires the password again, as does switching security to
None. GET /api/v1/providers/selectableis anonymous (for the sign-in picker), returns only ids and names of active directories, and is rate-limited (providers-selectable, 30 per minute).- The console marks directories using
Noneas not encrypted.
Details: Directories → Several directories.
Role mappings and role permissions
roles.manage · /api/v1/role-mappings, /api/v1/roles · console: Role mappings, Roles
Map directory groups (by DN) to roles, globally or per directory, and choose what each role may do. Admin is fixed. See Access control.
Rate limits
ratelimits.manage · /api/v1/rate-limit-settings · console: Rate limiting
Fixed-window limits, retunable without a restart (picked up within 30 seconds): per client address, and for sign-in also per account.
| Policy | Applies to | Default |
|---|---|---|
auth-login | /api/v1/auth/login, /api/v1/auth/register | 5 requests / 60 s |
providers-selectable | /api/v1/providers/selectable | 30 requests / 60 s |
auth-login-account | /api/v1/auth/login, per login and directory, from any address | 5 failed sign-ins / 900 s, then 429 with Retry-After until the window ends; a successful sign-in clears the count |
Each has permitLimit, windowSeconds, queueLimit (requests held rather than refused, default 0) and enabled. Over the limit: 429 with a message and Retry-After.
The client address is the TCP peer, or the X-Forwarded-For value from a proxy listed in ReverseProxy. Behind an unlisted proxy, everyone shares one bucket.
Audit log
audit.read · GET /api/v1/audit-events?pageSize=&pageToken= · console: Audit log, and the dashboard
Written for every sign-in, and for every command that changes the directory or the application’s settings, whether it was allowed, refused or failed. Newest first.
| Field | |
|---|---|
eventType | The operation, e.g. CreateUser, UpdateGroup, DeleteComputer, UpsertRateLimitSettings |
actor | Who: the signed-in account, or for a queued bulk job the person who queued it. Never taken from the request body. |
objectId, objectDn | What it was done to, where known |
action, details | Description, including the error type and message for failures |
success | Whether it went through |
timestamp | UTC |
Bulk batches get one entry for the batch and one per row. The audit log lives in the same database as everything else and is written by the same process; it records what happened, but is not tamper-proof.
Console
The React console at frontend/. In development it runs on http://localhost:5500; in the container it is served by nginx on port 8080.
| Screen | Shows | Needs |
|---|---|---|
| Sign-in | login, password, directory picker, language; an API address field on the development server only | — |
| Dashboard | counts (users, directories, role mappings, groups, units), recent audit events; tiles the role cannot see say so | any |
| Users | search, create, edit, delete, raw attributes; Bulk operations tab with the generator | users.read and up |
| Groups | list, search, create, edit, members, delete | groups.read |
| Organizational units | list, search, create, rename/move, delete | orgunits.read |
| Computers | list, search, pre-stage, edit, enable/disable, delete | computers.read |
| LDAP providers | directories in the database | providers.manage |
| Role mappings, Roles | group → role mappings; role → permissions | roles.manage |
| Rate limiting | the two policies | ratelimits.manage |
| Audit log | paged events | audit.read |
The sidebar footer has the theme switch (light/dark), the language picker and sign-out (which forgets the token in the browser; the token itself stays valid until it expires).
Menus and buttons are hidden by role for convenience only. The API enforces everything.
Languages
English (default), Russian, Kazakh, German, Spanish, French, Chinese, Arabic and Hebrew. Arabic and Hebrew mirror the layout.
The console sends the chosen language as Accept-Language, so the API’s own messages (errors, validation) come back in it too. Any API client can do the same.
Translations other than English and Russian are machine-quality. Adding a language is one dictionary in frontend/src/lib/i18n/ and three .resx files on the API side; both sides fail the build or the tests if a message is missing.