Skip to content
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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker-compose-image-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ x-superset-volumes:
&superset-volumes # /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
- ./docker:/app/docker
- superset_home:/app/superset_home
- ./docker/custom_assets:/app/superset/static/assets/custom_assets

services:
redis:
Expand Down
1 change: 1 addition & 0 deletions docker-compose-non-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ x-superset-volumes:
&superset-volumes # /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
- ./docker:/app/docker
- superset_home:/app/superset_home
- ./docker/custom_assets:/app/superset/static/assets/custom_assets

x-common-build: &common-build
context: .
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ x-superset-volumes: &superset-volumes
- ./superset-frontend:/app/superset-frontend
- superset_home:/app/superset_home
- ./tests:/app/tests

- ./docker/custom_assets:/app/superset/static/assets/custom_assets
x-common-build: &common-build
context: .
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
Expand Down
11 changes: 11 additions & 0 deletions docker/.env
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,14 @@ ENABLE_PLAYWRIGHT=false
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
BUILD_SUPERSET_FRONTEND_IN_DOCKER=true
SUPERSET_LOG_LEVEL=info

# Keycloak configurations
AUTH_USER_REGISTRATION_ROLE=Public

# Encryption key
SECRET_KEY=6iiO6nBM8xO8U/dl8emvWeO1QFjzs+ljKhxFckE6GoQ
ENCRYPTION_KEY=gF83nvQFwuRYIeQ98OMYf74q45dk9mF+eddFuj4E45E

# Styles
APP_ICON = "/static/assets/custom_assets/cedia-logo-2025.png"
APP_NAME = "CMI"
Binary file added docker/custom_assets/cedia-logo-2025.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions docker/nginx/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ http {
proxy_http_version 1.1;
port_in_redirect off;
proxy_connect_timeout 300;

# Increase buffer sizes to allow for big upstream headers.
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
}
}
1 change: 1 addition & 0 deletions docker/pythonpath_dev/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
!.gitignore
!superset_config.py
!superset_config_local.example
!keycloak_security_manager.py
93 changes: 93 additions & 0 deletions docker/pythonpath_dev/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)
30 changes: 30 additions & 0 deletions docker/pythonpath_dev/superset_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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")
Comment on lines +127 to +128
Copy link

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

Tell me more

Security Issue: Hardcoded Secret Key and Encryption Key

The SECRET_KEY and ENCRYPTION_KEY are hardcoded with default values in the superset_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.


# ------------------------------------------------------------------------------
# Keycloak (OpenID Connect) authentication config and other settings...
# ------------------------------------------------------------------------------
AUTH_TYPE = AUTH_OID
OIDC_CLIENT_SECRETS = "/app/docker/pythonpath_dev/client_secret.json"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-configurable OIDC Client Secrets Path category Functionality

Tell me more
What is the issue?

Hardcoded path to client secrets file could cause authentication failures if the file location changes or in different environments.

Why this matters

A non-configurable secrets file location reduces deployment flexibility and could break authentication if the file is stored elsewhere.

Suggested change ∙ Feature Preview

Make 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")
2 changes: 2 additions & 0 deletions requirements/development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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