Releases: collective/pas-plugins-identity
Release list
1.0.0a7
1.0.0a6 (2026-09-12)
Backend
Breaking
- Made the login callback URL, the provider discovery timeout, and both enumeration-state lists required in the identity settings. The control panel refuses an empty callback URL, a timeout below one second, and a list that names no state. @ericof #82
- Changed the default of
profile_container_typeandgroup_container_typefromFoldertoPrincipalsContainer, and removed the fallback toDocumentorFolderwhen a parent refuses the configured type. Creating the container now fails with an error naming the record, the type and the parent. @ericof #85
Feature
- Published
storeandsync_portraitinpas.plugins.identity.api.portrait, so a package that seeds its users' pictures no longer imports them fromcore.portraits. @ericof #78 - Added an address preference to GitHub providers: an ordered list of
@domainentries and*that decides which of an account's addresses becomes theemailclaim and the order a new profile lists them in. Linking by email now tries every address a provider verified, in that order, instead of the first one only; when two of them belong to different accounts, the higher-ranked one wins and the conflict is logged. A site can also ask a person whose first sign-in brought more than one verified address which of them stands for them: with the newconfirm_email_at_first_loginsetting on, their profile stays incomplete until they answer through the newPOST @confirm-emailendpoint, and@my-profilereports the question asconfirm_email. @ericof #80 - Added
GET @identity-providers/@export, which returns every provider as one registry document, and put both provider exports behind a permission of their own,pas.plugins.identity: Export Identity Providers, granted to Manager alone. The provider listing says whether the caller holds it, and an upgrade step applies the permission's floor on existing sites. @ericof #83 - Added
PATCH @identity-providers, which stores every provider in the order given, in one request, and took the Order field off the provider form. The field stays in the registry, so profiles still import and export it. @ericof #84 - Added the
PrincipalsContainercontent type, titled Principals folder, which is what the package now creates to file profiles and groups in. It keeps no order among its items, carries blocks and starts with a title block, and has an add permission of its own,pas.plugins.identity: Add Principals Container, granted to Manager and Site Administrator. @ericof #85 - Gave every shipped driver a default login-button icon, copied from volto-authomatic and Volto. A provider with no icon of its own is drawn with its driver's in
@login-providersand@identities, an uploaded icon still wins, and@identity-driversserves each driver's default. The default is never stored, so an export carries only uploaded icons. @ericof #86 - Added a
keycloakdriver for Keycloak realms, built on the generic OpenID Connect driver. It takes the realm URL as its issuer, and a new provider on it starts with the realm'spreferred_usernameas the userid source and its email verification trusted. Like every shipped driver, it has a default login-button icon. @ericof #87 - Added an
initialprofile for developers. A site created withmake backend-create-sitenow has an example front page, and GitHub and Google sign-in that work onlocalhostwithout registering an application first. @ericof
Bugfix
- Declared
@portraitzope.Public, so a site that takesViewaway fromAnonymousstill serves the pictures it publishes as the OIDCpictureclaim. A relying party fetching one got401. Reaching the endpoint on such a site exposed a second failure:plone.restapidecides whether a portrait is Plone's placeholder by traversing to the placeholder with a permission check, which answers nothing there, so every user without a Profile picture got500. The fall-through to a member portrait is now served by this package, and answers404for a user with no picture of their own. @ericof #68 - Held provider icons written by a GenericSetup import to the same rule as the control panel.
plone.registrystores fields without their constraints, so the SVG check declared onIProviderRecords.icononly ever ran on the control panel form. An import accepted any bytes —b''among them — and the next read of the providers failed@login-providersfor every provider on the site; an SVG carrying a script was stored as it came, sanitized only when read, so the registry and every later export held the unsafe version. The record is now checked as it is written: a document that is not an SVG fails the import and names the provider, and an SVG is stored sanitized, exactly as the control panel stores it. @ericof #79 - Fixed moving a principal container through the identity settings form. Saving the form wrote the old user and group container paths back over the ones just derived from the new location, so users and groups went on being created in the old folder. The four derived records are now read-only in the settings schema: the form no longer shows them, and a control panel save never writes them. @ericof #82
- Added
group_urlto the entries in@group-members'nested_groupsandparent_groups: the group's own page, ornullfor a group the site holds no entry for. Each entry's@idis that group's@group-memberslisting, which is not a page, so a group page had nothing to link another group to. @ericof #90
Frontend
Breaking
- Moved the add-on's frontend settings under
config.settings.identity.config.settings.identityShowPloneLoginis nowconfig.settings.identity.showPloneLogin, and the old key is no longer read;RAZZLE_IDENTITY_SHOW_PLONE_LOGINstill overrides it. The avatar palette is configurable asconfig.settings.identity.avatarColors, and defaults to the shipped palette. A palette a project supplies is not checked for contrast. @ericof #89
Feature
- Added the
/confirm-emailroute, where a person whose profile is waiting on an address confirmation chooses which of their verified addresses this site uses for them.ProfileGateand the first-login route send them there instead of to the edit form, which cannot answer the question; a profile missing fields as well still goes to the form first. @ericof #80 - Gave the providers control panel's settings, add and edit forms their own routes, so each can be linked to and reloaded, and the browser's Back button returns to the provider list. @ericof #81
- Added export actions to the providers control panel, one per provider and one for every provider, each downloading the registry document under the filename it belongs under in a profile. The panel says beside them that the file carries client secrets in the clear, and offers them only to someone allowed to export. @ericof #83
- Gave the providers control panel's list a Login screen column, and let its rows be dragged into the order the login page offers them. A dropped row moves at once, and goes back if the new order cannot be saved. @ericof #84
- The provider form's Icon field shows the driver's default icon until one is uploaded. @ericof #86
- Drew the login button of a provider on the new
keycloakdriver in the OpenID Connect colours. @ericof #87 - Added a Sign-in block, offered inside a grid, and kept out of a page's block chooser until a project lifts its restriction. It shows a visitor the login card
/loginshows, and greets somebody signed in with a welcome message and a summary: a link to their Profile, their preferred address, what they signed in with and when they last did, each line switchable in the sidebar. The login page's store and routing moved into auseLoginhook the two share, and its card intoLoginCard, whichLoginPanelputs on a page of its own. @ericof #99 - Made the login page's redirect to a site's only provider something a site can turn off, with
config.settings.identity.redirectToSoleProvideror, at run time,RAZZLE_IDENTITY_REDIRECT_TO_SOLE_PROVIDER. It stays on by default. Even when on, it no longer fires for a visitor who arrived already signed in, which looped against a provider that still had a session, or for/login?choose=1, which the callback page now links to when a sign-in fails. @ericof #100
Bugfix
- Gave the Plone site driver's login button the Plone colours. Its class is
identity-provider--plone-identity, and the stylesheet knew onlyidentity-provider--plone, which the password button ke...
1.0.0a6
1.0.0a5 (2026-09-10)
Backend
Feature
- Released the server layer's claims through one serializer per scope, registered as a named multi-adapter on the site and the request, so a downstream package can add a claim or a whole scope without editing this one. It was two module-level dicts in
server/claims.py— a scope-to-claim-names mapping and a claim-name-to-lambda mapping — which a site could only extend by mutating at import time, while the scopes vocabulary's own docstring advertised the extension as though it were supported. A serializer declares its claim names as a class attribute and produces their values in__call__, and both halves are needed:scopes_supported,claims_supportedand the consent screen all ask what a scope releases with no user in hand, so a scope that could only serialize somebody would be released without ever being offered or consented to. A new scope reaches the discovery document, the client registration form, the consent screen and issued tokens from that one registration.sub,iss,aud,expandiatare reserved and a serializer returning one is ignored on that key. Empty values are dropped for every serializer alike, and the rule is absence rather than falsehood, soemail_verifiedstill reportsFalse. @ericof #69 - Offered
@my-profileas aplone.restapiexpandable component, so a signed-in user's profile state rides along with the content request Volto was already making rather than costing a second round trip on every navigation. It is registered on any content rather than on the site root alone, and publishes the site's own@my-profileURL whatever page carries it — the endpoint is registered for the site root and resolves nowhere else. For an anonymous caller the component is absent entirely rather than an@id: Volto'sapiExpanderscannot mark an entry authenticated-only, so the expansion is asked for on every page of a public site, and answering with nothing leaves such a response exactly as it was.test_zero_wake.pynow covers an expanded content request, because a catalog-only read matters more when it runs on every page view than when it ran once.services/myprofile.pybecame a package, with the endpoint, the component and the body they share in separate modules. @ericof #71 - Added
pas.plugins.identity.api, a public façade carrying the interfaces, events, content classes and functions a downstream package needs, so nothing has to reach intocoreorserverto find them. @ericof #73 - Added
server_unreleased_groups, so a site can keep chosen groups out of thegroupsclaim without subclassing a serializer.AuthenticatedUsersstays out whatever it says. @ericof #75 - Added a
global_rolesbehavior to user groups, carrying the site-wide roles a group grants. It reads and writes the groups control panel rather than storing a copy, so the two cannot drift, and it is guarded by a new Manager-only permission. Export and import carry it. @ericof #76
Frontend
Feature
- Asked for the caller's profile with the content request instead of separately.
@my-profileis registered inapiExpandersforGET_CONTENT, and the profile gate now prefers the answer that arrived with the page, falling back to its own request on a route that fetches no content —/login,/identities, a control panel — or against a backend too old to offer the component. The expanded answer is used only when the content in the store is the page being rendered: Volto keeps the last content it loaded, so trusting it anywhere else would read a stale answer to a question whose whole point is freshness.FirstLoginandIdentitiesno longer ask at all while anonymous; both routes are registered like any other, so an anonymous visitor opening them directly fired requests that could only answer 401. @ericof #71
Project
Documentation
- Added a how-to for serializing a claim, covering both shapes of the job: adding a claim to a scope the package ships, and registering a scope of your own. Two pages said a site adding a field to its
UserProfiletype had no claim to put it in, and that the extension point for it was deliberately not built; both are corrected, and the claims reference gained the contract a downstream serializer is written against. @ericof #69 - Documented
my-profileas an expandable component in the endpoints reference — where it is registered, which URL it publishes, and why it is the one component here that answers an anonymous caller with nothing rather than a URL — and added an API expanders table to the frontend reference. @ericof #71 - Documented the public Python API in a new reference page, and pointed the driver, enricher, claim-serializer and events guides at
pas.plugins.identity.apiinstead of the modules their names are implemented in. @ericof #73 - Documented
server_unreleased_groupson the claims and settings reference pages. @ericof #75 - Documented the group
global_rolesbehavior and its Manager-only permission. @ericof #76 - Corrected the dispatch example in the profile enricher guide. The walrus bound the result of the
is Nonecomparison rather than the handler, so the example raisedTypeError: 'bool' object is not callableon every login it matched a driver on — the only case it existed for — andenrich_profilecaught it, leaving a logged traceback and an enricher that appeared never to run. @ericof
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 in `config.views.conte...
1.0.0a4
1.0.0a3 (2026-09-08)
Backend
Bugfix
-
The four sign-in endpoints answer on a site whose anonymous visitors cannot view it.
@login-providers,@identity-callback,@magic-linkand@magic-link-confirmeach said "anonymous by design" in their own registration and were each declared againstzope2.View, which is not that.zope2.Viewis looked up as a permission and inherited from the root, so a site that takesViewaway fromAnonymous— a closed intranet, which is the kind of site most likely to want federated login — answered every one of them with a 401. The login page could not list its providers, a provider redirect could not be completed, and neither half of the magic link could be reached; local login kept working throughout, becauseplone.restapideclares its own@loginaszope.Public. All four are declared that way now, whichAccessControl.security.protectClassspecial-cases intodeclareObjectPublic()so that no role is required at all. Nothing else moved: the callback is still authorized by the single-use state bound to its signed flow cookie, and the magic link by its rate limiter and its signed, single-use, short-lived token. @ericof #34 -
A migrated account reaches the profile enrichers with the provider's payload.
Both authomatic paths —
migration.authomatic.migrateon a live site, and the--from-authomaticdump conversion — link each identity through the plugin, which firesIdentityLinked, whose subscriber runs the site's installedIProfileEnricherutilities. That much always worked. What those enrichers were handed did not: each path built its claims snapshot with an emptyraw, andrawis the whole contract, because the property map deliberately refuses a structured claim and an enricher is what a site has instead. So every enricher ran against an empty document, wrote nothing, reported nothing wrong, and a migration produced Profiles missing exactly the fields the enricher had been installed to fill. The payload now comes across: for a live migration, the attributes authomatic parsed out of the provider's response layered over the document it kept indata, in the order its own property sheet resolves them; for a dump, thepropertiesthe documented extraction wrote. Credentials do not come with it. Authomatic keeps a serializedCredentialsholding the account's access and refresh tokens on every identity, and a claims snapshot is stored on the identity record and written out again by the exporter, so a newcore.utils.claims.scrub_payloadstrips that and the other credential-bearing key names on the way in. An identity already linked is still skipped, so re-running an import does not enrich the accounts that arrived the first time. @ericof #35
Frontend
No significant changes.
Project
Documentation
-
The profile enricher guide says what a migration hands you.
Its table of payload shapes listed both authomatic paths as
Empty, which described the behaviour accurately and made it look intended. It is a fourth shape now — authomatic's own record for that account, closest to the plain OAuth2 row — with the two things an enricher author has to know beside it: the snapshot dates from whenever that person last signed in to the old site, so it can lack a key the provider sends today, and an enricher runs once per identity the import actually links, so re-running an import does not reach accounts that arrived on the first one. @ericof #35
1.0.0a3
1.0.0a2 (2026-09-07)
Backend
Breaking
-
A login may be changed by a Manager, and only after the account exists.
loginwas declared with the same write permission asfullname, which the owner of a Profile holds on their own Profile because that is what self-service means. Correcting your name and becoming somebody else were the same action, and a Site Administrator could rename anybody. It is half of the case-folded index user enumeration queries, so rewriting it moves an account away from every sign-in, every Sharing entry written against the old name, and every provider that maps a user by it. It now has a permission of its own,pas.plugins.identity.content.editlogin, and the interesting part is where it applies:user_profile_workflowgrants it toManageralone in every state, while the container Profiles are filed in grants it toManagerandSite Administratorbeside the add permission. An add form checks a field against the container and an edit form against the object, so anybody who may create an account may name it and only a Manager may rename one afterwards. The machine paths are untouched —doAddUserelevates to Manager and writes through the Dexterity factory, which consults no field permission. Existing sites need the 1002 upgrade step: a workflow import does not touch content that already exists, and until its permission maps are rewritten the new permission is acquired rather than managed on every Profile a site already has. @ericof -
The Profile's fields moved onto behaviors, and the addresses onto a tab of their own.
emailsandemailare now declared bypas.plugins.identity.email_addresses, andhome_page,locationandimagebypas.plugins.identity.profile_details. The type itself declareslogin,fullnameanddescriptionand nothing else. Both behaviors are schema-only, so nothing about storage changed: a Profile still answersprofile.emails, the catalog still indexesemail, and the properties that normalize a write and derive the single address are still what runs. A stock site sees the same fields, with the two addresses moved to an Email tab — which is where they belong, being the only fields read underView Personal Identifiable Informationrather than the ordinary view permission. A site running its own user type now composes the same Profile out of the same parts rather than redeclaring five fields and keeping their permissions in step by hand. What breaks is code that read those fields offIUserProfileSchema: they are on the behaviors' schemata now, anditerSchematais what sees all of them.completenesswas one such reader, and would have judged a profile with no address complete. @ericof -
INDEXESandMETADATAno longer exist inpas.plugins.identity.core.catalog.They described the user catalog's shape, and
identity-catalog.xmldescribes it now, so keeping a Python copy would only have meant two lists that agree until they do not.PROFILE_METADATAandGROUP_METADATAstay where they are: they say which columns mean something on which of the two types, which is not something the catalog's own XML can express. Also removespas.plugins.identity.setuphandlers.catalogand the three builders in it, which the import step replaces. @ericof
Feature
-
A deployment can now write its own fields onto a Profile at login, through a named
IProfileEnricherutility.The claim-to-field property map carries a scalar from a provider document to one of four fields, and an add-on whose behavior adds a field of its own could express nothing through it: the target has to be in
WRITABLE_FIELDS, and a claim resolving to a list or a mapping is read as an absent claim rather than written as a Python representation. That refusal is deliberate — it is what stops an OIDCaddressobject landing in somebody's location — so the answer is a second path rather than a wider map. An enricher runs after this package's own writes and after the addresses have been recorded, is handed the Profile, the whole normalized claims mapping including the provider'srawpayload, and the provider configuration the login came through, writes what it likes, and returns the names of the fields it changed. The provider is what keeps a site running several of them from turning into guesswork:driver_idsays what shape the payload is,provider_idtells two deployments of one kind apart, and the provider itself isNoneon the paths that carry no payload either. One modification event is fired for all of them together, beforereconcile, so a required field an enricher fills counts towards completeness in the same login. Each enricher gets a persistent mapping private to its registered name, because this package's own "the provider may replace only what it wrote" fence compares scalars and answers the wrong question about a list — an enricher that ignores the mapping will hand back an entry its owner deleted, on every login. One that raises is logged and skipped: an add-on defect must not lock out the site's users, including whoever would remove it. See the newdocs/how-to-guides/write-a-profile-enricher.md. @ericof -
The user catalog's indexes, columns and lexicon are declared in GenericSetup XML, with import and export steps of their own.
profiles/default/identity-catalog.xmlis now the source of truth, applied by a newidentity-catalogimport step and readable back out by the matching export step. Adding an index or a metadata column is an edit to that file: no Python, andremove="True"takes one away, which the hand-written handlers could never do. The stock GenericSetupcatalogstep cannot reach this catalog -- it resolves its target withqueryUtility(ICatalogTool), which answersportal_catalogand has no notion of a second catalog -- so a step of our own is what was missing. The adapter was not:ZCatalogXMLAdapteris registered forIZCatalogand has adapted this catalog all along, soIdentityCatalogXMLAdaptersubclasses it to change one thing, the filename. That name is load-bearing rather than cosmetic: acatalog.xmlin this profile would not be ignored, it would be applied to the site catalog. Applying the profile still does not populate what it creates, sopas.plugins.identity:rebuild-catalogremains the second half of any such change. @ericof
Bugfix
-
Group membership written by an import now takes effect.
The importer wrote the fields and called
reindexObject(), which maintainsportal_catalogand fires no event. The identity catalog is maintained entirely by the subscribers incore.indexers, which answerIObjectModifiedEventand three others, so every object came out of an import correct and every brain stayed stale.getGroupsForPrincipalreadsgroup_idsoff the brain, so an import wrote the membership and nobody was in the group; a re-imported login or address was equally invisible, which meant an account could still be enumerated under the name it had stopped using. Newly created principals were never affected, becauseapi.content.createfiresObjectAddedEventand the catalog does listen for that: it was only the subsequent field writes that were lost, and applying membership is always one of those. The three call sites now firezope.lifecycleevent.modified, which is what the rest of the package already did. The existing tests missed it because they assert the object, or a count, and the count was right. @ericof #30 -
Converting a
pas.plugins.authomaticdump now reads the target site's property map.convert_authomaticmapped the dump onto Profile fields with its own hardcoded table, which never consulted the provider record the site had configured. A dump carries the provider's own vocabulary, so that table was a guess, and where it disagreed with the site the site lost. GitHub is the worked example: authomatic's parser setslinkfromhtml_url, solinktohome_pagegave every migrated person their GitHub profile page as their homepage and discarded the real one inblog, while abiothe table had no name for was dropped entirely. The site's map for the provider each user signed in with is now applied first, and the built-in table fills only the fields it leaves unset.identity-importer --from-authomatictherefore converts inside the site rather than on the way in; the dump's shape is still checked before Zope starts, so pointing it at the wrong file is still answered in a second. A provider with no record in the target site still converts on the built-in table, and is now counted and warned about rather than losing fields quietly. @ericof
Internal
-
The three behaviors mark themselves with
@provider(IFormFieldProvider)rather than a trailingalsoProvides.Identical in effect -- a
model.Schemasubclass directly provides nothing, so thedirectlyProvidesthe decorator performs discards nothing analsoProvideswould have kept -- and it is what the behaviors inplone.app.contenttypesandplone.app.dexterityuse. The marker now sits on the class instead of sixty lines below the schema, where its absence is the thing to notice: without it the fields store, index and serialize correctly and appear on no form. @ericof
Frontend
No significant changes.
Project
Documentation
-
Added a how-to for adding an index or a metadata column to the user catalog, and documented the
identity-catalogimport and export steps. @ericof -
Added a how-to for writing a profile enricher.
Covers when the property map is the right answer and when it runs out, the four arguments an enricher is handed, why it keys on the provider rather than on a key it hopes means something, why it has to remember what it wrote, and what
claims["raw"]actually holds — which is the userinfo document only for a plain OAuth2 provider such as GitHub, the token's claims fo...
1.0.0a2
1.0.0a1 (2026-09-07)
Backend
Feature
-
Groups nest by containment: a
UserGroupmay be added inside aUserGroup, and everybody in the inner group is in the outer one.The tree is now a second way of writing the edge
group_idsalready carried, and the two are unioned — a group can be filed inside one group and name others, and a group that does both contributes the edge once. Everything that reads membership walks one graph and cannot tell which way an edge was written, sogetGroupsForPrincipal,getGroupMembers,getNestedGroupIdsand@group-membersall follow with no change to their contracts. A contained group is a group of the site like any other: enumerated, grantable on the Sharing tab, and resolvable by id, which it was not before. Because group ids are what local roles and memberships are stored in terms of and object ids are only unique within a folder, a second group claiming an id already in use is now refused when it is created or renamed. Export and import carry the hierarchy in a new optionalcontainer_groupkey, so a restored site comes back with the shape it had rather than flat. @ericof
Bugfix
-
Added the upgrade step that lets an existing site nest groups by containment.
An FTI is a persistent object written into
portal_typesat install, so a site installed before containment kept an emptyallowed_content_typeson theUserGrouptype: the add menu offered nothing inside a group, and nothing said why. The profile is now at version1001, and a site behind it is offered a step that re-importstypeinfo. Upgrade steps live one package per version underupgrades/, each with its ownconfigure.zcml— this is the first of them, so it sets the shape for the next. @ericof -
Deleting a Plone site that holds a Profile no longer fails with
CannotGetPortalError.The indexing subscribers asked the current site for the catalog, which is a different question from the one they had the answer to: they are handed the object, and the object knows which site it is in. The two agree on every request and disagree exactly where nothing has called
setSite— azconsolescript working onapp, and the deletion of a site from the Zope root, which is whatDELETE_EXISTING=1 make create-sitedoes. There the lookup raised out of the handler and took the deletion with it, naming neither the site nor the catalog. The catalog is now acquired from the object, so the unindex happens rather than merely not raising, andquery_cataloganswersNoneinstead of raising when there is no current site at all. @ericof
Documentation
-
Added the PyPI badges to the README.
The package is on PyPI as of
1.0.0a1, andpyproject.tomlnames this file as the long description — so the version and the interpreters it declares are now visible at the top of the page somebody lands on before deciding to install it. @ericof
Frontend
Documentation
-
Added the registry badges to the README.
The package is on npm as of
1.0.0-alpha.1. The README carries its version badge and, beside it, the version of the backend package it requires — the two are released together, and a reader on npm cannot otherwise see whether the halves they are about to install match. @ericof
Project
Documentation
-
Documented that groups nest by containment as well as by
group_ids.The concepts page now states both ways of writing the edge and that the graph unions them, along with the two consequences a reader will otherwise meet as surprises: clearing
group_idsdoes not un-nest a group that is nested by containment, and a group id is unique across the site rather than within a folder. The reference tables, the glossary entry and the principal-document format gained the same, the last of them describing the new optionalcontainer_groupkey and the import pass that reads it. @ericof -
Said in the READMEs that the packages are published.
1.0.0a1is on PyPI and on npm, souv add pas.plugins.identityand adding@plone-collective/volto-identityare now instructions that work rather than instructions for later. All three READMEs carry the registry badge for what they describe, and the packages table names each registry as a link rather than as a word. @ericof