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#

UserProfile

UserGroup

Portal type

UserProfile

UserGroup

Schema

IUserProfileSchema

IUserGroupSchema

Marker it provides

IUserContent

IGroupContent

Workflow

user_profile_workflow

user_group_workflow

Identifier

the object's id in its container is the userid

the object's id is the group id

Membership

group_ids, from the pas.plugins.identity.group_membership behavior

the same field, naming the groups this group is nested inside

Containment

nothing may be added inside a Profile

UserGroup only; a group filed inside a group belongs to it

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

Read permission

Write permission

login

TextLine

yes

View Profile

Edit Profile

fullname

TextLine

yes

View Profile

Edit Profile

emails

Tuple of Email

yes

View PII

Edit Profile

email

Email

no, read-only

View PII

home_page

TextLine

no

View Profile

Edit Profile

description

Text

no

View Profile

Edit Profile

location

TextLine

no

View Profile

Edit Profile

image

NamedBlobImage

no

View Profile

Edit Profile

group_ids

from the behavior

no

View Profile

Edit Profile Group Membership

Permission titles are shortened here; see Permissions for the ids.

UserGroup fields#

Field

Type

Required

title

TextLine

yes

description

Text

no

group_ids

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 email identity for the address.

What creates one

A magic link, or a login through a provider the operator marked as trusting.

A provider asserting email_verified

Nothing, unless that provider is marked as trusted here.

Writing email

Moves that address to the front of emails. It does not replace a field.

Writing an empty email

Ignored.

Where email is served from

Catalog metadata. Confirming or removing an email identity updates the owner's catalog entry.

email on the edit form

Absent. plone.restapi omits a read-only field from a type's edit schema; it is still serialized with the content.

See About email verification.

What a login writes#

Event

Effect on emails

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?

incomplete

Missing information the site requires. The initial state.

yes

complete

Nothing required is missing.

yes

deactivated

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…

complete

complete

Modify portal content

can make it

reopen

incomplete

Modify portal content

can make it

deactivate

deactivated

Manage users

cannot

reactivate

incomplete

Manage users

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

incomplete

complete

deactivated

View

Manager, Site Administrator, Owner

+ Member

Manager, Site Administrator

Access contents information

Manager, Site Administrator, Owner

+ Member

Manager, Site Administrator

Modify portal content

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

Edit Profile Group Membership

Manager, Site Administrator

same

same

Delete objects

Manager, Site Administrator

same

same

Add portal content

Manager, Site Administrator

same

same

Add portal folders

Manager, Site Administrator

same

same

Manage properties

Manager, Site Administrator

same

same

Modify constrain types

Manager, Site Administrator

same

same

Modify view template

Manager, Site Administrator

same

same

Undo changes

Manager, Site Administrator

same

same

View management screens

Manager, Site Administrator

same

same

Two things to read off this table:

  • Owner never holds Delete objects, in any state. Stock Plone would grant it with acquisition on; the workflow states it and stops it at the site administrator.

  • Owner never holds Edit Profile Group Membership, in any state. Writing group_ids is 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

View and Access contents information

Enumerated, and grants membership?

active

Manager, Site Administrator, Member

yes

deactivated

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 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 itself marks required: login, emails and fullname for the type in this package.

Value

Counts as filled?

None

no

'' or whitespace

no

an empty collection

no

0

yes

False

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.

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 plone.restapi request (IAPIRequest)

Volto fetches the edit form over the API.

2

It is not a GET for text/html

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 Manager or Site Administrator

A required field nobody can supply must not lock the site.

7

The gate is switched off

enforce_required_profile_fields.

8

The user has no incomplete profile

Nothing to hold them for.

9

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.

Authorizing another application#

A federated sign-in touches only exempt routes, so the gate never fires during one. @@oauth-authorize therefore enforces completeness itself: it pauses the request at the profile's edit form and passes the authorization request along as return_url, resuming it once the profile is complete.

The client is told nothing meanwhile; the request is paused, exactly as it is while the user signs in. With prompt=none, where the specification forbids interacting with the user, the client is told interaction_required instead.

Turning enforce_required_profile_fields off turns this off with it.

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 UserProfile, nothing written yet

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

portrait_timeout, 5 seconds

Size cap

portrait_max_bytes, 2 MiB, counted off the stream

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

api.group.add_user / api.group.remove_user

The ordinary Plone API, and the Users and Groups control panel that calls it. Reaches the plugin through PlonePAS's group tool.

Editing the UserProfile

The Groups field on the edit form. Needs Edit Profile Group Membership.

Situation

Result

A group is deleted

No UserProfile is edited. Members naming it grant nothing.

The group is recreated

Exactly the membership it had.

A UserProfile names a group that does not exist

Grants nothing; the consistency check reports it.

A group inside a group

Allowed. api.group.add_user(groupname='staff', username='developers') writes the edge on developers.

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.

Maintenance#

Step

Does

Does not

The consistency check

Reports drift

Repair anything

pas.plugins.identity:rebuild-catalog

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.