From fa11c218716eae196b203f0f550c0a2c16ff3ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Fri, 6 Dec 2024 19:40:02 -0300 Subject: [PATCH 1/3] Implement control panel to configure pas.plugins.oidc (Implements #65) --- news/65.feature | 1 + src/pas/plugins/oidc/configure.zcml | 9 +- src/pas/plugins/oidc/controlpanel/__init__.py | 0 src/pas/plugins/oidc/controlpanel/classic.py | 165 ++++++++++++++++++ .../plugins/oidc/controlpanel/configure.zcml | 27 +++ .../plugins/oidc/controlpanel/deserializer.py | 79 +++++++++ src/pas/plugins/oidc/controlpanel/oidc.py | 20 +++ .../plugins/oidc/controlpanel/serializer.py | 44 +++++ src/pas/plugins/oidc/interfaces.py | 117 +++++++++++++ .../de/LC_MESSAGES/pas.plugins.oidc.po | 114 +++++++++++- .../en/LC_MESSAGES/pas.plugins.oidc.po | 114 +++++++++++- .../es/LC_MESSAGES/pas.plugins.oidc.po | 116 +++++++++++- .../plugins/oidc/locales/pas.plugins.oidc.pot | 118 ++++++++++++- .../oidc/profiles/default/controlpanel.xml | 21 +++ .../oidc/profiles/default/metadata.xml | 2 +- src/pas/plugins/oidc/upgrades/__init__.py | 0 src/pas/plugins/oidc/upgrades/configure.zcml | 25 +++ tests/controlpanel/conftest.py | 59 +++++++ tests/controlpanel/test_controlpanel_api.py | 24 +++ tests/functional/test_controlpanel_classic.py | 19 ++ tests/setup/test_setup_install.py | 2 +- 21 files changed, 1046 insertions(+), 30 deletions(-) create mode 100644 news/65.feature create mode 100644 src/pas/plugins/oidc/controlpanel/__init__.py create mode 100644 src/pas/plugins/oidc/controlpanel/classic.py create mode 100644 src/pas/plugins/oidc/controlpanel/configure.zcml create mode 100644 src/pas/plugins/oidc/controlpanel/deserializer.py create mode 100644 src/pas/plugins/oidc/controlpanel/oidc.py create mode 100644 src/pas/plugins/oidc/controlpanel/serializer.py create mode 100644 src/pas/plugins/oidc/profiles/default/controlpanel.xml create mode 100644 src/pas/plugins/oidc/upgrades/__init__.py create mode 100644 src/pas/plugins/oidc/upgrades/configure.zcml create mode 100644 tests/controlpanel/conftest.py create mode 100644 tests/controlpanel/test_controlpanel_api.py create mode 100644 tests/functional/test_controlpanel_classic.py diff --git a/news/65.feature b/news/65.feature new file mode 100644 index 0000000..7cc98e7 --- /dev/null +++ b/news/65.feature @@ -0,0 +1 @@ +Implement control panel to configure pas.plugins.oidc [@ericof] diff --git a/src/pas/plugins/oidc/configure.zcml b/src/pas/plugins/oidc/configure.zcml index 931a26c..2560960 100644 --- a/src/pas/plugins/oidc/configure.zcml +++ b/src/pas/plugins/oidc/configure.zcml @@ -20,6 +20,7 @@ + - + + + + + + + + + + + + + diff --git a/src/pas/plugins/oidc/controlpanel/deserializer.py b/src/pas/plugins/oidc/controlpanel/deserializer.py new file mode 100644 index 0000000..3486d60 --- /dev/null +++ b/src/pas/plugins/oidc/controlpanel/deserializer.py @@ -0,0 +1,79 @@ +from pas.plugins.oidc.interfaces import IOIDCControlpanel +from plone import api +from plone.restapi.deserializer import json_body +from plone.restapi.deserializer.controlpanels import ControlpanelDeserializeFromJson +from plone.restapi.deserializer.controlpanels import FakeDXContext +from plone.restapi.interfaces import IDeserializeFromJson +from plone.restapi.interfaces import IFieldDeserializer +from z3c.form.interfaces import IManagerValidator +from zExceptions import BadRequest +from zope.component import adapter +from zope.component import queryMultiAdapter +from zope.interface import implementer +from zope.interface.exceptions import Invalid +from zope.schema import getFields +from zope.schema.interfaces import ValidationError + + +@implementer(IDeserializeFromJson) +@adapter(IOIDCControlpanel) +class OIDCControlpanelDeserializeFromJson(ControlpanelDeserializeFromJson): + @property + def proxy(self): + portal = api.portal.get() + plugin = portal.acl_users.oidc + return plugin + + def __call__(self, mask_validation_errors=True): + controlpanel = self.controlpanel + request = controlpanel.request + data = json_body(request) + + proxy = self.proxy + + schema_data = {} + errors = [] + + # Make a fake context + fake_context = FakeDXContext() + + for name, field in getFields(self.schema).items(): + field_data = schema_data.setdefault(self.schema, {}) + + if field.readonly: + continue + + if name in data: + deserializer = queryMultiAdapter( + (field, fake_context, request), IFieldDeserializer + ) + try: + # Make it sane + value = deserializer(data[name]) + # Validate required etc + field.validate(value) + # Set the value. + setattr(proxy, name, value) + except ValidationError as e: + errors.append({"message": e.doc(), "field": name, "error": e}) + except (ValueError, Invalid) as e: + errors.append({"message": str(e), "field": name, "error": e}) + else: + field_data[name] = value + + # Validate schemata + for schema, field_data in schema_data.items(): + validator = queryMultiAdapter( + (self.context, request, None, schema, None), IManagerValidator + ) + for error in validator.validate(field_data): + errors.append({"error": error, "message": str(error)}) + + if errors: + for error in errors: + if mask_validation_errors: + # Drop Python specific error classes in order to + # be able to better handle errors on front-end + error["error"] = "ValidationError" + error["message"] = api.env.translate(error["message"], context=request) + raise BadRequest(errors) diff --git a/src/pas/plugins/oidc/controlpanel/oidc.py b/src/pas/plugins/oidc/controlpanel/oidc.py new file mode 100644 index 0000000..a1d0a4b --- /dev/null +++ b/src/pas/plugins/oidc/controlpanel/oidc.py @@ -0,0 +1,20 @@ +from pas.plugins.oidc import _ +from pas.plugins.oidc.interfaces import IDefaultBrowserLayer +from pas.plugins.oidc.interfaces import IOIDCControlpanel +from pas.plugins.oidc.interfaces import IOIDCSettings +from plone.restapi.controlpanels import RegistryConfigletPanel +from zope.component import adapter +from zope.interface import implementer +from zope.interface import Interface + + +@adapter(Interface, IDefaultBrowserLayer) +@implementer(IOIDCControlpanel) +class OIDCSettingsConfigletPanel(RegistryConfigletPanel): + """Control Panel endpoint""" + + schema = IOIDCSettings + configlet_id = "oidc_admin" + configlet_category_id = "plone-users" + title = _("OIDC settings") + group = "" diff --git a/src/pas/plugins/oidc/controlpanel/serializer.py b/src/pas/plugins/oidc/controlpanel/serializer.py new file mode 100644 index 0000000..b2bd1da --- /dev/null +++ b/src/pas/plugins/oidc/controlpanel/serializer.py @@ -0,0 +1,44 @@ +from pas.plugins.oidc.interfaces import IOIDCControlpanel +from pas.plugins.oidc.plugins import OIDCPlugin +from plone import api +from plone.restapi.interfaces import ISerializeToJson +from plone.restapi.serializer.controlpanels import ControlpanelSerializeToJson +from plone.restapi.serializer.controlpanels import get_jsonschema_for_controlpanel +from plone.restapi.serializer.controlpanels import SERVICE_ID +from zope.component import adapter +from zope.interface import implementer + + +@implementer(ISerializeToJson) +@adapter(IOIDCControlpanel) +class OIDCControlpanelSerializeToJson(ControlpanelSerializeToJson): + def config_data(self) -> dict: + data = {} + portal = api.portal.get() + plugin = portal.acl_users.oidc + properties = OIDCPlugin._properties + for prop in properties: + key = prop["id"] + data[key] = getattr(plugin, key, "") + return data + + def __call__(self): + json_data = self.config_data() + controlpanel = self.controlpanel + context = controlpanel.context + request = controlpanel.request + url = context.absolute_url() + json_schema = get_jsonschema_for_controlpanel( + controlpanel, + context, + request, + ) + response = { + "@id": f"{url}/{SERVICE_ID}/{controlpanel.__name__}", + "title": "OIDC settings", + "description": "Configure OIDC connection strings", + "group": controlpanel.group, + "data": json_data, + "schema": json_schema, + } + return response diff --git a/src/pas/plugins/oidc/interfaces.py b/src/pas/plugins/oidc/interfaces.py index 07657d6..18350fe 100644 --- a/src/pas/plugins/oidc/interfaces.py +++ b/src/pas/plugins/oidc/interfaces.py @@ -1,7 +1,124 @@ """Module where all interfaces, events and exceptions live.""" +from pas.plugins.oidc import _ +from plone.restapi.controlpanels.interfaces import IControlpanel +from zope import schema +from zope.interface import Interface from zope.publisher.interfaces.browser import IDefaultBrowserLayer class IPasPluginsOidcLayer(IDefaultBrowserLayer): """Marker interface that defines a browser layer.""" + + +class IOIDCSettings(Interface): + """OIDC plugin settings""" + + issuer = schema.TextLine( + title=_("OIDC/OAuth2 Issuer"), + description=_(""), + required=False, + default="", + ) + client_id = schema.TextLine( + title=_("Client ID"), + description=_(""), + required=False, + default="", + ) + client_secret = schema.TextLine( + title=_("Client secret"), + description=_(""), + required=False, + default="", + ) + redirect_uris = schema.List( + title=_("Redirect uris"), + description=_(""), + value_type=schema.TextLine( + title=_("URI"), + description=_(""), + ), + required=False, + default=[], + ) + use_session_data_manager = schema.Bool( + title=_("Use Zope session data manager"), + description=_(""), + required=False, + default=False, + ) + create_user = schema.Bool( + title=_("Create user / update user properties"), + description=_(""), + required=False, + default=True, + ) + create_groups = schema.Bool( + title=_("Create groups / update group memberships"), + description=_(""), + required=False, + default=False, + ) + user_property_as_groupid = schema.TextLine( + title=_("User info property used as groupid, default 'groups'"), + description=_(""), + required=False, + default="groups", + ) + create_ticket = schema.Bool( + title=_("Create authentication ticket"), + description=_("Create authentication __ac ticket"), + required=False, + default=True, + ) + create_restapi_ticket = schema.Bool( + title=_("Create restapi ticket"), + description=_("Create authentication auth_token (volto/restapi) ticket"), + required=False, + default=True, + ) + scope = schema.List( + title=_("Open ID scopes"), + description=_("Open ID scopes to request to the server"), + value_type=schema.TextLine( + title=_("Scope"), + description=_(""), + ), + required=False, + default=["profile", "email", "phone"], + ) + use_pkce = schema.Bool( + title=_("Use PKCE"), + description=_(""), + required=False, + default=False, + ) + use_deprecated_redirect_uri_for_logout = schema.Bool( + title=_("Use deprecated redirect_uri"), + description=_( + "Use deprecated redirect_uri for logout url(/Plone/acl_users/oidc/logout)" + ), + required=False, + default=False, + ) + use_modified_openid_schema = schema.Bool( + title=_("Use modified OpenID Schema"), + description=_( + "Use a modified OpenID Schema for email_verified " + "and phone_number_verified boolean values coming as string" + ), + required=False, + default=False, + ) + + user_property_as_userid = schema.TextLine( + title=_("Property used as userid"), + description=_("User info property used as userid, default 'sub'."), + required=False, + default="sub", + ) + + +class IOIDCControlpanel(IControlpanel): + """OIDC Control panel""" diff --git a/src/pas/plugins/oidc/locales/de/LC_MESSAGES/pas.plugins.oidc.po b/src/pas/plugins/oidc/locales/de/LC_MESSAGES/pas.plugins.oidc.po index 482b35e..482fb1c 100644 --- a/src/pas/plugins/oidc/locales/de/LC_MESSAGES/pas.plugins.oidc.po +++ b/src/pas/plugins/oidc/locales/de/LC_MESSAGES/pas.plugins.oidc.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Plone\n" -"POT-Creation-Date: 2024-05-23 11:20+0000\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI +ZONE\n" "PO-Revision-Date: 2024-07-09 15:55+0000\n" "Last-Translator: Leonardo J. Caballero G. \n" "Language-Team: Deutsch \n" @@ -21,22 +21,90 @@ msgstr "" msgid "Add" msgstr "Hinzufügen" +#: pas/plugins/oidc/interfaces.py:24 +msgid "Client ID" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:30 +msgid "Client secret" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:71 +msgid "Create authentication __ac ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:77 +msgid "Create authentication auth_token (volto/restapi) ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:70 +msgid "Create authentication ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:58 +msgid "Create groups / update group memberships" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:76 +msgid "Create restapi ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:52 +msgid "Create user / update user properties" +msgstr "" + #: pas/plugins/oidc/www/oidcPluginForm.zpt:26 msgid "Id" msgstr "" -#: pas/plugins/oidc/configure.zcml:32 +#: pas/plugins/oidc/configure.zcml:33 msgid "Installs the pas.plugins.oidc add-on." msgstr "Installiert das Add-on pas.plugins.oidc." +#: pas/plugins/oidc/controlpanel/classic.py:144 +msgid "OIDC Plugin Settings" +msgstr "" + #: pas/plugins/oidc/www/oidcPluginForm.zpt:13 msgid "OIDC Plugin manage the details of the OpenID Connect Authentication plugin Pluggable Auth Service functionality." msgstr "OIDC Plugin verwaltet die Details der OpenID Connect Authentication Plugin Pluggable Auth Service Funktionalität." +#: pas/plugins/oidc/profiles/default/controlpanel.xml +msgid "OIDC Settings" +msgstr "" + +#: pas/plugins/oidc/controlpanel/oidc.py:19 +msgid "OIDC settings" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:18 +msgid "OIDC/OAuth2 Issuer" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:82 +msgid "Open ID scopes" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:83 +msgid "Open ID scopes to request to the server" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:116 +msgid "Property used as userid" +msgstr "" + #: pas/plugins/oidc/services/oidc/oidc.py:95 msgid "Provider is not properly configured." msgstr "Der Provider ist nicht richtig konfiguriert." +#: pas/plugins/oidc/interfaces.py:36 +msgid "Redirect uris" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:85 +msgid "Scope" +msgstr "" + #: pas/plugins/oidc/browser/view.py:61 #: pas/plugins/oidc/services/oidc/oidc.py:107 msgid "There was an error during the login process. Please try again." @@ -50,14 +118,50 @@ msgstr "Es ist ein Fehler beim Abrufen des oauth2-Clients aufgetreten." msgid "Title" msgstr "Titel" -#: pas/plugins/oidc/configure.zcml:41 +#: pas/plugins/oidc/interfaces.py:39 +msgid "URI" +msgstr "" + +#: pas/plugins/oidc/configure.zcml:42 msgid "Uninstalls the pas.plugins.oidc add-on." msgstr "Deinstalliert das Add-on pas.plugins.oidc." -#: pas/plugins/oidc/configure.zcml:32 +#: pas/plugins/oidc/interfaces.py:92 +msgid "Use PKCE" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:46 +msgid "Use Zope session data manager" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:107 +msgid "Use a modified OpenID Schema for email_verified and phone_number_verified boolean values coming as string" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:98 +msgid "Use deprecated redirect_uri" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:99 +msgid "Use deprecated redirect_uri for logout url(/Plone/acl_users/oidc/logout)" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:106 +msgid "Use modified OpenID Schema" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:64 +msgid "User info property used as groupid, default 'groups'" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:117 +msgid "User info property used as userid, default 'sub'." +msgstr "" + +#: pas/plugins/oidc/configure.zcml:33 msgid "pas.plugins.oidc" msgstr "pas.plugins.oidc" -#: pas/plugins/oidc/configure.zcml:41 +#: pas/plugins/oidc/configure.zcml:42 msgid "pas.plugins.oidc (uninstall)" msgstr "pas.plugins.oidc (deinstallieren)" diff --git a/src/pas/plugins/oidc/locales/en/LC_MESSAGES/pas.plugins.oidc.po b/src/pas/plugins/oidc/locales/en/LC_MESSAGES/pas.plugins.oidc.po index 050eb7f..2eb4d0c 100644 --- a/src/pas/plugins/oidc/locales/en/LC_MESSAGES/pas.plugins.oidc.po +++ b/src/pas/plugins/oidc/locales/en/LC_MESSAGES/pas.plugins.oidc.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2024-05-23 11:20+0000\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI +ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,22 +18,90 @@ msgstr "" msgid "Add" msgstr "" +#: pas/plugins/oidc/interfaces.py:24 +msgid "Client ID" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:30 +msgid "Client secret" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:71 +msgid "Create authentication __ac ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:77 +msgid "Create authentication auth_token (volto/restapi) ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:70 +msgid "Create authentication ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:58 +msgid "Create groups / update group memberships" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:76 +msgid "Create restapi ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:52 +msgid "Create user / update user properties" +msgstr "" + #: pas/plugins/oidc/www/oidcPluginForm.zpt:26 msgid "Id" msgstr "" -#: pas/plugins/oidc/configure.zcml:32 +#: pas/plugins/oidc/configure.zcml:33 msgid "Installs the pas.plugins.oidc add-on." msgstr "" +#: pas/plugins/oidc/controlpanel/classic.py:144 +msgid "OIDC Plugin Settings" +msgstr "" + #: pas/plugins/oidc/www/oidcPluginForm.zpt:13 msgid "OIDC Plugin manage the details of the OpenID Connect Authentication plugin Pluggable Auth Service functionality." msgstr "" +#: pas/plugins/oidc/profiles/default/controlpanel.xml +msgid "OIDC Settings" +msgstr "" + +#: pas/plugins/oidc/controlpanel/oidc.py:19 +msgid "OIDC settings" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:18 +msgid "OIDC/OAuth2 Issuer" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:82 +msgid "Open ID scopes" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:83 +msgid "Open ID scopes to request to the server" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:116 +msgid "Property used as userid" +msgstr "" + #: pas/plugins/oidc/services/oidc/oidc.py:95 msgid "Provider is not properly configured." msgstr "" +#: pas/plugins/oidc/interfaces.py:36 +msgid "Redirect uris" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:85 +msgid "Scope" +msgstr "" + #: pas/plugins/oidc/browser/view.py:61 #: pas/plugins/oidc/services/oidc/oidc.py:107 msgid "There was an error during the login process. Please try again." @@ -47,14 +115,50 @@ msgstr "" msgid "Title" msgstr "" -#: pas/plugins/oidc/configure.zcml:41 +#: pas/plugins/oidc/interfaces.py:39 +msgid "URI" +msgstr "" + +#: pas/plugins/oidc/configure.zcml:42 msgid "Uninstalls the pas.plugins.oidc add-on." msgstr "" -#: pas/plugins/oidc/configure.zcml:32 +#: pas/plugins/oidc/interfaces.py:92 +msgid "Use PKCE" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:46 +msgid "Use Zope session data manager" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:107 +msgid "Use a modified OpenID Schema for email_verified and phone_number_verified boolean values coming as string" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:98 +msgid "Use deprecated redirect_uri" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:99 +msgid "Use deprecated redirect_uri for logout url(/Plone/acl_users/oidc/logout)" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:106 +msgid "Use modified OpenID Schema" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:64 +msgid "User info property used as groupid, default 'groups'" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:117 +msgid "User info property used as userid, default 'sub'." +msgstr "" + +#: pas/plugins/oidc/configure.zcml:33 msgid "pas.plugins.oidc" msgstr "" -#: pas/plugins/oidc/configure.zcml:41 +#: pas/plugins/oidc/configure.zcml:42 msgid "pas.plugins.oidc (uninstall)" msgstr "" diff --git a/src/pas/plugins/oidc/locales/es/LC_MESSAGES/pas.plugins.oidc.po b/src/pas/plugins/oidc/locales/es/LC_MESSAGES/pas.plugins.oidc.po index f0960d3..4b6bc8a 100644 --- a/src/pas/plugins/oidc/locales/es/LC_MESSAGES/pas.plugins.oidc.po +++ b/src/pas/plugins/oidc/locales/es/LC_MESSAGES/pas.plugins.oidc.po @@ -3,11 +3,10 @@ msgid "" msgstr "" "Project-Id-Version: pas.plugins.oidc\n" -"POT-Creation-Date: 2024-05-23 10:58+0000\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI +ZONE\n" "PO-Revision-Date: 2024-05-23 07:00-0400\n" "Last-Translator: Leonardo J. Caballero G. \n" "Language-Team: ES \n" -"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -16,6 +15,7 @@ msgstr "" "Language-Name: Español\n" "Preferred-Encodings: utf-8\n" "Domain: pas.plugins.oidc\n" +"Language: es\n" "X-Is-Fallback-For: es-ar es-bo es-cl es-co es-cr es-do es-ec es-es es-sv es-gt es-hn es-mx es-ni es-pa es-py es-pe es-pr es-us es-uy es-ve\n" "X-Generator: Poedit 3.4.4\n" @@ -23,22 +23,90 @@ msgstr "" msgid "Add" msgstr "Añadir" +#: pas/plugins/oidc/interfaces.py:24 +msgid "Client ID" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:30 +msgid "Client secret" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:71 +msgid "Create authentication __ac ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:77 +msgid "Create authentication auth_token (volto/restapi) ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:70 +msgid "Create authentication ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:58 +msgid "Create groups / update group memberships" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:76 +msgid "Create restapi ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:52 +msgid "Create user / update user properties" +msgstr "" + #: pas/plugins/oidc/www/oidcPluginForm.zpt:26 msgid "Id" msgstr "Id" -#: pas/plugins/oidc/configure.zcml:32 +#: pas/plugins/oidc/configure.zcml:33 msgid "Installs the pas.plugins.oidc add-on." msgstr "Instala el complemento pas.plugins.oidc." +#: pas/plugins/oidc/controlpanel/classic.py:144 +msgid "OIDC Plugin Settings" +msgstr "" + #: pas/plugins/oidc/www/oidcPluginForm.zpt:13 msgid "OIDC Plugin manage the details of the OpenID Connect Authentication plugin Pluggable Auth Service functionality." msgstr "El plugin OIDC gestiona los detalles de la funcionalidad del plugin de autenticación OpenID Connect Pluggable Auth Service." +#: pas/plugins/oidc/profiles/default/controlpanel.xml +msgid "OIDC Settings" +msgstr "" + +#: pas/plugins/oidc/controlpanel/oidc.py:19 +msgid "OIDC settings" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:18 +msgid "OIDC/OAuth2 Issuer" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:82 +msgid "Open ID scopes" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:83 +msgid "Open ID scopes to request to the server" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:116 +msgid "Property used as userid" +msgstr "" + #: pas/plugins/oidc/services/oidc/oidc.py:95 msgid "Provider is not properly configured." msgstr "El proveedor no está configurado correctamente." +#: pas/plugins/oidc/interfaces.py:36 +msgid "Redirect uris" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:85 +msgid "Scope" +msgstr "" + #: pas/plugins/oidc/browser/view.py:61 #: pas/plugins/oidc/services/oidc/oidc.py:107 msgid "There was an error during the login process. Please try again." @@ -52,14 +120,50 @@ msgstr "Hubo un error al obtener el cliente oauth2." msgid "Title" msgstr "Título" -#: pas/plugins/oidc/configure.zcml:41 +#: pas/plugins/oidc/interfaces.py:39 +msgid "URI" +msgstr "" + +#: pas/plugins/oidc/configure.zcml:42 msgid "Uninstalls the pas.plugins.oidc add-on." msgstr "Desinstala el complemento pas.plugins.oidc." -#: pas/plugins/oidc/configure.zcml:32 +#: pas/plugins/oidc/interfaces.py:92 +msgid "Use PKCE" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:46 +msgid "Use Zope session data manager" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:107 +msgid "Use a modified OpenID Schema for email_verified and phone_number_verified boolean values coming as string" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:98 +msgid "Use deprecated redirect_uri" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:99 +msgid "Use deprecated redirect_uri for logout url(/Plone/acl_users/oidc/logout)" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:106 +msgid "Use modified OpenID Schema" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:64 +msgid "User info property used as groupid, default 'groups'" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:117 +msgid "User info property used as userid, default 'sub'." +msgstr "" + +#: pas/plugins/oidc/configure.zcml:33 msgid "pas.plugins.oidc" msgstr "pas.plugins.oidc" -#: pas/plugins/oidc/configure.zcml:41 +#: pas/plugins/oidc/configure.zcml:42 msgid "pas.plugins.oidc (uninstall)" msgstr "pas.plugins.oidc (desinstalar)" diff --git a/src/pas/plugins/oidc/locales/pas.plugins.oidc.pot b/src/pas/plugins/oidc/locales/pas.plugins.oidc.pot index 2a79a6a..1b680d0 100644 --- a/src/pas/plugins/oidc/locales/pas.plugins.oidc.pot +++ b/src/pas/plugins/oidc/locales/pas.plugins.oidc.pot @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2024-05-23 11:20+0000\n" +"POT-Creation-Date: 2024-12-06 22:39+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,26 +17,98 @@ msgstr "" "Preferred-Encodings: utf-8 latin1\n" "Domain: pas.plugins.oidc\n" +#: pas/plugins/oidc/interfaces.py:19 +msgid "" +msgstr "" + #: pas/plugins/oidc/www/oidcPluginForm.zpt:54 msgid "Add" msgstr "" +#: pas/plugins/oidc/interfaces.py:24 +msgid "Client ID" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:30 +msgid "Client secret" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:71 +msgid "Create authentication __ac ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:77 +msgid "Create authentication auth_token (volto/restapi) ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:70 +msgid "Create authentication ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:58 +msgid "Create groups / update group memberships" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:76 +msgid "Create restapi ticket" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:52 +msgid "Create user / update user properties" +msgstr "" + #: pas/plugins/oidc/www/oidcPluginForm.zpt:26 msgid "Id" msgstr "" -#: pas/plugins/oidc/configure.zcml:32 +#: pas/plugins/oidc/configure.zcml:33 msgid "Installs the pas.plugins.oidc add-on." msgstr "" +#: pas/plugins/oidc/controlpanel/classic.py:144 +msgid "OIDC Plugin Settings" +msgstr "" + #: pas/plugins/oidc/www/oidcPluginForm.zpt:13 msgid "OIDC Plugin manage the details of the OpenID Connect Authentication plugin Pluggable Auth Service functionality." msgstr "" +#: pas/plugins/oidc/profiles/default/controlpanel.xml +msgid "OIDC Settings" +msgstr "" + +#: pas/plugins/oidc/controlpanel/oidc.py:19 +msgid "OIDC settings" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:18 +msgid "OIDC/OAuth2 Issuer" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:82 +msgid "Open ID scopes" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:83 +msgid "Open ID scopes to request to the server" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:116 +msgid "Property used as userid" +msgstr "" + #: pas/plugins/oidc/services/oidc/oidc.py:95 msgid "Provider is not properly configured." msgstr "" +#: pas/plugins/oidc/interfaces.py:36 +msgid "Redirect uris" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:85 +msgid "Scope" +msgstr "" + #: pas/plugins/oidc/browser/view.py:61 #: pas/plugins/oidc/services/oidc/oidc.py:107 msgid "There was an error during the login process. Please try again." @@ -50,14 +122,50 @@ msgstr "" msgid "Title" msgstr "" -#: pas/plugins/oidc/configure.zcml:41 +#: pas/plugins/oidc/interfaces.py:39 +msgid "URI" +msgstr "" + +#: pas/plugins/oidc/configure.zcml:42 msgid "Uninstalls the pas.plugins.oidc add-on." msgstr "" -#: pas/plugins/oidc/configure.zcml:32 +#: pas/plugins/oidc/interfaces.py:92 +msgid "Use PKCE" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:46 +msgid "Use Zope session data manager" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:107 +msgid "Use a modified OpenID Schema for email_verified and phone_number_verified boolean values coming as string" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:98 +msgid "Use deprecated redirect_uri" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:99 +msgid "Use deprecated redirect_uri for logout url(/Plone/acl_users/oidc/logout)" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:106 +msgid "Use modified OpenID Schema" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:64 +msgid "User info property used as groupid, default 'groups'" +msgstr "" + +#: pas/plugins/oidc/interfaces.py:117 +msgid "User info property used as userid, default 'sub'." +msgstr "" + +#: pas/plugins/oidc/configure.zcml:33 msgid "pas.plugins.oidc" msgstr "" -#: pas/plugins/oidc/configure.zcml:41 +#: pas/plugins/oidc/configure.zcml:42 msgid "pas.plugins.oidc (uninstall)" msgstr "" diff --git a/src/pas/plugins/oidc/profiles/default/controlpanel.xml b/src/pas/plugins/oidc/profiles/default/controlpanel.xml new file mode 100644 index 0000000..a345416 --- /dev/null +++ b/src/pas/plugins/oidc/profiles/default/controlpanel.xml @@ -0,0 +1,21 @@ + + + + + Manage portal + + + diff --git a/src/pas/plugins/oidc/profiles/default/metadata.xml b/src/pas/plugins/oidc/profiles/default/metadata.xml index 9820946..6ba7cc4 100644 --- a/src/pas/plugins/oidc/profiles/default/metadata.xml +++ b/src/pas/plugins/oidc/profiles/default/metadata.xml @@ -1,4 +1,4 @@ - 1001 + 1002 diff --git a/src/pas/plugins/oidc/upgrades/__init__.py b/src/pas/plugins/oidc/upgrades/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pas/plugins/oidc/upgrades/configure.zcml b/src/pas/plugins/oidc/upgrades/configure.zcml new file mode 100644 index 0000000..1ec2a73 --- /dev/null +++ b/src/pas/plugins/oidc/upgrades/configure.zcml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/tests/controlpanel/conftest.py b/tests/controlpanel/conftest.py new file mode 100644 index 0000000..f474bbd --- /dev/null +++ b/tests/controlpanel/conftest.py @@ -0,0 +1,59 @@ +from plone import api +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.restapi.testing import RelativeSession +from zope.component.hooks import setSite + +import pytest +import transaction + + +@pytest.fixture() +def app(restapi): + return restapi["app"] + + +@pytest.fixture() +def portal(restapi, keycloak): + portal = restapi["portal"] + setSite(portal) + plugin = portal.acl_users.oidc + with api.env.adopt_roles(["Manager", "Member"]): + for key, value in keycloak.items(): + setattr(plugin, key, value) + transaction.commit() + yield portal + with api.env.adopt_roles(["Manager", "Member"]): + for key, value in keycloak.items(): + if key != "scope": + value = "" + setattr(plugin, key, value) + transaction.commit() + + +@pytest.fixture() +def http_request(restapi): + return restapi["request"] + + +@pytest.fixture() +def request_api_factory(portal): + def factory(): + url = portal.absolute_url() + api_session = RelativeSession(f"{url}/++api++") + return api_session + + return factory + + +@pytest.fixture() +def api_anon_request(request_api_factory): + return request_api_factory() + + +@pytest.fixture() +def api_manager_request(request_api_factory): + request = request_api_factory() + request.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + yield request + request.auth = () diff --git a/tests/controlpanel/test_controlpanel_api.py b/tests/controlpanel/test_controlpanel_api.py new file mode 100644 index 0000000..c4cd03e --- /dev/null +++ b/tests/controlpanel/test_controlpanel_api.py @@ -0,0 +1,24 @@ +import pytest + + +class TestControlPanel: + + @pytest.fixture(autouse=True) + def _initialize(self, api_manager_request): + self.api_session = api_manager_request + + @pytest.mark.parametrize( + "key,type_", + ( + ("@id", str), + ("data", dict), + ("group", str), + ("schema", dict), + ("title", str), + ), + ) + def test_serialization(self, key, type_): + response = self.api_session.get("/@controlpanels/oidc_admin") + data = response.json() + assert key in data + assert isinstance(data[key], type_) diff --git a/tests/functional/test_controlpanel_classic.py b/tests/functional/test_controlpanel_classic.py new file mode 100644 index 0000000..917a1d8 --- /dev/null +++ b/tests/functional/test_controlpanel_classic.py @@ -0,0 +1,19 @@ +from plone import api + +import pytest + + +class TestControlPanel: + url: str = "" + + @pytest.fixture(autouse=True) + def _initialize(self, browser_manager): + self.browser = browser_manager + self.portal_url = api.portal.get().absolute_url() + self.url = f"{self.portal_url}/@@oidc-controlpanel" + + def test_exists(self): + browser = self.browser + self.browser.open(self.url) + assert browser.url == self.url + assert browser.headers["status"] == "200 OK" diff --git a/tests/setup/test_setup_install.py b/tests/setup/test_setup_install.py index 9c79e6e..d2c668f 100644 --- a/tests/setup/test_setup_install.py +++ b/tests/setup/test_setup_install.py @@ -14,7 +14,7 @@ def test_addon_installed(self, installer): def test_latest_version(self, profile_last_version): """Test latest version of default profile.""" - assert profile_last_version(f"{PACKAGE_NAME}:default") == "1001" + assert profile_last_version(f"{PACKAGE_NAME}:default") == "1002" def test_browserlayer(self, browser_layers): """Test that IPasPluginsOidcLayer is registered.""" From 0c341860a62e81edb57ca06c56669926b277f2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Fri, 6 Dec 2024 19:44:54 -0300 Subject: [PATCH 2/3] Fix dependencies --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index ff61681..4e78c0b 100644 --- a/setup.py +++ b/setup.py @@ -56,10 +56,12 @@ "Zope", "Products.CMFCore", "plone.api", + "plone.app.registry", "plone.base", "plone.protect", "plone.restapi>=8.34.0", "oic", + "z3c.form", ], extras_require={ "test": [ From da111a9f5b93022ed70979479e63ca1dc07b35e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Mon, 9 Dec 2024 22:56:17 -0300 Subject: [PATCH 3/3] Use PLUGIN_ID instead of hardcoded value for oidc --- src/pas/plugins/oidc/controlpanel/classic.py | 3 ++- src/pas/plugins/oidc/controlpanel/deserializer.py | 3 ++- src/pas/plugins/oidc/controlpanel/serializer.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pas/plugins/oidc/controlpanel/classic.py b/src/pas/plugins/oidc/controlpanel/classic.py index 3ec39d3..0a3dfb8 100644 --- a/src/pas/plugins/oidc/controlpanel/classic.py +++ b/src/pas/plugins/oidc/controlpanel/classic.py @@ -1,4 +1,5 @@ from pas.plugins.oidc import _ +from pas.plugins.oidc import PLUGIN_ID from pas.plugins.oidc.interfaces import IOIDCSettings from plone import api from plone.app.registry.browser import controlpanel @@ -15,7 +16,7 @@ def __init__(self, context): self.context = context self.portal = api.portal.get() self.encoding = "utf-8" - self.settings = self.portal.acl_users.oidc + self.settings = self.portal.acl_users[PLUGIN_ID] @property def issuer(self): diff --git a/src/pas/plugins/oidc/controlpanel/deserializer.py b/src/pas/plugins/oidc/controlpanel/deserializer.py index 3486d60..fdacf62 100644 --- a/src/pas/plugins/oidc/controlpanel/deserializer.py +++ b/src/pas/plugins/oidc/controlpanel/deserializer.py @@ -1,3 +1,4 @@ +from pas.plugins.oidc import PLUGIN_ID from pas.plugins.oidc.interfaces import IOIDCControlpanel from plone import api from plone.restapi.deserializer import json_body @@ -21,7 +22,7 @@ class OIDCControlpanelDeserializeFromJson(ControlpanelDeserializeFromJson): @property def proxy(self): portal = api.portal.get() - plugin = portal.acl_users.oidc + plugin = portal.acl_users[PLUGIN_ID] return plugin def __call__(self, mask_validation_errors=True): diff --git a/src/pas/plugins/oidc/controlpanel/serializer.py b/src/pas/plugins/oidc/controlpanel/serializer.py index b2bd1da..f264bf3 100644 --- a/src/pas/plugins/oidc/controlpanel/serializer.py +++ b/src/pas/plugins/oidc/controlpanel/serializer.py @@ -1,3 +1,4 @@ +from pas.plugins.oidc import PLUGIN_ID from pas.plugins.oidc.interfaces import IOIDCControlpanel from pas.plugins.oidc.plugins import OIDCPlugin from plone import api @@ -15,7 +16,7 @@ class OIDCControlpanelSerializeToJson(ControlpanelSerializeToJson): def config_data(self) -> dict: data = {} portal = api.portal.get() - plugin = portal.acl_users.oidc + plugin = portal.acl_users[PLUGIN_ID] properties = OIDCPlugin._properties for prop in properties: key = prop["id"]