Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Azure Key Vault settings source #272

Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
8cab66d
Add Azure Key Vault settings source
AndreuCodina Apr 21, 2024
ab2c17f
Add Azure Key Vault settings source
AndreuCodina Apr 21, 2024
192da43
Add Azure Key Vault settings source
AndreuCodina Apr 21, 2024
8202ebe
Add optional dependencies
AndreuCodina Apr 21, 2024
357df2b
Fix lint errors
AndreuCodina Apr 21, 2024
30681de
Fix lint error
AndreuCodina Apr 21, 2024
468a55b
Fix mypy errors
AndreuCodina Apr 21, 2024
221731f
Imports inside a function
AndreuCodina Apr 21, 2024
92b6c34
Fix lint errors
AndreuCodina Apr 21, 2024
3e74b93
Merge branch 'main' into feature/azure-key-vault-source-settings-source
AndreuCodina Apr 27, 2024
574debf
make refresh-lockfiles
AndreuCodina Apr 27, 2024
9452ba3
make refresh-lockfiles
AndreuCodina Apr 27, 2024
df7e900
Add unit tests
AndreuCodina Apr 28, 2024
be0e201
Fix lint errors
AndreuCodina Apr 28, 2024
9d08fa1
Merge branch 'main' into feature/azure-key-vault-source-settings-source
AndreuCodina Apr 28, 2024
46de93c
make refresh-lockfiles with Python 3.8
AndreuCodina Apr 28, 2024
5edb8f5
Resolve PR comments
AndreuCodina Apr 28, 2024
a4157e3
Merge branch 'pydantic:main' into feature/azure-key-vault-source-sett…
AndreuCodina May 14, 2024
4b3df3f
Add tests
AndreuCodina May 14, 2024
643264f
Fix lint errors
AndreuCodina May 14, 2024
794487c
Fix lint errors
AndreuCodina May 14, 2024
176233d
Merge branch 'pydantic:main' into feature/azure-key-vault-source-sett…
AndreuCodina Jun 7, 2024
24a1fed
Add alias support
AndreuCodina Jun 7, 2024
468bf15
Fix CI errors
AndreuCodina Jun 7, 2024
97ade35
Fix CI errors
AndreuCodina Jun 7, 2024
a6cb2b8
make refresh-lockfiles
AndreuCodina Jun 7, 2024
9dbb8f0
Typo
AndreuCodina Jun 7, 2024
3c2f276
Merge branch 'main' into feature/azure-key-vault-source-settings-source
AndreuCodina Jun 12, 2024
f1a3950
Improve documentation
AndreuCodina Jun 12, 2024
4a09d62
Remove environment variable
AndreuCodina Jun 13, 2024
4cbe55e
Fix lint error
AndreuCodina Jun 13, 2024
decfcb9
Inherit from EnvSettingsSource
AndreuCodina Jun 20, 2024
aebe421
Merge branch 'main' into feature/azure-key-vault-source-settings-source
AndreuCodina Jun 20, 2024
a8d7366
Fix lint errors
AndreuCodina Jun 20, 2024
991a417
make refresh-lockfile
AndreuCodina Jun 20, 2024
b163475
Remove not needed code
AndreuCodina Jun 21, 2024
f556d6f
Remove prefix support
AndreuCodina Jun 21, 2024
7f6f742
Remove unused code
AndreuCodina Jun 21, 2024
5a05c59
Move documentation
AndreuCodina Jun 24, 2024
4f4175c
Merge branch 'main' into feature/azure-key-vault-source-settings-source
AndreuCodina Jun 24, 2024
f3f14b2
Remove line
AndreuCodina Jul 2, 2024
6b7d057
Merge branch 'main' into feature/azure-key-vault-source-settings-source
AndreuCodina Jul 4, 2024
db1e42e
Merge branch 'main' into feature/azure-key-vault-source-settings-source
AndreuCodina Jul 7, 2024
c57ad80
Merge branch 'main' into feature/azure-key-vault-source-settings-source
AndreuCodina Jul 12, 2024
2bafd5d
Add AzureKeyVaultMapping
AndreuCodina Jul 15, 2024
e95d900
Fix tests
AndreuCodina Jul 15, 2024
df2ad20
Resolve comments
AndreuCodina Jul 15, 2024
df81e61
Update docs/index.md
hramezani Jul 18, 2024
1142877
Update docs/index.md
hramezani Jul 18, 2024
86eb90c
Update docs/index.md
hramezani Jul 18, 2024
5a682f5
Resolve comment
AndreuCodina Jul 18, 2024
5b875a6
Resolve comment
AndreuCodina Jul 18, 2024
1a79594
Update docs/index.md
hramezani Jul 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ refresh-lockfiles:
find requirements/ -name '*.txt' ! -name 'all.txt' -type f -delete
pip-compile -q --no-emit-index-url --resolver backtracking -o requirements/linting.txt requirements/linting.in
pip-compile -q --no-emit-index-url --resolver backtracking -o requirements/testing.txt requirements/testing.in
pip-compile -q --no-emit-index-url --resolver backtracking --extra toml --extra yaml -o requirements/pyproject.txt pyproject.toml
pip-compile -q --no-emit-index-url --resolver backtracking --extra toml --extra yaml --extra azure-key-vault -o requirements/pyproject.txt pyproject.toml
pip install --dry-run -r requirements/all.txt

