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
This chapter contains login and logout related code snippets.
Login entry points¶
There are two login points in Plone
view (appended to any content URL) directs you to the page
where you came from after the login.
view does login without the redirect back to the original
In addition, the
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
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
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¶
Using username and password¶
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)
Using username only¶
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
You can use the collective.onlogin package to set up many actions for you.
If you need more control, post-login code can be executed with events defined in PluggableAuthService service.
IUserInitialLoginInEvent(logs for the first time)
Here is an example how to redirect a user to a custom folder after he/she logs in (overrides standard Plone login behavior)
<configure xmlns="http://namespaces.zope.org/zope" i18n_domain="my.package"> <subscriber for="Products.PluggableAuthService.interfaces.events.IUserLoggedInEvent" handler=".postlogin.logged_in_handler" /> </configure>
# 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 from zope.globalrequest import getRequest # CMFCore imports from Products.CMFCore import permissions # 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: URL if we should redirect, otherwise None """ # Fetch the site related to the current HTTP request portal = getSite() # 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 None # 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)) return obj.absolute_url() + "/edit" 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 None def logged_in_handler(event): """ Listen to the event and perform the action accordingly. """ user = event.object url = redirect_to_edit_access_folder(user) if url: request = getRequest() if request is None: # HTTP request is not present e.g. # when doing unit testing / calling scripts from command line return # check if came_from is not empty, then clear it up, otherwise further # Plone scripts will override our redirect if request.get('came_from', None): request['came_from'] = '' request.form['came_from'] = '' request.RESPONSE.redirect(url)
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)
Entry points to logged in member handling¶
Products.PluggableAuthService.PluggableAuthService._extractUserIds(). It will try to extract credentials from incoming HTTP
request, using different "extract" plug-ins of
is also known as
persistent 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.
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
PAS cache settings¶
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" />
Login as another user ("sudo")¶
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
Getting logged in users¶
Was somewhere, but can't find where.
Locking user account after too many retries¶
For security reasons, you might want to locking users after too many tries of logins. This protects user accounts against brute force attacks.
Hyperlinks to authenticated Plone content in Microsoft Office¶
Microsoft Office applications (in the first instance Word
and Excel), have been observed to attempt to resolve
hyperlinks once clicked, prior to sending the hyperlink to
the user's browser. So, if such a link points to some
Plone content that requires authentication, the Office
application will request the URL first, and receive a 302
Redirect to the
Python script on the relevant Plone instance. So, if your
original hyperlink was like so:
and this URL requires authentication, then the Office application will send your browser to this URL:
Normally, this isn't a problem if a user is logged out at
the time. They will be presented with the relevant login
form and upon login, they will be redirected accordingly
However, if the user is already logged in on the
site, visiting this URL will result in an
page being displayed. This is to be expected of Plone (as
this URL is normally only reached if the given user has no
access), but because of Microsoft Office's mangling of the
URL, may not necessarily be correct as the user may indeed
The following drop-in replacement for the
script has been tested in Plone 4.1.3 (YMMV). Upon a
request coming into this script, it attempts (a hack) to
traverse to the given path. If permission is actually
allowed, Plone redirects the user back to the content.
Otherwise, things proceed normally and the user has no
access (and is shown the appropriate message):
## Script (Python) "require_login" ##bind container=container ##bind context=context ##bind namespace= ##bind script=script ##bind subpath=traverse_subpath ##parameters= ##title=Login ## login = 'login' portal = context.portal_url.getPortalObject() # if cookie crumbler did a traverse instead of a redirect, # this would be the way to get the value of came_from #url = portal.getCurrentUrl() #context.REQUEST.set('came_from', url) if context.portal_membership.isAnonymousUser(): return portal.restrictedTraverse(login)() else: expected_location = context.REQUEST.get('came_from') try: #XXX Attempt a traverse to the given path portal.restrictedTraverse(expected_location.replace(portal.absolute_url()+'/','')) container.REQUEST.RESPONSE.redirect(expected_location) except: return portal.restrictedTraverse('insufficient_privileges')()
For further reading see:
Single Sign-On and Active Directory¶
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: