diff --git a/Makefile b/Makefile index 0c40a7b17b..e570748c6d 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,6 @@ update: ## Update Make and Buildout wget -O requirements.txt https://raw.githubusercontent.com/kitconcept/buildout/5.2/requirements.txt wget -O plone-5.2.x.cfg https://raw.githubusercontent.com/kitconcept/buildout/5.2/plone-5.2.x.cfg wget -O ci.cfg https://raw.githubusercontent.com/kitconcept/buildout/5.2/ci.cfg - wget -O versions.cfg https://raw.githubusercontent.com/kitconcept/buildout/5.2/versions.cfg .installed.cfg: bin/buildout *.cfg bin/buildout diff --git a/base.cfg b/base.cfg index 8f22ca3720..c54d23ed9b 100644 --- a/base.cfg +++ b/base.cfg @@ -35,6 +35,9 @@ allow-hosts = [versions] # Do not use a release of plone.restapi: plone.restapi = +# Fix Zope root `/acl_users` logout +Products.PluggableAuthService = >=2.7.0 +Products.PlonePAS = >=7.0.0a3 [instance] recipe = plone.recipe.zope2instance diff --git a/news/1303.feature b/news/1303.feature new file mode 100644 index 0000000000..7100014e67 --- /dev/null +++ b/news/1303.feature @@ -0,0 +1,2 @@ +Logging in to or out of Plone classic or the API does the same in the other. +[rpatterson] diff --git a/plone-5.2.x-performance.cfg b/plone-5.2.x-performance.cfg index 8285f1ddf0..02d3217951 100644 --- a/plone-5.2.x-performance.cfg +++ b/plone-5.2.x-performance.cfg @@ -1,7 +1,7 @@ [buildout] extends = plone-5.2.x.cfg parts += instance plonesite -auto-checkout = Products.ZCatalog +auto-checkout += Products.ZCatalog [instance] recipe = plone.recipe.zope2instance diff --git a/plone-6.0.x.cfg b/plone-6.0.x.cfg index 6ab61a6ada..8493c97cf3 100644 --- a/plone-6.0.x.cfg +++ b/plone-6.0.x.cfg @@ -5,7 +5,7 @@ extends = base.cfg find-links = https://dist.plone.org/release/6.0.0a3/ versions=versions -auto-checkout = +auto-checkout += Products.CMFPlone always-checkout = true diff --git a/site.cfg b/site.cfg index eb81698e9d..0114638d9c 100644 --- a/site.cfg +++ b/site.cfg @@ -2,7 +2,7 @@ extensions = mr.developer extends = buildout.cfg eggs += plone.restapi -auto-checkout = plone.restapi +auto-checkout += plone.restapi parts = instance plonesite 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/pas/plugin.py b/src/plone/restapi/pas/plugin.py index 643c77e35f..5fd961faef 100644 --- a/src/plone/restapi/pas/plugin.py +++ b/src/plone/restapi/pas/plugin.py @@ -2,6 +2,7 @@ from AccessControl.SecurityInfo import ClassSecurityInfo from BTrees.OIBTree import OIBTree from BTrees.OOBTree import OOBTree +from DateTime import DateTime from datetime import datetime from datetime import timedelta from plone.keyring.interfaces import IKeyManager @@ -13,13 +14,18 @@ from Products.PluggableAuthService.interfaces.plugins import IAuthenticationPlugin from Products.PluggableAuthService.interfaces.plugins import IChallengePlugin from Products.PluggableAuthService.interfaces.plugins import IExtractionPlugin +from Products.PluggableAuthService.interfaces.plugins import ICredentialsUpdatePlugin +from Products.PluggableAuthService.interfaces.plugins import ICredentialsResetPlugin from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin +from zope import component from zope.component import getUtility from zope.interface import implementer import jwt +import logging import time +logger = logging.getLogger(__name__) manage_addJWTAuthenticationPlugin = PageTemplateFile( "add_plugin", globals(), __name__="manage_addJWTAuthenticationPlugin" @@ -39,7 +45,13 @@ def addJWTAuthenticationPlugin(self, id_, title=None, REQUEST=None): ) -@implementer(IAuthenticationPlugin, IChallengePlugin, IExtractionPlugin) +@implementer( + IAuthenticationPlugin, + IChallengePlugin, + IExtractionPlugin, + ICredentialsUpdatePlugin, + ICredentialsResetPlugin, +) class JWTAuthenticationPlugin(BasePlugin): """Plone PAS plugin for authentication with JSON web tokens (JWT).""" @@ -51,6 +63,7 @@ class JWTAuthenticationPlugin(BasePlugin): store_tokens = False _secret = None _tokens = None + cookie_name = "auth_token" # ZMI tab for configuration page manage_options = ( @@ -59,9 +72,11 @@ class JWTAuthenticationPlugin(BasePlugin): security.declareProtected(ManagePortal, "manage_config") manage_config = PageTemplateFile("config", globals(), __name__="manage_config") - def __init__(self, id_, title=None): + def __init__(self, id_, title=None, cookie_name=None): self._setId(id_) self.title = title + if cookie_name: + self.cookie_name = cookie_name # Initiate a challenge to the user to provide credentials. @security.private @@ -95,6 +110,8 @@ def extractCredentials(self, request): return creds creds = {} + + # Prefer the Authorization Bearer header if present auth = request._auth if auth is None: return @@ -102,6 +119,12 @@ def extractCredentials(self, request): creds["token"] = auth.split()[-1] return creds + # Finally, use the cookie if present + cookie = request.get(self.cookie_name, "") + if cookie: + creds["token"] = cookie + return creds + # IAuthenticationPlugin implementation @security.private def authenticateCredentials(self, credentials): @@ -127,6 +150,51 @@ def authenticateCredentials(self, credentials): return (userid, userid) + @security.private + def updateCredentials(self, request, response, login, new_password): + """ + Generate a new token for use both in the Bearer header and the cookie. + """ + # Unfortunately PAS itself is confused as to whether this plugin method should + # get the immutable user ID or the mutable, user-facing user login/name. Real + # usage in the Plone code base also uses both. Do our best to guess which. + user_id = login + data = dict(fullname="") + user = self._getPAS().getUserById(login) + if user is None: + user = self._getPAS().getUser(login) + if user is not None: + user_id = user.getId() + data["fullname"] = user.getProperty("fullname") + payload, token = self.create_payload_token(user_id, data=data) + # Make available on the request for further use such as returning it in the JSON + # body of the response if the current request is for the REST API login view. + request[self.cookie_name] = token + # Make the token available to the client browser for use in UI code such as when + # the login happened through Plone Classic so that the the Volro React + # components can retrieve the token that way and use the Authorization Bearer + # header from then on. + cookie_kwargs = {} + if "exp" in payload: + # Match the token expiration date/time. + cookie_kwargs["expires"] = DateTime(payload["exp"]).toZone("GMT").rfc822() + response.setCookie( + self.cookie_name, + token, + path="/", + **cookie_kwargs, + ) + + @security.private + def resetCredentials(self, request, response): + """ + Expire the token and remove the cookie. + """ + if self.cookie_name in request: + if self.store_tokens: + self.delete_token(request[self.cookie_name]) + response.expireCookie(self.cookie_name, path="/") + @security.protected(ManagePortal) @postonly def manage_updateConfig(self, REQUEST): @@ -146,7 +214,15 @@ def manage_updateConfig(self, REQUEST): def _decode_token(self, token, verify=True): if self.use_keyring: - manager = getUtility(IKeyManager) + manager = component.queryUtility(IKeyManager) + if manager is None: + logger.error( + "JWT token plugin configured to use IKeyManager " + "but no utility is registered: %r\n" + "Have you upgraded the `plone.restapi:default` profile?", + "/".join(self.getPhysicalPath()), + ) + return for secret in manager["_system"]: if secret is None: continue @@ -184,7 +260,10 @@ def delete_token(self, token): del self._tokens[userid][token] return True - def create_token(self, userid, timeout=None, data=None): + def create_payload_token(self, userid, timeout=None, data=None): + """ + Create and return both a JWT payload and the signed token. + """ payload = {} payload["sub"] = userid if timeout is None: @@ -201,4 +280,15 @@ def create_token(self, userid, timeout=None, data=None): if userid not in self._tokens: self._tokens[userid] = OIBTree() self._tokens[userid][token] = int(time.time()) + return payload, token + + def create_token(self, userid, timeout=None, data=None): + """ + Create a JWT payload and the signed token, return the token. + """ + _, token = self.create_payload_token( + userid, + timeout=timeout, + data=data, + ) return token 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 47813440d5..e9014b8b3d 100644 --- a/src/plone/restapi/services/auth/login.py +++ b/src/plone/restapi/services/auth/login.py @@ -7,8 +7,11 @@ from zope.interface import alsoProvides from zope import component +import logging import plone.protect.interfaces +logger = logging.getLogger(__name__) + class Login(Service): """Handles login and returns a JSON web token (JWT).""" @@ -28,8 +31,10 @@ def reply(self): if "IDisableCSRFProtection" in dir(plone.protect.interfaces): alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection) - userid = data["login"] - password = data["password"] + # Also add credentials to the request for other code that depends on it. In + # particular, the PAS cookie authentication plugin depends on `__ac_password`. + userid = self.request.form["__ac_name"] = data["login"] + password = self.request.form["__ac_password"] = data["password"] uf = self._find_userfolder(userid) if uf is not None: @@ -43,10 +48,16 @@ def reply(self): if plugin is None: self.request.response.setStatus(501) + message = "JWT authentication plugin not installed" + logger.error( + "%s: %s", + message, + "/".join(uf.getPhysicalPath()), + ) return dict( error=dict( type="Login failed", - message="JWT authentication plugin not installed.", + message=message, ) ) @@ -75,9 +86,27 @@ def reply(self): ) login_view._post_login() - payload = {} - payload["fullname"] = user.getProperty("fullname") - return {"token": plugin.create_token(user.getId(), data=payload)} + 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 2755e39a5c..7b01de357f 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,19 +26,28 @@ def getNonInstallableProducts(self): # pragma: no cover def install_pas_plugin(context): - uf_parent = aq_inner(context) - while True: - uf = getToolByName(uf_parent, "acl_users") - if IPluggableAuthService.providedBy(uf) and "jwt_auth" not in uf: + """ + Install the JWT token PAS plugin in every PAS acl_users here and above. + + 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: plugin = JWTAuthenticationPlugin("jwt_auth") uf._setObject(plugin.getId(), plugin) plugin = uf["jwt_auth"] plugin.manage_activateInterfaces( - ["IAuthenticationPlugin", "IExtractionPlugin"] + [ + "IAuthenticationPlugin", + "IExtractionPlugin", + "ICredentialsUpdatePlugin", + "ICredentialsResetPlugin", + ], ) - if uf_parent is uf_parent.getPhysicalRoot(): - break - uf_parent = aq_parent(uf_parent) + if not is_plone_site: + plugin.use_keyring = False def post_install_default(context): diff --git a/src/plone/restapi/testing.py b/src/plone/restapi/testing.py index 303eed97cc..30197569ab 100644 --- a/src/plone/restapi/testing.py +++ b/src/plone/restapi/testing.py @@ -18,7 +18,7 @@ from plone.registry.interfaces import IRegistry from plone.restapi.tests.dxtypes import INDEXES as DX_TYPES_INDEXES from plone.restapi.tests.helpers import add_catalog_indexes -from plone.testing import z2 +from plone.testing import zope from plone.testing.layer import Layer from plone.uuid.interfaces import IUUIDGenerator from Products.CMFCore.utils import getToolByName @@ -118,7 +118,7 @@ def setUpZope(self, app, configurationContext): xmlconfig.file("testing.zcml", plone.restapi, context=configurationContext) self.loadZCML(package=collective.MockMailHost) - z2.installProduct(app, "plone.restapi") + zope.installProduct(app, "plone.restapi") def setUpPloneSite(self, portal): portal.acl_users.userFolderAddUser( @@ -145,7 +145,7 @@ def setUpPloneSite(self, portal): bases=(PLONE_RESTAPI_DX_FIXTURE,), name="PloneRestApiDXLayer:Integration" ) PLONE_RESTAPI_DX_FUNCTIONAL_TESTING = FunctionalTesting( - bases=(PLONE_RESTAPI_DX_FIXTURE, z2.ZSERVER_FIXTURE), + bases=(PLONE_RESTAPI_DX_FIXTURE, zope.WSGI_SERVER_FIXTURE), name="PloneRestApiDXLayer:Functional", ) @@ -175,7 +175,7 @@ def setUpZope(self, app, configurationContext): xmlconfig.file("configure.zcml", plone.restapi, context=configurationContext) xmlconfig.file("testing.zcml", plone.restapi, context=configurationContext) - z2.installProduct(app, "plone.restapi") + zope.installProduct(app, "plone.restapi") def setUpPloneSite(self, portal): portal.acl_users.userFolderAddUser( @@ -201,7 +201,7 @@ def setUpPloneSite(self, portal): bases=(PLONE_RESTAPI_DX_PAM_FIXTURE,), name="PloneRestApiDXPAMLayer:Integration" ) PLONE_RESTAPI_DX_PAM_FUNCTIONAL_TESTING = FunctionalTesting( - bases=(PLONE_RESTAPI_DX_PAM_FIXTURE, z2.ZSERVER_FIXTURE), + bases=(PLONE_RESTAPI_DX_PAM_FIXTURE, zope.WSGI_SERVER_FIXTURE), name="PloneRestApiDXPAMLayer:Functional", ) @@ -216,7 +216,7 @@ def setUpZope(self, app, configurationContext): xmlconfig.file("configure.zcml", plone.restapi, context=configurationContext) xmlconfig.file("testing.zcml", plone.restapi, context=configurationContext) - z2.installProduct(app, "plone.restapi") + zope.installProduct(app, "plone.restapi") PLONE_RESTAPI_ITERATE_FIXTURE = PloneRestApiDXIterateLayer() @@ -225,7 +225,7 @@ def setUpZope(self, app, configurationContext): name="PloneRestApiDXIterateLayer:Integration", ) PLONE_RESTAPI_ITERATE_FUNCTIONAL_TESTING = FunctionalTesting( - bases=(PLONE_RESTAPI_ITERATE_FIXTURE, z2.ZSERVER_FIXTURE), + bases=(PLONE_RESTAPI_ITERATE_FIXTURE, zope.WSGI_SERVER_FIXTURE), name="PloneRestApiDXIterateLayer:Functional", ) @@ -243,7 +243,7 @@ def setUpPloneSite(self, portal): bases=(PLONE_RESTAPI_BLOCKS_FIXTURE,), name="PloneRestApIBlocksLayer:Integration" ) PLONE_RESTAPI_BLOCKS_FUNCTIONAL_TESTING = FunctionalTesting( - bases=(PLONE_RESTAPI_BLOCKS_FIXTURE, z2.ZSERVER_FIXTURE), + bases=(PLONE_RESTAPI_BLOCKS_FIXTURE, zope.WSGI_SERVER_FIXTURE), name="PloneRestApIBlocksLayer:Functional", ) diff --git a/src/plone/restapi/tests/test_addons.py b/src/plone/restapi/tests/test_addons.py index 78ab4fa2e9..b19a58a74a 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), @@ -190,7 +193,7 @@ def _get_upgrade_info(self): "available": True, "hasProfile": True, "installedVersion": "0002", - "newVersion": "0006", + "newVersion": "0007", "required": True, }, _get_upgrade_info(self), diff --git a/src/plone/restapi/tests/test_auth.py b/src/plone/restapi/tests/test_auth.py index e619e0ab75..2db395dfca 100644 --- a/src/plone/restapi/tests/test_auth.py +++ b/src/plone/restapi/tests/test_auth.py @@ -86,7 +86,8 @@ def test_login_with_zope_user_fails_without_pas_plugin(self): res = service.reply() self.assertIn("error", res) self.assertEqual( - "JWT authentication plugin not installed.", res["error"]["message"] + "jwt authentication plugin not installed", + res["error"]["message"].lower(), ) self.assertNotIn("token", res) diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index fe6d391de1..90f1b38a3f 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -27,7 +27,7 @@ from plone.restapi.testing import RelativeSession from plone.restapi.tests.statictime import StaticTime from plone.scale import storage -from plone.testing.z2 import Browser +from plone.testing.zope import Browser from zope.component import createObject from zope.component import getUtility from zope.component.hooks import getSite diff --git a/src/plone/restapi/tests/test_functional_auth.py b/src/plone/restapi/tests/test_functional_auth.py index 59b8b31dec..2c1b87c823 100644 --- a/src/plone/restapi/tests/test_functional_auth.py +++ b/src/plone/restapi/tests/test_functional_auth.py @@ -1,3 +1,4 @@ +from plone.app import testing as pa_testing from plone.app.testing import login from plone.app.testing import setRoles from plone.app.testing import SITE_OWNER_NAME @@ -5,6 +6,9 @@ from plone.app.testing import TEST_USER_NAME from plone.app.testing import TEST_USER_PASSWORD from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING +from plone.testing import zope +from Products.PluggableAuthService.interfaces import plugins as plugins_ifaces +from Products.PluggableAuthService.plugins import CookieAuthHelper import base64 import requests @@ -17,14 +21,28 @@ class TestFunctionalAuth(unittest.TestCase): layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING def setUp(self): + """ + Set initial test conditions and convenience attributes. + """ + # Zope root references + self.app = self.layer["app"] + self.root_acl_users = self.app.acl_users + + # Plone portal references self.portal = self.layer["portal"] self.portal_url = self.portal.absolute_url() + + # User permissions and authentication setRoles(self.portal, TEST_USER_ID, ["Manager"]) login(self.portal, SITE_OWNER_NAME) + + # Create a page that can't be publicly accessed self.private_document = self.portal[ self.portal.invokeFactory("Document", id="doc1", title="My Document") ] self.private_document_url = self.private_document.absolute_url() + + # This is a functional fixture, have to commit our changes transaction.commit() def test_login_without_credentials_fails(self): @@ -60,34 +78,98 @@ def test_login_with_valid_credentials_returns_token(self): headers={"Accept": "application/json"}, json={"login": TEST_USER_NAME, "password": TEST_USER_PASSWORD}, ) - self.assertEqual(200, response.status_code) - self.assertIn("token", response.json()) + self.assertEqual( + 200, + response.status_code, + "Wrong API login response status code", + ) + self.assertIn( + "token", + response.json(), + "Authentication token missing from API response JSON", + ) - def test_api_login_grants_zmi(self): + def test_api_login_sets_classic_cookie(self): """ - Logging in via the API also grants access to the Zope root ZMI. + Logging in via the API also sets the Plone classic auth cookie. """ session = requests.Session() self.addCleanup(session.close) - login_resp = session.post( + session.post( self.portal_url + "/@login", headers={"Accept": "application/json"}, json={"login": SITE_OWNER_NAME, "password": TEST_USER_PASSWORD}, ) self.assertIn( "__ac", - login_resp.cookies, + session.cookies, "Plone session cookie missing from API login POST response", ) + + def test_classic_login_sets_api_token_cookie(self): + """ + Logging in via Plone classic login form also sets cookie with the API token. + + The cookie that Volto React components will recognize on first request and use + as the Authorization Bearer header for subsequent requests. + """ + session = requests.Session() + self.addCleanup(session.close) + challenge_resp = session.get(self.private_document_url) + self.assertEqual( + challenge_resp.status_code, + 200, + "Wrong Plone login challenge status code", + ) + self.assertTrue( + ' + + diff --git a/src/plone/restapi/upgrades/to0007.py b/src/plone/restapi/upgrades/to0007.py new file mode 100644 index 0000000000..0c3af18424 --- /dev/null +++ b/src/plone/restapi/upgrades/to0007.py @@ -0,0 +1,45 @@ +""" +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): + if not is_plone_site and jwt_plugin.use_keyring: + logger.info( + "Disabling keyring for plugin outside of Plone: %s", + "/".join(jwt_plugin.getPhysicalPath()), + ) + jwt_plugin.use_keyring = False + 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) diff --git a/versions.cfg b/versions.cfg deleted file mode 100644 index 28a57f5575..0000000000 --- a/versions.cfg +++ /dev/null @@ -1,28 +0,0 @@ -[versions] -# Buildout -setuptools = -zc.buildout = -plone.restapi = - -# code analysis -black = 20.8b1 - -# Error: The requirement ('virtualenv>=20.0.35') is not allowed by your [versions] constraint (20.0.26) -virtualenv = 20.0.35 - -# Error: The requirement ('pep517>=0.9') is not allowed by your [versions] constraint (0.8.2) -pep517 = 0.9.1 - -# Error: The requirement ('importlib-metadata>=1') is not allowed by your [versions] constraint (0.23) -importlib-metadata = 2.0.0 - -# cryptography 3.4 requires a rust compiler installed on the system: -# https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst#34---2021-02-07 -cryptography = 3.3.2 - -# cffi 1.14.3 fails on apple m1 -cffi = 1.14.4 - -# requirement for json widget tests to pass -plone.schema = 1.3.0 -plone.dexterity = 2.9.8 \ No newline at end of file