infrasynth-backend-kit/config/settings/base.py
jcv-dev 5df0be1f5c feat: uniform extensibility across every module
Close the remaining places where an extension point was hardcoded, and
expose a stable public import surface for every app.

- files: register_storage_backend(...) / STORAGE_BACKENDS[name]["CLASS"];
  unknown backends now fail loudly instead of silently using local
- files: PipelineStepRegistry + @pipeline_step; PIPELINE_EXECUTOR setting
- billing: INVOICE_PDF_BUILDER setting
- security: TWO_FACTOR_SERVICE / TWO_FACTOR_RECOVERY_SERVICE settings
- all app packages expose lazy public exports (PEP 562); infrasynth.shared
  re-exports its primitives eagerly
- README documents the per-module extension-point table
- tests/test_extensibility.py pins each hook plus the public surface
2026-09-24 11:08:08 -05:00

394 lines
13 KiB
Python

from datetime import timedelta
from pathlib import Path
from typing import cast
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = "change-me-in-production"
DEBUG = False
ALLOWED_HOSTS = []
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"django_filters",
"corsheaders",
"django_celery_results",
"django_celery_beat",
"rest_framework_simplejwt.token_blacklist",
"infrasynth.tenancy",
"infrasynth.audit",
"infrasynth.security",
"infrasynth.files",
"infrasynth.notifications",
"infrasynth.webhooks",
"infrasynth.workflows",
"infrasynth.scheduler",
"infrasynth.features",
"infrasynth.billing",
]
MIDDLEWARE = [
"infrasynth.api.middleware.RequestIdMiddleware",
"django.middleware.security.SecurityMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"infrasynth.security.auth.middleware.JWTAuthenticationMiddleware",
"infrasynth.tenancy.middleware.TenantMiddleware",
"infrasynth.security.two_factor.middleware.TwoFactorMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"infrasynth.audit.middleware.AuditAPIMiddleware",
"infrasynth.api.middleware.RateLimitHeadersMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "config.wsgi.application"
LANGUAGE_CODE = "es"
TIME_ZONE = "America/Bogota"
USE_TZ = True
STATIC_URL = "static/"
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
MIGRATION_MODULES = {
"tenancy": "infrasynth.tenancy.migrations",
"infrasynth_audit": "infrasynth.audit.migrations",
"infrasynth_security": "infrasynth.security.migrations",
"infrasynth_files": "infrasynth.files.migrations",
"infrasynth_notifications": "infrasynth.notifications.migrations",
"infrasynth_webhooks": "infrasynth.webhooks.migrations",
"infrasynth_workflows": "infrasynth.workflows.migrations",
"infrasynth_scheduler": "infrasynth.scheduler.migrations",
"infrasynth_features": "infrasynth.features.migrations",
"infrasynth_billing": "infrasynth.billing.migrations",
}
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "infrasynth",
"USER": "infrasynth",
"PASSWORD": "infrasynth",
"HOST": "localhost",
"PORT": "5432",
},
}
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"infrasynth.security.auth.cookies.CookieJWTAuthentication",
"infrasynth.security.auth.api_keys.APIKeyAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
"infrasynth.gates.GatePermission",
],
"DEFAULT_RENDERER_CLASSES": [
"infrasynth.api.renderers.EnvelopeJSONRenderer",
],
"EXCEPTION_HANDLER": "infrasynth.api.exceptions.envelope_exception_handler",
"DEFAULT_PAGINATION_CLASS": "infrasynth.api.pagination.CursorPagination",
"DEFAULT_THROTTLE_CLASSES": ["infrasynth.api.throttling.TenantRateThrottle"],
"PAGE_SIZE": 25,
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
"DEFAULT_THROTTLE_RATES": {"tenant": "1000/hour"},
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
SPECTACULAR_SETTINGS = {
"TITLE": "InfraSynth Base API",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"COMPONENT_SPLIT_REQUEST": True,
}
CELERY_BROKER_URL = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND = "redis://localhost:6379/1"
CELERY_RESULT_EXTENDED = True
CELERY_TASK_SOFT_TIME_LIMIT = 300
CELERY_TASK_TIME_LIMIT = 600
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://localhost:6379/1",
},
}
INFRASYNTH_AUDIT = {
"EXCLUDED_MODELS": [
"sessions.Session",
"admin.LogEntry",
"contenttypes.ContentType",
"migrations.Migration",
"infrasynth_audit.ModelChangeLog",
"infrasynth_audit.APIInteractionLog",
"infrasynth_audit.SecurityEvent",
"infrasynth_features.FeatureFlag",
"infrasynth_features.FeatureFlagOverride",
],
"EXCLUDED_FIELDS": ["password", "token", "secret", "credit_card"],
"SENSITIVE_KEYS": ["password", "token", "secret", "authorization", "api_key"],
"MAX_BODY_SIZE_BYTES": 5000,
"STORE_IN_DB": True,
"RETENTION_DAYS": 365,
"ENABLE_API_LOGGING": True,
"ENABLE_MODEL_CHANGE_TRACKING": True,
"ENABLE_SECURITY_EVENTS": True,
}
INFRASYNTH_SECURITY = {
"ACCESS_TOKEN_LIFETIME_MINUTES": 30,
"REFRESH_TOKEN_LIFETIME_DAYS": 7,
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"ACCESS_COOKIE_NAME": "access_token",
"REFRESH_COOKIE_NAME": "refresh_token",
"COOKIE_SECURE": True,
"COOKIE_HTTPONLY": True,
"COOKIE_SAMESITE": "Lax",
"PRE_AUTH_COOKIE_NAME": "pre_auth_token",
"CRYPTO_KEY": None,
"AUTH_BACKEND_CLASS": "infrasynth.security.auth.backends.EmailOrUsernameBackend",
"LOGIN_RATE_LIMIT": "10/m",
"IP_BLACKLIST_THRESHOLD": 100,
"IP_BLACKLIST_WINDOW_MINUTES": 15,
"TWO_FACTOR_ISSUER_NAME": "InfraSynth",
"TWO_FACTOR_RECOVERY_CODES_COUNT": 8,
"TWO_FACTOR_TOTP_VALIDITY_WINDOW": 1,
"TWO_FACTOR_SERVICE": "infrasynth.security.two_factor.services.TOTPService",
"TWO_FACTOR_RECOVERY_SERVICE": "infrasynth.security.two_factor.services.RecoveryCodeService",
"PRE_AUTH_TOKEN_LIFETIME_MINUTES": 5,
"ALTCHA_DIFFICULTY": 10000,
"ALTCHA_CHALLENGE_EXPIRY_SECONDS": 300,
"ALTCHA_PROTECT_LOGIN": False,
"ALTCHA_HEADER": "X-Altcha",
"API_KEY_PREFIX_LENGTH": 8,
"API_KEY_HASH_ALGORITHM": "pbkdf2_sha256",
"API_KEY_DEFAULT_EXPIRY_DAYS": 365,
"PASSWORD_MIN_LENGTH": 8,
"PASSWORD_REQUIRE_UPPERCASE": True,
"PASSWORD_REQUIRE_DIGIT": True,
"PASSWORD_REQUIRE_SPECIAL_CHAR": True,
"ENABLE_WORKSPACE_SWITCHING": True,
}
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "infrasynth.security.password_validation.PasswordPolicyValidator"},
]
AUTHENTICATION_BACKENDS = [
INFRASYNTH_SECURITY["AUTH_BACKEND_CLASS"],
]
# JWT lifetimes/rotation are derived from the INFRASYNTH_SECURITY block so there
# is a single source of truth.
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=cast(int, INFRASYNTH_SECURITY["ACCESS_TOKEN_LIFETIME_MINUTES"])),
"REFRESH_TOKEN_LIFETIME": timedelta(days=cast(int, INFRASYNTH_SECURITY["REFRESH_TOKEN_LIFETIME_DAYS"])),
"ROTATE_REFRESH_TOKENS": cast(bool, INFRASYNTH_SECURITY["ROTATE_REFRESH_TOKENS"]),
"BLACKLIST_AFTER_ROTATION": cast(bool, INFRASYNTH_SECURITY["BLACKLIST_AFTER_ROTATION"]),
}
INFRASYNTH_FILES = {
"DEFAULT_STORAGE_BACKEND": "local",
"STORAGE_BACKENDS": {
"S3": {
"ACCESS_KEY": None,
"SECRET_KEY": None,
"BUCKET_NAME": None,
"REGION": "us-east-1",
"ENDPOINT_URL": None,
},
"cloudinary": {
"CLOUD_NAME": None,
"API_KEY": None,
"API_SECRET": None,
},
"gcs": {
"PROJECT_ID": None,
"BUCKET_NAME": None,
"CREDENTIALS_PATH": None,
},
"local": {},
},
"SIGNED_URL_EXPIRY_SECONDS": 3600,
"MAX_UPLOAD_SIZE_MB": 100,
"ENABLE_PROCESSING_PIPELINES": True,
"PROCESSING_BACKEND": "celery",
"PIPELINE_EXECUTOR": "infrasynth.files.processing.PipelineExecutor",
"ENABLE_X_SENDFILE": False,
"VIRUS_SCANNER": "noop",
"CLAMAV_SOCKET": "/var/run/clamav/clamd.ctl",
"REQUIRE_VIRUS_SCAN": False,
}
INFRASYNTH_NOTIFICATIONS = {
"DEFAULT_FROM_EMAIL": "noreply@example.com",
"CHANNELS": {
"email": {
"primary": "infrasynth.notifications.channels.email_smtp.SMTPChannel",
"fallback": "infrasynth.notifications.channels.email_sendgrid.SendGridChannel",
},
"sms": {
"primary": "infrasynth.notifications.channels.sms_twilio.TwilioSMSChannel",
},
},
"DISPATCH_BACKEND": "celery",
"MAX_RETRIES": 3,
"RETRY_DELAY_SECONDS": [60, 300, 900],
"RATE_LIMIT_PER_CHANNEL": {
"email": "50/m",
"sms": "10/m",
},
"STORE_DISPATCH_LOGS": True,
"DISPATCH_LOG_RETENTION_DAYS": 90,
"RETRY_SCAN_BATCH_SIZE": 100,
}
INFRASYNTH_WEBHOOKS = {
"DEFAULT_TIMEOUT_SECONDS": 10,
"MAX_RETRIES": 5,
"RETRY_BACKOFF": "exponential",
"RETRY_INITIAL_DELAY_SECONDS": 60,
"SIGNATURE_ALGORITHM": "sha256",
"SIGNATURE_HEADER": "X-Webhook-Signature",
"DELIVERY_BACKEND": "celery",
"INBOUND_PROCESSING_BACKEND": "sync",
"INBOUND_SIGNATURE_TOLERANCE_SECONDS": 300,
"MAX_PAYLOAD_SIZE_BYTES": 1048576,
}
INFRASYNTH_WORKFLOWS = {
"MAX_INSTANCES_PER_WORKFLOW": 10000,
"DEFAULT_APPROVAL_STRATEGY": "ALL",
"AUTO_CLONE_ASSIGNEES_ON_REENTRY": True,
"ALLOW_SELF_ASSIGNMENT": False,
"ROUTE_MAX_DEPTH": 50,
}
INFRASYNTH_SCHEDULER = {
"BACKEND": "celery",
"CELERY_BROKER_URL": "redis://localhost:6379/0",
"CELERY_RESULT_BACKEND": "redis://localhost:6379/1",
"CELERY_TASK_SOFT_TIME_LIMIT": 300,
"CELERY_TASK_TIME_LIMIT": 600,
"CELERY_WORKER_PREFETCH_MULTIPLIER": 1,
"DEFAULT_QUEUE": "default",
"MAX_EXECUTION_HISTORY_PER_TASK": 1000,
"AUTO_DISCOVER_TASKS": True,
}
INFRASYNTH_FEATURES = {
"CACHE_BACKEND": "default",
"CACHE_TTL_SECONDS": 60,
"CACHE_KEY_PREFIX": "features",
"ROLLOUT_HASH_ALGORITHM": "md5",
"AUTO_REGISTER_FROM_SETTINGS": True,
"FLAGS": {},
"EXPOSE_PERMISSIONS_IN_ACTIVE_ENDPOINT": True,
"EXPOSE_ROLES_IN_ACTIVE_ENDPOINT": True,
}
# Deployment environment used by feature-flag ``environments`` targeting.
ENVIRONMENT = "development"
INFRASYNTH_TENANCY = {
"ENABLED": True,
"TENANT_MODEL": "infrasynth.tenancy.Tenant",
"MEMBERSHIP_MODEL": "infrasynth.tenancy.TenantMembership",
"TENANT_CLAIM": "tenant",
"REQUIRE_TENANT_BY_DEFAULT": True,
"TENANT_ALLOWLIST_PATHS": [
"/api/v1/auth/login/",
"/api/v1/auth/refresh/",
"/api/v1/auth/select-workspace/",
"/api/v1/auth/altcha/",
"/api/v1/auth/2fa/",
"/api/v1/billing/webhook/",
"/api/v1/schema/",
"/api/v1/tenancy/invitations/accept/",
"/healthz",
"/readyz",
],
"ENABLE_WORKSPACE_SWITCHING": True,
"DEFAULT_LOCALE": "es",
"DEFAULT_TIMEZONE": "UTC",
}
INFRASYNTH_BILLING = {
"INVOICE_NUMBER_PREFIX": "INV-",
"INVOICE_PDF_BUILDER": "infrasynth.billing.invoice_generator._build_invoice_pdf",
"GRACE_PERIOD_DAYS": 5,
"MAX_RETRY_FAILED_PAYMENTS": 3,
"DEFAULT_CURRENCY": "USD",
"TAX_PERCENTAGE": 0,
"TAX_NAME": "",
"INVOICE_GENERATION_DAYS_BEFORE_RENEWAL": 3,
"WEBHOOK_TOLERANCE_SECONDS": 300,
"SYNC_SUBSCRIPTIONS_EVERY_HOURS": 24,
"ENTITLEMENT_CACHE_TTL_SECONDS": 60,
}
# Periodic work. Every task binds the tenant(s) it touches explicitly.
CELERY_BEAT_SCHEDULE = {
"notifications-retry-pending": {
"task": "infrasynth.notifications.retry_pending_dispatches",
"schedule": 60.0,
},
"notifications-purge-old": {
"task": "infrasynth.notifications.purge_old_dispatches",
"schedule": 86400.0,
},
"billing-sync-subscriptions": {
"task": "infrasynth.billing.sync_subscriptions",
"schedule": cast(int, INFRASYNTH_BILLING["SYNC_SUBSCRIPTIONS_EVERY_HOURS"]) * 3600,
},
"billing-advance-lifecycle": {
"task": "infrasynth.billing.advance_entitlement_lifecycle",
"schedule": 3600.0,
},
"billing-expire-entitlements": {
"task": "infrasynth.billing.expire_entitlements",
"schedule": 3600.0,
},
"billing-generate-renewal-invoices": {
"task": "infrasynth.billing.generate_renewal_invoices",
"schedule": 86400.0,
},
"audit-purge-expired": {
"task": "infrasynth.audit.purge_expired_logs",
"schedule": 86400.0,
},
}