Compare commits
10 commits
5676194e56
...
c6155a40e5
| Author | SHA1 | Date | |
|---|---|---|---|
| c6155a40e5 | |||
| b457e3ba37 | |||
|
|
ba5cba6719 | ||
| 5e7796c1c7 | |||
| 4813b905d0 | |||
|
|
d05c52c778 | ||
| e14f2361e8 | |||
| aa4b844b86 | |||
| b49f8f05cc | |||
| 1e7d99d58a |
14 changed files with 129 additions and 9 deletions
|
|
@ -24,6 +24,9 @@ dbpassword = getenv('DBPASSWORD', 'secret')
|
||||||
dbschema = getenv('DBSCHEMA', 'demo')
|
dbschema = getenv('DBSCHEMA', 'demo')
|
||||||
|
|
||||||
# OpenID Connect (OIDC, e.g. via zitadel) authentication settings
|
# OpenID Connect (OIDC, e.g. via zitadel) authentication settings
|
||||||
|
authentication_method = 'cookie' # 'legacy'|'select'|'oidc'|'cookie'
|
||||||
|
|
||||||
|
# OpenID Connect (OIDC) authentication settings
|
||||||
oidc_provider = getenv('OIDC_PROVIDER', '') #'https://instance1-abcdef.zitadel.cloud')
|
oidc_provider = getenv('OIDC_PROVIDER', '') #'https://instance1-abcdef.zitadel.cloud')
|
||||||
oidc_client_id = getenv('OIDC_CLIENT_ID', '12345')
|
oidc_client_id = getenv('OIDC_CLIENT_ID', '12345')
|
||||||
oidc_params = dict(
|
oidc_params = dict(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,27 @@
|
||||||
<configure xmlns="http://namespaces.zope.org/zope">
|
<configure xmlns="http://namespaces.zope.org/zope"
|
||||||
|
xmlns:browser="http://namespaces.zope.org/browser">
|
||||||
|
|
||||||
|
<browser:page
|
||||||
|
for="zope.security.interfaces.IUnauthorized"
|
||||||
|
name="index.html"
|
||||||
|
class="loops.server.auth.Unauthorized"
|
||||||
|
permission="zope.Public" />
|
||||||
|
|
||||||
|
<!--<browser:page
|
||||||
|
for="zope.publisher.interfaces.INotFound"
|
||||||
|
name="index.html"
|
||||||
|
class="loops.browser.common.NotFound"
|
||||||
|
permission="zope.Public" />
|
||||||
|
|
||||||
|
<browser:page
|
||||||
|
for="zope.interface.common.interfaces.IException"
|
||||||
|
name="index.html"
|
||||||
|
class="loops.browser.common.SystemErrorView"
|
||||||
|
permission="zope.Public" />-->
|
||||||
|
|
||||||
|
<!--<browser:defaultSkin name="Loops" />-->
|
||||||
|
|
||||||
|
<include package="loops.server" file="loginform.zcml" />
|
||||||
|
|
||||||
</configure>
|
</configure>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,24 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</metal:login>
|
</metal:login>
|
||||||
|
|
||||||
|
|
||||||
|
<metal:loggedout define-macro="loggedout"
|
||||||
|
i18n:domain="loops"
|
||||||
|
tal:define="principal request/principal/id">
|
||||||
|
<h2 i18n:translate="">Logged out</h2>
|
||||||
|
<div>
|
||||||
|
<p i18n:translate=""
|
||||||
|
tal:condition="python: principal == 'zope.anybody'">Your have been successfully logged out from this application. Please login again if needed or close this browser tab</p>
|
||||||
|
<p i18n:translate=""
|
||||||
|
tal:condition="python: principal != 'zope.anybody'">
|
||||||
|
You are not authorized to perform this action. However, you may login as a
|
||||||
|
different user who is authorized.</p>
|
||||||
|
<div class="row">
|
||||||
|
<button i18n:translate="">
|
||||||
|
<a i18n:translate=""
|
||||||
|
tal:attributes="href view/baseUrl">Log in again</a></button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</metal:loggedout>
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,24 @@ class Logout(object):
|
||||||
return nx
|
return nx
|
||||||
|
|
||||||
|
|
||||||
|
class Loggedout(NodeView):
|
||||||
|
|
||||||
|
template = template
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def macro(self):
|
||||||
|
return self.template.macros['loggedout']
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def item(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def baseUrl(self):
|
||||||
|
url = self.menu.url
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
class Unauthorized(ConceptView):
|
class Unauthorized(ConceptView):
|
||||||
|
|
||||||
isTopLevel = True
|
isTopLevel = True
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,11 @@
|
||||||
class="loops.browser.auth.Logout"
|
class="loops.browser.auth.Logout"
|
||||||
permission="zope.View" />
|
permission="zope.View" />
|
||||||
|
|
||||||
|
<page for="loops.interfaces.INode"
|
||||||
|
name="loggedout.html"
|
||||||
|
class="loops.browser.auth.Loggedout"
|
||||||
|
permission="zope.View" />
|
||||||
|
|
||||||
<!-- see also view/adapter "login.html" in section "query views" -->
|
<!-- see also view/adapter "login.html" in section "query views" -->
|
||||||
|
|
||||||
<!-- macros -->
|
<!-- macros -->
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,8 @@
|
||||||
tal:define="results reportView/results">
|
tal:define="results reportView/results">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="white-space: nowrap"
|
<th style="white-space: nowrap"
|
||||||
|
tal:attributes="class col/cssClass"
|
||||||
tal:repeat="col results/displayedColumns">
|
tal:repeat="col results/displayedColumns">
|
||||||
<span tal:attributes="class col/cssClass">
|
|
||||||
<a title="tooltip_sort_column"
|
<a title="tooltip_sort_column"
|
||||||
tal:define="colName col/name"
|
tal:define="colName col/name"
|
||||||
tal:omit-tag="python:not item.isSortableColumn(tableName, colName)"
|
tal:omit-tag="python:not item.isSortableColumn(tableName, colName)"
|
||||||
|
|
@ -64,7 +64,6 @@
|
||||||
tal:condition="src"
|
tal:condition="src"
|
||||||
tal:attributes="src src" />
|
tal:attributes="src src" />
|
||||||
</a>
|
</a>
|
||||||
</span>
|
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr tal:repeat="row results"
|
<tr tal:repeat="row results"
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -3,7 +3,7 @@ msgstr ""
|
||||||
|
|
||||||
"Project-Id-Version: 3.0.1\n"
|
"Project-Id-Version: 3.0.1\n"
|
||||||
"POT-Creation-Date: 2007-05-22 12:00 CET\n"
|
"POT-Creation-Date: 2007-05-22 12:00 CET\n"
|
||||||
"PO-Revision-Date: 2026-01-23 12:00 CET\n"
|
"PO-Revision-Date: 2026-01-09 12:00 CET\n"
|
||||||
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
|
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
|
||||||
"Language-Team: loops developers <helmutm@cy55.de>\n"
|
"Language-Team: loops developers <helmutm@cy55.de>\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
|
@ -604,9 +604,18 @@ msgstr "Nicht angemeldet"
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr "Anmelden"
|
msgstr "Anmelden"
|
||||||
|
|
||||||
|
msgid "Log in again"
|
||||||
|
msgstr "Erneut anmelden"
|
||||||
|
|
||||||
msgid "Presence"
|
msgid "Presence"
|
||||||
msgstr "Anwesenheit"
|
msgstr "Anwesenheit"
|
||||||
|
|
||||||
|
msgid "Logged out"
|
||||||
|
msgstr "Abgemeldet"
|
||||||
|
|
||||||
|
msgid "Your have been successfully logged out from this application. Please login again if needed or close this browser tab"
|
||||||
|
msgstr "Sie haben sich erfolgreich von dieser Anwendung abgemeldet. Bitte loggen Sie sich bei Bedarf wieder ein oder schließen Sie das Browserfenster"
|
||||||
|
|
||||||
# general
|
# general
|
||||||
|
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
|
|
@ -1494,7 +1503,7 @@ msgid "authentication-method-legacy"
|
||||||
msgstr "Klassisches Login-Verfahren"
|
msgstr "Klassisches Login-Verfahren"
|
||||||
|
|
||||||
msgid "authentication-method-oidc"
|
msgid "authentication-method-oidc"
|
||||||
msgstr "Login mit OpenID Connect"
|
msgstr "Immer mit neuem Login-Verfahren anmelden"
|
||||||
|
|
||||||
msgid "authentication-method-select"
|
msgid "authentication-method-select"
|
||||||
msgstr "Verfahren beim Login auswählen"
|
msgstr "Verfahren beim Login auswählen"
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@ class MediaAsset(MediaAssetFile, ExternalFileAdapter):
|
||||||
data = property(ExternalFileAdapter.getData, setData)
|
data = property(ExternalFileAdapter.getData, setData)
|
||||||
|
|
||||||
def setExternalAddress(self, addr):
|
def setExternalAddress(self, addr):
|
||||||
|
if isinstance(addr, bytes):
|
||||||
|
addr = addr.decode('UTF-8')
|
||||||
ExternalFileAdapter.setExternalAddress(self, addr)
|
ExternalFileAdapter.setExternalAddress(self, addr)
|
||||||
if addr and self.getMimeType().startswith('image/'):
|
if addr and self.getMimeType().startswith('image/'):
|
||||||
self.transform(self.rules)
|
self.transform(self.rules)
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ dayTo = TrackDateField('dayTo', u'End Day',
|
||||||
executionSteps=['query'])
|
executionSteps=['query'])
|
||||||
day = TrackDateField('day', u'Day',
|
day = TrackDateField('day', u'Day',
|
||||||
description=u'The day the work was done.',
|
description=u'The day the work was done.',
|
||||||
cssClass='center',
|
cssClass='left',
|
||||||
executionSteps=['sort', 'output'])
|
executionSteps=['sort', 'output'])
|
||||||
dayStart = TrackDateField('dayStart', u'Start Day',
|
dayStart = TrackDateField('dayStart', u'Start Day',
|
||||||
description=u'The day the unit of work was started.',
|
description=u'The day the unit of work was started.',
|
||||||
|
|
@ -190,15 +190,18 @@ dayEnd = TrackDateField('dayEnd', u'End Day',
|
||||||
executionSteps=['sort', 'output'])
|
executionSteps=['sort', 'output'])
|
||||||
timeStart = TrackTimeField('start', u'Start',
|
timeStart = TrackTimeField('start', u'Start',
|
||||||
description=u'The time the unit of work was started.',
|
description=u'The time the unit of work was started.',
|
||||||
|
cssClass='noprint',
|
||||||
executionSteps=['sort', 'output'])
|
executionSteps=['sort', 'output'])
|
||||||
timeEnd = TrackTimeField('end', u'End',
|
timeEnd = TrackTimeField('end', u'End',
|
||||||
description=u'The time the unit of work was finished.',
|
description=u'The time the unit of work was finished.',
|
||||||
|
cssClass='noprint',
|
||||||
executionSteps=['output'])
|
executionSteps=['output'])
|
||||||
task = TargetField('taskId', u'Task',
|
task = TargetField('taskId', u'Task',
|
||||||
description=u'The task to which work items belong.',
|
description=u'The task to which work items belong.',
|
||||||
executionSteps=['sort', 'output'])
|
executionSteps=['sort', 'output'])
|
||||||
party = PartyQueryField('userName', u'Party',
|
party = PartyQueryField('userName', u'Party',
|
||||||
description=u'The party (usually a person) who did the work.',
|
description=u'The party (usually a person) who did the work.',
|
||||||
|
cssClass='noprint',
|
||||||
fieldType='selection',
|
fieldType='selection',
|
||||||
executionSteps=['sort', 'output', 'query'])
|
executionSteps=['sort', 'output', 'query'])
|
||||||
#partyQuery = TargetField('userName', u'Party',
|
#partyQuery = TargetField('userName', u'Party',
|
||||||
|
|
@ -229,6 +232,7 @@ partyState = PartyStateField('partyState', u'Party State',
|
||||||
executionSteps=['query', 'output'])
|
executionSteps=['query', 'output'])
|
||||||
activity = ActivityField('activity', u'LA',
|
activity = ActivityField('activity', u'LA',
|
||||||
description=u'The activity assigned to the work item.',
|
description=u'The activity assigned to the work item.',
|
||||||
|
cssClass='noprint',
|
||||||
fieldType='selection',
|
fieldType='selection',
|
||||||
executionSteps=['query', 'sort', 'output'])
|
executionSteps=['query', 'sort', 'output'])
|
||||||
# process
|
# process
|
||||||
|
|
|
||||||
|
|
@ -550,6 +550,8 @@ class IndexAttributes(object):
|
||||||
txt = transformToText(actx)
|
txt = transformToText(actx)
|
||||||
if txt is not None:
|
if txt is not None:
|
||||||
return txt
|
return txt
|
||||||
|
if isinstance(actx.contentType, bytes):
|
||||||
|
actx.contentType = actx.contentType.decode('UTF-8')
|
||||||
if not actx.contentType.startswith('text'):
|
if not actx.contentType.startswith('text'):
|
||||||
return u''
|
return u''
|
||||||
data = actx.data
|
data = actx.data
|
||||||
|
|
@ -613,6 +615,8 @@ def transformToText(obj, data=None, contentType=None):
|
||||||
data = obj.data
|
data = obj.data
|
||||||
if contentType is None:
|
if contentType is None:
|
||||||
contentType = obj.contentType
|
contentType = obj.contentType
|
||||||
|
if type(contentType) == bytes:
|
||||||
|
contentType = contentType.decode('UTF-8')
|
||||||
transform = component.queryAdapter(obj, ITextTransform, name=contentType)
|
transform = component.queryAdapter(obj, ITextTransform, name=contentType)
|
||||||
if transform is not None:
|
if transform is not None:
|
||||||
#rfa = component.queryAdapter(IReadFile, obj)
|
#rfa = component.queryAdapter(IReadFile, obj)
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@
|
||||||
# and other authentication and authorization stuff.
|
# and other authentication and authorization stuff.
|
||||||
|
|
||||||
from scopes.web.auth import oidc
|
from scopes.web.auth import oidc
|
||||||
from zope.authentication.interfaces import IAuthentication
|
from zope.authentication.interfaces import IAuthentication, IUnauthenticatedPrincipal
|
||||||
from zope.browserpage import ViewPageTemplateFile
|
from zope.browserpage import ViewPageTemplateFile
|
||||||
|
from zope.cachedescriptors.property import Lazy
|
||||||
from zope.component import provideAdapter, getUtility, provideUtility
|
from zope.component import provideAdapter, getUtility, provideUtility
|
||||||
from zope.interface import implementer, Interface
|
from zope.interface import implementer, Interface
|
||||||
from zope.publisher.interfaces.browser import IBrowserRequest, IBrowserPage
|
from zope.publisher.interfaces.browser import IBrowserRequest, IBrowserPage
|
||||||
|
|
@ -14,6 +15,9 @@ from zope.security.proxy import removeSecurityProxy
|
||||||
|
|
||||||
import config
|
import config
|
||||||
|
|
||||||
|
from logging import getLogger
|
||||||
|
logger = getLogger("loops.server.auth")
|
||||||
|
|
||||||
def registerAuthUtility(config):
|
def registerAuthUtility(config):
|
||||||
baseAuth = getUtility(IAuthentication)
|
baseAuth = getUtility(IAuthentication)
|
||||||
print('*** registerAuthUtility, baseAuth:', baseAuth)
|
print('*** registerAuthUtility, baseAuth:', baseAuth)
|
||||||
|
|
@ -38,6 +42,10 @@ class LoginPage:
|
||||||
return self.authOidc()
|
return self.authOidc()
|
||||||
return self.index()
|
return self.index()
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def isAnonymous(self):
|
||||||
|
return IUnauthenticatedPrincipal.providedBy(self.request.principal)
|
||||||
|
|
||||||
def authOidc(self):
|
def authOidc(self):
|
||||||
oidc.Authenticator(self.request).login()
|
oidc.Authenticator(self.request).login()
|
||||||
return ''
|
return ''
|
||||||
|
|
@ -49,12 +57,33 @@ class LoginPageSelect(LoginPage):
|
||||||
def showSelection(self):
|
def showSelection(self):
|
||||||
return getConfigAuthMethod() == 'cookie'
|
return getConfigAuthMethod() == 'cookie'
|
||||||
|
|
||||||
|
def authMethodCookieString(self):
|
||||||
|
domain = getattr(config, 'authentication_method_cookie_domain', None)
|
||||||
|
return 'document.cookie=`loops_auth_method=${this.value}; path=/; expires=Sun, 31 Jan 2027 12:00:00 UTC%s`' % (domain and f'; domain={domain}' or '')
|
||||||
|
|
||||||
|
|
||||||
|
class Unauthorized(LoginPage):
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
response = self.request.response
|
||||||
|
# make sure that squid does not keep the response in the cache
|
||||||
|
response.setHeader('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT')
|
||||||
|
response.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')
|
||||||
|
response.setHeader('Pragma', 'no-cache')
|
||||||
|
logger.warn(f'unauthorized: user={self.request.principal.id}, authMethod={self.authMethod}')
|
||||||
|
if self.isAnonymous:
|
||||||
|
return super(Unauthorized, self).__call__() # open or redirect to login page
|
||||||
|
else:
|
||||||
|
response.setStatus(403)
|
||||||
|
return 'Unauthorized: You are not allowed to access this ressource.'
|
||||||
|
|
||||||
|
|
||||||
def getConfigAuthMethod():
|
def getConfigAuthMethod():
|
||||||
return getattr(config, 'authentication_method', 'legacy')
|
return getattr(config, 'authentication_method', 'legacy')
|
||||||
|
|
||||||
def getAuthMethodCookieValue(request):
|
def getAuthMethodCookieValue(request):
|
||||||
return request.cookies.get('loops_auth_method') or 'legacy'
|
default = getattr(config, 'authentication_method_cookie_default', 'legacy')
|
||||||
|
return request.cookies.get('loops_auth_method') or default
|
||||||
|
|
||||||
|
|
||||||
# OIDC authentication
|
# OIDC authentication
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@
|
||||||
<select name="auth_method"
|
<select name="auth_method"
|
||||||
onchange="document.cookie=`loops_auth_method=${this.value}; path=/; expires=Sun, 31 Jan 2027 12:00:00 UTC`"
|
onchange="document.cookie=`loops_auth_method=${this.value}; path=/; expires=Sun, 31 Jan 2027 12:00:00 UTC`"
|
||||||
tal:define="meth view/authMethod"
|
tal:define="meth view/authMethod"
|
||||||
tal:attributes="value meth">
|
tal:attributes="value meth;
|
||||||
|
onchange view/authMethodCookieString">
|
||||||
<option value="legacy" i18n:translate="authentication-method-legacy"
|
<option value="legacy" i18n:translate="authentication-method-legacy"
|
||||||
tal:attributes="selected python:meth=='legacy'">Legacy</option>
|
tal:attributes="selected python:meth=='legacy'">Legacy</option>
|
||||||
<option value="oidc" i18n:translate="authentication-method-oidc"
|
<option value="oidc" i18n:translate="authentication-method-oidc"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ license = {text = "MIT"}
|
||||||
keywords = ["loops"]
|
keywords = ["loops"]
|
||||||
authors = [{name = "Helmut Merz", email = "helmutm@cy55.de"}]
|
authors = [{name = "Helmut Merz", email = "helmutm@cy55.de"}]
|
||||||
|
|
||||||
|
# zope.publisher: v5.2.1 needed for handling of :records fields,
|
||||||
|
# should be replaced by patched zope.publisher fork (with multipart monkey patch)
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cybertools",
|
"cybertools",
|
||||||
"py-scopes",
|
"py-scopes",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue