diff --git a/api/app/settings/common.py b/api/app/settings/common.py index cd660dd4b3d7..72587c505551 100644 --- a/api/app/settings/common.py +++ b/api/app/settings/common.py @@ -15,6 +15,7 @@ import os import warnings from datetime import datetime, time, timedelta +from functools import lru_cache from typing import Any import dj_database_url @@ -174,6 +175,28 @@ DJANGO_DB_CONN_MAX_AGE = 0 if db_conn_max_age == -1 else db_conn_max_age DATABASE_ROUTERS: list[str] = [] +DATABASES: dict[str, Any] = {} + +USE_AZURE_MANAGED_IDENTITY_FOR_DB_AUTH = env( + "USE_AZURE_MANAGED_IDENTITY_FOR_DB_AUTH", default=False +) + + +@lru_cache(maxsize=1) +def _get_db_password(env_var_name: str) -> str | None: + if USE_AZURE_MANAGED_IDENTITY_FOR_DB_AUTH: + # Ignore the environment variable and get the password + # using Azure Managed Identities authentication. + from azure.identity import DefaultAzureCredential + + azure_token = DefaultAzureCredential().get_token( + "https://ossrdbms-aad.database.windows.net" + ) + return azure_token.token + + return os.environ.get(env_var_name) + + # Allows collectstatic to run without a database, mainly for Docker builds to collectstatic at build time if "DATABASE_URL" in os.environ: DATABASES = { @@ -239,7 +262,7 @@ "ENGINE": "django.db.backends.postgresql", "NAME": os.environ["DJANGO_DB_NAME"], "USER": os.environ["DJANGO_DB_USER"], - "PASSWORD": os.environ["DJANGO_DB_PASSWORD"], + "PASSWORD": _get_db_password("DJANGO_DB_PASSWORD"), "HOST": os.environ["DJANGO_DB_HOST"], "PORT": os.environ["DJANGO_DB_PORT"], "CONN_MAX_AGE": DJANGO_DB_CONN_MAX_AGE, @@ -250,7 +273,7 @@ "ENGINE": "django.db.backends.postgresql", "NAME": os.environ["DJANGO_DB_NAME_ANALYTICS"], "USER": os.environ["DJANGO_DB_USER_ANALYTICS"], - "PASSWORD": os.environ["DJANGO_DB_PASSWORD_ANALYTICS"], + "PASSWORD": _get_db_password("DJANGO_DB_PASSWORD_ANALYTICS"), "HOST": os.environ["DJANGO_DB_HOST_ANALYTICS"], "PORT": os.environ["DJANGO_DB_PORT_ANALYTICS"], "CONN_MAX_AGE": DJANGO_DB_CONN_MAX_AGE, @@ -261,9 +284,8 @@ # Task processor database — OPTIONALLY SEPARATED TASK_PROCESSOR_DATABASE_URL = env("TASK_PROCESSOR_DATABASE_URL", default=None) TASK_PROCESSOR_DATABASE_USER = env("TASK_PROCESSOR_DATABASE_USER", default="") -TASK_PROCESSOR_DATABASE_PASSWORD = env( - "TASK_PROCESSOR_DATABASE_PASSWORD", - default="", +TASK_PROCESSOR_DATABASE_PASSWORD = ( + _get_db_password("TASK_PROCESSOR_DATABASE_PASSWORD") or "", ) TASK_PROCESSOR_DATABASE_HOST = env("TASK_PROCESSOR_DATABASE_HOST", default="") TASK_PROCESSOR_DATABASE_PORT = env("TASK_PROCESSOR_DATABASE_PORT", default="") diff --git a/api/poetry.lock b/api/poetry.lock index 98a0f5a5dff6..0daa9c3d59ae 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -152,6 +152,45 @@ files = [ [package.dependencies] pycodestyle = ">=2.10.0" +[[package]] +name = "azure-core" +version = "1.39.0" +description = "Microsoft Azure Core Library for Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "azure_core-1.39.0-py3-none-any.whl", hash = "sha256:4ac7b70fab5438c3f68770649a78daf97833caa83827f91df9c14e0e0ea7d34f"}, + {file = "azure_core-1.39.0.tar.gz", hash = "sha256:8a90a562998dd44ce84597590fff6249701b98c0e8797c95fcdd695b54c35d74"}, +] + +[package.dependencies] +requests = ">=2.21.0" +typing-extensions = ">=4.6.0" + +[package.extras] +aio = ["aiohttp (>=3.0)"] +tracing = ["opentelemetry-api (>=1.26,<2.0)"] + +[[package]] +name = "azure-identity" +version = "1.25.3" +description = "Microsoft Azure Identity Library for Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "azure_identity-1.25.3-py3-none-any.whl", hash = "sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c"}, + {file = "azure_identity-1.25.3.tar.gz", hash = "sha256:ab23c0d63015f50b630ef6c6cf395e7262f439ce06e5d07a64e874c724f8d9e6"}, +] + +[package.dependencies] +azure-core = ">=1.31.0" +cryptography = ">=2.5" +msal = ">=1.35.1" +msal-extensions = ">=1.2.0" +typing-extensions = ">=4.0.0" + [[package]] name = "backoff" version = "2.2.1" @@ -2999,6 +3038,44 @@ server = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0) ssm = ["PyYAML (>=5.1)"] xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] +[[package]] +name = "msal" +version = "1.35.1" +description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "msal-1.35.1-py3-none-any.whl", hash = "sha256:8f4e82f34b10c19e326ec69f44dc6b30171f2f7098f3720ea8a9f0c11832caa3"}, + {file = "msal-1.35.1.tar.gz", hash = "sha256:70cac18ab80a053bff86219ba64cfe3da1f307c74b009e2da57ef040eb1b5656"}, +] + +[package.dependencies] +cryptography = ">=2.5,<49" +PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} +requests = ">=2.0.0,<3" + +[package.extras] +broker = ["pymsalruntime (>=0.14,<0.21) ; python_version >= \"3.8\" and platform_system == \"Windows\"", "pymsalruntime (>=0.17,<0.21) ; python_version >= \"3.8\" and platform_system == \"Darwin\"", "pymsalruntime (>=0.18,<0.21) ; python_version >= \"3.8\" and platform_system == \"Linux\""] + +[[package]] +name = "msal-extensions" +version = "1.3.1" +description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca"}, + {file = "msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4"}, +] + +[package.dependencies] +msal = ">=1.29,<2" + +[package.extras] +portalocker = ["portalocker (>=1.4,<4)"] + [[package]] name = "mypy" version = "1.15.0" @@ -5701,4 +5778,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">3.11,<3.14" -content-hash = "217ca3391ad82c2c46ef67930e3627b836b45a9818e15536eee91262e6a212c0" +content-hash = "dd5760c6178490bbb9bc4cef1b58302dcd3bc1ef5074e7cfde2d47d82fdf39b2" diff --git a/api/pyproject.toml b/api/pyproject.toml index 3c9e2065c453..2dad76ce418f 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -184,6 +184,7 @@ djangorestframework-simplejwt = "^5.5.1" structlog = "^24.4.0" prometheus-client = "^0.21.1" django_cockroachdb = "~4.2" +azure-identity = ">=1.19.0,<2" [tool.poetry.group.auth-controller] optional = true