.PHONY: format
Expand Down
55 changes: 55 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,61 @@ Last, run your application inside a Docker container and supply your newly creat
docker service create --name pydantic-with-secrets --secret my_secret_data pydantic-app:latest
```

## Azure Key Vault

You must set two parameters:

- `url`: For example, `https://my-resource.vault.azure.net/`.
- `credential`: If you use `DefaultAzureCredential`, in local you can execute `az login` to get your identity credentials. The identity must have a role assignment (the recommended one is `Key Vault Secrets User`), so you can access the secrets.

You must have the same naming convention in the field name as in the Key Vault secret name. For example, if the secret is named `SqlServerPassword`, the field name must be the same. You can use an alias too.

In Key Vault, nested models are supported with the `--` separator. For example, `SqlServer--Password`.

Key Vault arrays (e.g. `MySecret--0`, `MySecret--1`) are not supported.

```python
import os

from azure.identity import DefaultAzureCredential
from pydantic import BaseModel

from pydantic_settings import BaseSettings, PydanticBaseSettingsSource
from pydantic_settings.sources import AzureKeyVaultSettingsSource


class SubModel(BaseModel):
a: str


class AzureKeyVaultSettings(BaseSettings):
foo: str
bar: int
sub: SubModel

@classmethod
def settings_customise_sources(
cls,
settings_cls: type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
az_key_vault_settings = AzureKeyVaultSettingsSource(
settings_cls,
os.environ['AZURE_KEY_VAULT_URL'],
DefaultAzureCredential(),
)
return (
init_settings,
env_settings,
dotenv_settings,
file_secret_settings,
az_key_vault_settings,
)
hramezani marked this conversation as resolved.
Show resolved Hide resolved
```

## Other settings source

Other settings sources are available for common configuration files:
Expand Down
81 changes: 81 additions & 0 deletions pydantic_settings/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
Any,
Callable,
Generic,
Iterator,
List,
Mapping,
Optional,
Sequence,
Tuple,
TypeVar,
Expand Down Expand Up @@ -83,6 +85,21 @@ def import_toml() -> None:
import tomllib


def import_azure_key_vault() -> None:
global TokenCredential
global SecretClient
global ResourceNotFoundError

try:
from azure.core.credentials import TokenCredential
from azure.core.exceptions import ResourceNotFoundError
from azure.keyvault.secrets import SecretClient
except ImportError as e:
raise ImportError(
'Azure Key Vault dependencies are not installed, run `pip install pydantic-settings[azure-key-vault]`'
) from e


