🔧 settings: Refactor to 12-factor env vars, eliminate config files

Replace the three config files read at import time (`config.yml`,
`client_secrets.json`, `fas-admin-details.json`) with environment
variables. This was the single biggest barrier to running the app in
containers — Django settings would crash on import if these files did
not exist on disk.

Settings changes (`base.py`):
- Remove `yaml` import and `config.yml` reading entirely
- `ADMINS` set to empty list (Django admin email notifications unused)
- OIDC endpoints configurable via env vars (defaulting to Fedora staging
  at `iddev.fedorainfracloud.org`)
- `OIDC_RP_IDP_SIGN_KEY` moved to env var (was hardcoded RSA key)
- New `OIDC_SUPERUSER_USERNAMES` setting (comma-separated env var)
  replaces the `config.yml` `auth.admins` list
- `REDIS_HOST` and `REDIS_PORT` configurable via env vars
- Consistent multi-line formatting for all `os.environ.get` calls that
  include default values

Settings changes (`dev.py`):
- Remove `client_secrets.json` and `fas-admin-details.json` file reads
  (`base.py` already reads OIDC and FAS credentials from env vars)
- Database connection configurable via `DB_HOST`, `DB_NAME`,
  `DB_USERNAME`, `DB_PASSWORD` env vars (defaults match
  `podman-compose.yml`)
- Remove commented-out Gmail SMTP config
- Remove `import json` (no longer needed)

Auth changes (`auth.py`):
- Remove `yaml` import and `config.yml` reading
- Use `settings.OIDC_SUPERUSER_USERNAMES` instead of
  `cfg['auth']['admins']`
- `provider_logout` reads `OIDC_OP_LOGOUT_URL` from env
- Modernize `super(`) calls and bare `except` clause

Container changes:
- `Containerfile`: Install `poetry-plugin-export` (required in Poetry
  2.x) and use `poetry export` to generate `requirements.txt` for
  `pip install`
- Commit `poetry.lock` for reproducible builds

Also:
- Remove `pyyaml` from `pyproject.toml` (no longer needed)
- Delete `generate_client_secrets.sh` and `config.yml.example`
- Add `.env` to `.gitignore`
- Expand `.env.example` with all new env vars

Assisted-by: Claude Opus 4.6 (1M context)
Signed-off-by: Justin Wheeler <jwheel@fedoraproject.org>
This commit is contained in:
Justin Wheeler 2026-03-29 15:52:45 -04:00
commit be5135a6b6
Signed by: jflory7
GPG key ID: 6BD803B36BF8F62E
10 changed files with 2831 additions and 111 deletions

View file

@ -1,16 +1,39 @@
# Copy this file to .env and fill in the values.
# Used by podman-compose for local development.
# Django
SECRET_KEY=only-for-development
# DJANGO_SETTINGS_MODULE=happinesspackets.settings.dev
# Database (defaults match podman-compose.yml)
# DB_HOST=db
# DB_NAME=postgres
# DB_USERNAME=postgres
# DB_PASSWORD=example
# Redis (for cache in deployment.py)
# REDIS_URL=redis://redis:6379/1
# OIDC credentials (register at Fedora OIDC provider)
OIDC_RP_CLIENT_ID=
OIDC_RP_CLIENT_SECRET=
# OIDC_RP_IDP_SIGN_KEY=
# OIDC endpoints (defaults to Fedora staging)
# OIDC_OP_AUTHORIZATION_ENDPOINT=https://id.fedoraproject.org/openidc/Authorization
# OIDC_OP_TOKEN_ENDPOINT=https://id.fedoraproject.org/openidc/Token
# OIDC_OP_USER_ENDPOINT=https://id.fedoraproject.org/openidc/UserInfo
# OIDC_OP_LOGOUT_URL=https://id.fedoraproject.org/logout
# FAS admin account for username lookups
ADMIN_USERNAME=
ADMIN_PASSWORD=
# Django secret key (generate with: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())")
SECRET_KEY=only-for-development
# Comma-separated list of FAS usernames granted superuser on OIDC login
OIDC_SUPERUSER_USERNAMES=jflory7
# Redis URL for cache (Celery uses its own config in settings)
REDIS_URL=redis://redis:6379/1
# Email (deployment.py only)
# SERVER_EMAIL=root@localhost
# DEFAULT_FROM_EMAIL=Happiness Packets <fedora.happinesspackets@gmail.com>
# EMAIL_USER=
# EMAIL_PASSWORD=

