From cf794613a53b55b47846bcca96fff4cf3c06d6ac Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Wed, 28 Feb 2024 19:58:11 +0100 Subject: [PATCH 01/21] add new @login endpoint to return available external login options --- src/plone/restapi/interfaces.py | 11 +++++ .../restapi/services/auth/configure.zcml | 7 +++ src/plone/restapi/services/auth/get.py | 16 ++++++ src/plone/restapi/tests/test_auth.py | 49 +++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 src/plone/restapi/services/auth/get.py diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index 5c2aa337e6..2107085523 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -240,3 +240,14 @@ class IBlockVisitor(Interface): def __call__(self, block): """Return an iterable of sub-blocks found inside `block`.""" + + + +class IExternalLoginProviders(Interface): + """ An interface needed to be implemented by providers that want to be listed + in the @login endpoint + """ + def get_providers(): + """ + return a list of login providers, with its id, title, plugin and url + """ diff --git a/src/plone/restapi/services/auth/configure.zcml b/src/plone/restapi/services/auth/configure.zcml index dec5304c50..f5604d81f2 100644 --- a/src/plone/restapi/services/auth/configure.zcml +++ b/src/plone/restapi/services/auth/configure.zcml @@ -3,6 +3,13 @@ xmlns:plone="http://namespaces.plone.org/plone" xmlns:zcml="http://namespaces.zope.org/zcml" > + Date: Wed, 28 Feb 2024 20:03:36 +0100 Subject: [PATCH 02/21] changelog --- news/1757.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/1757.feature diff --git a/news/1757.feature b/news/1757.feature new file mode 100644 index 0000000000..185b56e4d3 --- /dev/null +++ b/news/1757.feature @@ -0,0 +1,2 @@ +Add a @login endpoint to get external login services' links +[erral] From 783a079f011c5440cfcad94bb0cc360233ec7e88 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Wed, 28 Feb 2024 20:04:09 +0100 Subject: [PATCH 03/21] lint --- src/plone/restapi/interfaces.py | 1 - src/plone/restapi/services/auth/get.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index 2107085523..ca58e1dace 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -242,7 +242,6 @@ def __call__(self, block): """Return an iterable of sub-blocks found inside `block`.""" - class IExternalLoginProviders(Interface): """ An interface needed to be implemented by providers that want to be listed in the @login endpoint diff --git a/src/plone/restapi/services/auth/get.py b/src/plone/restapi/services/auth/get.py index b0ecd277da..b613a637a2 100644 --- a/src/plone/restapi/services/auth/get.py +++ b/src/plone/restapi/services/auth/get.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- +from plone.restapi.interfaces import IExternalLoginProviders from plone.restapi.services import Service from zope.component import getAdapters -from plone.restapi.interfaces import IExternalLoginProviders class Login(Service): From cc1347cb9170c72e70aa230200809e40ee9a4f03 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Wed, 28 Feb 2024 20:04:27 +0100 Subject: [PATCH 04/21] lint --- src/plone/restapi/tests/test_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/restapi/tests/test_auth.py b/src/plone/restapi/tests/test_auth.py index 6df213b580..4a271e4e04 100644 --- a/src/plone/restapi/tests/test_auth.py +++ b/src/plone/restapi/tests/test_auth.py @@ -256,4 +256,4 @@ def test_provider_returns_list(self): self.assertEqual(service.request.response.status, 200) self.assertTrue(isinstance(res, dict)) self.assertIn('options', res) - self.assertTrue(isinstance(res.get('options'), list)) \ No newline at end of file + self.assertTrue(isinstance(res.get('options'), list)) From 6cf2636df52468f78361ea9a05932546c39fd250 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Wed, 28 Feb 2024 20:04:35 +0100 Subject: [PATCH 05/21] lint --- src/plone/restapi/tests/test_auth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plone/restapi/tests/test_auth.py b/src/plone/restapi/tests/test_auth.py index 4a271e4e04..8be5316801 100644 --- a/src/plone/restapi/tests/test_auth.py +++ b/src/plone/restapi/tests/test_auth.py @@ -249,7 +249,6 @@ def traverse(self, path="/plone/@login", accept="application/json", method="GET" notify(PubStart(request)) return request.traverse(path) - def test_provider_returns_list(self): service = self.traverse() res = service.reply() From 98c0cf5e30e0701867523630887a01f9e744c50b Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Wed, 28 Feb 2024 20:08:37 +0100 Subject: [PATCH 06/21] lint --- src/plone/restapi/interfaces.py | 5 +++-- src/plone/restapi/services/auth/get.py | 6 ++---- src/plone/restapi/tests/test_auth.py | 25 ++++++++++++++----------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index ca58e1dace..498b662168 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -243,9 +243,10 @@ def __call__(self, block): class IExternalLoginProviders(Interface): - """ An interface needed to be implemented by providers that want to be listed - in the @login endpoint + """An interface needed to be implemented by providers that want to be listed + in the @login endpoint """ + def get_providers(): """ return a list of login providers, with its id, title, plugin and url diff --git a/src/plone/restapi/services/auth/get.py b/src/plone/restapi/services/auth/get.py index b613a637a2..a1ae92beef 100644 --- a/src/plone/restapi/services/auth/get.py +++ b/src/plone/restapi/services/auth/get.py @@ -9,8 +9,6 @@ def reply(self): adapters = getAdapters(self.context, IExternalLoginProviders) external_providers = [] for adapter in adapters: - external_providers.extend( - adapter.get_providers() - ) + external_providers.extend(adapter.get_providers()) - return {'options': external_providers} \ No newline at end of file + return {"options": external_providers} diff --git a/src/plone/restapi/tests/test_auth.py b/src/plone/restapi/tests/test_auth.py index 8be5316801..0af61af3b2 100644 --- a/src/plone/restapi/tests/test_auth.py +++ b/src/plone/restapi/tests/test_auth.py @@ -213,20 +213,21 @@ def test_renew_fails_on_invalid_token(self): res["error"]["type"], "Invalid or expired authentication token" ) + class MyExternalLinks: def get_providers(self): return [ { - 'id': 'myprovider', - 'title': 'Provider', - 'plugin': 'myprovider', - 'url': 'https://some.example.com/login-url' + "id": "myprovider", + "title": "Provider", + "plugin": "myprovider", + "url": "https://some.example.com/login-url", }, { - 'id': 'github', - 'title': 'GitHub', - 'plugin': 'github', - 'url': 'https://some.example.com/login-authomatic/github' + "id": "github", + "title": "GitHub", + "plugin": "github", + "url": "https://some.example.com/login-authomatic/github", }, ] @@ -238,7 +239,9 @@ def setUp(self): self.portal = self.layer["portal"] self.request = self.layer["request"] - provideAdapter(MyExternalLinks, adapts=(IPloneSiteRoot,), provides=IExternalLoginProviders) + provideAdapter( + MyExternalLinks, adapts=(IPloneSiteRoot,), provides=IExternalLoginProviders + ) def traverse(self, path="/plone/@login", accept="application/json", method="GET"): request = self.layer["request"] @@ -254,5 +257,5 @@ def test_provider_returns_list(self): res = service.reply() self.assertEqual(service.request.response.status, 200) self.assertTrue(isinstance(res, dict)) - self.assertIn('options', res) - self.assertTrue(isinstance(res.get('options'), list)) + self.assertIn("options", res) + self.assertTrue(isinstance(res.get("options"), list)) From 68ce3be4461a4ab1e4d1419ea3d395157f70f3e1 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Wed, 28 Feb 2024 20:09:41 +0100 Subject: [PATCH 07/21] lint --- src/plone/restapi/tests/test_auth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plone/restapi/tests/test_auth.py b/src/plone/restapi/tests/test_auth.py index 0af61af3b2..8cdcb02578 100644 --- a/src/plone/restapi/tests/test_auth.py +++ b/src/plone/restapi/tests/test_auth.py @@ -7,7 +7,6 @@ from zExceptions import Unauthorized from zope.event import notify from ZPublisher.pubevents import PubStart -from zope.interface import implementer from zope.component import provideAdapter from plone.restapi.interfaces import IExternalLoginProviders from Products.CMFPlone.interfaces import IPloneSiteRoot From 879752e0e1d0aec564db71753e7413e2acb9b830 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Thu, 29 Feb 2024 10:52:27 +0100 Subject: [PATCH 08/21] Update news/1757.feature Co-authored-by: Steve Piercy --- news/1757.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/1757.feature b/news/1757.feature index 185b56e4d3..bc1bbeb35d 100644 --- a/news/1757.feature +++ b/news/1757.feature @@ -1,2 +1,2 @@ -Add a @login endpoint to get external login services' links +Add a @login endpoint to get external login services' links. @erral [erral] From 53002d454c2ec653f28afd02b8249c3017153803 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Thu, 29 Feb 2024 10:52:33 +0100 Subject: [PATCH 09/21] Update news/1757.feature Co-authored-by: Steve Piercy --- news/1757.feature | 1 - 1 file changed, 1 deletion(-) diff --git a/news/1757.feature b/news/1757.feature index bc1bbeb35d..4c963a5f72 100644 --- a/news/1757.feature +++ b/news/1757.feature @@ -1,2 +1 @@ Add a @login endpoint to get external login services' links. @erral -[erral] From 37c47e798edbc3089f42ff4d905286124420f8de Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Thu, 29 Feb 2024 12:33:42 +0100 Subject: [PATCH 10/21] Update news/1757.feature Co-authored-by: Steve Piercy --- news/1757.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/1757.feature b/news/1757.feature index 4c963a5f72..c678441b4e 100644 --- a/news/1757.feature +++ b/news/1757.feature @@ -1 +1 @@ -Add a @login endpoint to get external login services' links. @erral +Add a `@login` endpoint to get external login services' links. @erral From 06be87c3c63fd2fd6a5e4d88bfca2e9306f3b18c Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Thu, 29 Feb 2024 19:02:40 +0100 Subject: [PATCH 11/21] add docs --- .../external-authentication-links.md | 69 +++++++++++++++++++ docs/source/endpoints/index.md | 9 +-- .../external_authentication_links.req | 3 + .../external_authentication_links.resp | 6 ++ src/plone/restapi/tests/test_documentation.py | 32 +++++++++ 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 docs/source/endpoints/external-authentication-links.md create mode 100644 src/plone/restapi/tests/http-examples/external_authentication_links.req create mode 100644 src/plone/restapi/tests/http-examples/external_authentication_links.resp diff --git a/docs/source/endpoints/external-authentication-links.md b/docs/source/endpoints/external-authentication-links.md new file mode 100644 index 0000000000..37926afad8 --- /dev/null +++ b/docs/source/endpoints/external-authentication-links.md @@ -0,0 +1,69 @@ +--- +myst: + html_meta: + 'description': 'The @history endpoint exposes history and versioning information on previous versions of the content.' + 'property=og:description': 'The @history endpoint exposes history and versioning information on previous versions of the content.' + 'property=og:title': 'History' + 'keywords': 'Plone, plone.restapi, REST, API, History' +--- + +# External authentication links + +It is common to have third party addons that allow logging in in your site using third party services. + +Such addons include using KeyCloak, GitHub or other OAuth2 or OpenID Connect enabled services. + +In such cases, an addon is installed in Plone and those addons modify the way that the login works, in order to direct to user to those third party services. + +To expose the links provided by those addons, plone.restapi provides an adapter based service registration, to let those addons know this REST API that those services could be used to authenticate users. + +This will be mostly used by frontends, that need to show the end user the links to those services. + +To achieve that, third party products need to register one or more adapters for the Plone site root object, providing the `plone.restapi.interfaces.IExternalLoginProviders` interface. + +In such adapter, the addon need to return the list of external links and some metadata like the id, title and plugin name. + +An example adapter would be the following (in an `adapter.py` file): + +```python +from zope.component import adapts +from zope.interface import implementer + +@adapts(IPloneSiteRoot) +@implementer(IExternalLoginProviders) +class MyExternalLinks: + def get_providers(self): + return [ + { + "id": "myprovider", + "title": "Provider", + "plugin": "myprovider", + "url": "https://some.example.com/login-url", + }, + { + "id": "github", + "title": "GitHub", + "plugin": "github", + "url": "https://some.example.com/login-authomatic/github", + }, + ] +``` + +With the corresponding ZCML stanza (in the corresponding `configure.zcml` file): + +```xml + +``` + +The API request would be as follows: + +```{eval-rst} +.. http:example:: curl httpie python-requests + :request: ../../../src/plone/restapi/tests/http-examples/external_authentication_links.req +``` + +The server will respond with a `Status 200` and the list of external providers: + +```{literalinclude} ../../../src/plone/restapi/tests/http-examples/external_authentication_links.resp +:language: http +``` diff --git a/docs/source/endpoints/index.md b/docs/source/endpoints/index.md index 76105b0d82..c8cbe1ac8d 100644 --- a/docs/source/endpoints/index.md +++ b/docs/source/endpoints/index.md @@ -1,10 +1,10 @@ --- myst: html_meta: - "description": "Usage of the Plone REST API." - "property=og:description": "Usage of the Plone REST API." - "property=og:title": "Usage of the Plone REST API" - "keywords": "Plone, plone.restapi, REST, API, Usage" + 'description': 'Usage of the Plone REST API.' + 'property=og:description': 'Usage of the Plone REST API.' + 'property=og:title': 'Usage of the Plone REST API' + 'keywords': 'Plone, plone.restapi, REST, API, Usage' --- (restapi-endpoints)= @@ -29,6 +29,7 @@ copymove database email-notification email-send +external-authentication-links groups history linkintegrity diff --git a/src/plone/restapi/tests/http-examples/external_authentication_links.req b/src/plone/restapi/tests/http-examples/external_authentication_links.req new file mode 100644 index 0000000000..92469012d8 --- /dev/null +++ b/src/plone/restapi/tests/http-examples/external_authentication_links.req @@ -0,0 +1,3 @@ +GET /plone/@login HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/src/plone/restapi/tests/http-examples/external_authentication_links.resp b/src/plone/restapi/tests/http-examples/external_authentication_links.resp new file mode 100644 index 0000000000..240762af39 --- /dev/null +++ b/src/plone/restapi/tests/http-examples/external_authentication_links.resp @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "options": [] +} diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index e10c2e97cf..a47e29a282 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -40,6 +40,10 @@ from plone.app.testing import popGlobalRegistry from plone.app.testing import pushGlobalRegistry from plone.restapi.testing import register_static_uuid_utility +from zope.component import provideAdapter +from plone.restapi.interfaces import IExternalLoginProviders +from Products.CMFPlone.interfaces import IPloneSiteRoot + import collections import json @@ -84,6 +88,24 @@ open_kw = {"newline": "\n"} +class MyExternalLinks: + def get_providers(self): + return [ + { + "id": "myprovider", + "title": "Provider", + "plugin": "myprovider", + "url": "https://some.example.com/login-url", + }, + { + "id": "github", + "title": "GitHub", + "plugin": "github", + "url": "https://some.example.com/login-authomatic/github", + }, + ] + + def normalize_test_port(value): # When you run these tests in the Plone core development buildout, # the port number is random. Normalize this to the default port. @@ -225,6 +247,10 @@ def setUp(self): super().setUp() self.document = self.create_document() alsoProvides(self.document, ITTWLockable) + provideAdapter( + MyExternalLinks, adapts=(IPloneSiteRoot,), provides=IExternalLoginProviders + ) + transaction.commit() def tearDown(self): @@ -785,6 +811,12 @@ def test_documentation_jwt_logout(self): ) save_request_and_response_for_docs("jwt_logout", response) + def test_documentation_external_doc_links(self): + response = self.api_session.get( + f"{self.portal.absolute_url()}/@login", + ) + save_request_and_response_for_docs("external_authentication_links", response) + def test_documentation_batching(self): folder = self.portal[ self.portal.invokeFactory("Folder", id="folder", title="Folder") From 8168d4ac9ca2b42149b15e8b79ae8b4a1f8f3cb5 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Fri, 1 Mar 2024 10:01:20 +0100 Subject: [PATCH 12/21] yaml --- docs/source/endpoints/external-authentication-links.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/endpoints/external-authentication-links.md b/docs/source/endpoints/external-authentication-links.md index 37926afad8..a1fc061b1c 100644 --- a/docs/source/endpoints/external-authentication-links.md +++ b/docs/source/endpoints/external-authentication-links.md @@ -1,10 +1,10 @@ --- myst: html_meta: - 'description': 'The @history endpoint exposes history and versioning information on previous versions of the content.' - 'property=og:description': 'The @history endpoint exposes history and versioning information on previous versions of the content.' - 'property=og:title': 'History' - 'keywords': 'Plone, plone.restapi, REST, API, History' + "description': "The @login endpoint exposes the list of external authentication services that may be used in the Plone site." + "property=og:description": "The @login endpoint exposes the list of external authentication services that may be used in the Plone site." + "property=og:title": 'External authentication links" + "keywords": 'Plone, plone.restapi, REST, API, Login, Authentication, External services" --- # External authentication links From 961def7e37cfaa80535ba47b0e1bb49f1ab3ed83 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Fri, 1 Mar 2024 10:02:07 +0100 Subject: [PATCH 13/21] yaml --- docs/source/endpoints/external-authentication-links.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/endpoints/external-authentication-links.md b/docs/source/endpoints/external-authentication-links.md index a1fc061b1c..3724ab07f6 100644 --- a/docs/source/endpoints/external-authentication-links.md +++ b/docs/source/endpoints/external-authentication-links.md @@ -1,7 +1,7 @@ --- myst: html_meta: - "description': "The @login endpoint exposes the list of external authentication services that may be used in the Plone site." + "description": "The @login endpoint exposes the list of external authentication services that may be used in the Plone site." "property=og:description": "The @login endpoint exposes the list of external authentication services that may be used in the Plone site." "property=og:title": 'External authentication links" "keywords": 'Plone, plone.restapi, REST, API, Login, Authentication, External services" From 3cd4daae9827f0d5d585c12e074668c1735f0b9a Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Fri, 1 Mar 2024 10:02:38 +0100 Subject: [PATCH 14/21] docs --- docs/source/endpoints/external-authentication-links.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/endpoints/external-authentication-links.md b/docs/source/endpoints/external-authentication-links.md index 3724ab07f6..be0f5d78ed 100644 --- a/docs/source/endpoints/external-authentication-links.md +++ b/docs/source/endpoints/external-authentication-links.md @@ -21,7 +21,7 @@ This will be mostly used by frontends, that need to show the end user the links To achieve that, third party products need to register one or more adapters for the Plone site root object, providing the `plone.restapi.interfaces.IExternalLoginProviders` interface. -In such adapter, the addon need to return the list of external links and some metadata like the id, title and plugin name. +In such adapter the addon needs to return the list of external links and some metadata like the id, title and plugin name. An example adapter would be the following (in an `adapter.py` file): From 0eccce4ba49ace1d94cbbb89694785cc754482cb Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Fri, 1 Mar 2024 10:03:18 +0100 Subject: [PATCH 15/21] docs --- docs/source/endpoints/external-authentication-links.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/endpoints/external-authentication-links.md b/docs/source/endpoints/external-authentication-links.md index be0f5d78ed..e90fb501ee 100644 --- a/docs/source/endpoints/external-authentication-links.md +++ b/docs/source/endpoints/external-authentication-links.md @@ -17,7 +17,7 @@ In such cases, an addon is installed in Plone and those addons modify the way th To expose the links provided by those addons, plone.restapi provides an adapter based service registration, to let those addons know this REST API that those services could be used to authenticate users. -This will be mostly used by frontends, that need to show the end user the links to those services. +This will be mostly used by frontends that need to show the end user the links to those services. To achieve that, third party products need to register one or more adapters for the Plone site root object, providing the `plone.restapi.interfaces.IExternalLoginProviders` interface. From 499d74a20a03d864721d843e844d1d76c4770636 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 1 Mar 2024 01:18:38 -0800 Subject: [PATCH 16/21] Review of docs --- docs/source/endpoints/index.md | 9 ++++--- ...ernal-authentication-links.md => login.md} | 25 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) rename docs/source/endpoints/{external-authentication-links.md => login.md} (59%) diff --git a/docs/source/endpoints/index.md b/docs/source/endpoints/index.md index c8cbe1ac8d..7e0f4e82ae 100644 --- a/docs/source/endpoints/index.md +++ b/docs/source/endpoints/index.md @@ -1,10 +1,10 @@ --- myst: html_meta: - 'description': 'Usage of the Plone REST API.' - 'property=og:description': 'Usage of the Plone REST API.' - 'property=og:title': 'Usage of the Plone REST API' - 'keywords': 'Plone, plone.restapi, REST, API, Usage' + 'description': 'Endpoints of the Plone REST API.' + 'property=og:description': 'Endpoints of the Plone REST API.' + 'property=og:title': 'Endpoints of the Plone REST API' + 'keywords': 'Plone, plone.restapi, REST, API, endpoints' --- (restapi-endpoints)= @@ -34,6 +34,7 @@ groups history linkintegrity locking +login navigation navroot actions diff --git a/docs/source/endpoints/external-authentication-links.md b/docs/source/endpoints/login.md similarity index 59% rename from docs/source/endpoints/external-authentication-links.md rename to docs/source/endpoints/login.md index e90fb501ee..3dc1e97ffd 100644 --- a/docs/source/endpoints/external-authentication-links.md +++ b/docs/source/endpoints/login.md @@ -3,27 +3,26 @@ myst: html_meta: "description": "The @login endpoint exposes the list of external authentication services that may be used in the Plone site." "property=og:description": "The @login endpoint exposes the list of external authentication services that may be used in the Plone site." - "property=og:title": 'External authentication links" - "keywords": 'Plone, plone.restapi, REST, API, Login, Authentication, External services" + "property=og:title": "@login for external authentication links" + "keywords": "Plone, plone.restapi, REST, API, login, authentication, external services" --- -# External authentication links +# Login for external authentication links -It is common to have third party addons that allow logging in in your site using third party services. +It is common to use add-ons that allow logging in to your site using third party services. +Such add-ons include using authentication services provided by KeyCloak, GitHub, or other OAuth2 or OpenID Connect enabled services. -Such addons include using KeyCloak, GitHub or other OAuth2 or OpenID Connect enabled services. +When you install one of these add-ons, it modifies the login process, directing the user to third party services. -In such cases, an addon is installed in Plone and those addons modify the way that the login works, in order to direct to user to those third party services. - -To expose the links provided by those addons, plone.restapi provides an adapter based service registration, to let those addons know this REST API that those services could be used to authenticate users. - -This will be mostly used by frontends that need to show the end user the links to those services. +To expose the links provided by these add-ons, `plone.restapi` provides an adapter based service registration. +It lets those add-ons know that the REST API can use those services to authenticate users. +This will mostly be used by frontends that need to show the end user the links to those services. To achieve that, third party products need to register one or more adapters for the Plone site root object, providing the `plone.restapi.interfaces.IExternalLoginProviders` interface. -In such adapter the addon needs to return the list of external links and some metadata like the id, title and plugin name. +In the adapter, the add-on needs to return the list of external links and some metadata, including the `id`, `title`, and name of the `plugin`. -An example adapter would be the following (in an `adapter.py` file): +An example adapter would be the following, in a file named {file}`adapter.py`: ```python from zope.component import adapts @@ -49,7 +48,7 @@ class MyExternalLinks: ] ``` -With the corresponding ZCML stanza (in the corresponding `configure.zcml` file): +With the corresponding ZCML stanza, in the corresponding {file}`configure.zcml` file: ```xml From c97c97b2ea07a00eb9da165d0af7a95973151989 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 1 Mar 2024 01:20:17 -0800 Subject: [PATCH 17/21] Revert `'` to `"` --- docs/source/endpoints/index.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/source/endpoints/index.md b/docs/source/endpoints/index.md index 7e0f4e82ae..e747f3d10c 100644 --- a/docs/source/endpoints/index.md +++ b/docs/source/endpoints/index.md @@ -1,10 +1,10 @@ --- myst: html_meta: - 'description': 'Endpoints of the Plone REST API.' - 'property=og:description': 'Endpoints of the Plone REST API.' - 'property=og:title': 'Endpoints of the Plone REST API' - 'keywords': 'Plone, plone.restapi, REST, API, endpoints' + "description": "Endpoints of the Plone REST API." + "property=og:description": "Endpoints of the Plone REST API." + "property=og:title": "Endpoints of the Plone REST API" + "keywords": "Plone, plone.restapi, REST, API, endpoints" --- (restapi-endpoints)= @@ -29,7 +29,6 @@ copymove database email-notification email-send -external-authentication-links groups history linkintegrity From 4cc428847998af2b229ede0daf6299f091855ba9 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Sun, 3 Mar 2024 19:26:19 +0100 Subject: [PATCH 18/21] properly implement the adapter in tests --- docs/source/endpoints/login.md | 9 ++++++--- src/plone/restapi/services/auth/get.py | 4 ++-- src/plone/restapi/tests/test_auth.py | 11 +++++++++-- src/plone/restapi/tests/test_documentation.py | 8 +++++++- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/source/endpoints/login.md b/docs/source/endpoints/login.md index 3dc1e97ffd..614fdf7972 100644 --- a/docs/source/endpoints/login.md +++ b/docs/source/endpoints/login.md @@ -25,12 +25,15 @@ In the adapter, the add-on needs to return the list of external links and some m An example adapter would be the following, in a file named {file}`adapter.py`: ```python -from zope.component import adapts +from zope.component import adapter from zope.interface import implementer -@adapts(IPloneSiteRoot) +@adapter(IPloneSiteRoot) @implementer(IExternalLoginProviders) class MyExternalLinks: + def __init__(self, context): + self.context = context + def get_providers(self): return [ { @@ -51,7 +54,7 @@ class MyExternalLinks: With the corresponding ZCML stanza, in the corresponding {file}`configure.zcml` file: ```xml - + ``` The API request would be as follows: diff --git a/src/plone/restapi/services/auth/get.py b/src/plone/restapi/services/auth/get.py index a1ae92beef..a1f3100845 100644 --- a/src/plone/restapi/services/auth/get.py +++ b/src/plone/restapi/services/auth/get.py @@ -6,9 +6,9 @@ class Login(Service): def reply(self): - adapters = getAdapters(self.context, IExternalLoginProviders) + adapters = getAdapters((self.context,), IExternalLoginProviders) external_providers = [] - for adapter in adapters: + for name, adapter in adapters: external_providers.extend(adapter.get_providers()) return {"options": external_providers} diff --git a/src/plone/restapi/tests/test_auth.py b/src/plone/restapi/tests/test_auth.py index 8cdcb02578..0d9154002b 100644 --- a/src/plone/restapi/tests/test_auth.py +++ b/src/plone/restapi/tests/test_auth.py @@ -212,8 +212,11 @@ def test_renew_fails_on_invalid_token(self): res["error"]["type"], "Invalid or expired authentication token" ) - class MyExternalLinks: + + def __init__(self, context): + self.context = context + def get_providers(self): return [ { @@ -239,7 +242,10 @@ def setUp(self): self.request = self.layer["request"] provideAdapter( - MyExternalLinks, adapts=(IPloneSiteRoot,), provides=IExternalLoginProviders + MyExternalLinks, + adapts=(IPloneSiteRoot,), + provides=IExternalLoginProviders, + name="test-external-links" ) def traverse(self, path="/plone/@login", accept="application/json", method="GET"): @@ -258,3 +264,4 @@ def test_provider_returns_list(self): self.assertTrue(isinstance(res, dict)) self.assertIn("options", res) self.assertTrue(isinstance(res.get("options"), list)) + self.assertTrue(len(res.get("options")), 2) diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index a47e29a282..972f791878 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -89,6 +89,9 @@ class MyExternalLinks: + def __init__(self, context): + self.context = context + def get_providers(self): return [ { @@ -248,7 +251,10 @@ def setUp(self): self.document = self.create_document() alsoProvides(self.document, ITTWLockable) provideAdapter( - MyExternalLinks, adapts=(IPloneSiteRoot,), provides=IExternalLoginProviders + MyExternalLinks, + adapts=(IPloneSiteRoot,), + provides=IExternalLoginProviders, + name="test-external-links", ) transaction.commit() From fd67f94f81b7627e0b32250820856fba534e53a1 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Sun, 3 Mar 2024 19:26:49 +0100 Subject: [PATCH 19/21] add docs rsults --- .../external_authentication_links.resp | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/plone/restapi/tests/http-examples/external_authentication_links.resp b/src/plone/restapi/tests/http-examples/external_authentication_links.resp index 240762af39..29ae4aa8d4 100644 --- a/src/plone/restapi/tests/http-examples/external_authentication_links.resp +++ b/src/plone/restapi/tests/http-examples/external_authentication_links.resp @@ -2,5 +2,30 @@ HTTP/1.1 200 OK Content-Type: application/json { - "options": [] + "options": [ + { + "id": "myprovider", + "plugin": "myprovider", + "title": "Provider", + "url": "https://some.example.com/login-url" + }, + { + "id": "github", + "plugin": "github", + "title": "GitHub", + "url": "https://some.example.com/login-authomatic/github" + }, + { + "id": "myprovider", + "plugin": "myprovider", + "title": "Provider", + "url": "https://some.example.com/login-url" + }, + { + "id": "github", + "plugin": "github", + "title": "GitHub", + "url": "https://some.example.com/login-authomatic/github" + } + ] } From 861673f381f9ee821d977798ce952e3b3de40697 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Sun, 3 Mar 2024 19:34:57 +0100 Subject: [PATCH 20/21] black --- src/plone/restapi/tests/test_auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plone/restapi/tests/test_auth.py b/src/plone/restapi/tests/test_auth.py index 0d9154002b..f0435d137d 100644 --- a/src/plone/restapi/tests/test_auth.py +++ b/src/plone/restapi/tests/test_auth.py @@ -212,8 +212,8 @@ def test_renew_fails_on_invalid_token(self): res["error"]["type"], "Invalid or expired authentication token" ) -class MyExternalLinks: +class MyExternalLinks: def __init__(self, context): self.context = context @@ -245,7 +245,7 @@ def setUp(self): MyExternalLinks, adapts=(IPloneSiteRoot,), provides=IExternalLoginProviders, - name="test-external-links" + name="test-external-links", ) def traverse(self, path="/plone/@login", accept="application/json", method="GET"): From 7a9e378b98234d60643bbe55e330654c423d7f38 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Sun, 3 Mar 2024 19:45:41 +0100 Subject: [PATCH 21/21] fix response --- .../http-examples/external_authentication_links.resp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/plone/restapi/tests/http-examples/external_authentication_links.resp b/src/plone/restapi/tests/http-examples/external_authentication_links.resp index 29ae4aa8d4..b88f62ab5e 100644 --- a/src/plone/restapi/tests/http-examples/external_authentication_links.resp +++ b/src/plone/restapi/tests/http-examples/external_authentication_links.resp @@ -3,18 +3,6 @@ Content-Type: application/json { "options": [ - { - "id": "myprovider", - "plugin": "myprovider", - "title": "Provider", - "url": "https://some.example.com/login-url" - }, - { - "id": "github", - "plugin": "github", - "title": "GitHub", - "url": "https://some.example.com/login-authomatic/github" - }, { "id": "myprovider", "plugin": "myprovider",