allow usage of OIDC authentication (via py-scopes) where appropriate, and provide corresponding views in loops/server/auth.zcml

This commit is contained in:
Helmut Merz 2025-05-05 09:58:38 +02:00
parent 80c83d5c9f
commit 1d264fc54f
9 changed files with 65 additions and 21 deletions

View file

@ -24,6 +24,8 @@
<include package="cyberapps.ccmkg" /> <include package="cyberapps.ccmkg" />
<include package="cyberapps.knowledge" />--> <include package="cyberapps.knowledge" />-->
<include package="loops.server" file="auth.zcml" />
<!-- Override registrations --> <!-- Override registrations -->
<includeOverrides package="loops" file="overrides.zcml" /> <includeOverrides package="loops" file="overrides.zcml" />
<includeOverrides file="overrides.zcml" /> <includeOverrides file="overrides.zcml" />

View file

@ -9,6 +9,7 @@ server_id = getenv('SERVER_ID')
zope_conf = getenv('ZOPE_CONF', 'zope.conf') zope_conf = getenv('ZOPE_CONF', 'zope.conf')
server_port = getenv('SERVER_PORT', server_port = getenv('SERVER_PORT',
server_id and getenv(f'SERVER_PORT_{server_id}')) or '8080' 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')) shell_pw = (getenv('SHELL_PW', 'dummy'))
loops_path = (getenv('LOOPS_PATH', 'loops/demo')) loops_path = (getenv('LOOPS_PATH', 'loops/demo'))
@ -20,3 +21,20 @@ dbname = getenv('DBNAME', 'demo')
dbuser = getenv('DBUSER', 'demo') dbuser = getenv('DBUSER', 'demo')
dbpassword = getenv('DBPASSWORD', 'secret') dbpassword = getenv('DBPASSWORD', 'secret')
dbschema = getenv('DBSCHEMA', 'demo') 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)
)

View file

@ -18,7 +18,6 @@ from loops.interfaces import HtmlText
from loops.organize.util import getPrincipalFolder, getPrincipalForUserId from loops.organize.util import getPrincipalFolder, getPrincipalForUserId
from loops import util from loops import util
from loops.util import _ from loops.util import _
from scopes.web.auth import oidc
ANNOTATION_KEY = 'loops.organize.person' ANNOTATION_KEY = 'loops.organize.person'
@ -34,7 +33,7 @@ def raiseValidationError(info):
class UserId(schema.TextLine): 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. TODO: transfer validation to loops.organize.browser.
""" """
@ -44,11 +43,6 @@ class UserId(schema.TextLine):
from loops.organize.party import getPersonForUser from loops.organize.party import getPersonForUser
context = removeSecurityProxy(self.context).context context = removeSecurityProxy(self.context).context
principal = getPrincipalForUserId(userId, 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: if principal is None:
raiseValidationError(_('User $userId does not exist', raiseValidationError(_('User $userId does not exist',
mapping={'userId': userId})) mapping={'userId': userId}))

View file

@ -33,7 +33,6 @@ from loops.security.common import getCurrentPrincipal
from loops.security.interfaces import ISecuritySetter from loops.security.interfaces import ISecuritySetter
from loops.type import TypeInterfaceSourceList from loops.type import TypeInterfaceSourceList
from loops import util from loops import util
from scopes.web.auth import oidc
# register type interfaces - (TODO: use a function for this) # register type interfaces - (TODO: use a function for this)
@ -87,7 +86,6 @@ class Person(AdapterBase, BasePerson):
setter = ISecuritySetter(self) setter = ISecuritySetter(self)
if userId: if userId:
principal = self.getPrincipalForUserId(userId) principal = self.getPrincipalForUserId(userId)
print('***', userId, principal)
if principal is None: if principal is None:
return return
person = getPersonForUser(self.context, principal=principal) person = getPersonForUser(self.context, principal=principal)
@ -144,14 +142,6 @@ class Person(AdapterBase, BasePerson):
def getPrincipalForUserId(self, userId=None): def getPrincipalForUserId(self, userId=None):
userId = userId or self.userId userId = userId or self.userId
return getPrincipalForUserId(userId, self.context, self.authentication) 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): def getAuthenticationUtility(context):

View file

@ -15,7 +15,6 @@ from zope.traversing.api import getParents
from loops.common import adapted from loops.common import adapted
from loops.security.common import getCurrentPrincipal from loops.security.common import getCurrentPrincipal
from loops.type import getOptionsDict from loops.type import getOptionsDict
from scopes.web.auth import oidc
defaultAuthPluginId = 'loops' defaultAuthPluginId = 'loops'
@ -93,6 +92,7 @@ def getPrincipalForUserId(id, context=None, auth=None):
try: try:
return auth.getPrincipal(id) return auth.getPrincipal(id)
except PrincipalLookupError: except PrincipalLookupError:
from scopes.web.auth import oidc
return oidc.Principal(id, dict(name=id)) return oidc.Principal(id, dict(name=id))
#return None #return None

18
loops/server/auth.zcml Normal file
View file

@ -0,0 +1,18 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser">
<browser:page
for="zope.interface.Interface"
name="auth_login"
class="loops.server.auth.LoginView"
permission="zope.Public" />
<browser:page
for="zope.interface.Interface"
name="auth_callback"
class="loops.server.auth.CallbackView"
permission="zope.Public" />
</configure>

View file

@ -15,6 +15,7 @@ import waitress
from zope.app.wsgi import config, getWSGIApplication from zope.app.wsgi import config, getWSGIApplication
def run(app, config): def run(app, config):
if config.oidc_provider:
oidc.startup() oidc.startup()
port = int(config.server_port) port = int(config.server_port)
print(f'Serving on port {port}.') print(f'Serving on port {port}.')

View file

@ -1,8 +1,8 @@
# py-scopes/demo/config.py # loops/tests/config.py
from dotenv import load_dotenv from dotenv import load_dotenv
from os import getenv from os import getenv
from scopes.server.app import zope_app_factory from scopes.web.app import zope_app_factory
load_dotenv() load_dotenv()
@ -18,3 +18,21 @@ dbuser = getenv('DBUSER', 'demo')
dbpassword = getenv('DBPASSWORD', 'secret') dbpassword = getenv('DBPASSWORD', 'secret')
dbschema = getenv('DBSCHEMA', 'demo') 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)
)

View file

@ -1,5 +1,8 @@
# loops.tests.test_loops # loops.tests.test_loops
import os, sys
sys.path = [os.path.dirname(__file__)] + sys.path
import unittest, doctest import unittest, doctest
import warnings import warnings
from zope.interface.verify import verifyClass from zope.interface.verify import verifyClass