Skip to main content

Groups and the attribute schema

Applies to: SimpleTwo 0.9.x, directory 0.2 · Verified: 24.09.2026

Groups, address books, mail aliases and the attribute schema live in a server role of their own, identity — its own Postgres database, its own host. All of it is managed in the coordinator's console: people, guests, groups and bots under Users; sign-in, applications and synchronisation under Security. In the platform console the Directory card (Settings → Directory) shows only the role's state: the host, the version, the public name, the account reconciliation, and a link to the portal /me (from 26.09.2026, version 0.9.286 and later; before that the same blocks were on the card too).

When the card is empty​

"no directory host yet" — no host is enrolled for the role. Enrolment is the calendar's: Servers → role identity, a private address is required (the coordinator and the services reach the role on it), a public name is optional. The role's store is embedded SQLite files on its own host (/var/lib/simpletwo/directory, since directory 0.76.0): it needs no database on the postgres role. An installation whose directory data lived in PostgreSQL moves it by itself at the first start of 0.76.0 and never reads that database again.

Reaching directory from outside​

A public name is what the directory sign-in, the portal /me and the console /admin need. With it, the service obtains its own certificate on 443. The Reach field at enrolment: direct — the name points at the host's own public address; nat — at the organisation's address, whose NAT forwards 443 to this host. The DNS record for the name is yours to create. Clients are handed the name only once the coordinator has reached it; the line under the card's heading shows the last check: what the name resolves to, whether it is announced, and why not.

Kinds of groups​

kindwho owns the membershipfor
syncedthe identity provider (ADFS/AD) — written at every sign-in, read-only hereroles, gateways, address-book policy
unita rule over attributes: company, department, site@dept, a team's calendar, an audience for rules
manageda list from the console, or a ruleon call, reviewers, a project's people
resourcethe rule entity: resourcerooms in the booking picker
publisheda list; visibility federated laid down, switched on with the federation phase"our contacts at X"
personalthe person; visible to them alonefavourites, "my team"
systemthe platform; not deletablerole groups

The kind cannot change after creation: a group of another kind is another group.

A dynamic group's rule​

Directory computes a dynamic group's membership itself: the coordinator sends it every directory event together with the whole profile, the rule is re-evaluated for that person, and membership rows are added or removed. Changing the rule recomputes the group over everybody directory already knows; a rule saved before the first directory event stays empty until the nightly reconcile — the log says so. Such a membership is never edited by hand.

rule is a map of key to value; every pair must match. The keys are what an account really carries: the schema's attributes plus user, display_name, is_guest, entity. entity takes person, service, resource, a comma-separated list, !resource ("everybody except the rooms") or any.

A key nothing carries is refused at save, naming the key and listing the ones that exist. Not pedantry: on 08.09.2026 a welcome rule was narrowed on is_room: false — no such variable existed, the rule stopped matching anybody, and for ten days it silently greeted nobody while the console showed it enabled.

Who may address a group​

writers — who may mention the group, write to its address, ring it: user:<name>, role:<name>, group:<id>. Empty means anybody who can see the group. There is no ceiling on reach: @sales in a company of four thousand is four thousand notifications, and the control is who may, not how many it hits. owners may edit the membership, writers and join without being administrators. address makes the group a distribution list — of any kind, dynamic included.

Messaging enforces this at send time: a message with the @handle of a group the sender may not address is refused whole, with the code group_not_addressable — it does not go out "as text" with the mention quietly dropped. For those who may, @handle counts as a mention of every member: a notification at the "mentions only" level, a Do-Not-Disturb breakthrough, auto-follow in a thread. Messaging receives its copy of the group from directory after every edit; between the edit and the copy there are seconds, and if messaging was unreachable the change arrives with the next edit of that group.

In automation rules a group is one more when condition: group: <id or @handle> beside entity narrows the rule to the group's members at the moment of the event.

Owners and self-service​

owners — people (user:<name>) or a role (role:<name>) who run the group without being administrators: they change its name, writers, join and default_convs, add and remove members, and set a membership's term (expires_at — a contractor until Friday, a stand-in on call). The group's kind, rule, source, address and the owners list itself stay with the administrator — those change what the group is, not who is in it.

join decides how people get in: closed (the default) — an owner adds them; approval — a person asks, the owners named by name are notified and decide in the request queue; open — anybody who can see the group walks in. Any member may leave. A lapsed membership is removed on its own within ten minutes and appears in the audit as group.member.expire.

default_convs — conversations a new member is added to on appearing in the group, by whichever path: an owner, an approved request, a sync from AD. A conversation that does not exist is skipped with a log line; the group itself still updates.

The attribute schema​

