From bc3aefc7afe38c0924628dc0de8c659429f577f1 Mon Sep 17 00:00:00 2001 From: Emmanuel Nyachoke Date: Thu, 5 Sep 2024 15:06:21 +0300 Subject: [PATCH] OZ-671: Use 'authlib' instead of Flask OIDC for SSO (#5) --- Dockerfile | 2 +- requirements.txt | 3 -- security.py | 102 +++++++++++++++++++++------------------------ superset-init.sh | 16 ++++--- superset_config.py | 37 +++++++++++----- 5 files changed, 82 insertions(+), 78 deletions(-) delete mode 100644 requirements.txt diff --git a/Dockerfile b/Dockerfile index 5168811..4a79ac7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM apache/superset:4.0.1 # Switching to root to install the required packages USER root -RUN pip install itsdangerous==2.0.1 flask-oidc==1.4.0 Flask-OpenID==1.3.0 +RUN pip install authlib # Switching back to using the `superset` user USER superset diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c31261d..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask-oidc==1.4.0 -Flask-OpenID==1.3.0 -itsdangerous==2.0.1 diff --git a/security.py b/security.py index 8a9a9a7..5325fd1 100644 --- a/security.py +++ b/security.py @@ -1,58 +1,50 @@ -from flask import redirect, request -from flask_appbuilder.security.manager import AUTH_OID +from math import log from superset.security import SupersetSecurityManager -from flask_oidc import OpenIDConnect -from flask_appbuilder.security.views import AuthOIDView -from flask_login import login_user -from urllib.parse import quote -from flask_appbuilder.views import ModelView, SimpleFormView, expose import logging -logger = logging.getLogger(__name__) - -class AuthOIDCView(AuthOIDView): - def add_role_if_missing(self, sm, user_id, role_name): - found_role = sm.find_role(role_name) - session = sm.get_session - user = session.query(sm.user_model).get(user_id) - if found_role and found_role not in user.roles: - user.roles += [found_role] - session.commit() - - @expose('/login/', methods=['GET', 'POST']) - def login(self, flag=True): - sm = self.appbuilder.sm - oidc = sm.oid - - - @self.appbuilder.sm.oid.require_login - def handle_login(): - user = sm.auth_user_oid(oidc.user_getfield('email')) - if user is None: - info = oidc.user_getinfo(['preferred_username', 'given_name', 'family_name', 'email','roles']) - user = sm.add_user(info.get('preferred_username'), info.get('given_name'), info.get('family_name'), info.get('email'), sm.find_role('Gamma')) - role_info = oidc.user_getinfo(['roles']) - if role_info is not None: - for role in role_info['roles']: - self.add_role_if_missing(sm, user.id, role) - login_user(user, remember=False) - return redirect(self.appbuilder.get_url_for_index) - - return handle_login() - - @expose('/logout/', methods=['GET', 'POST']) - def logout(self): - - oidc = self.appbuilder.sm.oid - - oidc.logout() - super(AuthOIDCView, self).logout() +from flask_appbuilder.security.views import AuthOAuthView +from flask_appbuilder.baseviews import expose +import time +from flask import ( + redirect, + request +) + +class CustomAuthOAuthView(AuthOAuthView): + + @expose("/logout/") + def logout(self, provider="keycloak", register=None): + provider_obj = self.appbuilder.sm.oauth_remotes[provider] redirect_url = request.url_root.strip('/') + self.appbuilder.get_url_for_login - - return redirect(oidc.client_secrets.get('issuer') + '/protocol/openid-connect/logout?redirect_uri=' + quote(redirect_url)) - -class OIDCSecurityManager(SupersetSecurityManager): - authoidview = AuthOIDCView - def __init__(self,appbuilder): - super(OIDCSecurityManager, self).__init__(appbuilder) - if self.auth_type == AUTH_OID: - self.oid = OpenIDConnect(self.appbuilder.get_app) \ No newline at end of file + url = ("logout?client_id={}&post_logout_redirect_uri={}".format( + provider_obj.client_id, + redirect_url + )) + + ret = super().logout() + time.sleep(1) + + return redirect("{}{}".format(provider_obj.api_base_url, url)) + + +class CustomSecurityManager(SupersetSecurityManager): + # override the logout function + authoauthview = CustomAuthOAuthView + + def oauth_user_info(self, provider, response=None): + logging.debug("Oauth2 provider: {0}.".format(provider)) + if provider == 'keycloak': + # superset_roles: list[str] = ["Admin", "Alpha", "Gamma", "Public", "granter", "sql_lab"] + me = self.appbuilder.sm.oauth_remotes[provider].get('userinfo').json() + roles = ["public", ] + if "roles" in me: + role_prefix = "superset-" + roles = [r[len(role_prefix):].lower() for r in me.get("roles", []) if r.startswith(role_prefix)] + + return { + "username": me.get("preferred_username", ""), + "first_name": me.get("given_name", ""), + "last_name": me.get("family_name", ""), + "email": me.get("email", ""), + "role_keys": roles, + } + return {} \ No newline at end of file diff --git a/superset-init.sh b/superset-init.sh index a94b3df..397e89c 100755 --- a/superset-init.sh +++ b/superset-init.sh @@ -14,9 +14,9 @@ superset db upgrade echo_step "1" "Complete" "Applying DB migrations" # Create an admin user -echo_step "2" "Starting" "Setting up admin user ( $ADMIN_USERNAME / $ADMIN_PASSWORD )" +echo_step "2" "Starting" "Setting up admin user ( admin / $ADMIN_PASSWORD )" superset fab create-admin \ - --username $ADMIN_USERNAME \ + --username admin \ --firstname Superset \ --lastname Admin \ --email admin@superset.com \ @@ -39,11 +39,9 @@ if [ "$SUPERSET_LOAD_EXAMPLES" = "yes" ]; then fi echo_step "4" "Complete" "Loading examples" fi -echo_step "5" "Complete" "Loading datasources" -# superset import-datasources -p /etc/superset/datasources/datasources.yaml -superset import-datasources --recursive --path /etc/superset/datasources -superset import-dashboards --recursive --path /etc/superset/dashboards -echo_step "5" "Complete" "Loading datasources" -echo_step "6" "Complete" "Updating datasources" -superset set_database_uri -d $ANALYTICS_DATASOURCE_NAME -u postgresql://$ANALYTICS_DB_USER:$ANALYTICS_DB_PASSWORD@$ANALYTICS_DB_HOST:5432/$ANALYTICS_DB_NAME +echo_step "5" "Start" "Updating dashboards" +superset import-directory /dashboards -f -o +echo_step "5" "Complete" "Updating dashboards" +echo_step "6" "Start" "Updating datasources" +superset set_database_uri --database_name $ANALYTICS_DATASOURCE_NAME --uri postgresql://$ANALYTICS_DB_USER:$ANALYTICS_DB_PASSWORD@$ANALYTICS_DB_HOST:5432/$ANALYTICS_DB_NAME echo_step "6" "Complete" "Updating datasources" \ No newline at end of file diff --git a/superset_config.py b/superset_config.py index 50f4bbe..e1dd294 100644 --- a/superset_config.py +++ b/superset_config.py @@ -4,9 +4,6 @@ from cachelib import RedisCache from cachelib.file import FileSystemCache -from flask_appbuilder.security.manager import AUTH_OID -from security import OIDCSecurityManager - logger = logging.getLogger() def password_from_env(url): @@ -99,14 +96,34 @@ def __call__(self, environ, start_response): ADDITIONAL_MIDDLEWARE = [ReverseProxied, ] -AUTH_TYPE = AUTH_OID -OIDC_CLIENT_SECRETS = '/etc/superset/client_secret.json' -OIDC_ID_TOKEN_COOKIE_SECURE = False -OIDC_REQUIRE_VERIFIED_EMAIL = False -AUTH_USER_REGISTRATION = True -AUTH_USER_REGISTRATION_ROLE = 'Gamma' -CUSTOM_SECURITY_MANAGER = OIDCSecurityManager ENABLE_PROXY_FIX = True # Enable the security manager API. FAB_ADD_SECURITY_API = True + +if os.getenv("ENABLE_OAUTH") == "true": + from flask_appbuilder.security.manager import AUTH_OAUTH + from security import CustomSecurityManager + AUTH_ROLES_SYNC_AT_LOGIN = True + AUTH_USER_REGISTRATION = True + AUTH_USER_REGISTRATION_ROLE = "Gamma" + CUSTOM_SECURITY_MANAGER = CustomSecurityManager + LOGOUT_REDIRECT_URL = os.environ.get("SUPERSET_URL") + AUTH_TYPE = AUTH_OAUTH + OAUTH_PROVIDERS = [ + { + 'name': 'keycloak', + 'token_key': 'access_token', # Name of the token in the response of access_token_url + 'icon': 'fa-key', # Icon for the provider + 'remote_app': { + 'client_id': os.environ.get("SUPERSET_CLIENT_ID","superset"), # Client Id (Identify Superset application) + 'client_secret': os.environ.get("SUPERSET_CLIENT_SECRET"), # Secret for this Client Id (Identify Superset application) + 'api_base_url': os.environ.get("ISSUER_URL").rstrip('/') + "/protocol/openid-connect/", + 'client_kwargs': { + 'scope': 'openid profile email', + }, + 'logout_redirect_uri': os.environ.get("SUPERSET_URL"), + 'server_metadata_url': os.environ.get("ISSUER_URL").rstrip('/') + '/.well-known/openid-configuration', # URL to get metadata from + } + } + ]