1.0.0a5
1.0.0a4 (2026-09-09)
Backend
Breaking
- A provider's property map may only write the four Profile fields a login actually writes:
fullname,home_page,descriptionandlocation. The target is aChoiceoverpas.plugins.identity.UserFieldsrather than free text, so a row naming anything else is refused by the API with a 400 and by GenericSetup on import, where before it was stored, exported, and dropped on every login without a word.MAPPABLE_FIELDSis now the single definition the login filter, the principal document format and the control panel all read. The default maps lost the rows that did nothing: no driver seedsemailany more, andplone-identityno longer seedspicture_url. Neither was ever applied — an address is appended bysync_addressesand a portrait is synced from thepicture_urlclaim. Profile version 1004 removes such rows from a site that has them, logging each one. @ericof #43 @group-membersrows now point@idat the member's Profile. It was the listing's own URL with the userid appended, which is not a resource: the service takes exactly one path segment, so following it answered400.profile_urlis unchanged and now holds the same URL, so a client already following it needs no change. @ericof #44
Feature
- Order a group's membership in the catalog. The identity catalog gained a
sortable_titleindex, filled by Plone's own indexer from a Profile's title, and@group-memberssorts on it. It previously read every member of a group and sorted the whole list in Python to render a page of it. @ericof #40 - Grouped the identity settings into tabs.
IIdentitySettingsdeclared thirteen fields and no fieldset, so the settings form was one flat column; it is now four — Login, User and group content, Portraits and Audit log.IProfileSettingsis grouped into the same three the settings reference already documents it in: where principals are filed, which states count, and the profile gate. @ericof #41 - Put the profile and group settings on the control panel. The thirteen records in
IProfileSettingsdecide where principals are filed, which of their workflow states count for enumeration, and what a profile must carry before its owner is let past the gate — and the panel namedIIdentitySettingsalone, so they were reachable through the generic registry editor and nowhere else. Both the REST panel and the Classic form now serve a schema derived from the two, which addresses the records that already exist rather than creating any. @ericof #42 - Moved a
@group-membersrow behindIGroupMemberSerializer, a multi-adapter on the site and the request, so a deployment can add a field to a membership row by subclassingGroupMemberSerializerand registering it for its own browser layer. Adding a field previously meant replacing the service. It adapts the site rather than the brain because a brain carries no__provides__and cannot be marked, and the only registration a brain could carry would answer for every brain in the site. @ericof #45 - Added
GET @identity-providers/<id>/export, which returns one provider as a self-contained registry fragment ready to paste into a profile'sregistry/directory: theIProviderRecordsfields as one grouped node, and a<record>per driver setting carrying its own field type, since those belong to no interface. Exporting the whole registry was not a substitute —runExportStepdumps every package's records, and importing that document fails on one of them. A trailing path segment on@identity-providers/<id>is now refused rather than ignored. @ericof #47 - A driver now carries the connection facts about its own provider:
static_metadatafor a provider that publishes fixed endpoints, andissuerfor one whose issuer the driver knows. Both were tables keyed by driver id incore/flows/metadata.py, which no driver mentioned and no third-party driver could add a row to. GitHub's four endpoints are now onGitHubDriver, beside theenrichment_endpoint()that reads one of them, and Google's issuer is onGoogleDriver.GitHubDriveralso seeds the map GitHub can actually fill:bio,blogandlocation. @ericof #66
Bugfix
- Show a driver's own defaults on the add-provider form.
@identity-driversnow serves each driver's real starting values as the schema defaults Volto seeds an add form from, so choosing Google no longer presents an empty scope box and an unticked "this provider's email verification counts" while saving a provider that has both. The form and the stored record are filled from one function and can no longer disagree. ATuplefield's default is also serialized as a JSON array rather than a Python tuple. @ericof #37 - Stop this package's indexers from answering for every catalog.
loginandSearchableTextwere declared for the Profile alone, which registers an indexer against any catalog that asks — and a Profile is ordinary content, catalogued inportal_catalogas well. So a Profile's entry in site search was its full name, login and email, and its biography was not searchable at all. Both are now bound to the identity catalog, where they are unchanged, and site search gets an answer of its own: the title, the userid and the biography. Neither the login nor the address, which have no business in a site's search box. @ericof #38 @group-membersno longer wakes one object per row. Rendering a row filledprofile_urlby userid, which searches the catalog a second time and then activates the Profile to ask for its URL, so drawing a page of a group cost one activation per person on it — on the endpoint whose whole premise is that a group of a thousand is one query rather than a thousand object loads. A brain already knows its URL. The endpoint has also joined the activation-counting suite that covered the PAS plugins and never covered it, which is where the regression landed unseen. @ericof #57
Internal
- Replaced
authlib.josewithjoserfceverywhere a JWT is minted or read: the authorization server's tokens and key ring, theid_tokena provider returns, the magic link's own signature, and a back-channel logout token.authlib.joseis deprecated and Authlib keeps it only until 2.0.0; the OAuth client that carries every request is not deprecated and stays.joserfcis now declared as a dependency rather than arriving through Authlib. Two tokens are refused that were not before: anid_tokenand a magic link with noexpclaim, which the old library treated as a token that never expires.tests/test_protocol_libraries.pyfails when either library is imported outside the five modules that own a protocol boundary. @ericof #48
Tests
-
Added
tests/core/indexers/test_declarations.py, asserting that everything the identity catalog declares is actually answered, and that nothing this package declares answers for a catalog it should not.The issue asked for an explicit indexer per index and per metadata column. Ten of the fourteen would have restated an attribute name the
IIndexableObjectwrapper already resolves, and a pass-through indexer that has itself gone stale is exactly as silent as no indexer at all. So the risk is tested instead of restated: a fully filled Profile and Group are required to leave a value in every index and every column, whichcore.doctorcannot check for itself — it reads the object through the same attribute the catalog does, and finds both sides equally empty.The other half is which catalog answers.
@indexer(IUserProfile)does not register for one argument; it registers for every catalog in the site, which is what made the leak fixed in #38 invisible. EveryIIndexerthis package registers is now required to name the identity catalog, with the site-search answer the single exception, pinned by identity rather than by name. @ericof #39 -
Extended the registry export tests to prove the export can be read back, rather than only that it mentions a provider. The fixture provider now carries an icon, both claim maps and colours, and the module exports it, wipes the site, imports this package's records and compares every field. It also covers the
<records interface= prefix=>form a hand-written profile uses, which is not the form the exporter emits. @ericof #46
Frontend
Feature
- The property map's target column is a picker over the fields a login writes, built from the vocabulary the provider schema serves rather than from a list held here. It was a text box, which accepted
email,portraitandusernamealike and stored rows that did nothing. A backend that serves no vocabulary still gets the text box. @ericof #43 - Both content views now render a
belowTitleslot, under the heading and above the description, so a deployment can put its own component on a profile or a group page without shadowing either view.aboveContentandbelowContentalready reached both pages, because Volto renders those around any view registered inconfig.views.contentTypesViews; nothing outside a view can place anything inside one, which is what the new slot is for. @ericof #51
Bugfix
- Label the rows of a provider's property and group maps. Volto's object-list widget takes a row's label and the add button's noun from the row schema's own title, and neither map declared one, so the Mapping tab read
UNDEFINED #1above+ Add undefined. The two columns of each row are labelled and translated as well, where they had been the raw field names. @ericof #53 ProfileViewshows the login when a Profile has no full name, instead of "Unnamed user". It fell back tocontent.title, which a Profile never carries:titleis computed on the backend rather than stored, soplone.restapidoes not serialize it, and the test fixture supplied one no real payload has. The heading now follows the same order the backend's ownTitle()does — full name, login, userid. @ericof #60
Internal
- Split
actions/index.tsandreducers/index.tsinto one module per domain — login, magic link, identities, profile, groups, account, drivers, providers, clients, keys and consent — with the request-lifecycle factory the reducers share inreducers/factory.ts. Bothindex.tsfiles stay as the re-export surface, so nothing importing from the package root changes. The tests moved with them: 46 test files became 55, and the count is unchanged at 563. @ericof #49 - Split
src/types.tsintosrc/types/api.tsand a newsrc/types/content.ts, re-exported fromsrc/types/index.tsso every existing import keeps resolving. The new file describesUserProfileandUserGroupas Plone content, tied to@plone/typeswithPickrather thanextends: neither type carries Dublin Core or blocks behaviors, so most ofContentis absent from the payload and inheriting it would promise fields no view can read. Every deviation is documented against a measured serialization.providerFormSchemaandclientFormSchemaare no longerRecord<string, any>. @ericof #50
Project
Documentation
- Grouped the site-wide settings reference under the control panel's own tabs, so a reader working through the page and an operator working through the form are looking at the same four questions in the same order. @ericof #41
- Documented how to map a provider's claims onto profile fields, in the guide to configuring a provider: the four targets, why an address and a portrait need no row, and what happens to one that names something else. Corrected the claim, repeated on four pages, that this add-on ships no GenericSetup upgrade steps — the default profile is at version 1004 and each step is now listed. @ericof #43
- Documented what a
@group-membersrow holds, key by key, how a deployment adds one, and why the serializer adapts the site rather than the brain. The endpoints reference described what the endpoint was for and never said what came back. @ericof #44 - Documented how to ship a provider in a profile, using the new per-provider export, and why the fragment has one grouped node for the provider's own fields and a typed
<record>per driver setting. @ericof #47 - Said which library does what, in the two READMEs and the documentation index: authlib carries the OAuth requests and
joserfcreads the tokens. The security guarantees named a grep-level CI rule enforcing that protocol messages are never constructed by hand; no such rule existed, and the table now names the test that does it. @ericof #48 - Added a how-to guide covering the three slots a profile page and a group page expose, with a worked example of a downstream package registering a row of badges into
belowTitleand narrowing it to profiles with a predicate. The frontend reference lists the three. @ericof #51 - Corrected the claim that a GenericSetup export omits provider secrets. It carries them as their stored values, and the
plone.registry.field.Passwordtype marks a record rather than encrypting it — so an export of a provider is a credential, not a document. The statement appeared eight times, including in the threat model, where the leak was listed as prevented, and in the security guarantees table. What limits exposure is who may take an export, which needsManage portal. @ericof #59 - The driver contract now lists
static_metadataandissuer, with two rules covering them, and explains why a provider's endpoints belong to its driver. The how-to guide's example driver declared abase_urlof its own and would have been refused at login for having no metadata source; it now extendsIOIDCSettingsand the guide opens by asking where the endpoints come from. @ericof #66
Tests
- Added a Playwright script that drives the add-provider form through Volto, chooses a driver, and photographs every tab it produces. It writes to the Sphinx build directory rather than to the documentation's screens, so it reports what an operator sees without adding an image any page has to reference. @ericof #37