Merge remote-tracking branch 'upstream/release_20.05' into dev

This commit is contained in:
Dannon Baker
2020-06-17 16:22:09 -04:00
4 changed files with 58 additions and 50 deletions
+27 -33
View File
@@ -19,11 +19,13 @@ from ..authnz import IdentityProvider
log = logging.getLogger(__name__)
STATE_COOKIE_NAME = 'custos-state'
NONCE_COOKIE_NAME = 'custos-nonce'
KEYCLOAK_BACKENDS = {'custos', 'cilogon', 'keycloak'}
class CustosAuthnz(IdentityProvider):
def __init__(self, provider, oidc_config, oidc_backend_config, idphint=None):
self.config = {'provider': provider.lower()}
provider = provider.lower()
self.config = {'provider': provider}
self.config['verify_ssl'] = oidc_config['VERIFY_SSL']
self.config['url'] = oidc_backend_config['url']
self.config['client_id'] = oidc_backend_config['client_id']
@@ -31,28 +33,19 @@ class CustosAuthnz(IdentityProvider):
if oidc_backend_config.get('credential_url'):
# Keycloak client secret used to get token for custos
self.config['credential_url'] = oidc_backend_config['credential_url']
self._get_custos_credentials()
if provider == 'custos':
self._get_custos_credentials()
self.config['redirect_uri'] = oidc_backend_config['redirect_uri']
self.config['ca_bundle'] = oidc_backend_config.get('ca_bundle', None)
self.config['extra_params'] = {
'kc_idp_hint': oidc_backend_config.get('idphint', 'oidc' if self.config['provider'] == 'custos' else 'cilogon')
'kc_idp_hint': oidc_backend_config.get('idphint', 'oidc' if self.config['provider'] in ['custos', 'keycloak'] else 'cilogon')
}
# Either get OIDC config from well-known config URI or lookup known urls based on provider name and realm
if 'well_known_oidc_config_uri' in oidc_backend_config:
self.config['well_known_oidc_config_uri'] = oidc_backend_config['well_known_oidc_config_uri']
well_known_oidc_config = self._load_well_known_oidc_config(
self.config['well_known_oidc_config_uri'])
self.config['authorization_endpoint'] = well_known_oidc_config['authorization_endpoint']
self.config['token_endpoint'] = well_known_oidc_config['token_endpoint']
self.config['userinfo_endpoint'] = well_known_oidc_config['userinfo_endpoint']
self.config['end_session_endpoint'] = well_known_oidc_config['end_session_endpoint']
elif provider == 'cilogon':
if provider == 'cilogon':
self._load_config_for_cilogon()
elif provider == 'custos':
self._load_config_for_custos()
else:
realm = oidc_backend_config['realm']
self._load_config_for_provider_and_realm(self.config['provider'], realm)
elif provider == 'keycloak':
self._load_config_for_keycloak()
def authenticate(self, trans, idphint=None):
base_authorize_url = self.config['authorization_endpoint']
@@ -226,16 +219,19 @@ class CustosAuthnz(IdentityProvider):
self.config['userinfo_endpoint'] = "https://cilogon.org/oauth2/userinfo"
def _load_config_for_custos(self):
self.config['well_known_oidc_config_uri'] = self._get_well_known_uri_from_url(self.config['provider'])
# Set custos endpoints
clientIdAndSec = self.config['client_id'] + ":" + self.config['client_secret']
eps = requests.get(self.config['url'],
headers={"Authorization": "Basic %s" % util.unicodify(base64.b64encode(util.smart_str(clientIdAndSec)))},
verify=False, params={'client_id': self.config['client_id']})
endpoints = eps.json()
self.config['authorization_endpoint'] = endpoints['authorization_endpoint']
self.config['token_endpoint'] = endpoints['token_endpoint']
self.config['userinfo_endpoint'] = endpoints['userinfo_endpoint']
self.config['end_session_endpoint'] = endpoints['end_session_endpoint']
well_known_oidc_config = eps.json()
self._load_well_known_oidc_config(well_known_oidc_config)
def _load_config_for_keycloak(self):
self.config['well_known_oidc_config_uri'] = self._get_well_known_uri_from_url(self.config['provider'])
well_known_oidc_config = self._fetch_well_known_oidc_config(self.config['well_known_oidc_config_uri'])
self._load_well_known_oidc_config(well_known_oidc_config)
def _get_custos_credentials(self):
clientIdAndSec = self.config['client_id'] + ":" + self.config['client_secret']
@@ -245,25 +241,17 @@ class CustosAuthnz(IdentityProvider):
credentials = creds.json()
self.config['iam_client_secret'] = credentials['iam_client_secret']
def _load_config_for_provider_and_realm(self, provider, realm):
self.config['well_known_oidc_config_uri'] = self._get_well_known_uri_for_provider_and_realm(provider, realm)
well_known_oidc_config = self._load_well_known_oidc_config(self.config['well_known_oidc_config_uri'])
self.config['authorization_endpoint'] = well_known_oidc_config['authorization_endpoint']
self.config['token_endpoint'] = well_known_oidc_config['token_endpoint']
self.config['userinfo_endpoint'] = well_known_oidc_config['userinfo_endpoint']
self.config['end_session_endpoint'] = well_known_oidc_config['end_session_endpoint']
def _get_well_known_uri_for_provider_and_realm(self, provider, realm):
def _get_well_known_uri_from_url(self, provider):
# TODO: Look up this URL from a Python library
if provider == 'custos':
if provider in ['custos', 'keycloak']:
base_url = self.config["url"]
# Remove potential trailing slash to avoid "//realms"
base_url = base_url if base_url[-1] != "/" else base_url[:-1]
return "{}/realms/{}/.well-known/openid-configuration".format(base_url, realm)
return "{}/.well-known/openid-configuration".format(base_url)
else:
raise Exception("Unknown Custos provider name: {}".format(provider))
def _load_well_known_oidc_config(self, well_known_uri):
def _fetch_well_known_oidc_config(self, well_known_uri):
try:
return requests.get(well_known_uri,
verify=self._get_verify_param()).json()
@@ -271,6 +259,12 @@ class CustosAuthnz(IdentityProvider):
log.error("Failed to load well-known OIDC config URI: {}".format(well_known_uri))
raise
def _load_well_known_oidc_config(self, well_known_oidc_config):
self.config['authorization_endpoint'] = well_known_oidc_config['authorization_endpoint']
self.config['token_endpoint'] = well_known_oidc_config['token_endpoint']
self.config['userinfo_endpoint'] = well_known_oidc_config['userinfo_endpoint']
self.config['end_session_endpoint'] = well_known_oidc_config['end_session_endpoint']
def _get_verify_param(self):
"""Return 'ca_bundle' if 'verify_ssl' is true and 'ca_bundle' is configured."""
# in requests_oauthlib, the verify param can either be a boolean or a CA bundle path
+7 -7
View File
@@ -21,7 +21,10 @@ from galaxy.util import (
string_as_bool,
unicodify,
)
from .custos_authnz import CustosAuthnz
from .custos_authnz import (
CustosAuthnz,
KEYCLOAK_BACKENDS,
)
from .psa_authnz import (
BACKENDS_NAME,
on_the_fly_config,
@@ -113,7 +116,7 @@ class AuthnzManager(object):
self.oidc_backends_config[idp] = self._parse_idp_config(child)
self.oidc_backends_implementation[idp] = 'psa'
self.app.config.oidc[idp] = {'icon': self._get_idp_icon(idp)}
elif idp == 'custos' or idp == 'cilogon':
elif idp in KEYCLOAK_BACKENDS:
self.oidc_backends_config[idp] = self._parse_custos_config(child)
self.oidc_backends_implementation[idp] = 'custos'
self.app.config.oidc[idp] = {'icon': self._get_idp_icon(idp)}
@@ -149,12 +152,9 @@ class AuthnzManager(object):
'client_id': config_xml.find('client_id').text,
'client_secret': config_xml.find('client_secret').text,
'redirect_uri': config_xml.find('redirect_uri').text,
'realm': config_xml.find('realm').text,
'enable_idp_logout': asbool(config_xml.findtext('enable_idp_logout', 'false'))}
if config_xml.find('credential_url') is not None:
rtv['credential_url'] = config_xml.find('credential_url').text
if config_xml.find('well_known_oidc_config_uri') is not None:
rtv['well_known_oidc_config_uri'] = config_xml.find('well_known_oidc_config_uri').text
if config_xml.find('idphint') is not None:
rtv['idphint'] = config_xml.find('idphint').text
if config_xml.find('ca_bundle') is not None:
@@ -177,7 +177,7 @@ class AuthnzManager(object):
provider = unified_provider_name
identity_provider_class = self._get_identity_provider_class(self.oidc_backends_implementation[provider])
try:
if (provider == 'cilogon' or provider == 'custos'):
if provider in KEYCLOAK_BACKENDS:
return True, "", identity_provider_class(unified_provider_name, self.oidc_config, self.oidc_backends_config[unified_provider_name], idphint=idphint)
else:
return True, "", identity_provider_class(unified_provider_name, self.oidc_config, self.oidc_backends_config[unified_provider_name])
@@ -273,7 +273,7 @@ class AuthnzManager(object):
success, message, backend = self._get_authnz_backend(provider, idphint=idphint)
if success is False:
return False, message, None
elif (provider == 'cilogon' or provider == 'custos'):
elif provider in KEYCLOAK_BACKENDS:
return True, "Redirecting to the `{}` identity provider for authentication".format(provider), backend.authenticate(trans, idphint)
return True, "Redirecting to the `{}` identity provider for authentication".format(provider), backend.authenticate(trans)
except Exception:
@@ -42,7 +42,7 @@ Some examples:
- using localhost to authenticate with Google:
http://localhost:8080/authnz/google/callback
- using localhost to authenticat with Globus:
- using localhost to authenticate with Globus:
http://localhost:8080/authnz/globus/callback
- using an instance hosted at `https://usegalaxy.org` with Google:
@@ -90,8 +90,6 @@ Please mind `http` and `https`.
<client_id> ... </client_id>
<client_secret> ... </client_secret>
<redirect_uri>http://localhost:8000/authnz/custos/callback</redirect_uri>
<realm> ... </realm>
<!-- (Optional) Trusted CA certificate file or directory to use when verify Custos authorization server.
See http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification for more information. -->
<!-- <ca_bundle>/path/to/ca_bundle</ca_bundle> -->
@@ -103,6 +101,25 @@ Please mind `http` and `https`.
<!-- <enable_idp_logout>true</enable_idp_logout> -->
<!-- <icon>https://path/to/icon</icon> -->
</provider>
<provider name="Keycloak">
<!-- The URL should include the base keycloak path as well as the realm -->
<url>https://keycloak.example.org/realms/master/</url>
<client_id> ... </client_id>
<client_secret> ... </client_secret>
<redirect_uri>http://localhost:8080/authnz/keycloak/callback</redirect_uri>
<!-- (Optional) Trusted CA certificate file or directory to use when verify Custos authorization server.
See http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification for more information. -->
<!-- <ca_bundle>/path/to/ca_bundle</ca_bundle> -->
<!-- (Optional) Override the default Custos well-known URL to point to a different instance -->
<!-- <well_known_oidc_config_uri>https://.../.well-known/openid-configuration</well_known_oidc_config_uri> -->
<!-- (Optional) Override the default idp hint -->
<!-- <idphint>cilogon</idphint> -->
<!-- (Optional) Enable logging out of the IDP when user logs out of Galaxy -->
<!-- <enable_idp_logout>true</enable_idp_logout> -->
<!-- <icon>https://path/to/icon</icon> -->
</provider>
<!-- Documentation: https://galaxyproject.org/authnz/config/oidc/idps/elixir-aai -->
<provider name="Elixir">
<client_id>...</client_id>
@@ -133,4 +150,5 @@ Please mind `http` and `https`.
-->
<api_url> ... </api_url>
</provider>
</OIDC>
+3 -7
View File
@@ -20,14 +20,11 @@ class CustosAuthnzTestCase(unittest.TestCase):
_get_userinfo_called = False
_raw_token = None
def _get_base_idp_url(self):
def _get_idp_url(self):
# it would be ideal is we can use a URI as the following:
# https://test_base_uri/auth
return 'https://iam.scigap.org/auth'
def _get_idp_url(self):
return "{}/realms/test-realm/.well-known/openid-configuration".format(self._get_base_idp_url())
def setUp(self):
self.orig_requests_get = requests.get
requests.get = self.mockRequest(self._get_idp_url(), {
@@ -39,7 +36,7 @@ class CustosAuthnzTestCase(unittest.TestCase):
self.custos_authnz = custos_authnz.CustosAuthnz('Custos', {
'VERIFY_SSL': True
}, {
'url': self._get_base_idp_url(),
'url': self._get_idp_url(),
'client_id': 'test-client-id',
'client_secret': 'test-client-secret',
'redirect_uri': 'https://test-redirect-uri',
@@ -203,7 +200,6 @@ class CustosAuthnzTestCase(unittest.TestCase):
self.assertEqual(self.custos_authnz.config['client_id'], 'test-client-id')
self.assertEqual(self.custos_authnz.config['client_secret'], 'test-client-secret')
self.assertEqual(self.custos_authnz.config['redirect_uri'], 'https://test-redirect-uri')
self.assertEqual(self.custos_authnz.config['well_known_oidc_config_uri'], self._get_idp_url())
self.assertEqual(self.custos_authnz.config['authorization_endpoint'], 'https://test-auth-endpoint')
self.assertEqual(self.custos_authnz.config['token_endpoint'], 'https://test-token-endpoint')
self.assertEqual(self.custos_authnz.config['userinfo_endpoint'], 'https://test-userinfo-endpoint')
@@ -236,7 +232,7 @@ class CustosAuthnzTestCase(unittest.TestCase):
self.custos_authnz = custos_authnz.CustosAuthnz('Custos', {
'VERIFY_SSL': True
}, {
'url': self._get_base_idp_url(),
'url': self._get_idp_url(),
'client_id': 'test-client-id',
'client_secret': 'test-client-secret',
'redirect_uri': 'http://localhost/auth/callback',