-
Notifications
You must be signed in to change notification settings - Fork 14.4k
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
feat(auth): integrate Keycloak authentication with SLO and configurable user roles #31821
Changes from all commits
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 |
---|---|---|
|
@@ -21,3 +21,4 @@ | |
!.gitignore | ||
!superset_config.py | ||
!superset_config_local.example | ||
!keycloak_security_manager.py |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
from flask_appbuilder.security.manager import AUTH_OID | ||
from superset.security import SupersetSecurityManager | ||
from flask_oidc import OpenIDConnect | ||
from flask_appbuilder.security.views import AuthOIDView | ||
from flask_login import login_user, logout_user | ||
from urllib.parse import quote | ||
from flask_appbuilder.views import ModelView, expose | ||
from flask import redirect, request, session | ||
from typing import Optional | ||
import logging | ||
import json | ||
|
||
class OIDCSecurityManager(SupersetSecurityManager): | ||
def __init__(self, appbuilder: ModelView) -> None: | ||
super(OIDCSecurityManager, self).__init__(appbuilder) | ||
if self.auth_type == AUTH_OID: | ||
self.oid = OpenIDConnect(self.appbuilder.get_app) | ||
self.authoidview = AuthOIDCView | ||
|
||
class AuthOIDCView(AuthOIDView): | ||
|
||
@expose('/login/', methods=['GET', 'POST']) | ||
def login(self, flag: bool = True) -> Optional[redirect]: | ||
sm = self.appbuilder.sm | ||
oidc = sm.oid | ||
|
||
@self.appbuilder.sm.oid.require_login | ||
def handle_login() -> Optional[redirect]: | ||
user = sm.auth_user_oid(oidc.user_getfield('email')) | ||
if user is None: | ||
info = oidc.user_getinfo(['preferred_username', 'given_name', 'family_name', 'email']) | ||
default_role = self.appbuilder.app.config.get("AUTH_USER_REGISTRATION_ROLE", "Public") | ||
|
||
user = sm.add_user( | ||
info.get('preferred_username'), | ||
info.get('given_name'), | ||
info.get('family_name'), | ||
info.get('email'), | ||
sm.find_role(default_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) -> redirect: | ||
sm = self.appbuilder.sm | ||
oidc = sm.oid | ||
|
||
# Initiate logout from the OIDC provider (Keycloak) | ||
oidc.logout() | ||
# Call the parent logout (which should log out the user locally) | ||
super(AuthOIDCView, self).logout() | ||
|
||
# Capture the redirect URL (typically pointing back to the login page) | ||
redirect_url = request.url_root.strip('/') + self.appbuilder.get_url_for_login | ||
|
||
# Retrieve the id_token from session if available | ||
auth_token = session.get("oidc_auth_token") | ||
id_token_hint = None | ||
if auth_token: | ||
if isinstance(auth_token, dict): | ||
id_token_hint = auth_token.get("id_token") | ||
elif isinstance(auth_token, (str, bytes)): | ||
try: | ||
token_info = json.loads(auth_token) | ||
id_token_hint = token_info.get("id_token") | ||
except Exception as e: | ||
logging.warning("Failed to parse oidc_auth_token: %s", e) | ||
|
||
# Build the logout URL | ||
if id_token_hint: | ||
logout_url = ( | ||
oidc.client_secrets.get('issuer') | ||
+ '/protocol/openid-connect/logout' | ||
+ '?post_logout_redirect_uri=' + quote(redirect_url) | ||
+ '&id_token_hint=' + quote(id_token_hint) | ||
) | ||
else: | ||
logout_url = ( | ||
oidc.client_secrets.get('issuer') | ||
+ '/protocol/openid-connect/logout' | ||
+ '?post_logout_redirect_uri=' + quote(redirect_url) | ||
+ '&client_id=' + quote(oidc.client_secrets.get('client_id')) | ||
) | ||
|
||
# Explicitly clear the entire session to remove all authentication data. | ||
session.clear() | ||
|
||
# Additionally, log out from Flask-Login if needed. | ||
logout_user() | ||
|
||
return redirect(logout_url) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,7 +24,9 @@ | |
import os | ||
|
||
from celery.schedules import crontab | ||
from flask_appbuilder.security.manager import AUTH_OID | ||
from flask_caching.backends.filesystemcache import FileSystemCache | ||
from keycloak_security_manager import OIDCSecurityManager | ||
|
||
logger = logging.getLogger() | ||
|
||
|
@@ -120,3 +122,31 @@ class CeleryConfig: | |
) | ||
except ImportError: | ||
logger.info("Using default Docker config...") | ||
|
||
# Basic secret key | ||
SECRET_KEY = os.getenv("SECRET_KEY", "your-constant-secret-key") | ||
ENCRYPTION_KEY = os.getenv("ENCRYPTION_KEY", "your-constant-encryption-key") | ||
|
||
# ------------------------------------------------------------------------------ | ||
# Keycloak (OpenID Connect) authentication config and other settings... | ||
# ------------------------------------------------------------------------------ | ||
AUTH_TYPE = AUTH_OID | ||
OIDC_CLIENT_SECRETS = "/app/docker/pythonpath_dev/client_secret.json" | ||
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. Non-configurable OIDC Client Secrets Path
Tell me moreWhat is the issue?Hardcoded path to client secrets file could cause authentication failures if the file location changes or in different environments. Why this mattersA non-configurable secrets file location reduces deployment flexibility and could break authentication if the file is stored elsewhere. Suggested change ∙ Feature PreviewMake the client secrets path configurable via environment variable: OIDC_CLIENT_SECRETS = os.getenv("OIDC_CLIENT_SECRETS", "/app/docker/pythonpath_dev/client_secret.json") Chat with Korbit by mentioning @korbit-ai, and give a 👍 or 👎 to help Korbit improve your reviews. |
||
OIDC_ID_TOKEN_COOKIE_SECURE = False | ||
OIDC_OPENID_REALM = "hpc" | ||
OIDC_INTROSPECTION_AUTH_METHOD = "client_secret_post" | ||
CUSTOM_SECURITY_MANAGER = OIDCSecurityManager | ||
|
||
# Disable role sync on every login | ||
AUTH_ROLES_SYNC_AT_LOGIN = False | ||
AUTH_USER_REGISTRATION = True | ||
AUTH_USER_REGISTRATION_ROLE = os.getenv("AUTH_USER_REGISTRATION_ROLE", "Public") | ||
|
||
# ... [rest of the configuration] ... | ||
logger.info("Superset configuration loaded successfully.") | ||
|
||
ENABLE_TEMPLATE_PROCESSING = True | ||
|
||
# Styles | ||
APP_NAME = os.getenv("APP_NAME", "Superset") | ||
APP_ICON = os.getenv("APP_ICON", "/static/assets/custom_assets/cedia-logo-2025.png") |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -883,3 +883,5 @@ zstandard==0.23.0 | |
# via | ||
# -c requirements/base.txt | ||
# flask-compress | ||
flask-oidc==2.2.2 | ||
flask-openid==1.3.1 |
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.
Hardcoded secret and encryption keys found.![category Security](https://camo.githubusercontent.com/2ccb52fbd114c8a6721342c025800d3f8a07dc6bdba508597a04054bdb6f777a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53656375726974792d653131643438)
Tell me more
Security Issue: Hardcoded Secret Key and Encryption Key
The
SECRET_KEY
andENCRYPTION_KEY
are hardcoded with default values in thesuperset_config.py
file. Hardcoding sensitive information like secret keys is a security risk because if the codebase is compromised, an attacker can easily obtain these keys and use them for malicious purposes.To resolve this issue, remove the hardcoded default values and ensure the secret key and encryption key are loaded from environment variables or a secure secrets management system at runtime. Do not commit the actual key values in the codebase.
Chat with Korbit by mentioning @korbit-ai, and give a 👍 or 👎 to help Korbit improve your reviews.