9
.gitignore vendored
View file

@ -21,13 +21,12 @@ selenium-screenshots/
*.swp
*.sqlite3
#OIDC credentials
# Environment variables (secrets)
.env
# Legacy config files (replaced by env vars, kept in gitignore for safety)
client_secrets.json
#Adding fas-admin-details.json so that the username and password does not get pushed
fas-admin-details.json
# YAML Configuration file for django
/config.yml
# Django-haystack files

View file

@ -1,14 +1,14 @@
# Stage 1: Install dependencies in a temporary builder image.
# This stage pulls in Poetry, compilers, and build tools that are
# needed to install Python packages but not needed at runtime.
# Uses Poetry to export a requirements.txt, then installs with pip
# to avoid Poetry/setuptools conflicts in the UBI container.
FROM registry.access.redhat.com/ubi9/python-312:latest AS builder
WORKDIR /app
COPY pyproject.toml poetry.lock* ./
RUN pip install --no-cache-dir poetry \
&& poetry config virtualenvs.create false \
&& poetry install --without dev,docs --no-interaction --no-ansi
RUN pip install --no-cache-dir poetry poetry-plugin-export \
&& poetry export --without dev,docs -f requirements.txt -o requirements.txt \
&& pip install --no-cache-dir -r requirements.txt
# Stage 2: Create the final runtime image.
# Starts from a clean base and copies only the installed packages

View file

@ -1,14 +0,0 @@
base:
admins:
# following will populate ADMINS variable in settings/base.py
- ['Anna Philips', 'algogator@fedoraproject.org']
- ['Jona Azizaj', 'jonatoni@fedoraproject.org']
- ['Bee Padalkar', 'bee2502@fedoraproject.org']
- ['Justin Wheeler', 'jflory7@fedoraproject.org']
auth:
admins:
# following will be given superuser privileges
- jflory7
- jonatoni
- bt0dotninja
- anxh3l0

View file

@ -1,19 +0,0 @@
#!/bin/sh
#
# Script to generate secret keys for developing on the application. Only needs
# to be ran on first set-up of project.
#
if [ ! -f "client_secrets.json" ]; then
echo "client_secrets.json not found, generating..."
curl -s \
--request POST \
--header "Content-Type: application/json" \
--data '{"redirect_uris": ["http://localhost:8000/oidc/callback/"],
"application_type": "native","token_endpoint_auth_method":
"client_secret_post"}' \
https://iddev.fedorainfracloud.org/openidc/Registration \
-o client_secrets.json
else
echo "client_secrets.json already exists. To regenerate, delete the file."
fi

View file

@ -1,18 +1,20 @@
import os
from django.conf import settings
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
import yaml
with open("config.yml", 'r') as ymlfile:
cfg = yaml.safe_load(ymlfile)
def provider_logout(request):
redirect_url = 'https://iddev.fedorainfracloud.org/logout'
return redirect_url
return os.environ.get(
'OIDC_OP_LOGOUT_URL',
'https://iddev.fedorainfracloud.org/logout',
)
class OIDC(OIDCAuthenticationBackend):
def update_user(self, user, claims):
if user.username in cfg['auth']['admins']:
if not user.is_superuser:
if user.username in settings.OIDC_SUPERUSER_USERNAMES:
if not user.is_superuser:
user.is_superuser = True
user.is_staff = True
else:
@ -21,15 +23,14 @@ class OIDC(OIDCAuthenticationBackend):
user.is_superuser = False
user.save()
return user
def create_user(self, claims):
user = super(OIDC, self).create_user(claims)
user = super().create_user(claims)
user.username = claims.get('nickname', '')
user.email = claims.get('email', '')
try:
user.first_name = claims.get('name', '')
except:
except Exception:
user.first_name = user.username
user.save()
return self.update_user(user,claims)
return self.update_user(user, claims)

