[installation] Change to nightly
This commit is contained in:
@ -12,8 +12,6 @@
|
||||
# Email: herrnkind@gempa.de
|
||||
###############################################################################
|
||||
|
||||
from functools import cmp_to_key
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from twisted.cred import portal
|
||||
@ -33,7 +31,7 @@ from . import utils
|
||||
|
||||
|
||||
DBMaxUInt = 18446744073709551615 # 2^64 - 1
|
||||
VERSION = "1.0.3"
|
||||
VERSION = "1.0.4"
|
||||
|
||||
|
||||
###############################################################################
|
||||
@ -806,7 +804,7 @@ class FDSNAvailabilityExtent(_Availability):
|
||||
elif ro.orderBy == ro.VOrderByUpdate:
|
||||
lines.sort(
|
||||
key=lambda x: (
|
||||
x[1].updated().seconds(),
|
||||
x[1].updated().epoch(),
|
||||
x[0].waveformID().networkCode(),
|
||||
x[0].waveformID().stationCode(),
|
||||
x[0].waveformID().locationCode(),
|
||||
@ -821,7 +819,7 @@ class FDSNAvailabilityExtent(_Availability):
|
||||
elif ro.orderBy == ro.VOrderByUpdateDesc:
|
||||
lines.sort(
|
||||
key=lambda x: (
|
||||
-x[1].updated().seconds(),
|
||||
-x[1].updated().epoch(),
|
||||
x[0].waveformID().networkCode(),
|
||||
x[0].waveformID().stationCode(),
|
||||
x[0].waveformID().locationCode(),
|
||||
@ -1349,19 +1347,21 @@ class FDSNAvailabilityQuery(_Availability):
|
||||
if ro.time.start.microseconds() == 0:
|
||||
q += f"AND {_T('end')} >= '{db.timeToString(ro.time.start)}' "
|
||||
else:
|
||||
startTimeStr = db.timeToString(ro.time.start)
|
||||
q += (
|
||||
"AND ({0} > '{1}' OR ("
|
||||
f"{_T('end')} = '{db.timeToString(ro.time.start)}' AND "
|
||||
f"AND ({_T('end')} > '{startTimeStr}' OR ("
|
||||
f"{_T('end')} = '{startTimeStr}' AND "
|
||||
f"end_ms >= {ro.time.start.microseconds()})) "
|
||||
)
|
||||
if ro.time.end is not None:
|
||||
if ro.time.end.microseconds() == 0:
|
||||
q += f"AND {_T('start')} < '{db.timeToString(ro.time.end)}' "
|
||||
else:
|
||||
endTimeStr = db.timeToString(ro.time.end)
|
||||
q += (
|
||||
"AND ({0} < '{1}' OR ("
|
||||
f"{_T('start')} = '{db.timeToString(ro.time.end)}' AND "
|
||||
"start_ms < {ro.time.end.microseconds()})) "
|
||||
f"AND ({_T('start')} < '{endTimeStr}' OR ("
|
||||
f"{_T('start')} = '{endTimeStr}' AND "
|
||||
f"start_ms < {ro.time.end.microseconds()})) "
|
||||
)
|
||||
if ro.quality:
|
||||
qualities = "', '".join(ro.quality)
|
||||
|
||||
201
lib/python/seiscomp/fdsnws/jwt.py
Normal file
201
lib/python/seiscomp/fdsnws/jwt.py
Normal file
@ -0,0 +1,201 @@
|
||||
################################################################################
|
||||
# Copyright (C) 2025 GFZ Helmholtz Centre for Geosciences
|
||||
#
|
||||
# JWT-based authentication
|
||||
################################################################################
|
||||
|
||||
import json
|
||||
import jwt
|
||||
import time
|
||||
from twisted.cred.checkers import ICredentialsChecker
|
||||
from twisted.cred.credentials import ICredentials, IAnonymous
|
||||
from twisted.cred.error import LoginFailed
|
||||
from twisted.cred.portal import IRealm, Portal
|
||||
from twisted.web.guard import HTTPAuthSessionWrapper
|
||||
from twisted.web.iweb import ICredentialFactory
|
||||
from twisted.web.resource import IResource
|
||||
from zope.interface import implementer
|
||||
from urllib.request import urlopen
|
||||
from seiscomp import logging
|
||||
|
||||
|
||||
class IBearerToken(ICredentials):
|
||||
"""
|
||||
IBearerToken credentials encapsulate a valid bearer token.
|
||||
|
||||
Parameters:
|
||||
token The bearer token str
|
||||
payload The decoded and valid token payload
|
||||
"""
|
||||
|
||||
|
||||
@implementer(IBearerToken)
|
||||
class JSONWebToken(object):
|
||||
"""The BearerToken credential object that will be passed to a Checker"""
|
||||
|
||||
def __init__(self, token, payload):
|
||||
self.token = token
|
||||
self.payload = payload
|
||||
|
||||
|
||||
@implementer(ICredentialFactory)
|
||||
class _CredentialFactory(object):
|
||||
|
||||
scheme = b"bearer"
|
||||
|
||||
def __init__(self, issuers, audience, algorithms, update_min, update_max):
|
||||
"""
|
||||
issuers list. List of valid issuer URLs
|
||||
audience list. List of valid audiences
|
||||
algorithms list. List of algorithms to check
|
||||
"""
|
||||
self.__issuers = issuers
|
||||
self.__audience = audience
|
||||
self.__algorithms = algorithms
|
||||
self.__update_min = update_min
|
||||
self.__update_max = update_max
|
||||
self.__keys = {}
|
||||
|
||||
for iss in issuers:
|
||||
self.__keys[iss] = (0, {})
|
||||
|
||||
def __update_keys(self, iss):
|
||||
isscfg = json.loads(urlopen(iss.rstrip('/') + '/.well-known/openid-configuration').read())
|
||||
aux = json.loads(urlopen(isscfg["jwks_uri"]).read())
|
||||
keys = {}
|
||||
|
||||
for k in aux['keys']:
|
||||
logging.notice(f"received JWT signing key {k['kid']} from issuer {iss}")
|
||||
keys[k['kid']] = jwt.PyJWK.from_dict(k)
|
||||
|
||||
self.__keys[iss] = (time.time(), keys)
|
||||
return keys
|
||||
|
||||
def __get_signing_key_from_jwt(self, tok):
|
||||
rawtoken = jwt.api_jwt.decode_complete(tok, options={"verify_signature": False})
|
||||
iss = rawtoken["payload"]["iss"]
|
||||
|
||||
try:
|
||||
t, keys = self.__keys[iss]
|
||||
|
||||
except KeyError:
|
||||
raise Exception("No matching keys")
|
||||
|
||||
kid = rawtoken["header"]["kid"]
|
||||
|
||||
if t < time.time() - self.__update_max or \
|
||||
(kid not in keys and t < time.time() - self.__update_min):
|
||||
keys = self.__update_keys(iss)
|
||||
|
||||
try:
|
||||
return keys[kid]
|
||||
|
||||
except KeyError:
|
||||
raise Exception("No matching keys")
|
||||
|
||||
@staticmethod
|
||||
def getChallenge(request):
|
||||
"""
|
||||
Generate the challenge for use in the WWW-Authenticate header
|
||||
|
||||
@param request: The L{twisted.web.http.Request}
|
||||
|
||||
@return: The C{dict} that can be used to generate a WWW-Authenticate
|
||||
header.
|
||||
"""
|
||||
if hasattr(request, "_jwt_error"):
|
||||
return {"error": "invalid_token", "error_description": request._jwt_error}
|
||||
|
||||
return {}
|
||||
|
||||
def decode(self, response, request):
|
||||
"""Returns JSONWebToken (Credentials)
|
||||
|
||||
Create a JSONWebToken object from the given authentication_str.
|
||||
|
||||
response str. The bearer str minus the scheme
|
||||
request obj. The request being processed
|
||||
"""
|
||||
try:
|
||||
signing_key = self.__get_signing_key_from_jwt(response)
|
||||
payload = jwt.decode(
|
||||
response,
|
||||
signing_key.key,
|
||||
audience=self.__audience,
|
||||
algorithms=self.__algorithms,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
request._jwt_error = str(e)
|
||||
raise LoginFailed(request._jwt_error)
|
||||
|
||||
if not payload:
|
||||
request._jwt_error = "No matching keys"
|
||||
raise LoginFailed(request._jwt_error)
|
||||
|
||||
# compatibility with legacy EIDA token
|
||||
if "email" in payload and not "mail" in payload:
|
||||
payload["mail"] = payload["email"]
|
||||
|
||||
payload["blacklisted"] = False
|
||||
|
||||
return JSONWebToken(token=response, payload=payload)
|
||||
|
||||
|
||||
@implementer(ICredentialsChecker)
|
||||
class _CredentialsChecker(object):
|
||||
|
||||
credentialInterfaces = [IBearerToken, IAnonymous]
|
||||
|
||||
def requestAvatarId(self, credentials):
|
||||
"""
|
||||
Portal will call this method if a BearerToken credential is provided.
|
||||
|
||||
Check the given credentials and return a suitable user identifier if
|
||||
they are valid.
|
||||
|
||||
credentials JSONWebToken. A IBearerToken object containing a decoded token
|
||||
"""
|
||||
if IBearerToken.providedBy(credentials):
|
||||
# Avatar ID is the full token payload
|
||||
return credentials.payload
|
||||
|
||||
if IAnonymous.providedBy(credentials):
|
||||
# Anonymous user
|
||||
return None
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@implementer(IRealm)
|
||||
class _Realm(object):
|
||||
# ---------------------------------------------------------------------------
|
||||
def __init__(self, service, args, kwargs):
|
||||
self.__service = service
|
||||
self.__args = args
|
||||
self.__kwargs = kwargs
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
def requestAvatar(self, avatarId, mind, *interfaces):
|
||||
if IResource in interfaces:
|
||||
return (
|
||||
IResource,
|
||||
self.__service(
|
||||
*self.__args,
|
||||
**self.__kwargs,
|
||||
user=avatarId,
|
||||
),
|
||||
lambda: None,
|
||||
)
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class JWT(object):
|
||||
def __init__(self, issuers, audience, algorithms, update_min, update_max):
|
||||
self.__cf = _CredentialFactory(issuers, audience, algorithms, update_min, update_max)
|
||||
|
||||
def getAuthSessionWrapper(self, service, *args, **kwargs):
|
||||
realm = _Realm(service, args, kwargs)
|
||||
portal = Portal(realm, [_CredentialsChecker()])
|
||||
return HTTPAuthSessionWrapper(portal, [self.__cf])
|
||||
@ -52,8 +52,10 @@ class Tracker:
|
||||
self.__data["userLocation"]["country"] = geoip.country_code_by_addr(userIP)
|
||||
|
||||
if (
|
||||
userName and userName.lower().endswith("@gfz-potsdam.de")
|
||||
) or userIP.startswith("139.17."):
|
||||
(userName and userName.lower().endswith("@gfz-potsdam.de"))
|
||||
or (userName and userName.lower().endswith("@gfz.de"))
|
||||
or userIP.startswith("139.17.")
|
||||
):
|
||||
self.__data["userLocation"]["institution"] = "GFZ"
|
||||
|
||||
# pylint: disable=W0613
|
||||
|
||||
Reference in New Issue
Block a user