Repository: samuelcolvin/pydantic (python)

Issue:
Pydantic fails to read content of files in secrets_dir on Linux
### Checks

* [x] I added a descriptive title to this issue
* [x] I have searched (google, github) for similar issues and couldn't find anything
* [x] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->

# Bug

To reproduce, on Linux OS: 
- build a config manager that subclasses `BaseSettings`
- add `secrets_dir` config pointing to dir with text file(s), as per docs
- add class attributes to use the files in `secrets_dir`, as per docs
- run the code and inspect/print the attributes
- observe that vars coming from .env file work fine, but those from `secrets_dir` are None, or error if not Optional, and there is no user warning for the `secrets_dir` itself

Repeat the same on MacOS - it should work OK, the content is read correctly.

I have added a repo with Dockerfile replicating the setup: https://github.com/dimitri-b/pydantic-secrets-dir-issue

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.8.2
            pydantic compiled: True
                 install path: /usr/local/lib/python3.8/site-packages/pydantic
               python version: 3.8.12 (default, Sep  3 2021, 02:35:42)  [GCC 8.3.0]
                     platform: Linux-5.10.47-linuxkit-x86_64-with-glibc2.2.5
     optional deps. installed: ['dotenv', 'typing-extensions']
```
<!-- or if you're using pydantic prior to v1.3, manually include: OS, python version and pydantic version -->

<!-- Please read the [docs](https://pydantic-docs.helpmanual.io/) and search through issues to
confirm your bug hasn't already been reported. -->

<!-- Where possible please include a self-contained code snippet describing your bug: -->


```py
import pydantic

class SettingsLocal(pydantic.BaseSettings):
    NORMAL_VAR: Optional[str]
    SECRET_VAR: Optional[str]

    class Config:
        env_file = "./.env"
        secrets_dir = "./secrets-in-project"
...
```


Pull request:
fix(secret): uppercase in filename on linux
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->
<!-- See https://pydantic-docs.helpmanual.io/contributing/ for help on Contributing -->

## Change Summary

Hi,

It's my first Pull request :)
I worked on the problem of path and secret on Linux

## Related issue number

<!-- Are there any issues opened that will be resolved by merging this change? -->

fix #3273

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)
* [x] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d997845c1..1eeafd90b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -223,7 +223,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        mypy-version: ['0.910', '0.921', '0.931', '0.942', '0.950']
+        mypy-version: ['0.910', '0.921', '0.931', '0.942', '0.950', '0.960']
 
     steps:
     - uses: actions/checkout@v3
diff --git a/changes/3273-JeanArhancet.md b/changes/3273-JeanArhancet.md
new file mode 100644
index 000000000..2d8b23be1
--- /dev/null
+++ b/changes/3273-JeanArhancet.md
@@ -0,0 +1,1 @@
+Update `SecretsSettingsSource` to respect `config.case_sensitive`
\ No newline at end of file
diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py
index 35b96970f..73945e31e 100644
--- a/pydantic/env_settings.py
+++ b/pydantic/env_settings.py
@@ -269,7 +269,11 @@ class SecretsSettingsSource:
 
         for field in settings.__fields__.values():
             for env_name in field.field_info.extra['env_names']:
-                path = secrets_path / env_name
+                path = find_case_path(secrets_path, env_name, settings.__config__.case_sensitive)
+                if not path:
+                    # path does not exist, we curently don't return a warning for this
+                    continue
+
                 if path.is_file():
                     secret_value = path.read_text().strip()
                     if field.is_complex():
@@ -279,12 +283,11 @@ class SecretsSettingsSource:
                             raise SettingsError(f'error parsing JSON for "{env_name}"') from e
 
                     secrets[field.alias] = secret_value
-                elif path.exists():
+                else:
                     warnings.warn(
                         f'attempted to load secret file "{path}" but found a {path_type(path)} instead.',
                         stacklevel=4,
                     )
-
         return secrets
 
     def __repr__(self) -> str:
@@ -304,3 +307,15 @@ def read_env_file(
         return {k.lower(): v for k, v in file_vars.items()}
     else:
         return file_vars
+
+
+def find_case_path(dir_path: Path, file_name: str, case_sensitive: bool) -> Optional[Path]:
+    """
+    Find a file within path's directory matching filename, optionally ignoring case.
+    """
+    for f in dir_path.iterdir():
+        if f.name == file_name:
+            return f
+        elif not case_sensitive and f.name.lower() == file_name.lower():
+            return f
+    return None


Test patch:
diff --git a/tests/test_settings.py b/tests/test_settings.py
index c11cdbb73..1e85a546e 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -890,6 +890,33 @@ def test_secrets_path(tmp_path):
     assert Settings().dict() == {'foo': 'foo_secret_value_str'}
 
 
+def test_secrets_case_sensitive(tmp_path):
+    (tmp_path / 'SECRET_VAR').write_text('foo_env_value_str')
+
+    class Settings(BaseSettings):
+        secret_var: Optional[str]
+
+        class Config:
+            secrets_dir = tmp_path
+            case_sensitive = True
+
+    assert Settings().dict() == {'secret_var': None}
+
+
+def test_secrets_case_insensitive(tmp_path):
+    (tmp_path / 'SECRET_VAR').write_text('foo_env_value_str')
+
+    class Settings(BaseSettings):
+        secret_var: Optional[str]
+
+        class Config:
+            secrets_dir = tmp_path
+            case_sensitive = False
+
+    settings = Settings().dict()
+    assert settings == {'secret_var': 'foo_env_value_str'}
+
+
 def test_secrets_path_url(tmp_path):
     (tmp_path / 'foo').write_text('http://www.example.com')
     (tmp_path / 'bar').write_text('snap')
@@ -940,6 +967,7 @@ def test_secrets_missing(tmp_path):
 
     with pytest.raises(ValidationError) as exc_info:
         Settings()
+
     assert exc_info.value.errors() == [{'loc': ('foo',), 'msg': 'field required', 'type': 'value_error.missing'}]