Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
allows to configure plugins for user authentication/authorization.
Includes methods to authenticate based on OIDC token (see issue galaxyproject/galaxy#15526)
- Loading branch information
1 parent
63332c6
commit f2d42ac
Showing
15 changed files
with
275 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
from abc import ABC | ||
|
||
import inspect | ||
|
||
|
||
class UserAuthManager(ABC): | ||
""" | ||
Authorization/Authentication manager. | ||
""" | ||
|
||
def __init__(self, config): | ||
self._authorization_methods = [] | ||
self._authentication_methods = [] | ||
|
||
try: | ||
user_auth = config.get("user_auth", None) | ||
if not user_auth: | ||
return | ||
authentications = user_auth.pop("authentication", []) | ||
authorizations = user_auth.pop("authorization", []) | ||
|
||
for authorization in authorizations: | ||
authorization.update(user_auth) | ||
obj = get_object("pulsar.user_auth.methods." + authorization["type"], "auth_type", | ||
authorization["type"]) | ||
self._authorization_methods.append(obj(authorization)) | ||
|
||
for authentication in authentications: | ||
authentication.update(user_auth) | ||
obj = get_object("pulsar.user_auth.methods." + authentication["type"], "auth_type", | ||
authentication["type"]) | ||
self._authentication_methods.append(obj(authentication)) | ||
except Exception as e: | ||
raise Exception("cannot read auth configuration") from e | ||
|
||
def authorize(self, job_id, job_directory): | ||
authentication_info = self.__authenticate(job_id, job_directory) | ||
|
||
if len(self._authorization_methods) == 0: | ||
return True | ||
for method in self._authorization_methods: | ||
res = method.authorize(authentication_info) | ||
if res: | ||
return True | ||
|
||
raise Exception("Could not authorize job execution on remote resource") | ||
|
||
def __authenticate(self, job_id, job_directory): | ||
if len(self._authentication_methods) == 0: | ||
return {} | ||
for method in self._authentication_methods: | ||
res = method.authenticate(job_directory) | ||
if res: | ||
return res | ||
|
||
raise Exception("Could not authenticate job %s" % job_id) | ||
|
||
|
||
def get_object(module_name, attribute_name, attribute_value): | ||
module = __import__(module_name) | ||
for comp in module_name.split(".")[1:]: | ||
module = getattr(module, comp) | ||
for _, obj in inspect.getmembers(module): | ||
if inspect.isclass(obj) and hasattr(obj, attribute_name) and getattr(obj, attribute_name) == attribute_value: | ||
return obj | ||
raise Exception("Cannot find object %s with attribute %s=%s " % (module_name, attribute_name, attribute_value)) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
from pulsar.user_auth.methods.interface import AuthMethod | ||
|
||
|
||
class AlwaysAllowAuthMethod(AuthMethod): | ||
""" | ||
Always allow | ||
""" | ||
|
||
def __init__(self, _config): | ||
pass | ||
|
||
auth_type = "allow_all" | ||
|
||
def authorize(self, authentication_info): | ||
return True | ||
|
||
def authenticate(self, job_directory): | ||
return {"username": "anonymous"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from abc import ABC, abstractmethod | ||
|
||
|
||
class AuthMethod(ABC): | ||
""" | ||
Defines the interface to various authentication/authorization methods. | ||
""" | ||
|
||
@abstractmethod | ||
def authorize(self, authentication_info): | ||
raise NotImplementedError("a concrete class should implement this") | ||
|
||
@abstractmethod | ||
def authenticate(self, job_directory): | ||
raise NotImplementedError("a concrete class should implement this") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import requests | ||
import base64 | ||
import json | ||
import jwt | ||
import re | ||
from cryptography.hazmat.backends import default_backend | ||
from cryptography.x509 import load_der_x509_certificate | ||
|
||
from pulsar.user_auth.methods.interface import AuthMethod | ||
|
||
import logging | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
def get_token(job_directory, provider): | ||
log.debug("Getting OIDC token for provider " + provider + " from Galaxy") | ||
endpoint = job_directory.load_metadata("launch_config")["token_endpoint"] | ||
endpoint = endpoint + "&provider=" + provider | ||
r = requests.get(url=endpoint) | ||
return r.text | ||
|
||
|
||
class OIDCAuth(AuthMethod): | ||
""" | ||
Authorization based on OIDC tokens | ||
""" | ||
auth_type = "oidc" | ||
|
||
def __init__(self, config): | ||
try: | ||
self._provider = config["oidc_provider"] | ||
self._jwks_url = config["oidc_jwks_url"] | ||
self._username_in_token = config["oidc_username_in_token"] | ||
self._username_template = config["oidc_username_template"] | ||
|
||
except Exception as e: | ||
raise Exception("cannot read OIDCAuth configuration") from e | ||
|
||
def _verify_token(self, token): | ||
try: | ||
# Obtain appropriate cert from JWK URI | ||
key_set = requests.get(self._jwks_url, timeout=5) | ||
encoded_header, rest = token.split('.', 1) | ||
headerobj = json.loads(base64.b64decode(encoded_header + '==').decode('utf8')) | ||
key_id = headerobj['kid'] | ||
for key in key_set.json()['keys']: | ||
if key['kid'] == key_id: | ||
x5c = key['x5c'][0] | ||
break | ||
else: | ||
raise jwt.DecodeError('Cannot find kid ' + key_id) | ||
cert = load_der_x509_certificate(base64.b64decode(x5c), default_backend()) | ||
# Decode token (exp date is checked automatically) | ||
decoded_token = jwt.decode( | ||
token, | ||
key=cert.public_key(), | ||
algorithms=['RS256'], | ||
options={'exp': True, 'verify_aud': False} | ||
) | ||
return decoded_token | ||
except Exception as error: | ||
raise Exception("Error verifying jwt token") from error | ||
|
||
def authorize(self, authentication_info): | ||
raise NotImplementedError("authorization not implemented for this class") | ||
|
||
def authenticate(self, job_directory): | ||
token = get_token(job_directory, self._provider) | ||
|
||
decoded_token = self._verify_token(token) | ||
user = decoded_token[self._username_in_token] | ||
user = re.match(self._username_template, user).group(0) | ||
return {"username": user} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from pulsar.user_auth.methods.interface import AuthMethod | ||
|
||
|
||
class UserListAuth(AuthMethod): | ||
""" | ||
Defines authorization user by username | ||
""" | ||
|
||
def __init__(self, config): | ||
try: | ||
self._allowed_users = config["userlist_allowed_users"] | ||
except Exception as e: | ||
raise Exception("cannot read UsernameAuth configuration") from e | ||
|
||
auth_type = "userlist" | ||
|
||
def authorize(self, authentication_info): | ||
return authentication_info["username"] in self._allowed_users | ||
|
||
def authenticate(self, job_directory): | ||
raise NotImplementedError("authentication not implemented for this class") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.