User Profiles¶
Plone Intranet is designed to provide an out-of-the-box user profile which provides the following:
- Authentication (using dexterity.membrane)
- Customisable profile fields (using dexterity behaviours which can be disabled or overriden)
- Support for external data sources (e.g. AD/LDAP)
The following key design decisions were made to fit the use cases of Plone Intranet:
Users as content
Rather than using default plone members, we use dexterity.membrane to create real dexterity content that can be managed in the same way as all other content, whilst still providing authentication.
Username as Userid
The default membrane implementation uses UUIDs as the unique id that Plone uses to grant roles and permissions (userid). We use the username instead, to ensure compatibility with external authentication sources such as AD/LDAP which have no knowledge of Plone’s UUIDs.
This means that userprofile.username is and always should be equal to the userid userprofile.getId(). Neither should ever be changed. Profiles should never get a new content id, this is not supported and will trigger bugs. Note that
getUserName()
will return a value different from.username
in case logging in by email is enabled. See ‘Login by email’ below and developer notes further down.The default membrane implementation uses email addresses as login names. We default to using the userid as stored in the username field, as login name. It is possible to configure your site to do login-by-email, see below.
User Management¶
You should not use the Zope rescue user for anything else, than creating users as documented here.
Warning
You should not create users via the Plone control panel. Also, directly creating user profiles in Barceloneta is invalid - this will appear to work but it will trigger bugs, like not being able to add such profiles as members to workspaces.
Use an external data source (Active Directory / LDAP) to manage your users, see below. Or use the bulk upload facility to create user profiles, as documented below.
Bulk Upload¶
There is a bulk upload from CSV option. Column names are mapped to field names, and the data is validated before users are created:
To use the bulk upload, visit the @@import-users browser view on the profiles folder in your site:
/plonesite/profiles/@@import-users
See this example csv file
to get you started.
Supported columns are listed in the @@import-users view.
If you check “Update details for existing users” existing user profiles are updated with the values provided in the csv file.
If you omit values in the password column, a password is auto-generated for new users, and the password of existing users will be kept.
Warning
Don’t upload the example CSV into a production site without changing at least the passwords…
Role assignments¶
After creating users with bulk upload, you can manage role assignments in the Plone control panel by using the Theme Switcher
http://cms.localhost:8080/Plone/@@usergroup-userprefs
Please do not add users in this control panel, it won’t work.
If you’re using AD/LDAP to manage users, you may want to manage roles and groups via LDAP instead. YMMV.
Avatar images¶
After creating users with bulk upload, you can add avatar images as follows:
In the siteroot, via the Barceloneta interface on cms.localhost:8080, add a Folder ‘avatars’ to the portal root
Upload images into this folder with ids like ‘johndoe.jpg’ matching userid ‘johndoe’
(As plone does not allow ids containing dots, use ‘john_doe.jpg’ as avatar image for userid ‘john.doe’)
Run
http://portal_url/avatars/@@import-avatars
Login by email¶
By default, the userid is the username is the login name.
If you’d like to provide users with a different login name, the only supported option is to enable logging in by email address. Any change to the email
attribute on a userprofile, will then result in a different login name for the user on the login page. To enable login by email, change the following dexterity.membrane registry setting in registry.xml
from the ploneintranet default False
to True
:
<record name="dexterity.membrane.behavior.settings.IDexterityMembraneSettings.use_email_as_username">
<value>True</value>
</record>
If you’re changing this in an existing site, you then need to either reindex the exact_getUserName
and getUserName
indexes in membrane_tool
.
Warning
In addition to the dexterity use_email_as_username
setting, there’s a plone setting use_email_as_login
as well. You should not touch this when using LDAP: it has an event handler which is not LDAP aware and will error out trying to update non-existent ZODB acl_users.
Our login and password page overrides retrieve the dexterity setting, not the plone setting to determine whether logging in by email is enabled. This will result in some textual adjustments on those pages.
External authentication and/or data sources (e.g. AD/LDAP)¶
The Plone Intranet UI always uses the membrane profile data as the source of user data, to ensure a consistent experience when assigning roles, searching or browsing users.
However there are various features in place to support external authentication and user data sources.
For instructions on installing LDAP, see Optional: LDAP
Automatic profile creation¶
If a user authenticates to the site using an alternative authentication system, the login event triggers the automatic creation of a matching membrane profile.
Manual profile creation¶
The sync-users browser view is available on the profiles directory, and can be used to synchronise the profiles on the site with a list of users provided by an external data source such as AD/LDAP.
To use this view, first load the Plone Intranet : Suite : LDAP profile in the ZMI in generic_setup.
This registers the ID of a PAS plugin as your main user source using the ploneintranet.userprofile.primary_external_user_source registry entry:
<record name="ploneintranet.userprofile.primary_external_user_source">
<field type="plone.registry.field.ASCIILine">
<title>Primary External User Source</title>
<description>
The ID of the PAS plugin that will be treated as the primary source of external users.
</description>
</field>
<value>ldap-plugin</value>
</record>
This PAS plugin will be used as the canonical set of user accounts that are supported on the site. Running the sync-users view will:
- Add missing membrane profiles for any users found in the primary PAS plugin
- Disable existing membrane profiles for any users missing from the primary PAS plugin.
This view is designed to be run periodically using a clock server or cron task.
The view requires Manager privileges.
/plonesite/profiles/@@sync-users
To make it easier to run this job from cron, there’s a special @@cron
view
that allows the sync to happen without providing a username/password:
/plonesite/@@cron/sync-users
This view is disabled by default. You can enable it via buildout:
[instance-cron]
zcml =
ploneintranet
ploneintranet.utils.cron
Warning
zcml += ploneintranet.utils.cron
is unreliable, make sure ploneintranet/configure.zcml is loaded.
Property sheet mapping¶
It is also possible to configure specific membrane properties to be regularly synchronised with an external data source (such as AD/LDAP) using Plone’s PAS properties infrastructure.
The registry entry ploneintranet.userprofile.property_sheet_mapping allows each user profile field to be mapped to a specific PAS plugin (using the id of the PAS plugin inside acl_users):
<record name="ploneintranet.userprofile.property_sheet_mapping">
<field type="plone.registry.field.Dict">
<title>Property sheet mapping</title>
<description>
A mapping of a user property to a specific property sheet which
should be used to obtain the data for this attribute.
</description>
<key_type type="plone.registry.field.ASCII" />
<value_type type="plone.registry.field.TextLine" />
</field>
<value>
<element key="username">ldap_plugin</element>
<element key="email">another_pas_plugin</element>
</value>
</record>
External property synchronisation¶
The sync-user-properties browser view is available on the profiles directory, and will use the above mapping to copy the relevant properties from the relevant PAS plugin property sheet, and store it on the membrane profile.
It supports any PAS plugin that provides PAS properties for a user, and will update all existing membrane profiles every sync, so could be expensive depending on the number of users in your site.
This view is designed to be run periodically using a clock server or cron task.
The view requires Manager privileges.
/plonesite/profiles/@@sync-user-properties
To make it easier to run this job from cron, there’s a special @@cron
view
that allows the sync to happen without providing a username/password:
/plonesite/@@cron/sync-user-properties
You can also sync an individual user profile using the sync view. This view also requires Manager privileges.
/plonesite/profiles/joe-bloggs/@@sync
Specific AD/LDAP synchronisation¶
If you have Products.PloneLDAP installed, a separate AD/LDAP view is provided that will query the AD server for any users that have changed since the last sync (using the whenChanged AD attribute).
This significantly improves the performance of the sync:
/plonesite/profiles/@@sync-user-properties-ldap
Customising User Profiles¶
User profiles are expected to be highly customised for each Plone Intranet deployment. As such, the profile views are built dynamically from dexterity behaviours, and support extra options such as hidden or read-only fields.
Vocabularies¶
The following vocabularies can be customised using their corresponding plone.app.registry entries (e.g. using GenericSetup).
- Primary Location : ploneintranet.userprofile.locations
Adding/Removing fields¶
The User Profile comes with a base set of fields that are required by the Plone Intranet templates. These fields are as follows:
-
interface
ploneintranet.userprofile.content.userprofile.
IUserProfile
¶ The core user profile schema.
Most of the plone intranet UI relies on these fields.
-
username
= <zope.schema._bootstrapfields.TextLine object>¶ Username
-
first_name
= <zope.schema._bootstrapfields.TextLine object>¶ First name
-
last_name
= <zope.schema._bootstrapfields.TextLine object>¶ Last name
-
person_title
= <zope.schema._bootstrapfields.TextLine object>¶ Person title
-
portrait
= <plone.namedfile.field.NamedBlobImage object>¶ Photo
-
recent_contacts
= <zope.schema._field.List object>¶ Last Contacts
-
email
= <ploneintranet.userprofile.content.userprofile.Email object>¶ Email
-
There is also an additional set of (optional) fields. The optional fields are provided by the behaviour IUserProfileAdditional
You can remove the optional fields by disabling the IUserProfileAdditional behaviour in your GenericSetup profile. For more information on adding/removing behaviours using GenericSetup, see docs.plone.org.
To add new fields, simply create a new behaviour and assign it to the ploneintranet.userprofile.userprofile type. For more information on adding custom field behaviours, see the Behaviours manual.
Hiding fields¶
To hide fields from the UI, add the relevant field name to the ploneintranet.userprofile.hidden_fields registry entry using GenericSetup:
<record name="ploneintranet.userprofile.hidden_fields">
<field type="plone.registry.field.Tuple">
<title>Hidden fields</title>
<description>
User profile fields that are hidden from the profile editing page
</description>
<value_type type="plone.registry.field.TextLine" />
</field>
<value>
<element>first_name</element>
<element>MyCustomBehaviour.my-hidden-fieldname</element>
</value>
</record>
Read-only fields¶
To mark a field as ‘read only’ in the UI (but leave the field editable via code), add the relevant field name to the ploneintranet.userprofile.read_only_fields registry entry using GenericSetup.
This is useful for field data that comes from a separate source (e.g. AD/LDAP)
<record name="ploneintranet.userprofile.read_only_fields">
<field type="plone.registry.field.Tuple">
<title>Read only fields</title>
<description>
User profile fields that are read-only
(shown on profile editing page but not editable)
</description>
<value_type type="plone.registry.field.TextLine" />
</field>
<value>
<element>username</element>
<element>MyCustomBehaviour.my-read-only-fieldname</element>
</value>
</record>
LDAP: Putting it all together¶
Plugin activation¶
- ZMI Install portal_setup > import > Plone Intranet Suite: LDAP
- ZMI http://localhost:8080/Plone/acl_users/ldap-plugin/acl_users/manage_servers Delete the existing ldap server and create a new one with the right hostname and port. For a default ploneintranet buildout that is: host ‘localhost’ and port ‘8389’.
- ZMI http://localhost:8080/Plone/acl_users/ldap-plugin/acl_users/manage_main
Configure the
User Base DN
,Group Base DN
,Manager DN
andUser password encryption
to match your local LDAP installation.
Attribute mapping¶
The variable names used in Plone may be different from the attribute names in your LDAP schema. The plugin maintains a mapping of the “LDAP key” to “Plone key” which you can inspect in the ZMI under “LDAP Schema” (direct ZMI URL: http://localhost:8080/Plone/acl_users/ldap-plugin/acl_users/manage_ldapschema )
The default mapping looks like this:
But even if you don’t need to change it you’ll need to be aware of this name mapping in order to understand how the registry configuration below interacts with your LDAP schema.
registry.xml¶
To use LDAP, you will typically need to add the following registry config
to your own policy GenericSetup. The below reflects the default plone.app.ldap
which was a dependency until Quaive version 2.0.0.
setup - you may wish to remove or change certain fields, depending on your
own LDAP schema.
<?xml version="1.0"?>
<registry xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="ploneintranet">
<record name="ploneintranet.userprofile.primary_external_user_source">
<field type="plone.registry.field.ASCIILine">
<title>Primary External User Source</title>
<description>
The ID of the PAS plugin that will be treated as the primary source of external users.
</description>
</field>
<value>ldap-plugin</value>
</record>
<record name="ploneintranet.userprofile.property_sheet_mapping">
<field type="plone.registry.field.Dict">
<title>Property sheet mapping</title>
<description>
A mapping of a user property to a specific property sheet which
should be used to obtain the data for this attribute.
</description>
<key_type type="plone.registry.field.ASCII" />
<value_type type="plone.registry.field.TextLine" />
</field>
<value>
<element key="username">ldap-plugin</element>
<element key="first_name">ldap-plugin</element>
<element key="last_name">ldap-plugin</element>
<element key="email">ldap-plugin</element>
<element key="department">ldap-plugin</element>
<element key="telephone">ldap-plugin</element>
<element key="address">ldap-plugin</element>
<element key="primary_location">ldap-plugin</element>
</value>
</record>
<record name="ploneintranet.userprofile.read_only_fields">
<field type="plone.registry.field.Tuple">
<title>Read only fields</title>
<description>
User profile fields that are read-only
(shown on profile editing page but not editable)
</description>
<value_type type="plone.registry.field.TextLine" />
</field>
<value>
<element>username</element>
<element>first_name</element>
<element>last_name</element>
<element>email</element>
<element>IUserProfileAdditional.department</element>
<element>IUserProfileAdditional.telephone</element>
<element>IUserProfileAdditional.address</element>
<element>IUserProfileAdditional.primary_location</element>
</value>
</record>
</registry>
There’s a gotcha: fullname
is a calculated property, so don’t try to set that directly.
User identifier versus login name, oh my¶
TL;DR: Of all the possible user-identifierish attributes and accessors: use getId()
by default. Unless you need an actual login name for authentication, in which case you should use .getUserName()
.
The problem: for our membrane userprofiles id == getId() == getUserId() == username == getUserName()
except when logging in by email, in which case: id == getId() == getUserId() == username != getUserName() == email
. Non-membrane acl_users like admin and test users behave differently again.
In Plone, semantically the username is the login name the user inputs in the login form. The userid is a stable user identifier the code uses to refer to a user object.
If you set dexterity.membrane.settings.IDexterityMembraneSettings.use_email_as_username
to True, then it becomes possible for users to change email addresses, hence login names as returned by getUserName()
- and in this scenario you want to keep the userid unchanges so you retain user profile history. Note that in this scenario getUserName()
will be different from username
which must remain equal to userid
.
The confusion between username and userid is endemic throughout the Plone stack, including the ploneintranet code base. In many many instances username
syntax is use for what semantically is a userid
. It’s impossible to search-and-replace that, since the upstream API’s of e.g. dexterity.membrane and plone.api already hardcode the mistake.
To make this even more of a mission impossible, much of our code and test code mixes membrane user profiles with addressing acl_users members. Which are different beasts, with different APIs, even though they’re facets of the same actual user profile object.
As a solution I offer you the following meditation on syntax versus semantics:
- A
userprofile.id
is the content object’s id in the userprofiles folder. This will be the same as the userid - seeploneintranet.api.userprofile.create
, and the assumption that the two are identical plays out in our code base. Changing content ids of userprofiles later on is not possible, see the tests inploneintranet.userprofile
that try to do that. - A
userprofile
does not have auserid
attribute. The dx.membrane behaviors do provide.getId()
and.getUserId()
accessors. These are identical. - A
userprofile
has a.username
attribute. The value of this will be identical to the user id, since this is whatploneintranet.api.userprofile.create
does on creating the profile. DO NOT CHANGE THE USERNAME. This attribute is used as an alias for userid throughout our code base, never mind that the name does not match the meaning. Changing the username invites disaster. - A user profile also has an
email
attribute. Here it gets interesting. If you enableuse_email_as_username
, the actual login name is the email address as returned bygetUserName()
- which is only available on a behavior, not directly on a userprofile. And this may, for some users, be an updated email address different from theusername
they were created with. - A acl_users member is a different thing again. It has a
getUserName()
accessor, and sometimes it has a workinggetUserId()
accessor (when it’s backed by a membrane user) and sometimes it doesn’t (e.g. admin acl_user). It always has agetId()
accessor and that’s what we’re using.
Further reading:
- https://github.com/collective/dexterity.membrane/pull/27
- ploneintranet/src/ploneintranet/userprofile/tests/test_username_userid.py
Dos and donts¶
- When working with authentication code, use
getUserName()
instead ofusername
to get the actual login name, as opposed to the user identifier. - In all other code paths, user references typically are user ids. For clarity, it’s best to use
getId()
, which is safer thangetUserId()
but for legacy compatibilityusername
is also fine, because that should always match the userid. Just remember thatusername
syntax is userid semantics, not the login name. - Never use
getUserName()
syntax for userid semantics. It will appear to work, until you toggleuse_email_as_username
and then you’re in a world of pain, where only those users whose email address is different from their user id suffer obscure bugs.
User Profile API¶
-
ploneintranet.api.userprofile.
avatar_tag
(username=None, link_to=None, link_class=None)¶ Get the tag that renders the user avatar wrapped in a link
Parameters: username (string) – Username for which to get the avatar url Returns: HTML for the avatar tag Return type: string
-
ploneintranet.api.userprofile.
avatar_url
(username=None)¶ Get the avatar image url for a user profile
Parameters: username (string) – Username for which to get the avatar url Returns: absolute url for the avatar image Return type: string
-
ploneintranet.api.userprofile.
create
(username, email=None, password=None, approve=False, properties=None)¶ Create a Plone Intranet user profile.
Parameters: - username (string) – [required] The userid for the new user. WTF? see #1043.
- email (string) – [required] Email for the new user.
- password (string) – Password for the new user. If it’s not set we generate a random 12-char alpha-numeric one.
- approve (boolean) – If True, the user profile will be automatically approved and be able to log in.
- properties (dict) – User properties to assign to the new user.
Returns: Newly created user
Return type: ploneintranet.userprofile.content.userprofile.UserProfile object
-
ploneintranet.api.userprofile.
get
(userid)¶ Get a Plone Intranet user profile by userid. userid == username, but username != getUsername(), see #1043.
Parameters: userid (string) – Usernid of the user profile to be found Returns: User profile matching the given userid Return type: ploneintranet.userprofile.content.userprofile.UserProfile object
-
ploneintranet.api.userprofile.
get_current
()¶ Get the Plone Intranet user profile for the current logged-in user
Returns: User profile matching the current logged-in user Return type: ploneintranet.userprofile.content.userprofile.UserProfile object
-
ploneintranet.api.userprofile.
get_user_suggestions
(context=None, full_objects=True, min_matches=5, **kwargs)¶ This is a wrapper around get_users with the intent of providing staggered suggestion of users for a user picker: 1. Users from the current context (workspace)
If not enough users, add:- Users followed by the current logged-in user If not enough combined users from 1+2, fallback to:
- All users in the portal.
List users from catalog, avoiding expensive LDAP lookups.
Parameters: - context (Content object) – Any content object that will be used to find the UserResolver context
- full_objects (boolean) – A switch to indicate if full objects or brains should be returned
- min_matches (int) – Keeps expanding search until this treshold is reached
Returns: user brains or user objects
Return type: iterator
-
ploneintranet.api.userprofile.
get_userids
()¶ For the moment it just returns all the ids of the userprofiles we have in the site.
Returns: the userprofile ids Return type: iterator
-
ploneintranet.api.userprofile.
get_users
(context=None, full_objects=True, **kwargs)¶ List users from catalog, avoiding expensive LDAP lookups.
Parameters: - context (Content object) – Any content object that will be used to find the UserResolver context
- full_objects (boolean) – A switch to indicate if full objects or brains should be returned
Returns: user brains or user objects
Return type: iterator
-
ploneintranet.api.userprofile.
get_users_from_userids_and_groupids
(ids=None)¶ Given a list of userids and groupids return the set of users
FIXME this has to be folded into get_users