Skip to content

Commit 4a6ffca

Browse files
makukhahramezani
andauthored
Add NestedSecretsSettings source (#690)
Co-authored-by: Hasan Ramezani <[email protected]>
1 parent 7a6e96e commit 4a6ffca

File tree

7 files changed

+1708
-851
lines changed

7 files changed

+1708
-851
lines changed

docs/index.md

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1859,6 +1859,248 @@ Last, run your application inside a Docker container and supply your newly creat
18591859
docker service create --name pydantic-with-secrets --secret my_secret_data pydantic-app:latest
18601860
```
18611861

1862+
## Nested Secrets
1863+
1864+
The default secrets implementation, `SecretsSettingsSource`, has behaviour that is not always desired or sufficient.
1865+
For example, the default implementation does not support secret fields in nested submodels.
1866+
1867+
`NestedSecretsSettingsSource` can be used as a drop-in replacement to `SecretsSettingsSource` to adjust the default behaviour.
1868+
All differences are summarized in the table below.
1869+
1870+
| `SecretsSettingsSource` | `NestedSecretsSettingsSourcee` |
1871+
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
1872+
| Secret fields must belong to a top level model. | Secrets can be fields of nested models. |
1873+
| Secret files can be placed in `secrets_dir`s only. | Secret files can be placed in subdirectories for nested models. |
1874+
| Secret files discovery is based on the same configuration options that are used by `EnvSettingsSource`: `case_sensitive`, `env_nested_delimiter`, `env_prefix`. | Default options are respected, but can be overridden with `secrets_case_sensitive`, `secrets_nested_delimiter`, `secrets_prefix`. |
1875+
| When `secrets_dir` is missing on the file system, a warning is generated. | Use `secrets_dir_missing` options to choose whether to issue warning, raise error, or silently ignore. |
1876+
1877+
### Use Case: Plain Directory Layout
1878+
1879+
```text
1880+
📂 secrets
1881+
├── 📄 app_key
1882+
└── 📄 db_passwd
1883+
```
1884+
1885+
In the example below, secrets nested delimiter `'_'` is different from env nested delimiter `'__'`.
1886+
Value for `Settings.db.user` can be passed in env variable `MY_DB__USER`.
1887+
1888+
```py
1889+
from pydantic import BaseModel, SecretStr
1890+
1891+
from pydantic_settings import (
1892+
BaseSettings,
1893+
NestedSecretsSettingsSource,
1894+
SettingsConfigDict,
1895+
)
1896+
1897+
1898+
class AppSettings(BaseModel):
1899+
key: SecretStr
1900+
1901+
1902+
class DbSettings(BaseModel):
1903+
user: str
1904+
passwd: SecretStr
1905+
1906+
1907+
class Settings(BaseSettings):
1908+
app: AppSettings
1909+
db: DbSettings
1910+
1911+
model_config = SettingsConfigDict(
1912+
env_prefix='MY_',
1913+
env_nested_delimiter='__',
1914+
secrets_dir='secrets',
1915+
secrets_nested_delimiter='_',
1916+
)
1917+
1918+
@classmethod
1919+
def settings_customise_sources(
1920+
cls,
1921+
settings_cls,
1922+
init_settings,
1923+
env_settings,
1924+
dotenv_settings,
1925+
file_secret_settings,
1926+
):
1927+
return (
1928+
init_settings,
1929+
env_settings,
1930+
dotenv_settings,
1931+
NestedSecretsSettingsSource(file_secret_settings),
1932+
)
1933+
```
1934+
1935+
### Use Case: Nested Directory Layout
1936+
1937+
```text
1938+
📂 secrets
1939+
├── 📂 app
1940+
│ └── 📄 key
1941+
└── 📂 db
1942+
└── 📄 passwd
1943+
```
1944+
```py
1945+
from pydantic import BaseModel, SecretStr
1946+
1947+
from pydantic_settings import (
1948+
BaseSettings,
1949+
NestedSecretsSettingsSource,
1950+
SettingsConfigDict,
1951+
)
1952+
1953+
1954+
class AppSettings(BaseModel):
1955+
key: SecretStr
1956+
1957+
1958+
class DbSettings(BaseModel):
1959+
user: str
1960+
passwd: SecretStr
1961+
1962+
1963+
class Settings(BaseSettings):
1964+
app: AppSettings
1965+
db: DbSettings
1966+
1967+
model_config = SettingsConfigDict(
1968+
env_prefix='MY_',
1969+
env_nested_delimiter='__',
1970+
secrets_dir='secrets',
1971+
secrets_nested_subdir=True,
1972+
)
1973+
1974+
@classmethod
1975+
def settings_customise_sources(
1976+
cls,
1977+
settings_cls,
1978+
init_settings,
1979+
env_settings,
1980+
dotenv_settings,
1981+
file_secret_settings,
1982+
):
1983+
return (
1984+
init_settings,
1985+
env_settings,
1986+
dotenv_settings,
1987+
NestedSecretsSettingsSource(file_secret_settings),
1988+
)
1989+
```
1990+
1991+
### Use Case: Multiple Nested Directories
1992+
1993+
```text
1994+
📂 secrets
1995+
├── 📂 default
1996+
│ ├── 📂 app
1997+
│ │ └── 📄 key
1998+
│ └── 📂 db
1999+
│ └── 📄 passwd
2000+
└── 📂 override
2001+
├── 📂 app
2002+
│ └── 📄 key
2003+
└── 📂 db
2004+
└── 📄 passwd
2005+
```
2006+
```py
2007+
from pydantic import BaseModel, SecretStr
2008+
2009+
from pydantic_settings import (
2010+
BaseSettings,
2011+
NestedSecretsSettingsSource,
2012+
SettingsConfigDict,
2013+
)
2014+
2015+
2016+
class AppSettings(BaseModel):
2017+
key: SecretStr
2018+
2019+
2020+
class DbSettings(BaseModel):
2021+
user: str
2022+
passwd: SecretStr
2023+
2024+
2025+
class Settings(BaseSettings):
2026+
app: AppSettings
2027+
db: DbSettings
2028+
2029+
model_config = SettingsConfigDict(
2030+
env_prefix='MY_',
2031+
env_nested_delimiter='__',
2032+
secrets_dir=['secrets/default', 'secrets/override'],
2033+
secrets_nested_subdir=True,
2034+
)
2035+
2036+
@classmethod
2037+
def settings_customise_sources(
2038+
cls,
2039+
settings_cls,
2040+
init_settings,
2041+
env_settings,
2042+
dotenv_settings,
2043+
file_secret_settings,
2044+
):
2045+
return (
2046+
init_settings,
2047+
env_settings,
2048+
dotenv_settings,
2049+
NestedSecretsSettingsSource(file_secret_settings),
2050+
)
2051+
```
2052+
2053+
### Configuration Options
2054+
2055+
#### secrets_dir
2056+
2057+
Path to secrets directory, same as `SecretsSettingsSource.secrets_dir`. If `list`, the last match wins.
2058+
If `secrets_dir` is passed in both source constructor and model config, values are not merged (constructor wins).
2059+
2060+
#### secrets_dir_missing
2061+
2062+
If `secrets_dir` does not exist, original `SecretsSettingsSource` issues a warning.
2063+
However, this may be undesirable, for example if we don't mount Docker Secrets in e.g. dev environment.
2064+
Use `secrets_dir_missing` to choose:
2065+
2066+
* `'ok'` — do nothing if `secrets_dir` does not exist
2067+
* `'warn'` (default) — print warning, same as `SecretsSettingsSource`
2068+
* `'error'` — raise `SettingsError`
2069+
2070+
If multiple `secrets_dir` passed, the same `secrets_dir_missing` action applies to each of them.
2071+
2072+
#### secrets_dir_max_size
2073+
2074+
Limit the size of `secrets_dir` for security reasons, defaults to `SECRETS_DIR_MAX_SIZE` equal to 16 MiB.
2075+
2076+
`NestedSecretsSettingsSource` is a thin wrapper around `EnvSettingsSource`,
2077+
which loads all potential secrets on initialization. This could lead to `MemoryError` if we mount
2078+
a large file under `secrets_dir`.
2079+
2080+
If multiple `secrets_dir` passed, the limit applies to each directory independently.
2081+
2082+
#### secrets_case_sensitive
2083+
2084+
Same as `case_sensitive`, but works for secrets only. If not specified, defaults to `case_sensitive`.
2085+
2086+
#### secrets_nested_delimiter
2087+
2088+
Same as `env_nested_delimiter`, but works for secrets only. If not specified, defaults to `env_nested_delimiter`.
2089+
This option is used to implement _nested secrets directory_ layout and allows to do even nasty things
2090+
like `/run/secrets/model/delim/nested1/delim/nested2`.
2091+
2092+
#### secrets_nested_subdir
2093+
2094+
Boolean flag to turn on _nested secrets directory_ mode, `False` by default. If `True`, sets `secrets_nested_delimiter`
2095+
to `os.sep`. Raises `SettingsError` if `secrets_nested_delimiter` is already specified.
2096+
2097+
#### secrets_prefix
2098+
2099+
Secret path prefix, similar to `env_prefix`, but works for secrets only. Defaults to `env_prefix`
2100+
if not specified. Works in both plain and nested directory modes, like
2101+
`'/run/secrets/prefix_model__nested'` and `'/run/secrets/prefix_model/nested'`.
2102+
2103+
18622104
## AWS Secrets Manager
18632105

18642106
You must set one parameter:

pydantic_settings/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
GoogleSecretManagerSettingsSource,
1919
InitSettingsSource,
2020
JsonConfigSettingsSource,
21+
NestedSecretsSettingsSource,
2122
NoDecode,
2223
PydanticBaseSettingsSource,
2324
PyprojectTomlConfigSettingsSource,
@@ -48,6 +49,7 @@
4849
'GoogleSecretManagerSettingsSource',
4950
'InitSettingsSource',
5051
'JsonConfigSettingsSource',
52+
'NestedSecretsSettingsSource',
5153
'NoDecode',
5254
'PydanticBaseSettingsSource',
5355
'PyprojectTomlConfigSettingsSource',

pydantic_settings/sources/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .providers.env import EnvSettingsSource
2626
from .providers.gcp import GoogleSecretManagerSettingsSource
2727
from .providers.json import JsonConfigSettingsSource
28+
from .providers.nested_secrets import NestedSecretsSettingsSource
2829
from .providers.pyproject import PyprojectTomlConfigSettingsSource
2930
from .providers.secrets import SecretsSettingsSource
3031
from .providers.toml import TomlConfigSettingsSource
@@ -53,6 +54,7 @@
5354
'GoogleSecretManagerSettingsSource',
5455
'InitSettingsSource',
5556
'JsonConfigSettingsSource',
57+
'NestedSecretsSettingsSource',
5658
'NoDecode',
5759
'PathType',
5860
'PydanticBaseEnvSettingsSource',

0 commit comments

Comments
 (0)