View file

@ -4,11 +4,6 @@ from django.contrib.messages import constants as messages
from django.core.exceptions import ImproperlyConfigured
from pathlib import Path
import yaml
with open("config.yml", 'r') as ymlfile:
cfg = yaml.safe_load(ymlfile)
PROJECT_DIR = Path(__file__).resolve().parent.parent.parent
# CKEditor configurations
@ -28,10 +23,16 @@ BASE_DIR = PROJECT_DIR
DEBUG = False
ADMINS = cfg['base']['admins']
SERVER_EMAIL = ADMINS[0][1]
ADMINS = []
SERVER_EMAIL = os.environ.get(
'SERVER_EMAIL',
'root@localhost',
)
DEFAULT_FROM_EMAIL = "Happiness Packets <fedora.happinesspackets@gmail.com>"
DEFAULT_FROM_EMAIL = os.environ.get(
'DEFAULT_FROM_EMAIL',
'Happiness Packets <fedora.happinesspackets@gmail.com>',
)
EMAIL_SUBJECT_PREFIX = "[happinesspackets] "
@ -134,16 +135,29 @@ AUTHENTICATION_BACKENDS = (
)
OIDC_RP_SIGN_ALGO = 'RS256'
OIDC_RP_IDP_SIGN_KEY = '-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAq/0/XjILQxF3OaQZtFE3wVJ5UUuxZbxiJ/z+Zai0EOHiaMMxVyoo\nibDRen615r525DQ8TmQyR0eMQEpQ6SUvaOunahpYohgAkbkYggUMQhcoCLme18ZJ\nBTNWTP8w4t7mcuZd1cy1KtHpEvH4gkrjp8N3vIv1lzFraSc+p2rHMbV+AX5CJQ1H\nohBdwaqyOBKp0nzY27gu2EH2vzCwXkO4zGtrHfjjGc0Ra4WG+xz1AWg833xcFj3p\nqM3vca09jDLBme+GT151LcCCXRNyOZPZ3ZX62NxkMyqvVJHC3Uu2Q1hSHO7f6AZk\nZXY88PXXEH52T2ZrWiISowjTcGUboP8goQIDAQAB\n-----END RSA PUBLIC KEY-----\n'
OIDC_RP_IDP_SIGN_KEY = os.environ.get(
'OIDC_RP_IDP_SIGN_KEY',
'',
)
OIDC_RP_CLIENT_ID = os.environ.get('OIDC_RP_CLIENT_ID')
OIDC_RP_CLIENT_SECRET = os.environ.get('OIDC_RP_CLIENT_SECRET')
ADMIN_USERNAME = os.getenv('ADMIN_USERNAME')
ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
ADMIN_USERNAME = os.environ.get('ADMIN_USERNAME')
ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD')
OIDC_OP_AUTHORIZATION_ENDPOINT = "https://iddev.fedorainfracloud.org/openidc/Authorization"
OIDC_OP_TOKEN_ENDPOINT = "https://iddev.fedorainfracloud.org/openidc/Token"
OIDC_OP_USER_ENDPOINT = "https://iddev.fedorainfracloud.org/openidc/UserInfo"
# OIDC endpoints — default to Fedora staging; override for production
OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ.get(
'OIDC_OP_AUTHORIZATION_ENDPOINT',
'https://iddev.fedorainfracloud.org/openidc/Authorization',
)
OIDC_OP_TOKEN_ENDPOINT = os.environ.get(
'OIDC_OP_TOKEN_ENDPOINT',
'https://iddev.fedorainfracloud.org/openidc/Token',
)
OIDC_OP_USER_ENDPOINT = os.environ.get(
'OIDC_OP_USER_ENDPOINT',
'https://iddev.fedorainfracloud.org/openidc/UserInfo',
)
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
LOGIN_REDIRECT_URL_FAILURE = '/error'
@ -151,6 +165,13 @@ LOGIN_URL = '/oidc/authenticate/'
OIDC_RP_SCOPES = 'openid profile email'
OIDC_OP_LOGOUT_URL_METHOD = 'happinesspackets.messaging.auth.provider_logout'
# Comma-separated list of usernames granted superuser on OIDC login
OIDC_SUPERUSER_USERNAMES = [
u.strip()
for u in os.environ.get('OIDC_SUPERUSER_USERNAMES', '').split(',')
if u.strip()
]
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
@ -204,8 +225,14 @@ LOGGING = {
}
}
REDIS_HOST = 'localhost'
REDIS_PORT = '6379'
REDIS_HOST = os.environ.get(
'REDIS_HOST',
'localhost',
)
REDIS_PORT = os.environ.get(
'REDIS_PORT',
'6379',
)
CELERY_BROKER_URL = 'redis://' + REDIS_HOST + ':' + REDIS_PORT + '/0'
BROKER_TRANSPORT_OPTIONS = {'visibility_timeout': 3600}
CELERY_RESULT_BACKEND = 'redis://' + REDIS_HOST + ':' + REDIS_PORT + '/0'
@ -227,4 +254,4 @@ def get_env_variable(var_name):
return os.environ[var_name]
except KeyError:
error_msg = "Set the %s environment variable" % var_name
raise ImproperlyConfigured(error_msg)
raise ImproperlyConfigured(error_msg)

View file

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
# noinspection PyUnresolvedReferences
import json
import sys
from .base import * # noqa
@ -12,30 +11,35 @@ CRISPY_FAIL_SILENTLY = False
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'HOST': 'db',
'NAME': 'postgres',
'USER': 'postgres',
'PASSWORD': 'example',
'HOST': os.environ.get(
'DB_HOST',
'db',
),
'NAME': os.environ.get(
'DB_NAME',
'postgres',
),
'USER': os.environ.get(
'DB_USERNAME',
'postgres',
),
'PASSWORD': os.environ.get(
'DB_PASSWORD',
'example',
),
'ATOMIC_REQUESTS': True,
'CONN_MAX_AGE': 300,
}
}
#Configurations to send email on console
# Send email to console in development
CELERY_EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
#Configurations to test sending emails using Gmail SMTP
# CELERY_EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
# EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend'
# EMAIL_HOST = 'smtp.gmail.com'
# EMAIL_HOST_USER = '<HOST@EMAIL.COM>'
# EMAIL_HOST_PASSWORD = '<HOST_EMAIL_PASSWORD>'
# EMAIL_USE_TLS = True
# EMAIL_PORT = 587
SECRET_KEY = 'only-for-testing'
SECRET_KEY = os.environ.get(
'SECRET_KEY',
'only-for-development',
)
INTERNAL_IPS = ('127.0.0.1',)
@ -70,21 +74,6 @@ if not TESTING:
SELENIUM_SCREENSHOT_DIR = PROJECT_DIR / 'selenium-screenshots'
# Uses a separate Docker container to act as the Redis server
# Redis connection for Celery
CELERY_BROKER_URL = 'redis://redis:6379/0'
CELERY_RESULT_BACKEND = 'redis://redis:6379/0'
# Loads OIDC Client ID and Secret from client_secrets.json
with open("client_secrets.json") as f:
secrets = json.load(f)
OIDC_RP_CLIENT_ID = secrets["client_id"]
OIDC_RP_CLIENT_SECRET = secrets["client_secret"]
# Reading the fas-id and Password
with open("fas-admin-details.json") as f:
secrets = json.load(f)
ADMIN_USERNAME = secrets["ADMIN_USERNAME"]
ADMIN_PASSWORD = secrets["ADMIN_PASSWORD"]

2715
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -25,7 +25,6 @@ nh3 = ">=0.2.15"
Whoosh = "==2.7.4"
django-haystack = ">=3.2,<3.4"
python-fedora = "==0.10.0"
pyyaml = ">=6.0"
django-ckeditor = ">=6.0,<7.0"
gunicorn = ">=22.0"