""" ACCESS Core Database — Base Settings Django + PostgreSQL — Normalized operational ledger for MetaFlow ACCESS program. Synthetic data only in dev/staging. No real PHI without explicit approval. """ import os from pathlib import Path from django.core.exceptions import ImproperlyConfigured from dotenv import load_dotenv _ = load_dotenv(".env") BASE_DIR = Path(__file__).resolve().parent.parent.parent _secret_key = os.getenv("DJANGO_SECRET_KEY") if not _secret_key: raise ImproperlyConfigured( "DJANGO_SECRET_KEY environment variable is required. Set it before running the application." ) SECRET_KEY = _secret_key DEBUG = os.getenv("DJANGO_DEBUG", "false").lower() == "true" ALLOWED_HOSTS = [ host.strip() for host in os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") if host.strip() ] INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", # ACCESS Core apps "access_core.identity", "access_core.intake", "access_core.clinical", "access_core.cms_integration", "access_core.governance", "access_core.monitoring", "access_core.curation", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", # Must run after AuthenticationMiddleware — captures request.user for audit rows "access_core.governance.audit.AuditRequestMiddleware", ] ROOT_URLCONF = "access_core.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 = "access_core.wsgi.application" DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": os.getenv("POSTGRES_DB", "access_core"), "USER": os.getenv("POSTGRES_USER", "access_user"), # No default on purpose: a hardcoded fallback would silently let an # unconfigured environment try to authenticate with a dev password. # CI, both compose files and the staging module all set this; local # CLI work should use access_core.settings.local (SQLite). "PASSWORD": os.getenv("POSTGRES_PASSWORD"), "HOST": os.getenv("POSTGRES_HOST", "localhost"), "PORT": os.getenv("POSTGRES_PORT", "5432"), } } AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" }, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_TZ = True STATIC_URL = "static/" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_FROM_EMAIL = "no-reply@access-core.local" HEALTHIE_API_BASE_URL = os.getenv( "HEALTHIE_API_BASE_URL", "https://api.gethealthie.com/graphql" ) HEALTHIE_API_KEY = os.getenv("HEALTHIE_API_KEY", "") HEALTHIE_WEBHOOK_SECRET = os.getenv("HEALTHIE_WEBHOOK_SECRET", "") HEALTHIE_WEBHOOK_VERIFY_SIGNATURE = ( os.getenv("HEALTHIE_WEBHOOK_VERIFY_SIGNATURE", "true").lower() == "true" ) HEALTHIE_INTAKE_FORM_ID = os.getenv("HEALTHIE_INTAKE_FORM_ID", "2360739") AWS_REGION = os.getenv("AWS_REGION", "us-east-1") AWS_S3_BUCKET = os.getenv("AWS_S3_BUCKET", "") # CMS ACCESS API. No public ACCESS sandbox exists yet — use the local mock for dev: # http://localhost:3001 (CMS_FHIR_VERSION_PATH=""). The default below is a placeholder; # note BCDA (bulk claims data) is a different CMS product, not an ACCESS endpoint. CMS_API_BASE_URL = os.getenv("CMS_API_BASE_URL", "https://sandbox.bcda.cms.gov") # Gates synthetic-data behavior toward the CMS API (see services/fhir_builder.py # and cms_integration/models.py). True only in environments that hold synthetic # data AND talk to a non-production endpoint. When False: records without a real # cmsMBI identifier fail closed instead of sending a PHI-derived pseudonym, and # stored CMS request/response payloads are redacted at save time. ALLOW_SYNTHETIC_MBI = os.getenv("ALLOW_SYNTHETIC_MBI", "false").lower() == "true" CMS_API_CLIENT_ID = os.getenv( "CMS_API_CLIENT_ID", "" ) # empty = skip auth (mock server mode) CMS_API_CLIENT_SECRET = os.getenv("CMS_API_CLIENT_SECRET", "") CMS_API_TOKEN_TTL_SECONDS = int( os.getenv("CMS_API_TOKEN_TTL_SECONDS", "3540") ) # 59 min (60min - buffer) CMS_POLL_INTERVAL_SECONDS = int(os.getenv("CMS_POLL_INTERVAL_SECONDS", "5")) CMS_POLL_LOOP_SECONDS = int( os.getenv("CMS_POLL_LOOP_SECONDS", "60") ) # cms_polling_worker cadence between poll_cms_submissions runs CMS_FHIR_VERSION_PATH = os.getenv( "CMS_FHIR_VERSION_PATH", "R4" ) # "" for mock server, "R4" for BCDA/production; fills {version} in the path templates below # Request path templates (relative to CMS_API_BASE_URL) for the two endpoint # families. The CMS API doc's example URLs are not guaranteed to match the real # production paths, so each environment configures its own shape here instead of # hardcoding it. {placeholders} are filled per call; a blank value drops its path # segment (so "" CMS_FHIR_VERSION_PATH removes the version segment from any # template). Available placeholders — POST: {version}, {resource_type}, # {id_suffix}, {operation} plus extra_values passed to post_operation # (e.g. ?entityID={entity_id}); submission-status: {version}, {resource_type}, # {submission_id}. The local mock server takes the submission id as a query param # instead of a path segment (?submissionID={submission_id}). CMS_API_POST_PATH = os.getenv( "CMS_API_POST_PATH", "/fhir/{version}/{resource_type}/{id_suffix}/{operation}", ) CMS_API_SUBMISSION_STATUS_PATH = os.getenv( "CMS_API_SUBMISSION_STATUS_PATH", "/fhir/{version}/{resource_type}/$submission-status/{submission_id}", ) CMS_POST_MAX_RETRIES = int( os.getenv("CMS_POST_MAX_RETRIES", "3") ) # retries for transient POST errors (5xx/network) CMS_REQUEST_TIMEOUT_SECONDS = float( os.getenv("CMS_REQUEST_TIMEOUT_SECONDS", "60") ) # per-request timeout in seconds # Audit trail client-IP resolution (governance.audit._client_ip). # X-Forwarded-For is only honored when the direct peer (REMOTE_ADDR) appears in # AUDIT_TRUSTED_PROXY_ADDRESSES; otherwise REMOTE_ADDR is used as-is so the # spoofable header can never influence audit rows. # AUDIT_PROXY_HOPS = number of trusted proxy layers that append to XFF between # the client and Django (e.g. 2 for CloudFront + ALB, 1 for a bare ALB). AUDIT_TRUSTED_PROXY_ADDRESSES = [ ip.strip() for ip in os.getenv("AUDIT_TRUSTED_PROXY_ADDRESSES", "").split(",") if ip.strip() ] AUDIT_PROXY_HOPS = int(os.getenv("AUDIT_PROXY_HOPS", "1")) TEST_RUNNER = "django.test.runner.DiscoverRunner" LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "verbose": { "format": "{levelname} {asctime} {name}:{lineno} {message}", "style": "{", }, }, "handlers": { "console": { "class": "logging.StreamHandler", "formatter": "verbose", }, }, "root": { "handlers": ["console"], "level": "INFO", }, "loggers": { "django": {"handlers": ["console"], "level": "WARNING", "propagate": False}, "access_core": {"handlers": ["console"], "level": "INFO", "propagate": False}, }, }