Profiles and groups#
The UserProfile and UserGroup content types, and the rules that govern them.
For the reasoning behind any of it, read About profiles and groups. For the contract a site's own types must meet instead, read Users and groups as content.
The two types#
|
|
|
|---|---|---|
Portal type |
|
|
Schema |
|
|
Marker it provides |
|
|
Workflow |
|
|
Identifier |
the object's id in its container is the userid |
the object's id is the group id |
Membership |
|
the same field, naming the groups this group is nested inside |
Containment |
nothing may be added inside a Profile |
|
Neither type declares group_ids. Both enable the behavior, which is what lets a
site's own user type gain membership without declaring the field, its
vocabulary, and its two permissions a second time.
A UserGroup does not store its own members. Membership is stored on each
member.
UserProfile fields#
Field |
Type |
Required |
Declared in |
Read permission |
Write permission |
|---|---|---|---|---|---|
|
|
yes |
the type |
View Account Details |
Edit Profile Login |
|
|
yes |
the type |
View Profile |
Edit Profile |
|
|
no |
the type |
View Profile |
Edit Profile |
|
|
no |
|
View Profile |
Edit Profile |
|
|
no |
|
View Profile |
Edit Profile |
|
|
no |
|
View Profile |
Edit Profile |
|
|
yes |
|
View PII |
Edit Profile |
|
|
no, read-only |
|
View PII |
— |
|
from the behavior |
no |
|
View Account Details |
Edit Profile Group Membership |
Permission titles are shortened here; see Permissions for the ids.
Only login, fullname and description are declared by the type. The rest
arrive through behaviors, each named pas.plugins.identity.<name> above, so a
site running its own user type composes the same Profile out of the same parts.
Every one of them is schema-only: the values are stored on the content object
exactly as declared fields were, and the catalog indexes them unchanged.
emails and email are the only fields on a fieldset of their own, an
Email tab. That is where the read permission differs from every other
field's.
UserProfile behaviors#
Behavior |
Adds |
|---|---|
|
|
|
|
|
|
|
the object id |
|
version history |
UserGroup fields#
Field |
Type |
Required |
|---|---|---|
|
|
yes |
|
|
no |
|
from the behavior |
no |
Addresses#
emails is an ordered tuple and is the required field. email is derived from
it and read-only:
the first verified address, or the first address at all when none is verified.
Rule |
Behaviour |
|---|---|
What counts as verified |
This site holds an |
What creates one |
A magic link, or a login through a provider the operator marked as trusting. |
A provider asserting |
Nothing, unless that provider is marked as trusted here. |
Writing |
Moves that address to the front of |
Writing an empty |
Ignored. |
Where |
Catalog metadata. Confirming or removing an email identity updates the owner's catalog entry. |
|
Absent. |
What a login writes#
Event |
Effect on |
|---|---|
First login |
Every address the provider reports, in the provider's order: primary first, then the ones it says it verified. |
A later login |
Appends only addresses no provider has offered before. |
An address you deleted |
Stays deleted. |
The order you chose |
Not rearranged. |
A provider changing your address |
The new one is added beside the old. |
Which address stands for a person is that order, so choosing one is moving it to
the front. The edit form's list does that, and so does Make preferred
on the Sign-in methods page. A verified address still wins over an
unverified one above it, because that is what email is derived from.
Workflow#
Profile states#
stateDiagram-v2
direction LR
[*] --> incomplete
incomplete --> complete: complete
complete --> incomplete: reopen
incomplete --> deactivated: deactivate
complete --> deactivated: deactivate
deactivated --> incomplete: reactivate
State |
Meaning |
Enumerated? |
|---|---|---|
|
Missing information the site requires. The initial state. |
yes |
|
Nothing required is missing. |
yes |
|
Excluded from enumeration and from property lookup. The object and its data are kept. |
no |
Which states count is profile_enumeration_states, ('incomplete', 'complete')
by default. See Settings.
Transition |
To |
Guarded by |
So the user themselves… |
|---|---|---|---|
|
|
|
can make it |
|
|
|
can make it |
|
|
|
cannot |
|
|
|
cannot |
Reactivating returns a profile to incomplete, never to complete.
Nothing asks a user to press a button: the package moves a profile between
incomplete and complete itself, whenever the profile is written to and
whenever its owner signs in. deactivated is never entered or left by that.
Who holds what, per state#
Every permission is mapped with acquired="False" in every state.
Permission |
|
|
|
|---|---|---|---|
|
Manager, Site Administrator, Owner |
+ Member |
Manager, Site Administrator |
|
Manager, Site Administrator, Owner |
+ Member |
Manager, Site Administrator |
|
Manager, Site Administrator, Owner |
same |
Manager, Site Administrator |
Edit Profile |
Manager, Site Administrator, Owner |
same |
Manager, Site Administrator |
View Profile |
Manager, Site Administrator, Owner |
+ Member |
Manager, Site Administrator |
View PII |
Manager, Site Administrator, Owner |
same |
Manager, Site Administrator |
View Account Details |
Manager, Site Administrator, Owner |
+ Member |
Manager, Site Administrator |
Edit Profile Group Membership |
Manager, Site Administrator |
same |
same |
|
Manager, Site Administrator |
same |
same |
|
Manager, Site Administrator |
same |
same |
|
Manager, Site Administrator |
same |
same |
|
Manager, Site Administrator |
same |
same |
|
Manager, Site Administrator |
same |
same |
|
Manager, Site Administrator |
same |
same |
|
Manager, Site Administrator |
same |
same |
|
Manager, Site Administrator |
same |
same |
Two things to read off this table:
Ownernever holdsDelete objects, in any state. Stock Plone would grant it with acquisition on; the workflow states it and stops it at the site administrator.Ownernever holds Edit Profile Group Membership, in any state. Writinggroup_idsis granting yourself roles.
A user gets Owner on their own UserProfile and nothing on anybody else's. The
role is computed by a local role provider rather than assigned at creation.
Group states#
stateDiagram-v2
direction LR
[*] --> active
active --> deactivated: deactivate
deactivated --> active: reactivate
State |
|
Enumerated, and grants membership? |
|---|---|---|
|
Manager, Site Administrator, Member |
yes |
|
Manager, Site Administrator |
no |
Modify portal content and Delete objects are Manager and Site Administrator
in both states. Both transitions are guarded by Manage users.
Deactivating a group stops it being enumerated and stops it granting membership,
without deleting it and without editing a single UserProfile. Reactivating
restores exactly the membership it had.
Which states count is group_enumeration_states, ('active',) by default.
Where principals are stored#
Eight registry records: parent path, id, title and content type, for profiles and again for groups. The group records default to the profile container's, so a site filing principals together sets none of them. See Settings.
The container type#
Unless a type record names another, the package creates a PrincipalsContainer.
|
|
|---|---|
Title |
Principals folder |
Allowed types |
|
Who may add one |
Manager and Site Administrator, through |
Order of its items |
none: the |
Behaviors |
|
When the package creates it |
excluded from navigation, with a title block as its only block |
When the parent does not allow the type a record names, or that type can't
hold content, creating the container fails with ContainerNotFound. The
message names the record, the type and the parent. The package does not
choose another type.
The catalog is not scoped to the container. It indexes a UserProfile
wherever the object actually is, so reorganizing content is not a
deauthentication.
Where principals may be created#
Only in that container.
Each type has its own add permission, and rolemap.xml grants both to no role
at all:
pas.plugins.identity.userprofile.add
pas.plugins.identity.usergroup.add
What makes either type addable is a local grant on the container itself, which the package writes when it creates the folder, when it installs into a site where the folder already exists, and when a folder appears at the configured path.
So nobody creates a UserProfile in an ordinary folder or pastes one into it,
and that includes a Manager. Neither type appears in the add menu anywhere
else. To file principals somewhere else as well, grant the permission on that
folder.
The two permissions are separate so that a site filing groups apart from users can open each container to one kind only.
The profile gate#
While a profile is incomplete, every page its owner asks for is answered with a
redirect to its edit form. Subscribed to IPubAfterTraversalEvent.
Controlled by enforce_required_profile_fields, on by default.
What counts as complete#
required_profile_fields names the fields. Empty, which is how it ships, means
the fields the profile type marks required, its behaviors' included: login,
fullname and emails for the type in this package.
Value |
Counts as filled? |
|---|---|
|
no |
|
no |
an empty collection |
no |
|
yes |
|
yes |
The record only ever adds. A field the type marks required stays required whatever the record says.
A field named here need not be required on the type: @types/UserProfile reports
the site's required fields alongside the type's, so the edit form asks for
everything the flow insists on.
Confirming an address#
With confirm_email_at_first_login on, a profile can be incomplete with every
field filled: its owner has not yet said which of their verified addresses
stands for them.
A profile waits on a confirmation while |
Otherwise |
|---|---|
|
Turning it off releases a held profile at its owner's next sign-in, or the next write to it. |
the sign-in that created it marked it |
Only a first sign-in made while the setting was on marks a profile. Linking another provider never does. |
it holds more than one verified address |
Removing an address releases it straight away. |
Only addresses this site holds verified count. An address from a provider whose
trust_email_verification is off is not one to choose between.
Only POST @confirm-email answers. A later sign-in does not, and neither does a
save of the edit form, even one that reorders emails.
The question comes after sign-in, so it has no say in which existing account a provider's addresses link to. The provider's address preference decides that.
What is never gated#
The checks run in this order, cheapest first, and the first match lets the request through.
# |
Passes when |
Why |
|---|---|---|
1 |
The request is a |
Volto fetches the edit form over the API. |
2 |
It is not a |
A gate on every request is a gate on every stylesheet. |
3 |
There is no Plone site yet |
Nothing to answer for. |
4 |
The user is anonymous |
The gate is about a profile's owner. |
5 |
The last path segment is exempt |
See the two lists below. |
6 |
The user holds |
A required field nobody can supply must not lock the site. |
7 |
The gate is switched off |
|
8 |
The user has no incomplete profile |
Nothing to hold them for. |
9 |
The profile is waiting only on an address confirmation |
The edit form cannot give one. The Volto add-on asks at |
10 |
The profile is already in the traversed path |
Its edit form, its widgets, its save. Redirecting the target is a loop. |
Exempt path segments:
login @@login login_form require_login @@require_login
logout @@logout logged_out
Exempt prefixes:
@@oauth-
Name any other view that must not be interrupted in gate_exempt_paths, matched
against the last segment of the path.
Claims refresh#
On every sign-in the provider's claims refresh the fields that provider still
owns, and only those. The rule is one comparison: the UserProfile remembers
what the provider last wrote, and the provider may write a field only while the
current value still equals that.
Situation |
Written? |
|---|---|
Fresh |
yes |
The provider changed the claim since the last sign-in |
yes |
The user edited the field |
no |
The user cleared the field |
no |
An administrator typed the value in by hand |
no |
login is never synced. It is half of the case-folded index that user
enumeration queries.
Provider avatars#
Off by default: sync_portraits.
When on, a changed picture_url claim is fetched during claims sync and stored
as the user's portrait.
Guard |
Value |
|---|---|
Scheme |
HTTPS only |
Timeout |
|
Size cap |
|
Content type |
Refused unless the server claims an image |
Warning
Read About profiles and groups before enabling this. picture_url is
a claim, and at many providers a claim is whatever the user typed. Turning it
into a server-side fetch makes the sign-in path a request forger.
Membership#
Membership is stored in group_ids on each member. Two paths write it, and they
reach the same place.
Path |
Notes |
|---|---|
|
The ordinary Plone API, and the Users and Groups control panel that calls it. Reaches the plugin through PlonePAS's group tool. |
Editing the |
The Groups field on the edit form. Needs Edit Profile Group Membership. |
Situation |
Result |
|---|---|
A group is deleted |
No |
The group is recreated |
Exactly the membership it had. |
A |
Grants nothing; the consistency check reports it. |
A group inside a group |
Allowed. |
A group inside itself |
Refused. |
An inactive group |
Grants nothing and passes nothing through. |
Nesting is closed over when the question is asked rather than stored expanded, so removing an edge takes effect everywhere at once.
Finding a user#
User enumeration is served from the identity catalog. What a search costs depends on what it asks for.
Search |
Served from |
Matching |
Reads |
|---|---|---|---|
Exact, by |
The |
Case-sensitive: |
The matches only |
Exact, by |
The |
Case-insensitive; the index stores logins folded |
The matches only |
Exact, by |
Both indexes, merged |
Either one matches; a user matching both is returned once |
The matches only |
Exact, by |
Every active Profile's brain |
Whole value, case-insensitive |
Every active Profile |
Part of a value, the default |
Every active Profile's brain |
Anywhere in the value, case-insensitive |
Every active Profile |
No criteria |
Every active Profile's brain |
Everybody |
Every active Profile |
PAS resolves every getUserById with an exact search by id, so that is the
row that decides how fast a listing of users or of a group's members is. None
of the rows wakes a UserProfile.
Only Profiles in a state named by profile_enumeration_states are found, on
every row.
PAS resolves a user by asking each enumeration plugin in turn, and stops at the
first that answers. A user who also has a source_users account is resolved there,
and this plugin is asked only about users that have a Profile alone—every
account an import or a federated sign-in created.
Maintenance#
Step |
Does |
Does not |
|---|---|---|
The consistency check |
Reports drift |
Repair anything |
|
Repairs |
Report anything |
They are kept apart so the check can be scheduled read-only. The check reports
catalog entries missing, entries whose object is gone, brains that disagree with
their object, duplicate user ids or login names, and UserProfile objects naming
groups that do not exist.
Coexistence with the stock plugins#
source_users, source_groups and auto_group keep working.
A user known to both this package and source_users appears once in a
search, because both return the same canonical user id and every consumer merges
on it. Group memberships are the union, with no duplicates.