Login and logout¶
Login and logout related programming activities in Plone
- Login entry points
- Extracting credentials
- Authenticating the user
- Post-login actions
- Post-logout actions
- Entry points to logged in member handling
- Login as another user ("sudo")
- Getting logged in users
- Locking user account after too many retries
- Hyperlinks to authenticated Plone content in Microsoft Office
- Single Sign-On and Active Directory
- Preventing duplicate logins from different browsers
There are two login points in Plone
/login view (appended to any content URL) directs you to the page where you came from after the login.
/login_form view does login without the redirect back to the original page.
In addition, the
/logout action logs the user out.
The logic that drives the login process is implemented using the CMF form controller framework (legacy). To customize it, you need to override one or more of the
login_* scripts. This can be accomplished in two ways: register your own skin directory or use z3c.jbot. Note that in both cases, you need to copy the
.metadata file as well.
Extracting credentials try to extract log-in (username, password) from HTTP request.
Below is an example how to extract and authenticate the user manually. It is mostly suitable for unit testing. Note that given login field isn't necessarily the username. For example, betahaus.emaillogin add-on authenticates users by their email addresses.
Credential extraction will go through all plug-ins registered for PlonePAS system.
The first found login/password pair attempt will be used for user authentication.
Unit test example:
def extract_credentials(self, login, password): """ Spoof HTTP login attempt. Functional test using zope.testbrowser would be more appropriate way to test this. """ request = self.portal.REQUEST # Assume publishing process has succeeded and object has been found by traversing # (this is usually set by ZPublisher) request['PUBLISHED'] = self.portal # More ugly ZPublisher stubs request['PARENTS'] = [self.portal] request.steps = [self.portal] # Spoof HTTP request login fields request["__ac_name"] = login request["__ac_password"] = password # Call PluggableAuthService._extractUserIds() # which will return a list of user ids extracted from the request plugins = self.portal.acl_users.plugins users = self.portal.acl_users._extractUserIds(request, plugins) if len(users) == 0: return None self.assertEqual(len(users), 1) # User will be none if the authentication fails # or anonymous if there were no credential fields in HTTP request return users
Authenticating the user will check that username and password are correct.
Pluggable Authentication Service (acl_users under site root) will go through all authentication plug-ins and return the first successful authenticated users.
Read more in PlonePAS.
Unit test example:
def authenticate_using_credentials(self, login, password): request = self.portal.REQUEST # Will return valid user object user = self.portal.acl_users.authenticate(login, password, request) self.assertNotEqual(user, None)
Useful for sudo style logins.
def loginUser(self, username): """ Login Plone user (without password) """ self.context.acl_users.session._setupSession(username, self.context.REQUEST.RESPONSE) self.request.RESPONSE.redirect(self.portal_state.portal_url())
Post-login actions are executed after a successful login. Post-login actions which you could want to change are
- Where to redirect the user after login
- Setting the status message after login
Post-login code can be executd with events defined in PluggableAuthService service.
IUserInitialLoginInEvent(logs for the first time)
Here is an
Grok based example how to redirect a user to
a custom folder after he/she logs in (overrides standard Plone login behavior)
# Python imports import logging # ZODB imports from ZODB.POSException import ConflictError # Zope imports from AccessControl import getSecurityManager from zope.interface import Interface from zope.component import getUtility from zope.app.component.hooks import getSite # CMFCore imports from Products.CMFCore import permissions from Products.PluggableAuthService.interfaces.events import IUserLoggedInEvent # Caveman imports from five import grok # Plone imports from Products.CMFPlone.interfaces.siteroot import IPloneSiteRoot # Logger output for this module logger = logging.getLogger(__name__) #: Site root relative path where we look for the folder with an edit access CUSTOM_USER_FOLDERS = "fi/yritykset" def redirect_to_edit_access_folder(user): """ Redirects the user to a folder he/she has editor access. This is for use cases where you have a owned content (e.g. company/product data) on a shared site. You want to make it simple for the users with limited knowledge to edit their own data by redirecting to the edit view right after the login. :return: True or False depending if we found a redirect target to the user or not """ # Get acce s to the site within we are currently processing # the HTTP request portal = getSite() # We need to access the HTTP requesrt object via # acquisition as it is not exposed by the event request = getattr(portal, "REQUEST", None) if not request: # HTTP request is not present e.g. # when doing unit testing / calling scripts from command line return False # Look for portal relative paths where the items are try: target = portal.unrestrictedTraverse(CUSTOM_USER_FOLDERS) except ConflictError: # Transaction retries must be # always handled specially in exception handlers raise except Exception, e: # Let the login proceed even if the folder has been deleted # don't make it impossible to login to the site logger.exception(e) return False # Check if the current user has Editor access # in the any items of the folder sm = getSecurityManager() for obj in target.listFolderContents(): if sm.checkPermission(permissions.ModifyPortalContent, obj): logger.info("Redirecting user %s to %s" % (user, obj)) request.response.redirect(obj.absolute_url() + "/edit") return True logger.warn("User %s did not have his/her own content item in %s" % (user, target)) # Let the normal login proceed to the page "You are now logged in" etc. return False @grok.subscribe(IUserLoggedInEvent) def logged_in_handler(event): """ Listen to the event and perform the action accordingly. """ user = event.object redirect_to_edit_access_folder(user)
when the user logs out via Log out menu item.
You cannot catch session timeout events this way... only explicit logout action.
<subscriber for="Products.PlonePAS.events.UserLoggedOutEvent" handler=".smartcard.clear_extra_cookies_on_logout" />
def clear_extra_cookies_on_logout(event): """ Logout event handler. When user explicitly logs out from the Logout menu, clear our privileges smartcard cookie. """ # Which cookie we want to clear cookie_name = SmartcardHelper.PRIVILEDGED_COOKIE_NAME request = event.object.REQUEST # YES CAPS LOCK WAS MUST WHEN ZOPE 2 WAS INVENTED # SOMEWHERE AROUND NINETIES. THEN IT WAS THE CRUISE # CONTROL FOR COOLNESS AND ZOPE IS SOO COOOOOL. response = request.RESPONSE # Voiding our special cookie on logout response.expireCookie(cookie_name)
It will try to extract credentials from incoming HTTP request, using
different "extract" plug-ins of PAS framework.
PluggableAuthService is also known as
object in the site root.
For each set of extracted credentials, try to authenticate a user; accumulate a list of the IDs of such users over all our authentication and extraction plugins.
PluggableAuthService may use ZCacheable
pattern to see if the user data exists already in the cache, based on
any extracted credentials, instead of actually checking whether
the credentials are valid or not. PluggableAuthService must
be set to have cache end. By default it is not set,
but installing LDAP sets it to RAM cache.
Here is a short view snippet to set PAS cache state:
from Products.Five.browser import BrowserView from zope.app.component.hooks import getSite from Products.CMFCore.utils import getToolByName class PASCacheController(BrowserView): """ Set PAS caching parameters from browser address bar. """ def getPAS(self): site=getSite() return getToolByName(site, "acl_users") def setPASCache(self, value): """ Enables or disables pluggable authentication service caching. The setting is stored persistently in PAS This caches credentials for authenticated users after the first login. This will make authentication and permission operations little bit faster. The downside is that the cache must be purged if you want to remove old values from there. (user has been deleted, etc.) More info * https://github.com/plone/plone.app.ldap/blob/master/plone/app/ldap/ploneldap/util.py """ pas = self.getPAS() if value: # Enable if pas.ZCacheable_getManager() is None: pas.ZCacheable_setManagerId(manager_id="RAMCache") pas.ZCacheable_setEnabled(True) else: # Disable pas.ZCacheable_setManagerId(None) pas.ZCacheable_setEnabled(False) def __call__(self): """ Serve HTTP GET queries. """ cache_value = self.request.form.get("cache", None) if cache_value is None: # Output help text return "Use: http://localhost/@@pas-cache-controller?cache=true" value = (cache_value == "true") self.setPASCache(value) return "Set value to:" + str(value)
... and related ZCML
<browser:page for="Products.CMFCore.interfaces.ISiteRoot" name="pas-cache-controller" class=".pascache.PASCacheController" permission="cmf.ManagePortal" />
If you need to login to production system another user and you do not know the password, there is an add-on product for it
For security reasons, you might want to locking users after too many tries of logins. This protects user accounts against brute force attacks.
Plone can be used in a Microsoft Active Directory environment (or standard Kerberos environment) such that users are automatically and transparently authenticated to Plone without requesting credentials from the user.
This is quite an advanced topic and requires some set up on the server, but can be achieved with Plone running on either Unix/Linux or Windows environments.
More details can be found in this presentation from Plone Open Garden 2013: