🔧 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:
parent
32a8f0b190
commit
be5135a6b6
10 changed files with 2831 additions and 111 deletions
31
.env.example
31
.env.example
|
|
@ -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
9
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
2715
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue