From b5471e664d6c82cffae0a49b8b2b6de9d21bdacf Mon Sep 17 00:00:00 2001 From: emelinda Date: Mon, 10 Aug 2026 08:13:07 +0300 Subject: [PATCH] =?UTF-8?q?Django:=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD=20=D1=87=D1=83=D0=B6=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=BA=D0=BE=D0=BD=D1=82=D1=83=D1=80=D0=B0=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BA=20?= =?UTF-8?q?=D0=B8=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=D0=BE=D0=B2=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Браузер получал 404 на https://sarex.contour.infra.sarex.tech/media/state/previews/....png то есть backend отдавал АБСОЛЮТНЫЕ ссылки на чужой хост. Файлы при этом лежали у нас — уходил не тот домен. Источников оказалось три, и все в base: * django-configmap, HOST = "https://sarex.contour.infra.sarex.tech" — именно по нему строятся абсолютные ссылки на медиа; * там же CSRF_TRUSTED_ORIGINS с тем же доменом: без нашего домена django отклонял бы POST-формы админки по проверке Referer; * envs backend SERVER_API_HOST и SERVER_HOST. У celery те же две переменные я поправил раньше, а у backend пропустил. Конфигмап скопирован в оверлей целиком: все настройки лежат одним значением data.production.py на 331 строку, kustomize заменяет его только целиком — та же причина, что у nginx-configmap рядом. Изменены ровно две строки, остальное побайтно совпадает с base, так что diff остаётся читаемым. После правки домена sarex.contour.infra.sarex.tech в рендере clusters/aero/apps не остаётся ни одного вхождения. --- apps/django/aero/django-configmap.yaml | 348 +++++++++++++++++++++++++ apps/django/aero/kustomization.yaml | 24 ++ 2 files changed, 372 insertions(+) create mode 100644 apps/django/aero/django-configmap.yaml diff --git a/apps/django/aero/django-configmap.yaml b/apps/django/aero/django-configmap.yaml new file mode 100644 index 0000000..11153ad --- /dev/null +++ b/apps/django/aero/django-configmap.yaml @@ -0,0 +1,348 @@ +# Копия django-configmap из base с заменой домена на домен контура. +# +# Патчем этого не сделать: все настройки лежат ОДНИМ значением data.production.py +# (331 строка python), kustomize умеет заменить его только целиком. Ровно та же +# причина, что у nginx-configmap.yaml рядом. +# +# ЧТО ИЗМЕНЕНО — две строки, обе про домен sarex.contour.infra.sarex.tech, +# принадлежащий ДРУГОМУ контуру: +# +# HOST — по нему backend строит АБСОЛЮТНЫЕ ссылки на медиа. +# Браузер получал +# https://sarex.contour.infra.sarex.tech/media/state/previews/....png +# и, разумеется, 404: чужой хост, наших файлов там нет. +# CSRF_TRUSTED_ORIGINS — без нашего домена django отклонял бы POST-формы +# админки с ошибкой проверки Referer. +# +# Больше в файле ничего не менялось: сравнивать с base удобно обычным diff. +apiVersion: v1 +kind: ConfigMap +metadata: + name: django-configmap + namespace: django +data: + production.py: | + import ast + import os + from .base import * + from logging.handlers import SysLogHandler + from datetime import timedelta + + def _load_env_file(path): + try: + with open(path, "r", encoding="utf-8") as f: + for raw_line in f: + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + key = key.strip() + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'): + try: + value = ast.literal_eval(value) + except (ValueError, SyntaxError): + value = value[1:-1] + if key and key not in os.environ: + os.environ[key] = value + except FileNotFoundError: + pass + + def _read_secret_file(path, default=""): + try: + with open(path, "r", encoding="utf-8") as f: + return f.read().strip() + except FileNotFoundError: + return default + + # Fallback for manage.py launched via `kubectl exec` (outside entrypoint), + # so Django can still read DB/JWT values from Vault-injected files. + _load_env_file("/vault/secrets/django-postgresql") + _load_env_file("/vault/secrets/django-rabbitmq") + _load_env_file("/vault/secrets/django-s3") + _load_env_file("/vault/secrets/django-kafka") + _load_env_file("/vault/secrets/django-common") + + if not os.environ.get("JWT_PRIVATE_KEY"): + os.environ["JWT_PRIVATE_KEY"] = _read_secret_file("/vault/secrets/django-jwt-private") + if not os.environ.get("JWT_PUBLIC_KEY"): + os.environ["JWT_PUBLIC_KEY"] = _read_secret_file("/vault/secrets/django-jwt-public") + + ALLOWED_HOSTS = ["*"] + FILE_UPLOAD_PERMISSIONS = 0o644 + DEBUG = False + CSRF_COOKIE_SECURE = True + CSRF_TRUSTED_ORIGINS = ["https://sarex.local.lonsdaleites.ru", "http://sarex.local.lonsdaleites.ru"] + SESSION_COOKIE_SECURE = True + SECURE_SSL_REDIRECT = False + + SECRET_KEY = 't2=9+($2f%7ptsdy4!rby$)mcfl1l%o2e@vs^d(g&(wwi&%k1v' + + CORS_ORIGIN_ALLOW_ALL = True + SERVERSETTINGS.cache_enabled = True + INSTALLED_APPS = list(INSTALLED_APPS) + ['corsheaders'] + + CORS_ALLOW_METHODS = ( + 'DELETE', + 'GET', + 'OPTIONS', + 'PATCH', + 'POST', + 'PUT', + ) + BASIC_USER_ID = 2 + + CORS_ALLOW_HEADERS = ( + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', + 'x-token', + 'Bearer', + ) + + HOST = "https://sarex.local.lonsdaleites.ru" + + POSTGRES_DATABASE = os.environ.get('DJANGO_POSTGRES_DATABASE') + POSTGRES_USER = os.environ.get('DJANGO_POSTGRES_USER') + POSTGRES_PASSWORD = os.environ.get('DJANGO_POSTGRES_PASSWORD') + POSTGRES_HOST = os.environ.get('DJANGO_POSTGRES_HOST') + POSTGRES_PORTS = os.environ.get('DJANGO_POSTGRES_PORTS', "5432") + + DATABASES = { + 'default': { + 'ENGINE': 'django_prometheus.db.backends.postgresql', + 'NAME': POSTGRES_DATABASE, + 'USER': POSTGRES_USER, + 'PASSWORD': POSTGRES_PASSWORD, + 'HOST': POSTGRES_HOST, + 'PORT': POSTGRES_PORTS, + } + } + + MEASUREMENTSETTINGS = MeasurementSettings() + WORKFLOWSSETTINGS = WorkFlowsSettings() + S3SETTINGS = S3Settings() + GATEWAYSETTINGS = GateWaySetttings() + + LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + } + }, + 'formatters': { + 'verbose': { + 'format': '[contactor] %(levelname)s %(asctime)s %(message)s', + }, + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + }, + 'sentry': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + '': { + 'handlers': ['console', 'sentry'], + 'level': 'INFO', + 'propagate': False, + }, + } + } + + COMPARATOR_JWT = os.environ.get("COMPARATOR_JWT", "default_jwt") + COMPARATOR_URL = os.environ.get("COMPARATOR_URL", "https://wb.sarex.io/comparator") + COMPARATOR_SECTION = os.environ.get("COMPARATOR_SECTION", "sarex-production-storage") + + SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(hours=1), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': True, + 'UPDATE_LAST_LOGIN': False, + 'ALGORITHM': 'RS512', + 'SIGNING_KEY': os.environ.get("JWT_PRIVATE_KEY", "").replace("\\n", "\n"), + 'VERIFYING_KEY': os.environ.get("JWT_PUBLIC_KEY", "").replace("\\n", "\n"), + 'AUDIENCE': None, + 'ISSUER': os.environ.get('SIMPLE_JWT_ISSUER', 'default_issuer'), + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', + 'JTI_CLAIM': 'jti', + 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', + 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), + } + + os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" + DEFAULT_FILE_STORAGE = 'sarex.core.storages.CustomS3Boto3Storage' + DATA_UPLOAD_MAX_MEMORY_SIZE = 268435456 + + if not os.environ.get('ISOLATED', False): + import sentry_sdk + from sentry_sdk.integrations.django import DjangoIntegration + + sentry_sdk.init( + dsn="https://3df2f4b8d3d14595a06c92e9d7c562cb@sentry.io/1501541", + integrations=[DjangoIntegration()], + environment=os.environ.get('SENTRY_ENVIRONMENT', 'production'), + send_default_pii=True, + ) + + COMPARISON_API_URL = f"{os.environ.get('WORKFLOWSSETTINGS_HOST')}/comparisons" + DOCUMENTATION_API_URL = f"{os.environ.get('WORKFLOWSSETTINGS_HOST')}/documentations" + PDM_FILES_API_URL = f"{os.environ.get('WORKFLOWSSETTINGS_HOST')}/files" + + WORKFLOWS_TASKS = { + "update_orthomosaic_data": { + "image": f"{os.environ.get('WORKFLOWSSETTINGS_REGISTRY')}/update-orthomosaic-data:dev", + "service_requests": ["django-auth"], + "backoff_limit": 3, + }, + } + + REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': ( + 'rest_framework.pagination.LimitOffsetPagination' ), + 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', + 'PAGE_SIZE': 1000, 'DEFAULT_FILTER_BACKENDS': [ + 'django_filters.rest_framework.DjangoFilterBackend' ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'sarex.authentication.backends.ZitadelJWTAuthentication', + 'rest_framework.authentication.RemoteUserAuthentication', + 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + 'sarex.authentication.backends.JWTAuthentication' ], + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', ] } + + AUTHENTICATION_BACKENDS = [ + 'sarex.authentication.backends.CustomRemoteUserBackend', + 'django.contrib.auth.backends.ModelBackend', + 'guardian.backends.ObjectPermissionBackend', + ] + + MIDDLEWARE = [ + 'django_prometheus.middleware.PrometheusBeforeMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + #'django_keycloak.middlewares.AuthorizationHeaderMiddleware', + #'django_keycloak.middlewares.KeycloakSessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + #'django.contrib.auth.middleware.RemoteUserMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django_user_agents.middleware.UserAgentMiddleware', + 'simple_history.middleware.HistoryRequestMiddleware', + 'django_prometheus.middleware.PrometheusAfterMiddleware', ] + + + class KeyCloakSettings(BaseSettings): + client_id: str = "client_id" + client_secret: str = "client_secret" + discovery_url: str = "https://login.wb.sarex.io/realms/sarex/.well-known/openid-configuration" + staff: Optional[str] = "Sarex staff" + superuser: Optional[str] = "Sarex superusers" + sync_with_django: bool = True + sync_admin: bool = False + group_prefix: str = 'Sarex-Role' + company_prefix: str = 'Sarex-Company' + department_prefix: str = 'Sarex-Department' + position_prefix: str = 'Sarex-Position' + separator: str = '__' + sync_user_groups: bool = False + sync_user_positions: bool = False + sync_user_departments: bool = False + sync_user_companies: bool = False + use_redirect_logout: bool = False + logout_redirect_uri: str = "/" + default_group_name: Optional[str] = 'Тест' + default_company_name: Optional[str] = 'Брусника' + trusted_uri: List[str] = ['/api/core/orthophotos/', '/api/token', '/api/token/me'] + trusted_uri: List[str] = [] + + class Config: + env_prefix = "KC_" + + + KEYCLOAKSETTINGS = KeyCloakSettings() + + REMOTE_USER_DEFAULT_COMPANY_ID = 1 + SAREX_MODULES = [ + { + "name": "Замечания", + "uri": "/remarks" + }, + # { + # "name": "Управление проектами", + # "uri": "/management/projects", + # }, + { + "name": "Замечания V2", + "uri": "/issues" + }, + { + "name": "Документация", + "uri": "/documentations", + }, + { + "name": "Согласование документов", + "uri": "/reviews" + }, + { + "name": "Рабочие процессы", + "uri": "/processes" + }, + { + "name": "Запросы", + "uri": "/rfi" + }, + { + "name": "Управление проектами", + "uri": "/management/projects" + }, + + # { + # "name": "Обзор", + # "uri": "/projects" + # }, + { + "name": "Передача документации", + "uri": "/transmittal" + }, + ] + AUTH_SETTINGS = { + "refresh_token": False, + "refresh_token_uri": "/api/token/me", + "refresh_oauth_token": True, + "refresh_oauth_token_uri": "/oauth/token", + "refresh_time": 240, + } + + + DEBUG=True + WEB_APP_AUTH_MODE='jwt-session-based' + + + SAREX_MODULES_SETTINGS = { + "aero": { + "enable_new_media": True + }, + "sso_logout_redirect": True + } diff --git a/apps/django/aero/kustomization.yaml b/apps/django/aero/kustomization.yaml index 982f086..175be03 100644 --- a/apps/django/aero/kustomization.yaml +++ b/apps/django/aero/kustomization.yaml @@ -19,6 +19,15 @@ patches: target: kind: ConfigMap name: nginx-configmap + # Настройки django: в base захардкожен домен ЧУЖОГО контура. По нему backend + # строил абсолютные ссылки на медиа, и браузер уходил на + # https://sarex.contour.infra.sarex.tech/media/state/previews/....png + # получая 404. Причина копии та же, что у nginx-configmap: настройки лежат + # одним значением data.production.py, заменить можно только целиком. + - path: django-configmap.yaml + target: + kind: ConfigMap + name: django-configmap # --- celery: те же правки, что и у backend -------------------------------- # Worker крутит тот же образ и те же настройки, поэтому и расхождения с base # у него ровно те же: нет Kafka, нет Zitadel, MinIO живёт внутри кластера. @@ -179,6 +188,21 @@ patches: path: /spec/values/services/backend/envs/2/value/_default value: "False" + # Адреса на чужой контур. У celery те же две переменные поправлены выше, + # у backend они оставались с sarex.contour.infra.sarex.tech. + - op: test + path: /spec/values/services/backend/envs/17/name + value: SERVER_API_HOST + - op: replace + path: /spec/values/services/backend/envs/17/value/_default + value: https://sarex.local.lonsdaleites.ru + - op: test + path: /spec/values/services/backend/envs/18/name + value: SERVER_HOST + - op: replace + path: /spec/values/services/backend/envs/18/value/_default + value: https://sarex.local.lonsdaleites.ru + # --- Администратор платформы -------------------------------------------- # Учётка приезжает из Vault под именами, которые ждёт штатная # createsuperuser --noinput. Секрет кладёт ansible