DotenvType = Union[Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...]]
PathType = Union[Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...]]
DEFAULT_PATH: PathType = Path('')
Expand Down Expand Up @@ -1725,6 +1742,70 @@ def _read_file(self, file_path: Path) -> dict[str, Any]:
return yaml.safe_load(yaml_file) or {}


class AzureKeyVaultMapping(Mapping[str, Optional[str]]):
_loaded_secrets: dict[str, str | None]
_secret_client: SecretClient # type: ignore
_secret_names: list[str]

def __init__(
self,
secret_client: SecretClient, # type: ignore
) -> None:
self._loaded_secrets = {}
self._secret_client = secret_client
self._secret_names: list[str] = [secret.name for secret in self._secret_client.list_properties_of_secrets()]

def __getitem__(self, key: str) -> str | None:
if key not in self._loaded_secrets:
try:
self._loaded_secrets[key] = self._secret_client.get_secret(key).value
except ResourceNotFoundError: # type: ignore
raise KeyError(key)

return self._loaded_secrets[key]

def __len__(self) -> int:
return len(self._secret_names)

def __iter__(self) -> Iterator[str]:
return iter(self._secret_names)


class AzureKeyVaultSettingsSource(EnvSettingsSource):
_url: str
_credential: TokenCredential # type: ignore
_secret_client: SecretClient # type: ignore

def __init__(
self,
settings_cls: type[BaseSettings],
url: str,
credential: TokenCredential, # type: ignore
env_prefix: str | None = None,
env_parse_none_str: str | None = None,
env_parse_enums: bool | None = None,
) -> None:
import_azure_key_vault()
self._url = url
self._credential = credential
super().__init__(
settings_cls,
case_sensitive=True,
env_prefix=env_prefix,
env_nested_delimiter='--',
env_ignore_empty=False,
env_parse_none_str=env_parse_none_str,
env_parse_enums=env_parse_enums,
)

def _load_env_vars(self) -> Mapping[str, Optional[str]]:
secret_client = SecretClient(vault_url=self._url, credential=self._credential) # type: ignore
return AzureKeyVaultMapping(secret_client)

def __repr__(self) -> str:
return f'AzureKeyVaultSettingsSource(url={self._url!r}, ' f'env_nested_delimiter={self.env_nested_delimiter!r})'


def _get_env_var_key(key: str, case_sensitive: bool = False) -> str:
return key if case_sensitive else key.lower()

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dynamic = ['version']
[project.optional-dependencies]
yaml = ["pyyaml>=6.0.1"]
toml = ["tomli>=2.0.1"]
azure-key-vault = ["azure-keyvault-secrets>=4.8.0", "azure-identity>=1.16.0"]

[project.urls]
Homepage = 'https://github.com/pydantic/pydantic-settings'
Expand Down
23 changes: 10 additions & 13 deletions requirements/linting.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,43 @@
#
# pip-compile --no-emit-index-url --output-file=requirements/linting.txt requirements/linting.in
#
black==24.4.0
black==24.4.2
# via -r requirements/linting.in
cfgv==3.4.0
# via pre-commit
click==8.1.7
# via black
distlib==0.3.8
# via virtualenv
filelock==3.13.4
filelock==3.15.3
# via virtualenv
identify==2.5.36
# via pre-commit
mypy==1.9.0
mypy==1.10.0
# via -r requirements/linting.in
mypy-extensions==1.0.0
# via
# black
# mypy
nodeenv==1.8.0
nodeenv==1.9.1
# via pre-commit
packaging==24.0
packaging==24.1
# via black
pathspec==0.12.1
# via black
platformdirs==4.2.0
platformdirs==4.2.2
# via
# black
# virtualenv
pre-commit==3.5.0
# via -r requirements/linting.in
pyupgrade==3.15.2
pyupgrade==3.16.0
# via -r requirements/linting.in
pyyaml==6.0.1
# via
# -r requirements/linting.in
# pre-commit
ruff==0.4.1
ruff==0.4.10
# via -r requirements/linting.in
tokenize-rt==5.2.0
# via pyupgrade
Expand All @@ -50,12 +50,9 @@ tomli==2.0.1
# mypy
types-pyyaml==6.0.12.20240311
# via -r requirements/linting.in
typing-extensions==4.11.0
typing-extensions==4.12.2
# via
# black
# mypy
virtualenv==20.25.3
virtualenv==20.26.2
# via pre-commit