Every attribute is one definition: kind (text, number, date, birthday, phone, address, url, choice, multichoice, user, bool), source — exactly one writer (self, admin, claim, ldap, hrms, scim), visibility (everyone, unit, managers, admins, self), searchable, required, the values of a choice, pattern. The LDAP and SCIM names are filled in with our own prefix (x-simpletwo-…, …:extension:simpletwo:2.0:User:…) unless you name standard ones.

What the schema refuses, so that it stays a schema:

  • a kind change — widening text to choice would silently invalidate every stored value; retire the definition and define a new key;
  • removing a choice a value may carry;
  • deleting a definition — only Retire: values stay readable to administrators, the key stays reserved, Restore brings it back;
  • computed attributes ("full name from two fields") — a stored derivation goes stale with nothing saying so.

A definition added here is a valid key in group rules and automations the same moment.

The base set. The schema ships with a base after SCIM 2.0 (RFC 7643): the core (nickname, photo, the name in parts — given_name, family_name, middle_name, phones, job title, preferred language), the Enterprise extension (department, company — SCIM's organization, division, employee_number, cost_center, manager) and the platform's extension (office, status, hire_date, birthday, the two HRMS managers, nativefullname — the full name in the native language). LDAP names are the standard ones where they exist; the rest sits under the x-simpletwo-… prefix. The directory seeds the base on start and never overwrites your edits. The photo, when the photo definition's source is claim, is taken from the ID token's claim (thumbnailPhoto with ADFS) at every sign-in through the provider and stored like a photo from LDAP; the LDAP photo pass stands down then.

SCIM. A provisioner — Entra, Okta, an HRMS — writes and reads people at /scim/v2 by these definitions (directory 0.91.0 and later): whatever a definition places is kept on the way in and given back on the way out — the name in its parts, the job title, the work and the mobile number, the Enterprise attributes and the platform's own; /scim/v2/Schemas describes them with their types. A standard attribute no definition places (an address, a fax number) is not kept, and the audit names it; an Enterprise or platform attribute nobody defined is refused until you define it. A PUT clears an extension's attributes only when it carries that extension, and leaves alone what SCIM does not carry. Filters take RFC 7644's whole grammar on any attribute. Pushing to an application sends the core and the Enterprise attributes, the manager by the application's own id. From directory 0.94.0 a person's place in the organisation's tree is served too, read-only, as orgUnit (the unit's id, name and path) under the platform's own schema; filtering on orgUnit.path selects a subtree.

Whole groups from Active Directory​

A sign-in through ADFS brings only the names of the groups the signing-in person is in. For a directory that is complete before the first sign-in — an owner on every group, the full membership — switch the LDAP source on: in the console, under Security → Synchronisation, the LDAP source — the directory's jobs card → Edit the source → group_base_dn (for example OU=Groups,DC=corp,DC=example; the default filter is (objectClass=group)). Every six hours and on Sync groups now directory reads group objects and people in paged searches, matches each member to an account — by userPrincipalName, then mail, then sAMAccountName; the unmatched are counted, never guessed — names managedBy as the owner, and writes the group under the same id a sign-in mints, so an AD group learned of twice stays one group. Two AD groups whose names reduce to one id (case, punctuation, the cut at 48 characters) are told apart by objectGUID: the id stays with the group already holding it, and the other gets a stable suffix from its GUID. A group in which nothing changed is not rewritten. The last pass line on the card shows groups, written, memberships, unmatched, owners — or the error.

Where the directory's jobs run. The group, people and photo passes and the SimpleOne HRMS module are directory's: directory is the directory and the issuer. The coordinator used to run them, and its settings move by themselves: as soon as a directory host is deployed in the installation, the coordinator hands it, once, the LDAP connection with its bind password, the incremental read's watermarks, the last results, the SimpleOne connection with its secret and the HRMS settings, and after directory's answer deletes its own copies — each secret has one owner. directory keeps the secrets sealed, like its other integration secrets. After the move the "Directory photos (AD / LDAP)" and "SimpleOne (HRMS)" cards in the platform console say "Managed on directory" and link to Security → Synchronisation; the "let everyone upload their own photo" switch stays on the platform, and directory is told its value. Without directory (a small installation) the jobs stay on the coordinator and are set on its cards as before; the same holds while directory is too old to take them, and the card says so.

People from the directory. On the same card — Sync people now and a pass every six hours: each account here takes from the directory the attributes the directory schema declares with source: ldap and an ldap_name (and only those — an attribute the schema does not know is never stored under its AD name), and its manager — the manager DN becomes the name of an account here. Standing (the disabled bit, accountExpires) is counted on every pass and applied only with the "apply standing" switch on: it is the one part of the pass that can lock a person out, so look at the number of differences first. Changes travel as ordinary directory events — to messaging and to directory's people projection — so a dynamic group on department moves the same hour AD does. directory writes the change straight into the account of record, and the coordinator takes it from the account change feed, as it takes SCIM's writes.

Leavers. LDAP does not announce a deletion, so the people pass finds the accounts that came from the directory (ADFS sign-in) and are no longer in it, and names them on the card. With the "apply standing" switch on they are blocked. If there are more than a tenth of them (or more than half in a small organisation), or the directory returned nobody, the pass blocks no one and says why — that is a broken base DN or filter, not a wave of departures. Local accounts and guests are never touched.

Changes only. Against Active Directory the people pass reads only what changed since the last time (by uSNChanged on the same domain controller); the whole directory is read on the first pass, when the controller changes, when the profile or the mapped attributes change, and once a day. A person deleted from AD is seen at once if the service account may read CN=Deleted Objects; otherwise on the daily full read. The card says which kind the last read was. The group pass reads the same way: only the groups that changed (a membership change is a change of the group); a group deleted in AD is removed from directory by its tombstone in CN=Deleted Objects or on the daily full read, and a renamed one is recognised by its objectGUID. Only groups this pass itself wrote are removed, and no more than a tenth at a time.

Passwords during coexistence. A directory never exports a password hash, so the same card has a password pass-through switch: while an account has no local credential, directory decides its password by binding to the directory as the person — over ldaps:// only; directory refuses a profile with ldap://. Nothing is stored: local credentials accumulate as people enrol them, and the Directory card counts how many still have none — the number the day the directory is switched off is planned by. A wrong password counts toward the lockout like a local one; a directory that does not answer is not a refusal but the message "the company directory did not answer", and it counts against nobody. The switch itself is on the LDAP source card under Security → Synchronisation; the connection profile lives in one place — directory, on the LDAP source — the directory's jobs card (before the jobs move, the platform console's card, from which it is copied to directory).

Photos from the directory. A person's photo (thumbnailPhoto, or the attribute the source names) is read by directory at their sign-in, for the people an incremental pass saw change, and on Sync all photos; directory keeps it — at most 512 KB, and only JPEG or PNG, checked by content — and hands it to messaging, which serves the avatars. An unchanged photo is not sent again; a photo removed from the directory is removed in directory and in messaging. Only accounts from the directory (signed in through the company provider) have one; everybody else uploads their own. While the platform's "let everyone upload their own photo" is on, or the photo arrives in an ID-token claim, the directory leaves photos alone.

SimpleOne HRMS. On the same tab: the connection to the SimpleOne instance (https:// only, Basic or Bearer; the password or token is kept sealed, and moving the instance to another address needs it entered again — a secret never goes to a new host; the same holds for the LDAP bind password) and the HRMS module — the path to the employee table, the field map (attr_map), the schedule map (schedule_map: a schedule's name → days and hours), the interval and dry run. Preview writes nothing and shows how many records matched, what would change and which schedule names the instance answers with; Sync now works only with the module on and dry run off. Attributes that feed the platform's sign-in claims are not written by HRMS — the report lists them. The working hours the schedule map translates a person's schedule to are written on the account, and the platform hands them to the calendar.

Address books​

Personal books are stored on the server for the sake of a second device, but they are not shown in the console and cannot be read through the coordinator — there is no route to another person's book for anybody. The organisation's book (owner = org) is shared: external partners, suppliers, hotlines; administrators maintain it.

Import and export. A book takes vCard (3.0 and 4.0) and CSV and gives back the same: POST …/books/{book}/import with the file as the body, GET …/books/{book}/export?format=vcf|csv. A contact here is a pointer (an address, a number, a user) plus a label and a note, so a card contributes its EMAIL, TEL and X-SIMPLETWO-USER, FN becomes the label and NOTE the note; everything else (ORG, ADR, photo) is counted in the report as dropped_properties, and a card with no pointer at all lands in refused with its line number. Duplicates by "kind + target" against what the book already holds are counted, not written; importing again — the book's own export included — creates nothing. An imported contact's id is a keyed MAC of the book and the pointer under the service's secret, so a database dump cannot test guessed addresses or numbers against it; contacts imported before keep their earlier ids. CSV: a header of kind,target,label,note,pinned in any order, , or ; as the separator. The audit line carries counts only.

The global address list: who sees whom​

Whom a person finds in search, sees in the contacts list and may start writing to is decided by the directory role from address-book policies, the way Exchange does it. The coordinator console, Users tab, Address book card.

  • An address list is a filter over the directory. Its conditions are AND-ed: members of the listed groups; the account kind (member, guest, partner, bot, system, resource); attribute values; "shares a group with the reader". A list with no condition is the whole organisation.
  • An address-book policy is a set of lists and the groups it is assigned to. It is assigned to groups only, never to roles.
  • A person sees the union of the lists of every policy their groups carry. When none of their groups carries one, the default policy applies — the built-in one, the whole organisation.
  • A guest and the employee who invited them always see each other, whatever the policies.
  • People one already shares a conversation with but whom one's lists leave out are found as a name only: no title, no phones, nothing else of the card.
  • An account with "Show in the address book" cleared is found by its exact address only.

A conversation may be started with somebody the address book shows in full, or somebody one already shares a conversation with; messaging asks directory and answers not_reachable otherwise. What does this person see shows the address book of a given login — check a policy with it before assigning it to a group.

After upgrading to the release with policies, employees see what they saw before. Guests and partners whom a role used to limit see the whole organisation until a policy is assigned to their group: assign one to the guests' group (the built-in «Свои группы» — people who share a group with the reader — for example) right after the upgrade.

Mailboxes​

Directory keeps no mailboxes. Every mailbox — personal, shared, resource — and the access to it belong to the mail role. It takes people and groups from directory and decides itself who may open which mailbox. Calendars and address books belong to the person's account.

Credential policy and the help desk​

In the console, under Security → Sign-in requirements, the Credential policy card: minimum length, no reuse of the last N, expiry in days (0 by default — never: expiry without evidence of compromise is harm, but an auditor may require it), lock after N misses for M minutes, and the leaked-password check. The check is local: a shipped list of the most common passwords is always there, and the directory host may name DIR_BREACH_LIST — the Pwned Passwords export (SHA-1, ordered by hash); the file is searched by bisection and never loaded, and nothing leaves the role. A password moving over from the coordinator at first sign-in is not judged — the person already has it.

In the same place, the Sign-in requirements block (all off by default). Require a second factor (mfa_required): off, for roles (mfa_roles; empty = the roles that open the console) or for everyone. A person in scope without a second factor who signs in with a password is taken to a set-up step right on the sign-in page: the secret for the authenticator app and a code, or a new passkey, then the recovery codes. The grace period (mfa_grace_days, 0 = at the next sign-in) counts from the day the requirement first applied to the person; while it lasts the step can be postponed. If the only factor is a passkey, it is asked for after the password. The messenger's password sign-in holds the same requirement by refusing: after the grace no token is issued, and the answer {"mfa_enrolment_required": true, "portal": "<identity>/me"} sends the person to the portal. A sign-in through the company's provider is outside it: the provider owns those factors. For one application, options.require_mfa: true in its options: a one-factor sign-in (a password without a code, a passkey without verification on the device, a live session made so) is stepped up with the second factor before the code is issued. The application sees how the person signed in in the ID token: amr (pwd, otp, hwk/swk, mfa) and acr (urn:simpletwo:mfa or urn:simpletwo:1fa). Sign in with passkeys only (passwordless) is the identity provider's policy, not the directory's, which refuses it.

In the same place, Forgot password? on the sign-in page (self_reset: off by default, or on). An eight-digit code arrives in the SimpleTwo app on the devices where the person is already signed in — a channel that whoever knows only the username cannot read; nothing is sent to an address typed on the page, and the code never goes by e-mail. The page never says whether the account exists. The code works for 10 minutes, once, five tries; with a second factor enrolled it is asked after the code (or a recovery code). Company-directory accounts (LDAP, a domain of the company's provider) are not reset here — the password is changed in the directory; neither is an account with passkeys and no code: it signs in with the passkey and changes the password in the portal. At most three codes an hour per account and ten requests an hour from one address. After a reset every session and refresh token ends, password.reset.self is written to the log, and the person gets a notice.

In a user's row menu (⋯), once the directory role is deployed: Unlock, Temporary password (twelve characters without look-alikes, shown once; at the next sign-in the person must set their own), Reset 2FA. "Log out" now also ends sessions on directory's sign-in page and the refresh tokens of applications. Enabling the second factor issues eight recovery codes — once; each is accepted at the factor step instead of the app's code and is consumed. After the password and the factor, directory's sign-in page demands a replacement for a temporary or expired password before it issues a code to the application.

Sessions: how long a sign-in lasts​

In the console, under Security → Sign-in requirements, the Sessions card. By default a session on the sign-in page lasts 12 hours, there is no idle timeout, an application's (refresh) token lives 90 days from its last use with no overall limit, and the console does not ask to sign in again. Browser session, hours (web_max_hours, up to 720) is the absolute lifetime, however busy the person is. Idle timeout, minutes (web_idle_minutes, 0 = off, otherwise 5 to 10080) ends a session unused for that long. Application token unused, days (refresh_idle_days, up to 365) restarts at every token refresh. Application token at most, days (refresh_max_days, 0 = no limit) counts from the sign-in the chain began with; a lowered limit holds for tokens already issued too. Lost refresh answer: retry within, seconds (refresh_replay_seconds, 120 by default, up to 600, −1 = never). Every refresh gives the application a new token and ends the old one, and the old one presented again is taken for a copy and ends the session. When the answer to a refresh is lost (the app went to the background, the network changed), the app retries with the old token: within this window, while nobody has used the new token, the retry gets a new pair and the session goes on (audited as oidc.refresh_replay). Tokens bound to the device key retry with a proof by that key at any time. A retry after the new token was used still ends the session (oidc.refresh_reuse). Console: fresh sign-in after, minutes (reauth_for_console_minutes, 0 = off) makes the console ask, for the directory's sections, for a fresh sign-in when the last one is older. A single application can have its own options.access_ttl_minutes (the access token's lifetime, 5…1440, an hour by default) and options.refresh_max_days (replaces the policy's limit) in its options. The Sign out everyone button, after a confirmation, ends every browser session and application token of the organisation, tells each application with a logout address (backchannel_logout_uri) about each person, and is recorded in the audit (sessions.revoke_all). An access token already issued lives out its lifetime.

Under Security → Applications each application has Options — one JSON document: scopes — what it may ask for (a request outside the registration is refused, never silently narrowed); options.claims — which of the claims the scope would give it receives; options.attrs — schema keys sent as claims; options.groups_as — groups by id, handle or name; options.client_credentials — the grant for a service (confidential only; a token with sub: client:<id>, no ID token); options.device_flow — sign-in from a device without a keyboard: the device gets a code from POST /oauth/device, the person types it at identity_url/oauth/device, signs in as usual and approves, and the device collects the tokens when it polls. An application not marked first-party asks the person's consent once, on a page in their language; the portal lists the consents and revokes them (with the application's refresh tokens).

Access reviews​

Under Security → Access reviews: "review guests older than N days" and "review role holders older than N days" — once a day directory opens a review: a guest's to whoever invited them, a role holder's to their manager (the manager attribute); the reviewer is told in the messenger and answers in the portal identity_url/me (Groups): "still needed" or "revoke". Silence past the due date revokes: a guest is disabled, a role holder loses every role — a write of record the platform console picks up through the change feed. Kept access is reviewed again after the same period. Beside it: "lock a source address after N misses" — the brute-force guard per address, independent of the accounts. A wrong password and a wrong second-factor code both count as misses — on directory's sign-in page and in the messenger's sign-in (the coordinator sends the client's address with the code); from a locked address even a right code is refused.

SCIM 2.0: an HRMS or Entra writes people and groups in​

Security → Synchronisation, the SCIM 2.0 card: issue a token (shown once) and give the provisioner identity_url/scim/v2 with the bearer token. Users and Groups are supported (create, read, the filters userName/externalId/ emails.value/displayName/active, PATCH — active, name, e-mail, extension attributes, group members); the enterprise extension (department, manager, organization, division, employeeNumber) lands under the schema keys of the same names, and the urn:ietf:params:scim:schemas:extension:simpletwo:2.0:User extension under the keys named by their scim_path; an attribute with no key in the schema is refused by name. A user written there is an account of record with source: scim; the platform console picks it up within a minute through directory's change feed, and everything a console-made account gets — the directory event, the welcome trigger, the assistant, the calendar — follows.

A first load is best sent as one Bulk request (POST /scim/v2/Bulk, up to 100 operations and 1 MB): a group may reference people created in the same request by bulkId:<id>, even when they are listed below it; an operation whose reference points at a failed create is itself refused (409), and failOnErrors stops the request after that many errors. Lists sort (sortBy — userName, displayName, meta.lastModified; sortOrder — ascending/descending). Every resource has a version — the weak ETag W/"<revision>" in meta.version and the ETag header: If-Match on PUT/PATCH/DELETE answers 412 when the resource changed meanwhile, If-None-Match on GET answers 304 when it did not.

The SCIM client: directory provisions applications​

The other direction — the console → Security → Synchronisation → SCIM targets: an application that wants people and groups is added with its base_url (the SCIM 2.0 root, https only), the bearer token the application issued (never shown back), and what to send: users, groups, filter_group (only that group's members). A new person goes out as POST /Users, an edit as PUT /Users/{id} with the id the application returned; a leaver (deleted, or out of filter_group) is disabled with PATCH active=false, not deleted. A group is created with POST /Groups, its membership replaced with PATCH /Groups/{id}, a deleted group deleted. The push follows the same account and group change feeds the platform console and mail read, within a minute; each target has its own cursor. On a failure the cursor stays on the row that did not go, and the next attempt waits with a growing back-off (30 seconds to 30 minutes); the table shows the last success, the last error and how many changes are pending, and "Push now" tries at once. A new base_url is a new application: the push starts over. Every push and failure is an audit row scim.push.

The LDAP server for applications​

The printer, the Wi-Fi controller, the NAS, a GitLab without OIDC — the console's Security → Synchronisation, the LDAP server block. A read-only tree: ou=people (uid=<username>, inetOrgPerson: cn, sn, givenName, displayName, mail, telephoneNumber, memberOf and the schema's attributes under their LDAP names), ou=groups (cn=<handle>, groupOfNames with member), ou=services — bind accounts for applications, each with its own scope (people, groups, which attributes), the secret shown once; outside its scope an application gets noSuchObject. A person binds with their own password under the same lockout as everywhere; an account with a second factor cannot bind until the switch allows it. Every write is refused. The listeners are switched on in the directory host's settings: Servers → the directory row → Settings → the LDAP server field: off, ldaps (port 636), starttls (port 389, for applications that speak only ldap:// with StartTLS; a bind before StartTLS is refused) or both. The host applies it on its next poll and opens the ports itself. The certificate is the one the host already holds for its public name, so the field needs a public name, and nobody places PEM files or edits the env file by hand any more. Point applications at the public name (ldaps://id.example.com). Turning it off closes 636 and 389 the same way — only the ports the host opened for directory itself. The LDAP line on the Directory card shows what actually listens and with which certificate, and says so when the setting is not applied yet.

The posixAccount and posixGroup switch in the LDAP server block adds the attributes Linux and NAS clients use: on people uidNumber, gidNumber (equal to their own uid), homeDirectory /home/<username>, loginShell /bin/bash; on groups gidNumber and memberUid. Numbers are handed out once from a counter starting at 10000, and never change or get reused — files on a NAS stay with the same person.

Webhooks and the audit export​

Security → Synchronisation, the Webhooks card: an endpoint subscribes to lifecycle events (account created / updated / deleted, group changed / deleted, consent granted / revoked, or *) and receives each as JSON signed with X-S2-Signature when a secret is set — the same contract messaging's bots use. Three attempts; every delivery is an audit row; "Test" sends a probe. Export audit downloads the whole security event log as JSON Lines for a SIEM; filters on /control/audit/export: action= (a prefix such as login.), actor=, subject=, from=/to= (RFC 3339), since=/until= (row numbers).

Pushing the audit log to a SIEM, and how long it is kept​

The console → Logs & audit → Audit → SIEM push: the log can be sent as well as pulled. kind: syslog-tls — RFC 5424 over TLS in RFC 5425 frames to address (host:port), optionally with ca_pem (the collector's CA) and a client certificate client_cert_pem/client_key_pem; syslog-tcp — the same without TLS; https — POST batches of JSON lines to url with the bearer token. Every message carries the whole log row with its hash (facility authpriv, severity by outcome, MSGID the action). A cursor remembers the last row taken: after a failure the batch goes again after a back-off and nothing is skipped; the state line shows the cursor, the last success or error and the queue, and "Push now" tries at once. Keep the log, days (retain_days, 0 — everything, the default; otherwise at least 30): older rows are removed once a minute, but never before the configured SIEM target has taken them. The last removed row becomes the chain's anchor — verification starts from it, so the chain stays verifiable; a delete that bypasses this is still refused by the database.

The sign-in page: branding, terms of use, logout of applications​

The Branding card under the console's Security → Sign-in page: the organisation's name, a logo, one colour and a tagline in both languages — taken by every page directory serves (sign-in, consent, device, terms, the portal). Terms of use: a version above 0 is shown once at sign-in after the password and the second factor — the summary on the page and a link to the full document; a decline is a refused sign-in; a new version asks everybody again. An application with options.backchannel_logout_uri receives a signed logout token when the person signs out on the sign-in page or the help desk ends their sessions.

The directory in the console​

The directory has no console of its own: its sections are parts of the coordinator's console, each where one looks for it.

WhereWhat
Users → People, Guests, Groups, Botsaccounts, guests, groups (members, requests), bots
Users → Roles"Who may open this console" — beside "Groups → roles"
Users → Address bookaddress lists, address-book policies, the organisation's books
Security → Sign-in requirementssecond factor, credential policy, sessions, app passwords, access to SimpleTwo
Security → Sign-in pagesign-in domains, the company provider (OIDC, SAML), terms of use, branding, people's fields from the provider
Security → ApplicationsOIDC applications and signing keys
Security → Synchronisationthe LDAP source and its jobs, SimpleOne HRMS, SCIM (inbound and targets), the LDAP server, webhooks
Security → Access reviewsthe policy and the open reviews
Security → Emergency accessthe coordinator's break-glass account, vendor support access
Settings → Directorythe connection to the directory, moving the services, the attribute schema
Logs & audit → Sign-ins, Auditthe sign-in log, the directory's audit and the SIEM push
Overviewwhat needs attention, sign-ins over 14 days, the directory's setup

The right comes from the account in the directory: the platform's owner/admin roles (or a group named in "Who may open this console") give identity-admin — everything; helpdesk/support give help-desk — People only, with its four actions (unlock, temporary password, reset 2FA, end sessions) and the person page's reads. Every act is audited under the person's name.

Since 27.09.2026:

  • The console's Overview shows what needs attention in the directory: administrators and help desk without a second factor, accounts locked now, group join requests, overdue access reviews, SCIM targets, the SIEM push and webhooks failing (with the last error), an LDAP directory not answering or a company provider refusing, the audit chain and its storage alarm. A card opens the section where it is fixed (for people, with the filter applied). Below: sign-ins over 14 days (signed in and refused per day) and the setup checklist: public address, signing keys, an administrator with a second factor, a company provider or LDAP source, branding, SIEM, an access review policy.
  • People is a table of name and username, e-mail, source, state (active, disabled, expired, locked, guest), second factor and last sign-in; filter chips (all, locked, disabled, guests, no second factor, no local credential, admins, admins without 2FA), search on username, name and e-mail, sort by a header, Export CSV of the filtered rows. A row opens the person page: the four help-desk actions in its header and the tabs Profile (attributes with the schema's labels, JSON collapsed), Credentials (password and its age, TOTP, recovery codes, passkeys and app passwords — each removable), Groups (remove, add), Sessions (end one or all), Sign-ins, Consents (third-party applications, revoke) and Activity (every audit row where the person acted or was acted on).
  • Logs & audit → Sign-ins is the organisation's sign-in log over 30 days: time, person, how, application, address, result with the reason; filters by person, result, application, method and date. A row opens a panel with every field and the audit row's number and hash.

The help desk sees People only: the person page's read tabs (not Consents or Activity), the four actions and ending a single session; removing a passkey or an app password and revoking a consent are identity-admin's.

The person's portal — identity_url/me​

Everybody signs in at identity_url/me through directory's own sign-in page (the client me, registered by the role itself at start once DIR_PUBLIC_URL is set) and sees: profile (what the directory knows about them, attributes through the schema with a label in their language and a note of who sees each); security (change the password — with the current one; enable the second factor — the secret and an otpauth:// link for the authenticator app, eight recovery codes shown once; sessions on the sign-in page and in applications, "sign out everywhere"); groups (mine, leave, join by id — open admits, approval sends a request to the owners); address books (create, import vCard/CSV, export, delete); applications — the launcher: everything they may sign in to, one click each. An application's launcher address is set under Security → Applications (⋯ → "Launcher address"). The portal works when the coordinator does not: it signs in through directory's provider.

Signing in through directory (OpenID Connect)​

Directory is an OpenID Connect provider: /.well-known/openid-configuration on its public address, the code flow with PKCE, EdDSA keys with rotation, a sign-in page in Russian and English (the language comes from the browser or ui_locales). An application is registered in the console under Security → Applications: public (a phone, a browser app — no secret, PKCE required) or confidential (GitLab, vCenter — the secret is shown once and can be rotated); redirect addresses are https only. Sign-in checks the password and the second factor held by directory, ten misses lock for fifteen minutes; every decision is an audit row. The SimpleTwo console and clients can name directory as their provider by its discovery address.

The company's providers behind directory. Where the organisation still has ADFS, Entra or Keycloak, it is connected as an upstream: the console → Security → Sign-in page → "Company provider" — issuer, client_id, secret, the list of mail domains. There may be several (the console → Security → Sign-in page → "Company provider", the list): each has an id, a button label in Russian and English (label), its own domains and provision. The sign-in page shows one button per provider; a provider's domain signs in only through it — a password here is refused, and another provider cannot vouch for that domain. After the provider's sign-in directory verifies the ID token and finds the account by claim (upn, preferred_username, email, or the configured username_claim). When there is no account: provision: off (the default) — "no account here"; provision: create — directory creates an account of record (source: oidc, name and e-mail from the standard claims, only in that provider's domains), the platform console receives it through the change feed, and the sign-in is audited (oidc.upstream.provision). The redirect address is one for every provider and is shown on the card. The earlier single-provider setting is read as a list of one (id: default). The provider's own second factor (mfa_via_upstream: true): when a sign-in made through this provider must step up — an application with require_mfa, a SAML application asking for MFA — directory does not ask for its own code or passkey but sends the person back to the provider with acr_values (mfa_acr_values; by default https://refeds.org/profile/mfa and http://schemas.microsoft.com/claims/multipleauthn, which ADFS understands; Entra reports MFA in amr whatever it was asked), prompt=login, max_age=0 and login_hint. The answer counts as two factors only when its ID token says so: amr with mfa (or a second kind of factor next to the first sign-in's), ADFS's multipleauthn in amr, or an acr among mfa_acr_values; it must also name the same person and not be an earlier sign-in (auth_time). Otherwise the application gets access_denied and the session stays one-factor. The sign-in log says company sign-in + the company's second factor (oidc.upstream.stepup). For a provider over SAML the same is done with an AuthnRequest with ForceAuthn="true" and a RequestedAuthnContext of the mfa_acr_values classes (the same two by default), compared by mfa_comparison: minimum (the default) or exact. The answer counts as two factors only when its AuthnContextClassRef (or ADFS's authnmethodsreferences attribute) is REFEDS MFA, ADFS's multipleauthn or a class among mfa_acr_values, its NameID is the one of the assertion the session was opened with (or the claims name the same person), and its AuthnInstant is after the step-up began.

The company's provider over SAML (ADFS over SAML). A provider in the same list may be kind: saml — directory is then its service provider (SP) rather than an OIDC client. The easiest way in is the import: the console → Security → Sign-in page → "Company provider" → "Import IdP metadata (SAML)" — paste the XML or give its address (for ADFS https://<host>/FederationMetadata/2007-06/FederationMetadata.xml). A disabled draft is added to the list with idp_entity_id, sso_url and sso_binding (HTTP-Redirect, or post when the IdP offers HTTP-POST only), slo_url (the IdP's single logout over HTTP-Redirect) and the signing certificates idp_certs; check it, set the domains, provision and the button label, and save. Under the card appear the SP metadata address https://<identity>/oauth/upstream/saml/<id>/metadata (also the entity ID), the ACS …/oauth/upstream/saml/<id>/acs and the single logout address …/oauth/upstream/saml/<id>/slo — ADFS creates its Relying Party Trust from that metadata address ("Import data about the relying party published online"). sso_binding: post sends the AuthnRequest in a form that submits itself (the signature inside the XML); redirect (the default) as a signed query. The other fields: nameid_format (empty asks for no format, and ADFS issues the one its rules give); attribute_map — which SAML attribute a claim is read from (upn, email, name, given_name, family_name, …); unlisted claims are read from ADFS's usual names (http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn, …/emailaddress, …/givenname, …/surname), Entra's and eduPerson's, and the NameID is the claim nameid (for example username_claim: nameid). want_assertions_signed (on by default) requires the signature on the assertion itself — which is how ADFS signs by default; off, a signature on the Response alone is accepted too, but an unsigned answer never is. sign_requests (on by default) signs the AuthnRequest with directory's RSA key. encrypt_assertions publishes an encryption certificate in the metadata and accepts encrypted assertions (ADFS encrypts with AES-256-CBC, which is accepted; AES-GCM too), and an encrypted NameID (EncryptedID) — in the assertion and in the IdP's logout request; it is decrypted with the same keys and algorithms, and only after the signature has verified. Without it an encrypted assertion or NameID is refused. The rest is as for an OIDC provider: domains, finding or creating the account (source: saml), the session. How the person signed in comes from the AuthnContextClassRef: ADFS's multipleauthn or REFEDS MFA is a two-factor sign-in (mfa). Refusals are in the sign-in log with the reason "company sign-in refused": a bad signature, another audience or ACS, an expired assertion (two minutes of clock skew are allowed), a replay of an assertion already accepted, a sign-in started at the IdP.

The address from SAML. SAML has no "the address is verified", so by default the asserted address finds no account. trust_email: true is the administrator's statement that the IdP only issues checked addresses: a sign-in whose login is not found is then linked to the one account with that address, as an OIDC provider's email_verified does (oidc.upstream.link). Do not turn it on for an IdP where people can change their own address.

A signing certificate rollover. With a metadata_url (https only) directory reads the IdP's metadata again every six hours (spread a little; after a failure sooner, then backing off) and at the "Refresh metadata now" button under the card. A certificate the IdP starts publishing is added at once; one the metadata stops listing stays for a day and is then dropped — which is how an ADFS rollover passes without an outage. A failed read (no answer, a document that does not parse, another entityID) changes nothing, and the last certificate is never removed; the last success, the error and the next refresh are shown under the card. Every change is an oidc.upstream.metadata audit row with the SHA-256 fingerprints added and removed. Pasted XML is never refreshed.

Single logout. With slo_url, a sign-out at directory (/oauth/logout, or a logout a SAML application started) first logs the person out of directory's own applications, then sends the browser to the IdP with a signed LogoutRequest (the NameID and SessionIndex of the assertion the session was opened with); the IdP's answer (signed with its certificate, or refused) brings the person back where the logout started. A logout started at the IdP (a signed LogoutRequest to …/oauth/upstream/saml/<id>/slo, over Redirect or POST, at most five minutes old, accepted once) ends the person's sessions that came through this IdP (only the one with the SessionIndex, when it names one), logs them out of directory's applications and answers the IdP with a signed LogoutResponse. The log: oidc.upstream.slo.

What the state line says​

v0.2.0 · http://10.40.0.7:8086 · https://id.example.com — the role's version, the private address the coordinator dials, and the public name if there is one. unreachable — the host is enrolled but does not answer: read the role's log on the host (journalctl -u simpletwo-identity); it is usually a DIR_DATA_DIR the service cannot write to or, at the first start after the move to SQLite, a DIR_IMPORT_PG_DSN that pg_hba does not admit.