Alpha — the platform is in development. Docs cover what ships today; anything unbuilt is marked. Concepts
-rw-rw-r-- content:content /docs/concepts/identity.dtmpl ● verified against v2.0

Identity & groups

Most systems collapse several different questions into a single "current user". This platform keeps four apart, because they have different answers and different lifetimes.

  • Authentication — who is making this request?
  • Elevation — what state is their session in right now?
  • Authorization — are they allowed to do this?
  • Ownership — who owns the thing that gets written?

The second is the one most systems do not have, and the one the other three are most often confused with. Membership is who you are; elevation is what state you are in. A person who is a member of the admin group is a member every hour of every day. A session that is elevated is elevated for a while, and then it is not. Where the platform would let membership alone decide something, it asks for the state as well — see Permissions, which describes the check that asks both questions.

Users, groups, and a hierarchy that is data#

Users belong to groups. Groups carry roles. Roles inherit from other roles through a runtime role hierarchy rather than a static configuration block — the graph lives in the database and is editable, with a cycle guard so an administrator cannot make a role its own ancestor.

The practical consequence: granting a capability across an organisation is an edit, not a deployment.

The admin group is the whole of it#

There is no root user. There is no super-administrator role. The most privileged thing a person can be is a member of the admin group, and that membership is the single fact every administrative check reads: isAdmin means is a member of admin and nothing more.

Membership is not a right. It does not bypass a permission on its own — the Permissions page has the seven places where the platform would once have let it, and asks for elevation there instead. It is a fact about a person that makes elevation possible; elevation is what makes the bypass happen.

A fresh installation creates the group and the system users and no account that can log in. That is deliberate: the account that can become elevated is created from outside the running system, not minted by an installer, and the next section says how.

Elevation: a state on a session, with a clock#

A session becomes elevated when the person proves they may: the admin password, and a second factor when the installation has one switched on. The grant belongs to that session — not to the account: another browser signed in as the same person is another session and is not elevated by it — and it carries an end time fixed when it is granted. The lifetime is a setting, fifteen minutes by default, with a ceiling of an hour that the platform enforces whatever the setting says. When the clock passes the end time the session is simply not elevated any more; nothing has to fire for that to be true, and a suspended tab or a moved clock does not extend it.

How it feels to use:

  1. An action the ordinary rule refuses answers 403, and the admin client raises the elevation prompt off that refusal, quoting the server's reason.
  2. The person types the password (and the code, if a second factor is on). The refused request is sent again, unchanged, and now succeeds.
  3. A badge shows the end time. The person can end the elevation early from it; the platform ends it at the end time regardless.

In the terminal, sudo does the same thing to the same session.

The admin password is not a login password and is not the person's own. It authenticates elevation and is set from outside the running system:

docker exec coolms_app php bin/console coolms:admin:credential

That command has two duties — it sets the password and it registers the first confirmed second factor — and until it has run there is no way to become elevated at all. Changing the password from inside the system later requires the second factor. The full account of elevation — its endpoints, its record, what a second factor is here and how it is sealed — is a page of its own, forthcoming.

Passwords#

Every password the platform accepts must meet a policy: a minimum length (and a higher one for system accounts), required character classes, and — when the installation has fetched the corpus — absence from a list of breached passwords, checked offline. The numbers are settings, and the settings tier is described on a forthcoming page; this page only needs you to know that the rule exists and that it is the same rule for registration, for an administrator setting a password, and for the elevation password.

System users#

A feature module that writes to the virtual filesystem does not write as the person who made the request. It writes as a system user registered for that module:

$systemUser = $this->systemUserRegistry->getForModule('content');
$this->vfs->createFile($path, $content, $systemUser, $mode);

This looks like indirection until you see what it prevents. File ownership on a page would otherwise record whichever editor happened to save first, and every later permission decision about that page would depend on an accident. Requests are authorized at the API gate, which is where the requesting user matters; the file is owned by the module, which is what actually owns it. On a fresh installation /content and its siblings are owned by the admin system user and grouped to the module that writes there — content, assets_editors — which is what lets an editor write into a subtree without owning the platform.

Warning

The mode bits on a written node are about content access — who may later read or edit this page — not about whether the current request was allowed. Those are different questions and reading one as the other produces a system that is permissive in exactly the places it looks strict.

Authentication is not the only gate#

A signed-in user is not an authorized one. Every mutating operation declares its own explicit gate; a resource that declares none inherits only "any authenticated user", which is almost never what was meant. And an authorized member is not an elevated one: where a gate would be passed on membership alone, it asks for the session's state too.

Object-level decisions — may this user edit this page — live in the service or processor that performs the write, verified independently of any data-layer backstop. A query that happens to return nothing is not an authorization check; it is a coincidence that holds until someone adds an index.

Linking an external account#

Single sign-on can attach a provider identity to an existing account. That link gates on a verified email from the provider. Linking on an unverified address means anyone who can claim that address at the provider can claim the account here — account takeover wearing the clothes of a convenience feature.