diff --git a/dashboard/__init__.py b/dashboard/__init__.py index 8b5f2670..ef1055da 100644 --- a/dashboard/__init__.py +++ b/dashboard/__init__.py @@ -1,10 +1,3 @@ -import os - -from everett.manager import ConfigManager # type: ignore -from everett.manager import ConfigOSEnv # type: ignore - -# -*- coding: utf-8 -*- - """Mozilla Single Signon Dashboard.""" __author__ = """Andrew Krug""" @@ -13,7 +6,3 @@ __all__ = ["app", "auth", "config", "models", "s3", "utils", "vanity"] - - -def get_config(): - return ConfigManager([ConfigOSEnv()]) diff --git a/dashboard/app.py b/dashboard/app.py index e17735af..e24b43d5 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -25,7 +25,6 @@ from dashboard import oidc_auth from dashboard import config -from dashboard import get_config from dashboard import vanity from dashboard.csp import DASHBOARD_CSP @@ -37,18 +36,19 @@ logging.config.fileConfig("dashboard/logging.ini") -if config.Config(None).settings.DEBUG: +app_config = config.Default() + +if app_config.DEBUG: # Set the log level to DEBUG for all defined loggers for logger_name in logging.root.manager.loggerDict.keys(): logging.getLogger(logger_name).setLevel("DEBUG") app = Flask(__name__) +app.config.from_object(app_config) talisman = Talisman(app, content_security_policy=DASHBOARD_CSP, force_https=False) -app.config.from_object(config.Config(app).settings) - -app_list = CDNTransfer(config.Config(app).settings) +app_list = CDNTransfer(app_config) def session_configure(app: Flask) -> SessionInterface: @@ -90,7 +90,7 @@ def session_configure(app: Flask) -> SessionInterface: # Hack to support serving .svg mimetypes.add_type("image/svg+xml", ".svg") -oidc_config = config.OIDCConfig() +oidc_config = config.OIDC() authentication = oidc_auth.OpenIDConnect(oidc_config) oidc = authentication.get_oidc(app) @@ -104,7 +104,7 @@ def favicon(): @app.route("/") def home(): - if config.Config(app).environment == "local": + if app.config["ENVIRONMENT"] == "local": return redirect("dashboard", code=302) url = request.url.replace("http://", "https://", 1) @@ -213,7 +213,7 @@ def dashboard(): # If an update is found, all gunicorn workers will be reloaded app_list.sync_config() - user = User(session, config.Config(app).settings) + user = User(session, app.config) apps = user.apps(Application(app_list.apps_yml).apps) return render_template("dashboard.html", config=app.config, user=user, apps=apps) @@ -221,7 +221,7 @@ def dashboard(): @app.route("/styleguide/dashboard") def styleguide_dashboard(): - user = FakeUser(config.Config(app).settings) + user = FakeUser(app.config) apps = user.apps(Application(app_list.apps_yml).apps) return render_template("dashboard.html", config=app.config, user=user, apps=apps) @@ -230,7 +230,7 @@ def styleguide_dashboard(): @app.route("/styleguide/notifications") @oidc.oidc_auth("default") def styleguide_notifications(): - user = FakeUser(config.Config(app).settings) + user = FakeUser(app.config) return render_template("notifications.html", config=app.config, user=user) diff --git a/dashboard/config.py b/dashboard/config.py index 9d79633d..4503a66d 100644 --- a/dashboard/config.py +++ b/dashboard/config.py @@ -1,70 +1,67 @@ """Configuration loader for different environments.""" +import os import base64 import datetime -from dashboard import get_config -CONFIG = get_config() - -class Config(object): - def __init__(self, app): - self.app = app - - self.environment = CONFIG("environment", default="local") - self.settings = self._init_env() - - def _init_env(self): - return DefaultConfig() - - -class DefaultConfig(object): +class Default: """Defaults for the configuration objects.""" - DEBUG = bool(CONFIG("debug", namespace="sso-dashboard", parser=bool, default="False")) - TESTING = bool(CONFIG("testing", namespace="sso-dashboard", parser=bool, default="False")) - - CSRF_ENABLED = bool(CONFIG("csrf_enabled", parser=bool, default="True")) - PERMANENT_SESSION = bool(CONFIG("permanent_session", namespace="sso-dashboard", parser=bool, default="True")) - seconds = int(CONFIG("permanent_session_lifetime", namespace="sso-dashboard", default="86400")) - PERMANENT_SESSION_LIFETIME = datetime.timedelta(seconds=seconds) + ENVIRONMENT: str = os.environ.get("ENVIRONMENT", "local") + DEBUG: bool = os.environ.get("SSO-DASHBOARD_DEBUG", "True") == "True" + TESTING: bool = os.environ.get("SSO-DASHBOARD_TESTING", "True") == "True" - SESSION_COOKIE_SAMESITE = CONFIG("session_cookie_samesite", namespace="sso-dashboard", default="lax") - SESSION_COOKIE_HTTPONLY = bool( - CONFIG("session_cookie_httponly", namespace="sso-dashboard", parser=bool, default="True") + CSRF_ENABLED: bool = os.environ.get("SSO-DASHBOARD_CSRF_ENABLED", "True") == "True" + PERMANENT_SESSION: bool = os.environ.get("SSO-DASHBOARD_PERMANENT_SESSION", "True") == "True" + PERMANENT_SESSION_LIFETIME: datetime.timedelta = datetime.timedelta( + seconds=int(os.environ.get("SSO-DASHBOARD_PERMANENT_SESSION_LIFETIME", "86400")) ) - SECRET_KEY = CONFIG("secret_key", namespace="sso-dashboard") - SERVER_NAME = CONFIG("server_name", namespace="sso-dashboard", default="localhost:8000") - SESSION_COOKIE_NAME = SERVER_NAME + "_session" + SESSION_COOKIE_SAMESITE: str = os.environ.get("SSO-DASHBOARD_SESSION_COOKIE_SAMESITE", "lax") + SESSION_COOKIE_HTTPONLY: bool = os.environ.get("SSO-DASHBOARD_SESSION_COOKIE_HTTPONLY", "True") == "True" - S3_BUCKET = CONFIG("s3_bucket", namespace="sso-dashboard") + SECRET_KEY: str + SERVER_NAME: str = os.environ.get("SSO-DASHBOARD_SERVER_NAME", "localhost:8000") + SESSION_COOKIE_NAME: str + CDN: str - CDN = CONFIG( - "cdn", - namespace="sso-dashboard", - default="https://cdn.{SERVER_NAME}".format(SERVER_NAME=SERVER_NAME), - ) + S3_BUCKET: str + FORBIDDEN_PAGE_PUBLIC_KEY: bytes - FORBIDDEN_PAGE_PUBLIC_KEY = base64.b64decode(CONFIG("forbidden_page_public_key", namespace="sso-dashboard")) + PREFERRED_URL_SCHEME: str = os.environ.get("SSO-DASHBOARD_PREFERRED_URL_SCHEME", "https") + REDIS_CONNECTOR: str - PREFERRED_URL_SCHEME = CONFIG("preferred_url_scheme", namespace="sso-dashboard", default="https") - - REDIS_CONNECTOR = CONFIG("redis_connector", namespace="sso-dashboard") + def __init__(self): + self.SESSION_COOKIE_NAME = f"{self.SERVER_NAME}_session" + self.CDN = os.environ.get("SSO-DASHBOARD_CDN", f"https://cdn.{self.SERVER_NAME}") + self.REDIS_CONNECTOR = os.environ["SSO-DASHBOARD_REDIS_CONNECTOR"] + self.SECRET_KEY = os.environ["SSO-DASHBOARD_SECRET_KEY"] + self.S3_BUCKET = os.environ["SSO-DASHBOARD_S3_BUCKET"] + self.FORBIDDEN_PAGE_PUBLIC_KEY = base64.b64decode(os.environ["SSO-DASHBOARD_FORBIDDEN_PAGE_PUBLIC_KEY"]) -class OIDCConfig(object): +class OIDC: """Convienience Object for returning required vars to flask.""" + OIDC_DOMAIN: str + OIDC_CLIENT_ID: str + OIDC_CLIENT_SECRET: str + OIDC_REDIRECT_URI: str + LOGIN_URL: str + def __init__(self): """General object initializer.""" - CONFIG = get_config() - self.OIDC_DOMAIN = CONFIG("oidc_domain", namespace="sso-dashboard") - self.OIDC_CLIENT_ID = CONFIG("oidc_client_id", namespace="sso-dashboard") - self.OIDC_CLIENT_SECRET = CONFIG("oidc_client_secret", namespace="sso-dashboard") - self.LOGIN_URL = "https://{DOMAIN}/login?client={CLIENT_ID}".format( - DOMAIN=self.OIDC_DOMAIN, CLIENT_ID=self.OIDC_CLIENT_ID - ) + self.OIDC_DOMAIN = os.environ["SSO-DASHBOARD_OIDC_DOMAIN"] + self.OIDC_CLIENT_ID = os.environ["SSO-DASHBOARD_OIDC_CLIENT_ID"] + self.OIDC_CLIENT_SECRET = os.environ["SSO-DASHBOARD_OIDC_CLIENT_SECRET"] + # Check for a prefixed environment variable, otherwise fallback to the + # unprefixed one. + if redirect_uri := os.environ.get("SSO-DASHBOARD_OIDC_REDIRECT_URI"): + self.OIDC_REDIRECT_URI = redirect_uri + else: + self.OIDC_REDIRECT_URI = os.environ["OIDC_REDIRECT_URI"] + self.LOGIN_URL = f"https://{self.OIDC_DOMAIN}/login?client={self.OIDC_CLIENT_ID}" @property def client_id(self): @@ -73,12 +70,3 @@ def client_id(self): @property def client_secret(self): return self.OIDC_CLIENT_SECRET - - def auth_endpoint(self): - return "https://{DOMAIN}/authorize".format(DOMAIN=self.OIDC_DOMAIN) - - def token_endpoint(self): - return "https://{DOMAIN}/oauth/token".format(DOMAIN=self.OIDC_DOMAIN) - - def userinfo_endpoint(self): - return "https://{DOMAIN}/userinfo".format(DOMAIN=self.OIDC_DOMAIN) diff --git a/dashboard/oidc_auth.py b/dashboard/oidc_auth.py index 5af44fd1..35e2f6bf 100644 --- a/dashboard/oidc_auth.py +++ b/dashboard/oidc_auth.py @@ -102,10 +102,6 @@ def connection(self) -> Optional[str]: def preferred_connection_name(self) -> Optional[str]: return self.jws_data.get("preferred_connection_name") - @property - def redirect_uri(self) -> str: - return self.jws_data.get("redirect_uri", "https://sso.mozilla.com") - def signed(self) -> bool: """ By the time we get here we've got a valid key, and a properly diff --git a/env.example b/env.example index 9bac4dc2..052f6125 100644 --- a/env.example +++ b/env.example @@ -1,20 +1,49 @@ ### This file is a sample environment file for use with the project. +# To see these in action see the `clouddeploy` directory. # This should be random in production deployment used in session security. -SECRET_KEY="this is a secret key" +# Easy way to generate: +# openssl rand -hex 64 +SSO-DASHBOARD_SECRET_KEY="this is a secret key" -# Development or production. -ENVIRONMENT="Development" +# local, development, staging, or production. +ENVIRONMENT="local" # OpenID Connect Specific Parameters -OIDC_DOMAIN="auth0.example.com" -OIDC_CLIENT_ID="" -OIDC_CLIENT_SECRET="" +# We use Auth0, the configs for this would be under the Application "Settings" +# page. +SSO-DASHBOARD_OIDC_DOMAIN="auth0.example.com" +SSO-DASHBOARD_OIDC_CLIENT_ID="" +SSO-DASHBOARD_OIDC_CLIENT_SECRET="" -# User avatar parameters -MOZILLIANS_API_URL='https://mozillians.org/api/v2/users/' -MOZILLIANS_API_KEY='' +# Yes, this one is not namespaced. +# DEBT(bhee): standardize at some point. +OIDC_REDIRECT_URI='https://localhost.localdomain:8000/redirect_uri' -SERVER_NAME="localhost:5000" -PERMANENT_SESSION=True -PERMANENT_SESSION_LIFETIME=900 +# Controls the logging levels +SSO-DASHBOARD_DEBUG=True + +# Unused right now. +SSO-DASHBOARD_TESTING=False + +# Reasonable for local development, you'll want to change these in production +# though. +SSO-DASHBOARD_CSRF_ENABLED=True +SSO-DASHBOARD_PERMANENT_SESSION=True +SSO-DASHBOARD_PERMANENT_SESSION_LIFETIME=86400 +SSO-DASHBOARD_SESSION_COOKIE_HTTPONLY=True +SSO-DASHBOARD_SERVER_NAME=localhost.localdomain:8000 +SSO-DASHBOARD_PREFERRED_URL_SCHEME=http + +# You'll need a running redis. +# See compose.yml. +SSO-DASHBOARD_REDIS_CONNECTOR=redis:6379 + +# Where we publish the `apps.yml` file. +SSO-DASHBOARD_CDN=https://cdn.sso.mozilla.com +SSO-DASHBOARD_S3_BUCKET=sso-dashboard.configuration + +# Used to decode the JWS from Auth0 (e.g. for if we redirect back and there's some +# context Auth0 passes us, like the error code.) +# Base64 + PEM encoded. +SSO-DASHBOARD_FORBIDDEN_PAGE_PUBLIC_KEY="" diff --git a/requirements.txt b/requirements.txt index 29a7841a..ef1ce4a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,6 @@ cryptography==43.0.1 cssmin==0.2.0 defusedxml==0.7.1 distlib==0.3.8 -everett==3.3.0 Faker==26.1.0 filelock==3.15.4 Flask==3.0.3