Skip to content

Commit

Permalink
fix(setup): Zope root cookie auth and login form
Browse files Browse the repository at this point in the history
The GenericSetup "various" import step that originally installs PAS into a Plone portal
also migrates the Zope root `/acl_users`.  Probably an accident over time, but it
results in a cookie auth plugin that doesn't work outside of the Plone portal:

    2021-12-27 11:12:02,243 ERROR   [Zope.SiteErrorLog:22][waitress-0] ComponentLookupError: http://localhost:49080/api/acl_users/credentials_cookie_auth/login
    Traceback (innermost last):
      Module ZPublisher.WSGIPublisher, line 162, in transaction_pubevents
      Module ZPublisher.WSGIPublisher, line 372, in publish_module
      Module ZPublisher.WSGIPublisher, line 266, in publish
      Module ZPublisher.mapply, line 85, in mapply
      Module ZPublisher.WSGIPublisher, line 63, in call_object
      Module Products.PlonePAS.plugins.cookie_handler, line 106, in login
      Module Products.PluggableAuthService.PluggableAuthService, line 1153, in updateCredentials
      Module Products.PlonePAS.plugins.cookie_handler, line 74, in updateCredentials
      Module zope.component._api, line 165, in getUtility
    zope.interface.interfaces.ComponentLookupError: (<InterfaceClass plone.registry.interfaces.IRegistry>, '')

This import step also removes the `login_form` template which breaks the challenge
response.

Add an interface check to decide whether to install Plone's `ExtendedCookieAuthHelper`
or PAS's vanilla `CookieAuthHelper`.
  • Loading branch information
rpatterson committed Dec 28, 2021
1 parent 0bc8832 commit 17deb97
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 6 deletions.
2 changes: 2 additions & 0 deletions news/65.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix broken Zope root `/acl_users` cookie plugin on `PlonePAS` install.
[rpatterson]
30 changes: 24 additions & 6 deletions src/Products/PlonePAS/setuphandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
from Acquisition import aq_base
from Acquisition import aq_parent
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone import interfaces as plone_ifaces
from Products.PlonePAS import config
from Products.PlonePAS.plugins import cookie_handler
from Products.PlonePAS.interfaces import group as igroup
from Products.PlonePAS.interfaces.plugins import ILocalRolesPlugin
from Products.PlonePAS.interfaces.plugins import IUserIntrospection
Expand All @@ -13,9 +15,11 @@
from Products.PluggableAuthService.interfaces.plugins import IChallengePlugin
from Products.PluggableAuthService.interfaces.plugins \
import ICredentialsResetPlugin
from Products.PluggableAuthService.plugins import CookieAuthHelper
from Products.PluggableAuthService.plugins.RecursiveGroupsPlugin \
import addRecursiveGroupsPlugin
from plone.session.plugins.session import manage_addSessionPlugin
from zope import component
import logging

logger = logging.getLogger('PlonePAS setup')
Expand Down Expand Up @@ -196,11 +200,25 @@ def setupAuthPlugins(portal, pas, plone_pas,
login_path = crumbler.auto_login_page
cookie_name = crumbler.auth_cookie

found = uf.objectIds(['Extended Cookie Auth Helper'])
if not found:
plone_pas.manage_addExtendedCookieAuthHelper('credentials_cookie_auth',
cookie_name=cookie_name)
logger.debug("Added Extended Cookie Auth Helper.")
is_plone_site = plone_ifaces.IPloneSiteRoot.providedBy(portal)
if is_plone_site:
cookie_meta_type = cookie_handler.ExtendedCookieAuthHelper.meta_type
add_cookie_plugin = plone_pas.manage_addExtendedCookieAuthHelper
else:
# Can't use the `ExtendedCookieAuthHelper` outside of a Plone portal.
cookie_meta_type = CookieAuthHelper.CookieAuthHelper.meta_type
add_cookie_plugin = pas.addCookieAuthHelper
cookie_auth_ids = uf.objectIds(cookie_meta_type)
if not cookie_auth_ids:
add_cookie_plugin(
"credentials_cookie_auth",
cookie_name=cookie_name,
)
logger.debug(
"Added %r: %r",
cookie_meta_type,
"/".join(uf.credentials_cookie_auth.getPhysicalPath()),
)
if deactivate_basic_reset:
disable = ['ICredentialsResetPlugin', 'ICredentialsUpdatePlugin']
else:
Expand All @@ -212,7 +230,7 @@ def setupAuthPlugins(portal, pas, plone_pas,
)

credentials_cookie_auth = uf._getOb('credentials_cookie_auth')
if 'login_form' in credentials_cookie_auth:
if is_plone_site and 'login_form' in credentials_cookie_auth:
credentials_cookie_auth.manage_delObjects(ids=['login_form'])
logger.debug("Removed default login_form from credentials cookie "
"auth.")
Expand Down
79 changes: 79 additions & 0 deletions src/Products/PlonePAS/tests/test_setup.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# -*- coding: utf-8 -*-
from plone.app import testing as pa_testing
from plone.testing import zope
from zope.component import hooks
from Products.PlonePAS import testing
from Products.PluggableAuthService.interfaces import plugins as plugins_ifaces
from Products.PluggableAuthService.plugins import CookieAuthHelper
from Products.PluggableAuthService.plugins import HTTPBasicAuthHelper

import transaction
import unittest
import urllib.parse


class PortalSetupTest(unittest.TestCase):
Expand Down Expand Up @@ -53,3 +58,77 @@ def test_zope_root_default_challenge(self):
"401 unauthorized",
"Wrong Zope root `/acl_users` default challenge response status",
)

def test_zope_root_cookie_login(self):
"""
The Zope root `/acl_users` cookie login works.
"""
# Make the cookie plugin the default auth challenge
self.assertIn(
"credentials_cookie_auth",
self.root_acl_users.objectIds(),
"Cookie auth plugin missing from Zope root `/acl_users`",
)
cookie_plugin = self.root_acl_users.credentials_cookie_auth
self.assertIs(
type(cookie_plugin.aq_base),
CookieAuthHelper.CookieAuthHelper,
"Wrong Zope root `/acl_users` cookie auth plugin type",
)
self.root_acl_users.plugins.activatePlugin(
plugins_ifaces.IChallengePlugin,
cookie_plugin.id,
)
self.root_acl_users.plugins.movePluginsTop(
plugins_ifaces.IChallengePlugin,
[cookie_plugin.id],
)
transaction.commit()
challenge_plugins = self.root_acl_users.plugins.listPlugins(
plugins_ifaces.IChallengePlugin,
)
_, default_challenge_plugin = challenge_plugins[0]
self.assertEqual(
"/".join(default_challenge_plugin.getPhysicalPath()),
"/".join(cookie_plugin.getPhysicalPath()),
"Wrong Zope root `/acl_users` default challenge plugin",
)

# Check the challenge response in the actual browser
browser = zope.Browser(self.app)
browser.open(self.app.absolute_url() + "/manage_main")
self.assertEqual(
browser.headers["Status"].lower(),
"200 ok",
"Wrong Zope root `/acl_users` cookie challenge response status",
)
login_form_url = urllib.parse.urlsplit(browser.url)
self.assertEqual(
login_form_url._replace(query="").geturl(),
cookie_plugin.absolute_url() + "/login_form",
"Wrong Zope root `/acl_users` cookie challenge redirect",
)

# Workaround the fact that the `zope.component` site is still the Plone portal
# when the test browser handles requests.
hooks.setSite(None)
zope.login(self.root_acl_users, pa_testing.SITE_OWNER_NAME)
self.app.REQUEST.form["__ac_name"] = pa_testing.SITE_OWNER_NAME
self.app.REQUEST.form["__ac_password"] = pa_testing.TEST_USER_PASSWORD
cookie_plugin.login()

# Submit the login form in the browser
login_form = browser.getForm()
login_form.getControl(name="__ac_name").value = pa_testing.SITE_OWNER_NAME
login_form.getControl(name="__ac_password").value = pa_testing.TEST_USER_PASSWORD
login_form.controls[-1].click()
self.assertEqual(
browser.headers["Status"].lower(),
"200 ok",
"Wrong Zope root `/acl_users` cookie login response status",
)
self.assertEqual(
browser.url,
self.app.absolute_url() + "/manage_main",
"Wrong Zope root `/acl_users` cookie login redirect",
)

0 comments on commit 17deb97

Please sign in to comment.