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
FDSNWS: implement EIDA token authentication and add authentication to routed Clients #1928
Changes from 21 commits
24c0a80
dd5ea55
84c32a7
a80b296
0ff630b
7b785a7
d09a3bd
d25e11a
dd3ac62
acef8ca
715364d
6be50ed
189085c
5be4a95
b94e151
b0bcf65
400c760
0a2f76c
369e0b2
9dbaa2c
2454712
dbe00da
df6471a
bf2133e
b8a831f
6181caa
07148de
fcbdf7f
eb646b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,7 @@ | |
|
||
import obspy | ||
from obspy import UTCDateTime, read_inventory | ||
from obspy.core.compatibility import urlparse | ||
from .header import (DEFAULT_PARAMETERS, DEFAULT_USER_AGENT, FDSNWS, | ||
OPTIONAL_PARAMETERS, PARAMETER_ALIASES, URL_MAPPINGS, | ||
WADL_PARAMETERS_NOT_TO_BE_PARSED, FDSNException, | ||
|
@@ -138,7 +139,8 @@ def _validate_base_url(cls, base_url): | |
|
||
def __init__(self, base_url="IRIS", major_versions=None, user=None, | ||
password=None, user_agent=DEFAULT_USER_AGENT, debug=False, | ||
timeout=120, service_mappings=None, force_redirect=False): | ||
timeout=120, service_mappings=None, force_redirect=False, | ||
eida_token=None): | ||
""" | ||
Initializes an FDSN Web Service client. | ||
|
||
|
@@ -193,10 +195,18 @@ def __init__(self, base_url="IRIS", major_versions=None, user=None, | |
when a redirect is discovered. This is done to improve security. | ||
Settings this flag to ``True`` will force all redirects to be | ||
followed even if credentials are given. | ||
:type eida_token: str | ||
:param eida_token: Token for EIDA authentication mechanism, see | ||
http://geofon.gfz-potsdam.de/waveform/archive/auth/index.php. If a | ||
token is provided, options ``user`` and ``password`` must be given. | ||
This mechanism is only available on select EIDA nodes. The token | ||
can be provided in form of the PGP message as a string, or the | ||
filename of a local file with the PGP message in it. | ||
""" | ||
self.debug = debug | ||
self.user = user | ||
self.timeout = timeout | ||
self._force_redirect = force_redirect | ||
|
||
# Cache for the webservice versions. This makes interactive use of | ||
# the client more convenient. | ||
|
@@ -220,23 +230,7 @@ def __init__(self, base_url="IRIS", major_versions=None, user=None, | |
|
||
self.base_url = base_url | ||
|
||
# Only add the authentication handler if required. | ||
handlers = [] | ||
if user is not None and password is not None: | ||
# Create an OpenerDirector for HTTP Digest Authentication | ||
password_mgr = urllib_request.HTTPPasswordMgrWithDefaultRealm() | ||
password_mgr.add_password(None, base_url, user, password) | ||
handlers.append(urllib_request.HTTPDigestAuthHandler(password_mgr)) | ||
|
||
if (user is None and password is None) or force_redirect is True: | ||
# Redirect if no credentials are given or the force_redirect | ||
# flag is True. | ||
handlers.append(CustomRedirectHandler()) | ||
else: | ||
handlers.append(NoRedirectionHandler()) | ||
|
||
# Don't install globally to not mess with other codes. | ||
self._url_opener = urllib_request.build_opener(*handlers) | ||
self._set_opener(user, password) | ||
|
||
self.request_headers = {"User-Agent": user_agent} | ||
# Avoid mutable kwarg. | ||
|
@@ -261,6 +255,128 @@ def __init__(self, base_url="IRIS", major_versions=None, user=None, | |
|
||
self._discover_services() | ||
|
||
# Use EIDA token if provided - this requires setting new url openers. | ||
# | ||
# This can only happen after the services have been discovered as | ||
# the clients needs to know if the fdsnws implementation has support | ||
# for the EIDA token system. | ||
# | ||
# This is a non-standard feature but we support it, given the number | ||
# of EIDA nodes out there. | ||
if eida_token is not None: | ||
# Make sure user/pw are not also given. | ||
if user is not None or password is not None: | ||
msg = ("EIDA authentication token provided, but " | ||
"user and password are also given.") | ||
raise FDSNException(msg) | ||
self.set_eida_token(eida_token) | ||
|
||
@property | ||
def _has_eida_auth(self): | ||
return self.services.get('eida-auth', False) | ||
|
||
def set_credentials(self, user, password): | ||
""" | ||
Set user and password resulting in subsequent web service | ||
requests for waveforms being authenticated for potential access to | ||
restricted data. | ||
|
||
:type user: str | ||
:param user: User name of credentials. | ||
:type password: str | ||
:param password: Password for given user name. | ||
""" | ||
self._set_opener(user, password) | ||
|
||
def set_eida_token(self, token): | ||
""" | ||
Fetch user and password from the server using the provided token, | ||
resulting in subsequent web service requests for waveforms being | ||
authenticated for potential access to restricted data. | ||
This only works for select EIDA nodes and relies on the auth mechanism | ||
described here: | ||
http://geofon.gfz-potsdam.de/waveform/archive/auth/index.php | ||
|
||
:type token: str | ||
:param token: Token for EIDA authentication mechanism, see | ||
http://geofon.gfz-potsdam.de/waveform/archive/auth/index.php. If | ||
the client was initialized with options ``user`` and ``password``, | ||
these settings will be overridden. This mechanism is only available | ||
on select EIDA nodes. The token can be provided in form of the PGP | ||
message as a string, or the filename of a local file with the PGP | ||
message in it. | ||
""" | ||
user, password = self._resolve_eida_token(token) | ||
self.set_credentials(user, password) | ||
|
||
def _set_opener(self, user, password): | ||
# Only add the authentication handler if required. | ||
handlers = [] | ||
if user is not None and password is not None: | ||
# Create an OpenerDirector for HTTP Digest Authentication | ||
password_mgr = urllib_request.HTTPPasswordMgrWithDefaultRealm() | ||
password_mgr.add_password(None, self.base_url, user, password) | ||
handlers.append(urllib_request.HTTPDigestAuthHandler(password_mgr)) | ||
|
||
if (user is None and password is None) or self._force_redirect is True: | ||
# Redirect if no credentials are given or the force_redirect | ||
# flag is True. | ||
handlers.append(CustomRedirectHandler()) | ||
else: | ||
handlers.append(NoRedirectionHandler()) | ||
|
||
# Don't install globally to not mess with other codes. | ||
self._url_opener = urllib_request.build_opener(*handlers) | ||
if self.debug: | ||
print('Installed new opener with handlers: {!s}'.format(handlers)) | ||
|
||
def _resolve_eida_token(self, token): | ||
""" | ||
Use the token to get credentials. | ||
""" | ||
if not self._has_eida_auth: | ||
msg = ("EIDA token authentication requested but service at '{}' " | ||
"does not specify /dataselect/auth in the " | ||
"dataselect/application.wadl.").format(self.base_url) | ||
raise FDSNException(msg) | ||
|
||
token_file = None | ||
# check if there's a local file that matches the provided string | ||
if os.path.isfile(token): | ||
token_file = token | ||
with open(token_file, 'rb') as fh: | ||
token = fh.read().decode() | ||
# sanity check on the token | ||
if not _validate_eida_token(token): | ||
if token_file: | ||
msg = ("Read EIDA token from file '{}' but it does not " | ||
"seem to contain a valid PGP message.").format( | ||
token_file) | ||
else: | ||
msg = ("EIDA token does not seem to be a valid PGP message. " | ||
"If you passed a filename, make sure the file " | ||
"actually exists.") | ||
raise ValueError(msg) | ||
|
||
# force https so that we don't send around tokens unsecurely | ||
url = 'https://{}/fdsnws/dataselect/1/auth'.format( | ||
urlparse(self.base_url).netloc + urlparse(self.base_url).path) | ||
# paranoid: check again that we only send the token to https | ||
if urlparse(url).scheme != "https": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice.. didn't know that one. :-) |
||
msg = 'This should not happen, please file a bug report.' | ||
raise Exception(msg) | ||
|
||
# retrieve user/password using the token | ||
if self.debug: | ||
print('Downloading {} with eida token data in POST'.format(url)) | ||
|
||
# Already does the error checking with fdsnws semantics. | ||
response = self._download(url=url, data=token.encode(), | ||
use_gzip=True, return_string=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, yeah.. didn't use it earlier because this was done pretty early in |
||
|
||
user, password = response.decode().split(':') | ||
return user, password | ||
|
||
def get_events(self, starttime=None, endtime=None, minlatitude=None, | ||
maxlatitude=None, minlongitude=None, maxlongitude=None, | ||
latitude=None, longitude=None, minradius=None, | ||
|
@@ -1372,7 +1488,15 @@ def run(self): | |
raise FDSNException("Timeout while requesting '%s'." % url) | ||
|
||
if "dataselect" in url: | ||
self.services["dataselect"] = WADLParser(wadl).parameters | ||
wadl_parser = WADLParser(wadl) | ||
self.services["dataselect"] = wadl_parser.parameters | ||
# check if EIDA auth endpoint is in wadl | ||
# we need to attach it to the discovered services, as these are | ||
# later loaded from cache and just attaching an attribute to | ||
# this client won't help knowing later if EIDA auth is | ||
# supported at the server. a bit ugly but can't be helped. | ||
if wadl_parser._has_eida_auth: | ||
self.services["eida-auth"] = True | ||
if self.debug is True: | ||
print("Discovered dataselect service") | ||
elif "event" in url and "application.wadl" in url: | ||
|
@@ -1390,7 +1514,6 @@ def run(self): | |
except ValueError: | ||
msg = "Could not parse the catalogs at '%s'." % url | ||
warnings.warn(msg) | ||
|
||
elif "event" in url and "contributors" in url: | ||
try: | ||
self.services["available_event_contributors"] = \ | ||
|
@@ -1776,6 +1899,17 @@ def get_bulk_string(bulk, arguments): | |
return bulk | ||
|
||
|
||
def _validate_eida_token(token): | ||
""" | ||
Just a basic check if the string contains something that looks like a PGP | ||
message | ||
""" | ||
if re.search(pattern='BEGIN PGP MESSAGE', string=token, | ||
flags=re.IGNORECASE): | ||
return True | ||
return False | ||
|
||
|
||
if __name__ == '__main__': | ||
import doctest | ||
doctest.testmod(exclude_empty=True) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
-----BEGIN PGP MESSAGE----- | ||
Version: GnuPG v2.0.9 (GNU/Linux) | ||
|
||
owGbwMvMwMR4USg04vQMEQbG0+5JDOEWH+OrlXITM3OUrBSUSlKLSxzyk4oLKvXy | ||
i9KVdBSUyhJzMlPiS/NKIAqMDIwMdA0MgSjEwMAKjKKUajsZZVgYGJkY2FiZQOYx | ||
cHEKwCx5Lcv+T0W79KhOxauJLEnP7t0768ZTvzk1Z7XbnSSmqvB7/rfsRfOvSrMt | ||
39GjosTgveqB7gHRYz0+jPP36dj073r2s8h6Xvu0+sAtLbfO6C3xNolRDPgQ4tud | ||
m6J8tm573KGFFTG/+uI/hSir56Uav33xN1DBwKPs1pt8H6vFirf81J5Y3eR8wbmm | ||
c9eXV5VeBy/9bYrd+TXsnFkiayjrxViWdm8uUXnj4hzDE5vvTeVZGH/nclVE1v6z | ||
DO3n3STZvjMKsRUxVPKcnTmHcU64qiTX10bRqi1yHzuMs35Gzz0sMpXbTencLduG | ||
hMrp91PMe04c0otcNytOKDFe/uDva3sd9jZKcxSXb3Gs2Rxf2QgA | ||
=VVHX | ||
-----END PGP MESSAGE----- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should read
If a token is provided, options ``user`` and ``password`` must **not** be given.