Make access gating a first-class, pip-consumable extension point so a consuming app can gate any of its own views behind 2FA / ALTCHA / entitlement / feature flag / permission, or gate nothing, without editing the kit. - infrasynth.gates: Gate, GateResult, GatePermission, @gated and built-ins TwoFactorGate, AltchaGate, EntitlementGate, FeatureGate, PermissionGate; denials raise the correct namespaced error/status (per-endpoint, opt-in, default is no gating) - mint a `2fa` JWT claim only after verification (preserved across workspace selection) so TwoFactorGate is meaningful for API/multi-workspace clients - GatePermission added to DEFAULT_PERMISSION_CLASSES; HybridPermission evaluates declared gates so kit permissions gate automatically - document the extension surface and stable import paths in README
390 lines
12 KiB
Python
390 lines
12 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,
|
|
"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",
|
|
"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-",
|
|
"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,
|
|
},
|
|
}
|