diff --git a/src/plone/restapi/pas/__init__.py b/src/plone/restapi/pas/__init__.py
index e69de29bb2..d761a9744b 100644
--- a/src/plone/restapi/pas/__init__.py
+++ b/src/plone/restapi/pas/__init__.py
@@ -0,0 +1,32 @@
+"""
+A JWT token authentication plugin for PluggableAuthService.
+"""
+
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone import interfaces as plone_ifaces
+from Products import PluggableAuthService # noqa, Ensure PAS patch in place
+from Products.PluggableAuthService.interfaces import authservice as authservice_ifaces
+
+import Acquisition
+
+
+def iter_ancestor_pas(context):
+ """
+ Walk up the ZODB OFS returning Pluggableauthservice `./acl_users/` for each level.
+ """
+ uf_parent = Acquisition.aq_inner(context)
+ while True:
+ is_plone_site = plone_ifaces.IPloneSiteRoot.providedBy(uf_parent)
+ uf = getToolByName(uf_parent, "acl_users", default=None)
+
+ # Skip ancestor contexts to which we don't/can't apply
+ if uf is None or not authservice_ifaces.IPluggableAuthService.providedBy(uf):
+ uf_parent = Acquisition.aq_parent(uf_parent)
+ continue
+
+ yield uf, is_plone_site
+
+ # Go up one more level
+ if uf_parent is uf_parent.getPhysicalRoot():
+ break
+ uf_parent = Acquisition.aq_parent(uf_parent)
diff --git a/src/plone/restapi/profiles/default/metadata.xml b/src/plone/restapi/profiles/default/metadata.xml
index 81970e34ed..1e4872e498 100644
--- a/src/plone/restapi/profiles/default/metadata.xml
+++ b/src/plone/restapi/profiles/default/metadata.xml
@@ -1,4 +1,4 @@
- 0006
+ 0007
diff --git a/src/plone/restapi/services/auth/login.py b/src/plone/restapi/services/auth/login.py
index 21fa9fa6b5..e9014b8b3d 100644
--- a/src/plone/restapi/services/auth/login.py
+++ b/src/plone/restapi/services/auth/login.py
@@ -86,7 +86,27 @@ def reply(self):
)
login_view._post_login()
- return {"token": self.request[plugin.cookie_name]}
+ response = {}
+ if plugin.cookie_name in self.request:
+ response["token"] = self.request[plugin.cookie_name]
+ else:
+ self.request.response.setStatus(501)
+ message = (
+ "JWT authentication token not created, plugin probably not activated "
+ "for `ICredentialsUpdatePlugin`"
+ )
+ logger.error(
+ "%s: %s",
+ message,
+ "/".join(plugin.getPhysicalPath()),
+ )
+ return dict(
+ error=dict(
+ type="Login failed",
+ message=message,
+ )
+ )
+ return response
def _find_userfolder(self, userid):
"""Try to find a user folder that contains a user with the given
diff --git a/src/plone/restapi/setuphandlers.py b/src/plone/restapi/setuphandlers.py
index 1bc174d40c..a1a3948172 100644
--- a/src/plone/restapi/setuphandlers.py
+++ b/src/plone/restapi/setuphandlers.py
@@ -1,11 +1,6 @@
-from Acquisition import aq_inner
-from Acquisition import aq_parent
+from plone.restapi import pas
from plone.restapi.pas.plugin import JWTAuthenticationPlugin
-from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.interfaces import INonInstallable
-from Products.PluggableAuthService.interfaces.authservice import (
- IPluggableAuthService,
-) # noqa: E501
from zope.component.hooks import getSite
from zope.interface import implementer
@@ -31,14 +26,12 @@ def getNonInstallableProducts(self): # pragma: no cover
def install_pas_plugin(context):
- uf_parent = aq_inner(context)
- while True:
- uf = getToolByName(uf_parent, "acl_users", default=None)
+ """
+ Install the JWT token PAS plugin in every PAS acl_users here and above.
- # Skip ancestor contexts to which we don't/can't apply
- if uf is None or not IPluggableAuthService.providedBy(uf):
- uf_parent = aq_parent(uf_parent)
- continue
+ Usually this means it is installed into Plone and into the Zope root.
+ """
+ for uf, is_plone_site in pas.iter_ancestor_pas(context):
# Add the API token plugin if not already installed at this level
if "jwt_auth" not in uf:
@@ -54,11 +47,6 @@ def install_pas_plugin(context):
],
)
- # Go up one more level
- if uf_parent is uf_parent.getPhysicalRoot():
- break
- uf_parent = aq_parent(uf_parent)
-
def post_install_default(context):
"""Post install of default profile"""
diff --git a/src/plone/restapi/tests/test_addons.py b/src/plone/restapi/tests/test_addons.py
index 9fd449e7ee..36b5e40f40 100644
--- a/src/plone/restapi/tests/test_addons.py
+++ b/src/plone/restapi/tests/test_addons.py
@@ -116,12 +116,15 @@ def _get_upgrade_info(self):
# Set need upgrade state
self.ps.setLastVersionForProfile("plone.restapi:default", "0002")
transaction.commit()
+ # FIXME: At least the `newVersion` should be extracted from
+ # `./profiles/default/metadata.xml` so that this test isn't constantly changing
+ # for unrelated code changes.
self.assertEqual(
{
"available": True,
"hasProfile": True,
"installedVersion": "0002",
- "newVersion": "0006",
+ "newVersion": "0007",
"required": True,
},
_get_upgrade_info(self),
@@ -135,8 +138,8 @@ def _get_upgrade_info(self):
{
"available": False,
"hasProfile": True,
- "installedVersion": "0006",
- "newVersion": "0006",
+ "installedVersion": "0007",
+ "newVersion": "0007",
"required": False,
},
_get_upgrade_info(self),
diff --git a/src/plone/restapi/upgrades/configure.zcml b/src/plone/restapi/upgrades/configure.zcml
index 66bb67b0d7..88b9b86425 100644
--- a/src/plone/restapi/upgrades/configure.zcml
+++ b/src/plone/restapi/upgrades/configure.zcml
@@ -69,4 +69,15 @@
handler="plone.restapi.upgrades.to0006.rename_iface_to_name_in_blocks_behavior"
/>
+
+
diff --git a/src/plone/restapi/upgrades/to0007.py b/src/plone/restapi/upgrades/to0007.py
new file mode 100644
index 0000000000..3f8103e40a
--- /dev/null
+++ b/src/plone/restapi/upgrades/to0007.py
@@ -0,0 +1,39 @@
+"""
+GenericSetup profile upgrades from version 0006 to 0007.
+"""
+
+from plone.restapi import pas
+from plone.restapi.pas import plugin
+from Products.CMFCore.utils import getToolByName
+from Products.PluggableAuthService.interfaces import plugins as plugins_ifaces
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def enable_new_pas_plugin_interfaces(context):
+ """
+ Enable new PAS plugin interfaces.
+
+ After correcting/completing the PAS plugin interfaces, those interfaces need to be
+ enabled for existing functionality to continue working.
+ """
+ portal = getToolByName(context, "portal_url").getPortalObject()
+ for uf, is_plone_site in pas.iter_ancestor_pas(portal):
+ for jwt_plugin in uf.objectValues(plugin.JWTAuthenticationPlugin.meta_type):
+ for new_iface in (
+ plugins_ifaces.ICredentialsUpdatePlugin,
+ plugins_ifaces.ICredentialsResetPlugin,
+ ):
+ active_plugin_ids = [
+ active_plugin_id for active_plugin_id, _ in
+ uf.plugins.listPlugins(new_iface)
+ ]
+ if jwt_plugin.id not in active_plugin_ids:
+ logger.info(
+ "Activating PAS interface %s: %s",
+ new_iface.__name__,
+ "/".join(jwt_plugin.getPhysicalPath())
+ )
+ uf.plugins.activatePlugin(new_iface, jwt_plugin.id)