Skip to content

Commit

Permalink
OZ-671: Use 'authlib' instead of Flask OIDC for SSO (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
enyachoke authored Sep 5, 2024
1 parent 4ae79ac commit bc3aefc
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 78 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions requirements.txt

This file was deleted.

102 changes: 47 additions & 55 deletions security.py
Original file line number Diff line number Diff line change
@@ -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)
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 {}
16 changes: 7 additions & 9 deletions superset-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected] \
Expand All @@ -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"
37 changes: 27 additions & 10 deletions superset_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
}
}
]

0 comments on commit bc3aefc

Please sign in to comment.