From 1d264fc54fd7537d5613008d471a3ed77cf4e5ac Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 5 May 2025 09:58:38 +0200 Subject: [PATCH] allow usage of OIDC authentication (via py-scopes) where appropriate, and provide corresponding views in loops/server/auth.zcml --- inst/loops/application.zcml | 2 ++ inst/loops/config.py | 18 ++++++++++++++++++ loops/organize/interfaces.py | 8 +------- loops/organize/party.py | 10 ---------- loops/organize/util.py | 2 +- loops/server/auth.zcml | 18 ++++++++++++++++++ loops/server/main.py | 3 ++- loops/tests/config.py | 22 ++++++++++++++++++++-- loops/tests/test_loops.py | 3 +++ 9 files changed, 65 insertions(+), 21 deletions(-) create mode 100644 loops/server/auth.zcml diff --git a/inst/loops/application.zcml b/inst/loops/application.zcml index 537344a..13636d5 100644 --- a/inst/loops/application.zcml +++ b/inst/loops/application.zcml @@ -24,6 +24,8 @@ --> + + diff --git a/inst/loops/config.py b/inst/loops/config.py index db7f054..d4a6866 100644 --- a/inst/loops/config.py +++ b/inst/loops/config.py @@ -9,6 +9,7 @@ server_id = getenv('SERVER_ID') zope_conf = getenv('ZOPE_CONF', 'zope.conf') server_port = getenv('SERVER_PORT', server_id and getenv(f'SERVER_PORT_{server_id}')) or '8080' +base_url = getenv('BASE_URL', 'https://test.example.com') shell_pw = (getenv('SHELL_PW', 'dummy')) loops_path = (getenv('LOOPS_PATH', 'loops/demo')) @@ -20,3 +21,20 @@ dbname = getenv('DBNAME', 'demo') dbuser = getenv('DBUSER', 'demo') dbpassword = getenv('DBPASSWORD', 'secret') dbschema = getenv('DBSCHEMA', 'demo') + +# OpenID Connect (OIDC, e.g. via zitadel) authentication settings +oidc_provider = getenv('OIDC_PROVIDER', '') #'https://instance1-abcdef.zitadel.cloud') +oidc_client_id = getenv('OIDC_CLIENT_ID', '12345') +oidc_params = dict( + op_config_url=oidc_provider + '/.well-known/openid-configuration', + op_uris=None, + op_keys=None, + callback_url=getenv('OIDC_CALLBACK_URL', base_url + '/auth_callback'), + client_id=oidc_client_id, + principal_prefix=getenv('OIDC_PRINCIPAL_PREFIX', 'loops.'), + cookie_name=getenv('OIDC_COOKIE_NAME', 'oidc_' + oidc_client_id), + cookie_domain=getenv('OIDC_COOKIE_DOMAIN', None), + cookie_lifetime=getenv('OIDC_COOKIE_LIFETIME', '86400'), + cookie_crypt=getenv('OIDC_COOKIE_CRYPT', None) +) + diff --git a/loops/organize/interfaces.py b/loops/organize/interfaces.py index e51ccff..7a5cca7 100644 --- a/loops/organize/interfaces.py +++ b/loops/organize/interfaces.py @@ -18,7 +18,6 @@ from loops.interfaces import HtmlText from loops.organize.util import getPrincipalFolder, getPrincipalForUserId from loops import util from loops.util import _ -from scopes.web.auth import oidc ANNOTATION_KEY = 'loops.organize.person' @@ -34,7 +33,7 @@ def raiseValidationError(info): class UserId(schema.TextLine): - """ Obsolete, as member registration does not use zope.formlib any more. + """ Note: member registration does not use zope.formlib any more. TODO: transfer validation to loops.organize.browser. """ @@ -44,11 +43,6 @@ class UserId(schema.TextLine): from loops.organize.party import getPersonForUser context = removeSecurityProxy(self.context).context principal = getPrincipalForUserId(userId, context) - #auth = component.getUtility(IAuthentication, context=context) - #try: - #principal = auth.getPrincipal(userId) - #except PrincipalLookupError: - #principal = oidc.Principal(userId, dict(name=userId)) if principal is None: raiseValidationError(_('User $userId does not exist', mapping={'userId': userId})) diff --git a/loops/organize/party.py b/loops/organize/party.py index d73a2ab..e440e4c 100644 --- a/loops/organize/party.py +++ b/loops/organize/party.py @@ -33,7 +33,6 @@ from loops.security.common import getCurrentPrincipal from loops.security.interfaces import ISecuritySetter from loops.type import TypeInterfaceSourceList from loops import util -from scopes.web.auth import oidc # register type interfaces - (TODO: use a function for this) @@ -87,7 +86,6 @@ class Person(AdapterBase, BasePerson): setter = ISecuritySetter(self) if userId: principal = self.getPrincipalForUserId(userId) - print('***', userId, principal) if principal is None: return person = getPersonForUser(self.context, principal=principal) @@ -144,14 +142,6 @@ class Person(AdapterBase, BasePerson): def getPrincipalForUserId(self, userId=None): userId = userId or self.userId return getPrincipalForUserId(userId, self.context, self.authentication) - if not userId: - return None - auth = self.authentication - try: - return auth.getPrincipal(userId) - except PrincipalLookupError: - return oidc.Principal(userId, dict(name=userId)) - #return None def getAuthenticationUtility(context): diff --git a/loops/organize/util.py b/loops/organize/util.py index df1d067..f728093 100644 --- a/loops/organize/util.py +++ b/loops/organize/util.py @@ -15,7 +15,6 @@ from zope.traversing.api import getParents from loops.common import adapted from loops.security.common import getCurrentPrincipal from loops.type import getOptionsDict -from scopes.web.auth import oidc defaultAuthPluginId = 'loops' @@ -93,6 +92,7 @@ def getPrincipalForUserId(id, context=None, auth=None): try: return auth.getPrincipal(id) except PrincipalLookupError: + from scopes.web.auth import oidc return oidc.Principal(id, dict(name=id)) #return None diff --git a/loops/server/auth.zcml b/loops/server/auth.zcml new file mode 100644 index 0000000..124c0e6 --- /dev/null +++ b/loops/server/auth.zcml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/loops/server/main.py b/loops/server/main.py index a1b0964..a638ceb 100644 --- a/loops/server/main.py +++ b/loops/server/main.py @@ -15,7 +15,8 @@ import waitress from zope.app.wsgi import config, getWSGIApplication def run(app, config): - oidc.startup() + if config.oidc_provider: + oidc.startup() port = int(config.server_port) print(f'Serving on port {port}.') waitress.serve(app, port=port) diff --git a/loops/tests/config.py b/loops/tests/config.py index b910662..b1005e5 100644 --- a/loops/tests/config.py +++ b/loops/tests/config.py @@ -1,8 +1,8 @@ -# py-scopes/demo/config.py +# loops/tests/config.py from dotenv import load_dotenv from os import getenv -from scopes.server.app import zope_app_factory +from scopes.web.app import zope_app_factory load_dotenv() @@ -18,3 +18,21 @@ dbuser = getenv('DBUSER', 'demo') dbpassword = getenv('DBPASSWORD', 'secret') dbschema = getenv('DBSCHEMA', 'demo') +base_url = 'test://' + +# authentication settings +oidc_provider = '' +oidc_client_id = getenv('OIDC_CLIENT_ID', '12345') +oidc_params = dict( + op_config_url=oidc_provider + '/.well-known/openid-configuration', + op_uris=None, + op_keys=None, + callback_url=getenv('OIDC_CALLBACK_URL', base_url + '/auth/callback'), + client_id=oidc_client_id, + principal_prefix=getenv('OIDC_PRINCIPAL_PREFIX', 'loops.'), + cookie_name=getenv('OIDC_COOKIE_NAME', 'oidc_' + oidc_client_id), + cookie_domain=getenv('OIDC_COOKIE_DOMAIN', None), + cookie_lifetime=getenv('OIDC_COOKIE_LIFETIME', '86400'), + cookie_crypt=getenv('OIDC_COOKIE_CRYPT', None) +) + diff --git a/loops/tests/test_loops.py b/loops/tests/test_loops.py index 52049b6..a529aa0 100755 --- a/loops/tests/test_loops.py +++ b/loops/tests/test_loops.py @@ -1,5 +1,8 @@ # loops.tests.test_loops +import os, sys +sys.path = [os.path.dirname(__file__)] + sys.path + import unittest, doctest import warnings from zope.interface.verify import verifyClass