Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new @login endpoint to return available external login options #1757

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions news/1757.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add a @login endpoint to get external login services' links
erral marked this conversation as resolved.
Show resolved Hide resolved
[erral]
erral marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 11 additions & 0 deletions src/plone/restapi/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be defined in plone.base instead of plone.restapi, so that an addon doesn't need to depend on plone.restapi just to register this adapter and provide information about what providers it supports?

On the other hand, that would make it harder to backport support for this feature to older versions of Plone.

I don't feel strongly about it but in general, plone.restapi's purpose is to provide API access to features that exist at a lower level and might also be accessed via other interfaces.

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the whole idea of the endpoint is to avoid duplicated @login endpoints such the ones we have in pas.plugins.authomatic and pas.plugins.oidc to support volto-authomatic

I agree that perhaps this should go in plone.base somewhere and integrate it with Plone's standard login page.

Perhaps we can open an issue in Plone and work on that later on?

7 changes: 7 additions & 0 deletions src/plone/restapi/services/auth/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
xmlns:plone="http://namespaces.plone.org/plone"
xmlns:zcml="http://namespaces.zope.org/zcml"
>
<plone:service
method="GET"
factory=".get.Login"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
permission="zope.Public"
name="@login"
/>

<plone:service
method="POST"
Expand Down
14 changes: 14 additions & 0 deletions src/plone/restapi/services/auth/get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from plone.restapi.interfaces import IExternalLoginProviders
from plone.restapi.services import Service
from zope.component import getAdapters


class Login(Service):
def reply(self):
adapters = getAdapters(self.context, IExternalLoginProviders)
external_providers = []
for adapter in adapters:
external_providers.extend(adapter.get_providers())

return {"options": external_providers}
50 changes: 50 additions & 0 deletions src/plone/restapi/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from zExceptions import Unauthorized
from zope.event import notify
from ZPublisher.pubevents import PubStart
from zope.component import provideAdapter
from plone.restapi.interfaces import IExternalLoginProviders
from Products.CMFPlone.interfaces import IPloneSiteRoot


class TestLogin(TestCase):
Expand Down Expand Up @@ -208,3 +211,50 @@ def test_renew_fails_on_invalid_token(self):
self.assertEqual(
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": "github",
"title": "GitHub",
"plugin": "github",
"url": "https://some.example.com/login-authomatic/github",
},
]


class TestExternalLoginServices(TestCase):
layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING

def setUp(self):
self.portal = self.layer["portal"]
self.request = self.layer["request"]

provideAdapter(
MyExternalLinks, adapts=(IPloneSiteRoot,), provides=IExternalLoginProviders
)

def traverse(self, path="/plone/@login", accept="application/json", method="GET"):
request = self.layer["request"]
request.environ["PATH_INFO"] = path
request.environ["PATH_TRANSLATED"] = path
request.environ["HTTP_ACCEPT"] = accept
request.environ["REQUEST_METHOD"] = method
notify(PubStart(request))
return request.traverse(path)

def test_provider_returns_list(self):
service = self.traverse()
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))