# The following packages are considered to be unsafe in a requirements file:
# setuptools
62 changes: 57 additions & 5 deletions requirements/pyproject.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,74 @@
# This file is autogenerated by pip-compile with Python 3.8
# by the following command:
#
# pip-compile --extra=toml --extra=yaml --no-emit-index-url --output-file=requirements/pyproject.txt pyproject.toml
# pip-compile --extra=azure-key-vault --extra=toml --extra=yaml --no-emit-index-url --output-file=requirements/pyproject.txt pyproject.toml
#
annotated-types==0.6.0
annotated-types==0.7.0
# via pydantic
pydantic==2.7.0
azure-core==1.30.2
# via
# azure-identity
# azure-keyvault-secrets
azure-identity==1.17.0
# via pydantic-settings (pyproject.toml)
azure-keyvault-secrets==4.8.0
# via pydantic-settings (pyproject.toml)
certifi==2024.6.2
# via requests
cffi==1.16.0
# via cryptography
charset-normalizer==3.3.2
# via requests
cryptography==42.0.8
# via
# azure-identity
# msal
# pyjwt
idna==3.7
# via requests
isodate==0.6.1
# via azure-keyvault-secrets
msal==1.28.1
# via
# azure-identity
# msal-extensions
msal-extensions==1.1.0
# via azure-identity
packaging==24.1
# via msal-extensions
portalocker==2.8.2
# via msal-extensions
pycparser==2.22
# via cffi
pydantic==2.7.4
# via pydantic-settings (pyproject.toml)
pydantic-core==2.18.1
pydantic-core==2.18.4
# via pydantic
pyjwt[crypto]==2.8.0
# via
# msal
# pyjwt
python-dotenv==1.0.1
# via pydantic-settings (pyproject.toml)
pyyaml==6.0.1
# via pydantic-settings (pyproject.toml)
requests==2.32.3
# via
# azure-core
# msal
six==1.16.0
# via
# azure-core
# isodate
tomli==2.0.1
# via pydantic-settings (pyproject.toml)
typing-extensions==4.11.0
typing-extensions==4.12.2
# via
# annotated-types
# azure-core
# azure-identity
# azure-keyvault-secrets
# pydantic
# pydantic-core
urllib3==2.2.2
# via requests
16 changes: 8 additions & 8 deletions requirements/testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
#
# pip-compile --no-emit-index-url --output-file=requirements/testing.txt requirements/testing.in
#
black==24.4.0
black==24.4.2
# via pytest-examples
click==8.1.7
# via black
coverage[toml]==7.4.4
coverage[toml]==7.5.3
# via -r requirements/testing.in
exceptiongroup==1.2.1
# via pytest
Expand All @@ -20,19 +20,19 @@ mdurl==0.1.2
# via markdown-it-py
mypy-extensions==1.0.0
# via black
packaging==24.0
packaging==24.1
# via
# black
# pytest
pathspec==0.12.1
# via black
platformdirs==4.2.0
platformdirs==4.2.2
# via black
pluggy==1.5.0
# via pytest
pygments==2.17.2
pygments==2.18.0
# via rich
pytest==8.1.1
pytest==8.2.2
# via
# -r requirements/testing.in
# pytest-examples
Expand All @@ -46,14 +46,14 @@ pytest-pretty==1.2.0
# via -r requirements/testing.in
rich==13.7.1
# via pytest-pretty
ruff==0.4.1
ruff==0.4.10
# via pytest-examples
tomli==2.0.1
# via
# black
# coverage
# pytest
typing-extensions==4.11.0
typing-extensions==4.12.2
# via
# black
# rich
Loading
Loading