Users and groups as content#
The mechanism that lets a Dexterity type be a user.
For why it is built this way, read About users as content. For the types this package ships on top of it, read Profiles and groups.
Registry records#
Four records control the mechanism. All four are empty by default, which means the feature is off and Plone's own plugins do the work.
Record |
Names |
|---|---|
|
Portal type created when somebody adds a user. Must provide |
|
Where those objects are created, relative to the site root. |
|
Portal type created when somebody adds a group. Must provide |
|
Where those objects are created, relative to the site root. |
Both records of a pair must be set. A type with nowhere to go would fail at the moment somebody adds a user, which is the worst time to discover a configuration gap.
Edit them in .
Note
Installing the package sets all four and keeps them pointed at its own types and container. It derives them from the container settings through a subscriber, so moving the container in the control panel does not require a reinstall. See Settings.
The marker contracts#
Interface |
Attributes it promises |
Declared on the interface |
Object's id in its container must equal |
|---|---|---|---|
|
|
|
|
|
|
|
|
Attribute |
Meaning |
|---|---|
|
The canonical Plone userid. Assigned once, never changed. |
|
The name the user signs in with. |
|
The canonical group id. Assigned once, never changed. |
|
On a user, the groups it belongs to. On a group, the groups it is nested inside, in addition to the group it is filed in. |
group_ids is supplied by the pas.plugins.identity.group_membership behavior.
Neither interface declares it as an Attribute, deliberately. Dexterity
answers a missing attribute from the schema's field default and finds the type's
own schema first, so an inherited Attribute would shadow the behavior's field.
IGroupContent declares no members accessor. Membership is named by each user's
group_ids and is read from there.
A layer that stores membership some other way should implement
IGroupManagement itself rather than claim IUserContent.
Providing IUserContent does not make a type a credential store. See
Credential storage.
When the plugin declines#
The plugin returns false, and the stock plugin acts instead, in all of these cases.
Case |
Logged? |
|---|---|
The content type record is empty |
no |
The container path record is empty |
no |
The container path does not resolve to an object |
warning |
The named portal type is not a Dexterity type |
warning |
The named type's schema does not provide the required marker |
warning |
The named type's schema fails to load |
warning |
Declining is the protocol rather than an error: ZODBUserManager.doAddUser
returns false on a duplicate id for the same reason. An unset record logs nothing
because unset is the default.
Plugin ordering#
The plugin must be registered first for both IUserAdderPlugin and
IGroupManagement. Both interfaces are walked until a plugin returns true, and
source_users and source_groups never decline—so registered below either of
them, this plugin is never reached.
Installing the package moves it to the top of both interfaces.
Warning
Reordering PAS plugins so this one sits below source_users or source_groups
switches the feature off. No error is raised and nothing is logged. Users and
groups are created as stock records again, and existing content-backed ones are
left where they are.
Which plugin does what#
Plugin |
Does |
|---|---|
|
Creates user and group objects, and authenticates. |
|
Enumerates them, serves their properties, and deletes them. |
Installing the package installs both. One without the other gives you a user that cannot be found: PAS looks a principal back up immediately after adding it.
Important
If you point the records at a content type of your own, make sure something on
the site enumerates it. UserProfile and UserGroup are enumerated by the
plugin this package installs; another type is your responsibility.
Credential storage#
By default the password of a user created this way is written to source_users,
not to the content object.
To keep the credential elsewhere, register an adapter from your content type to
ICredentialStorage:
Method |
Returns |
|---|---|
|
Nothing. Stores a password, hashed. |
|
Whether a password matches the stored one. False when nothing is stored. |
When the adaptation succeeds, core writes nothing to source_users.
Situation |
Result |
|---|---|
An empty password |
Never stored anywhere. |
An externally authenticated user |
No |
Nothing claims the login |
The login still succeeds. The principal exists as an identity and nothing else, and a warning names the type that was not created. |
A subscriber to IExternalIdentityAuthenticated creates the object—this
package's own, or yours. The plugin writes nothing itself.
Warning
Never store a credential in a Dexterity field. A field is serialized by
plone.restapi, exported by GenericSetup, indexed by the catalog, and
snapshotted by versioning. An annotation is invisible to the first three by
construction.
Versioning is the exception: CMFEditions copies annotations into a snapshot, so the package registers a modifier that keeps the hash out of the version repository, and a superseded password is not recoverable from a profile's history.
The password behavior this package ships keeps a hash in an annotation for this reason.
Deleting a user#
api.user.delete removes the content object, through IUserManagement on the
identity_profile plugin. The users listing offers the button because the plugin
also provides IDeleteCapability.
Deleted with the account |
Left behind |
|---|---|
The content object |
The identity records |
Local roles, revoked by Plone and not restored by a later sign-in |
The audit entries |
A login through an identity whose account is gone recreates the object and logs a warning naming the userid.
Warning
Deleting a user does not erase everything the site holds about them.
The identity record keeps a snapshot of the claims the provider last sent, which
typically includes an address and a name. The audit entries keep the login
history, with the IP address and user agent as well on a site that has switched
that on. Both are keyed to a userid that no longer resolves to anybody, and
neither is reachable through @users.
A deployment with an erasure obligation has to remove them deliberately: unlink the identities in the Identities panel before deleting the user, which also drops the store's record of them.
Refusals#
Operation |
Result |
Reason |
|---|---|---|
A group inside itself |
|
It would grant nothing, and the edit form would show a row nobody can account for. A group inside a different group is supported. |
Choosing a container at creation time |
Not accepted |
The registry records decide. |
|
Return false |
Declared by |
|
Raises |
The error PlonePAS expects from a plugin that cannot set a password. See Credential storage. |