"""Superset configuration for Petrolyst (ATOMA fuel monitoring). Customization only, and no secrets: those are read from /run/secrets/, which stacks/fuel-monitoring/compose.yaml mounts from /opt/petrolyst/secrets/. Baked into the image at /app/pythonpath/, which is on PYTHONPATH in apache/superset:6.1.0, so Superset loads it without SUPERSET_CONFIG_PATH. """ import os import pathlib from copy import deepcopy from flask_appbuilder.security.manager import AUTH_OAUTH from sqlalchemy.engine.url import URL from petrolyst_security import PetrolystSecurityManager from superset import config as superset_defaults def _read_secret(name: str, env_var: str | None = None) -> str: """Prefer /run/secrets/; fall back to an env var for local dev. Uses rstrip('\r\n') (not strip()) so passwords or keys that legitimately contain leading/trailing spaces are not silently mutated. Raises on empty values so boot fails loudly instead of accepting a blank secret. """ path = pathlib.Path(f"/run/secrets/{name}") if path.is_file(): value = path.read_text().rstrip("\r\n") else: value = os.environ[env_var or name.upper()] if not value: raise RuntimeError(f"secret {name!r} must not be empty") return value SECRET_KEY = _read_secret("superset_secret_key", "SUPERSET_SECRET_KEY") SQLALCHEMY_DATABASE_URI = URL.create( "postgresql+psycopg2", username="superset", password=_read_secret("superset_db_password", "SUPERSET_DB_PASSWORD"), host=os.environ.get("SUPERSET_DB_HOST", "superset-db"), port=5432, database="superset", ).render_as_string(hide_password=False) # --- Sign-in through Keycloak (client "petrolyst") ---------------------------- # Users and roles are managed in Keycloak; there is no local admin account. # Break-glass access: `superset fab ...` inside the container on the server. KEYCLOAK_REALM_URL = os.environ.get( "KEYCLOAK_REALM_URL", "https://iam.thedigitalbridge.io/realms/thedigitalbridge" ).rstrip("/") AUTH_TYPE = AUTH_OAUTH CUSTOM_SECURITY_MANAGER = PetrolystSecurityManager OAUTH_PROVIDERS = [ { "name": "keycloak", "icon": "fa-key", "token_key": "access_token", "remote_app": { "client_id": os.environ.get("KEYCLOAK_CLIENT_ID", "petrolyst"), "client_secret": _read_secret( "keycloak_petrolyst_client_secret", "KEYCLOAK_CLIENT_SECRET" ), "server_metadata_url": f"{KEYCLOAK_REALM_URL}/.well-known/openid-configuration", "api_base_url": f"{KEYCLOAK_REALM_URL}/protocol/openid-connect/", "client_kwargs": { "scope": "openid email profile", "code_challenge_method": "S256", }, }, } ] # Keys are Keycloak client roles of "petrolyst" (claim petrolyst_roles). AUTH_ROLES_MAPPING = { "superset-admin": ["Admin"], "superset-editor": ["Alpha"], "superset-viewer": ["Gamma"], } AUTH_ROLES_SYNC_AT_LOGIN = True AUTH_USER_REGISTRATION = True # Public has no permissions: a Keycloak user without a Superset role signs in # and sees nothing. AUTH_USER_REGISTRATION_ROLE = "Public" # Viewers (Gamma) see a dashboard once it lists their role. FEATURE_FLAGS = {"DASHBOARD_RBAC": True} # --- Behind Traefik ----------------------------------------------------------- ENABLE_PROXY_FIX = True PREFERRED_URL_SCHEME = "https" SESSION_COOKIE_SECURE = True SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = "Lax" TALISMAN_CONFIG = deepcopy(superset_defaults.TALISMAN_CONFIG) TALISMAN_CONFIG["session_cookie_secure"] = True # --- Redis: caches and rate limits (no Celery, so no results backend) --------- REDIS_URL = os.environ.get("REDIS_URL", "redis://redis:6379") CACHE_CONFIG = { "CACHE_TYPE": "RedisCache", "CACHE_DEFAULT_TIMEOUT": 300, "CACHE_KEY_PREFIX": "superset_cache_", "CACHE_REDIS_URL": f"{REDIS_URL}/0", } DATA_CACHE_CONFIG = { "CACHE_TYPE": "RedisCache", "CACHE_DEFAULT_TIMEOUT": 3600, "CACHE_KEY_PREFIX": "superset_data_", "CACHE_REDIS_URL": f"{REDIS_URL}/0", } FILTER_STATE_CACHE_CONFIG = { "CACHE_TYPE": "RedisCache", "CACHE_DEFAULT_TIMEOUT": 86400, "CACHE_KEY_PREFIX": "superset_filter_", "CACHE_REDIS_URL": f"{REDIS_URL}/0", } EXPLORE_FORM_DATA_CACHE_CONFIG = { "CACHE_TYPE": "RedisCache", "CACHE_DEFAULT_TIMEOUT": 86400, "CACHE_KEY_PREFIX": "superset_explore_", "CACHE_REDIS_URL": f"{REDIS_URL}/0", } RATELIMIT_STORAGE_URI = f"{REDIS_URL}/1" # --- Petrolyst look and feel -------------------------------------------------- # Values from petrolyst-ui/design/brand/07_UI_Tokens/petrolyst_tokens.css. # Assets live in assets/branding/ and are served from /static/assets/branding/. BRANDING = "/static/assets/branding" APP_NAME = "Petrolyst" APP_ICON = f"{BRANDING}/petrolyst_logo_standard_fullcolor_no_tagline.svg" FAVICONS = [ {"href": f"{BRANDING}/favicon.svg", "type": "image/svg+xml"}, {"href": f"{BRANDING}/favicon.ico", "type": "image/x-icon"}, {"href": f"{BRANDING}/petrolyst_icon_180.png", "rel": "apple-touch-icon"}, ] THEME_DEFAULT = deepcopy(superset_defaults.THEME_DEFAULT) THEME_DEFAULT["token"].update( { "brandAppName": APP_NAME, "brandLogoAlt": "Petrolyst", "brandLogoUrl": APP_ICON, "brandLogoHref": "/", # The standard logo is stacked (mark over wordmark); 40px keeps the Y # legible and, with the margin, the 60px navbar height. "brandLogoHeight": "40px", "brandLogoMargin": "10px 0", # Action teal, not the raw brand teal #009688: the brand guide keeps # raw teal off small text and controls. "colorPrimary": "#00695C", "colorLink": "#00695C", "colorSuccess": "#0F7B6C", "colorWarning": "#B45309", "colorError": "#B3261E", "colorInfo": "#2563EB", "colorTextBase": "#0B1D33", "colorBgLayout": "#F8FAFC", "colorBorder": "#E2E8F0", "borderRadius": 8, "fontFamily": "Inter, system-ui, sans-serif", } ) THEME_DARK = deepcopy(THEME_DEFAULT) THEME_DARK["algorithm"] = "dark" THEME_DARK["token"].update( { "brandLogoUrl": f"{BRANDING}/petrolyst_logo_standard_white_no_tagline.svg", "colorPrimary": "#35C5B6", "colorLink": "#35C5B6", "colorSuccess": "#35C5B6", "colorWarning": "#F59E0B", "colorError": "#FF6B6B", "colorInfo": "#60A5FA", "colorTextBase": "#F8FAFC", "colorBgBase": "#071425", "colorBgLayout": "#071425", "colorBgContainer": "#12263D", "colorBorder": "#526D82", "colorEditorSelection": superset_defaults.THEME_DARK["token"][ "colorEditorSelection" ], } ) # Chart colours. Validated with the dataviz skill's validator on both chart # surfaces (#FFFFFF light, #12263D dark): all slots in both lightness bands and # >= 3:1; worst adjacent CVD dE 13.7, normal-vision dE 26.2; the first three # slots also pass all-pairs (scatter, maps). Brand teal leads; brand cyan # #00B6E6 is snapped to #0097C4 because it is too light for the dark surface # and below 3:1 on white. The other hues come from the validated reference set. # Status meaning (warning, critical) never rides on these colours. EXTRA_CATEGORICAL_COLOR_SCHEMES = [ { "id": "petrolyst", "description": "Petrolyst categorical palette, brand teal first", "label": "Petrolyst", "isDefault": True, "colors": [ "#009688", # brand teal "#d95926", # orange "#9085e9", # violet "#c98500", # amber "#0097C4", # brand cyan, snapped "#e34948", # red "#3987e5", # blue "#d55181", # magenta ], } ] # One hue, light to dark; the lightest step means "near zero" and may recede. EXTRA_SEQUENTIAL_COLOR_SCHEMES = [ { "id": "petrolyst_teal", "description": "Petrolyst teal, light to dark", "label": "Petrolyst Teal", "isDiverging": False, "isDefault": True, "colors": [ "#E6F4F2", "#B3DED8", "#7FC8BE", "#4CB2A4", "#009688", "#00695C", "#004D44", ], }, { "id": "petrolyst_navy", "description": "Petrolyst navy, light to dark", "label": "Petrolyst Navy", "isDiverging": False, "isDefault": False, "colors": [ "#E8EEF5", "#C3D1E2", "#8FA7C4", "#5C7DA6", "#2F5585", "#183A63", "#0B1D33", ], }, ]