Files
petrolyst-infra/petrolyst-insights/superset_config.py
T
Nasim SadatandClaude Opus 5.5 78a498f1ce Sign-in by customer: realm petrolyst, row rules, guest role (PD-21)
Keycloak import for realm petrolyst: two clients, petrolyst with MFA for
Superset and Traefik, petrolyst-app password only. It lists only
app.petrolyst.com and traefik.petrolyst.com, and no customer names, as this
repository is public. public.v_operator shows each customer's group. Dashboards
overview and site, role Petrolyst Guest, filtered by row. Superset refuses a
user with no mapped role. The realm travels with the release: each release's
compose files name their realm and secret file, and a rollback needs only its
target's secret. Not deployed.

Checks earlier on this content: tests/run.sh 109 of 109 and read-only, dashboards
import twice, column drift 0, compose config on old and new trees, the secret
check on a scratch folder, a rehearsal on a throwaway Keycloak 26.4. Never a real
deploy or rollback; shellcheck not installed today, clean when last run. Codex
inspected once; its fixes are not re-inspected, on the owner's word.

Bands: persistence, Superset. Rules: 1, 2, 3, 6, 14.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:31:14 +01:00

282 lines
10 KiB
Python

"""Superset configuration for Petrolyst (ATOMA fuel monitoring).
Customization only, and no secrets: those are read from /run/secrets/<name>,
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/<name>; 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)
# --- The data: fuel-db ----------------------------------------------------------
# Superset reads fuel-db directly as the read-only role fuel_ro;
# superset-init registers that connection, "Fuel", without a password. Passwords of data
# connections come from secret files at connect time, so none is kept in Superset's own
# database. A connection added in the UI gets no password this way.
_DATABASE_PASSWORD_SECRETS = {"fuel_ro": "fuel_db_ro_password"}
def _database_password(url: URL) -> str | None:
secret = _DATABASE_PASSWORD_SECRETS.get(url.username or "")
return _read_secret(secret) if secret else None
SQLALCHEMY_CUSTOM_PASSWORD_STORE = _database_password
# --- 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/petrolyst"
).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"],
# Customers' users have a role of client "petrolyst-app", not of this one: refused here.
}
AUTH_ROLES_SYNC_AT_LOGIN = True
AUTH_USER_REGISTRATION = True
# Public has no permissions. A Keycloak user without a mapped role never gets
# this far: PetrolystSecurityManager refuses the sign-in and makes no account.
AUTH_USER_REGISTRATION_ROLE = "Public"
# Viewers (Gamma) see a dashboard once it lists their role.
# FILTERBAR_CLOSED_BY_DEFAULT: a dashboard's filter column opens collapsed, a
# narrow strip with an expand button (owner, 29 Oct); ?expand_filters=true
# opens it from a link.
FEATURE_FLAGS = {"DASHBOARD_RBAC": True, "FILTERBAR_CLOSED_BY_DEFAULT": True}
# deck.gl maps: OpenStreetMap raster tiles only. The default list adds Mapbox
# styles, which fail without MAPBOX_API_KEY. The default CSP already allows the
# tile host.
DECKGL_BASE_MAP = [
["tile://https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap"],
]
# --- 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-web/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_wordmark_fullcolor.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 wordmark alone (owner, 29 Oct): in the stacked logo, at 40px, its
# letters were about 5px high. Its own Y carries the brand accent. 24px
# (letters about 20px) and the margin fill the 60px navbar height. On the
# left, 15px more than the navbar's 16px: the P (1.4px inside the image)
# lines up with the dashboard's content at 32px.
"brandLogoHeight": "24px",
"brandLogoMargin": "18px 0 18px 15px",
# 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_wordmark_white.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",
],
},
]