Repository: rochacbruno/dynaconf (python)

Issue:
Add the ability to load only a specified list of keys from envvars
**Describe the bug**
Migrating from other configuration systems I'd need to configure `envvar_prefix` to empty string (`""`) so I can load any environment variable I used before. But it doesn't seem to work with _dynaconf_.

**To Reproduce**
Steps to reproduce the behavior:

1. Create a basic dynaconf configuration

In `config.py`:

```py
from dynaconf import Dynaconf

settings = Dynaconf(
    # This is the important line.
    envvar_prefix="",
)

print(settings.FOO)
```

2. Now create some settings file.

In `settings.py`:
```py
FOO = 'bar'
```

3. Finally try running.
```sh
export SETTINGS_FILE_FOR_DYNACONF=settings
FOO='ham' python config.py
```

You will get:
```
bar
```

**Expected behavior**

The print output states:
```
ham
```

This means the value from environment variable has overridden the default value from `settings.py` module

**Environment (please complete the following information):**
 - OS: Ubuntu Linux 20.04
 - Python version: 3.8.5
 - Dynaconf version: 3.1.2
 - Frameworks in use: none.

**Additional context**
Interestingly running in a "hacky" way changes the variable value:
```sh
_FOO='ham' python config.py
```

This is not what I'd expect though. The point is to modify the bare `FOO` variable.

Pull request description:
Fix: Environment variables filtering #470
Fixes #470 

Added new global option `ignore_unknown_envvars` / `IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF`.
Included is example app and some docs changes describing the usage.

Patch:
diff --git a/docs/envvars.md b/docs/envvars.md
index 9902be5..e6a4548 100644
--- a/docs/envvars.md
+++ b/docs/envvars.md
@@ -50,9 +50,9 @@ assert settings.STRING_NUM == "76"
 ```
 
 !!! tip
-    Dynaconf has multiple valid ways to access settings keys so it is compatible with your 
-    existing settings solution. You can access using `.notation`, `[item] indexing`, `.get method` 
-    and also it allows `.notation.nested` for data structures like dicts.  
+    Dynaconf has multiple valid ways to access settings keys so it is compatible with your
+    existing settings solution. You can access using `.notation`, `[item] indexing`, `.get method`
+    and also it allows `.notation.nested` for data structures like dicts.
     **Also variable access is case insensitive for the first level key**
 
 ## Custom Prefix
@@ -82,6 +82,14 @@ assert settings.db.port == 2000
 assert settings.db.scheme == 'main'
 ```
 
+If you don't want to use any prefix (load unprefixed variables) the correct
+way is to set it like so:
+```py
+from dynaconf import Dynaconf
+
+settings = Dynaconf(envvar_prefix=False)
+```
+
 ## Dot env
 
 When you have multiple environment variables to set it is useful to automate the definition
@@ -125,3 +133,21 @@ export PREFIX_PATH='@format {env{"HOME"}/.config/{this.DB_NAME}'
 export PREFIX_PATH='@jinja {{env.HOME}}/.config/{{this.DB_NAME}} | abspath'
 ```
 
+## Environment variables filtering
+
+All environment variables (naturally, accounting for prefix rules) will be
+used and pulled into the settings space.
+
+You can change that behaviour by enabling the filtering of unknown environment
+variables:
+
+```py
+from dynaconf import Dynaconf
+
+settings = Dynaconf(ignore_unknown_envvars=True)
+```
+
+In that case, only previously defined variables (e.g. specified in
+`settings_file`/`settings_files`, `preload` or `includes`) will be loaded
+from the environment. This way your setting space can avoid pollution
+or potentially loading of sensitive information.
diff --git a/dynaconf/default_settings.py b/dynaconf/default_settings.py
index c048e3c..66601b0 100644
--- a/dynaconf/default_settings.py
+++ b/dynaconf/default_settings.py
@@ -113,6 +113,15 @@ DEFAULT_ENV_FOR_DYNACONF = get("DEFAULT_ENV_FOR_DYNACONF", "DEFAULT")
 # This namespace is used for files and also envvars
 ENVVAR_PREFIX_FOR_DYNACONF = get("ENVVAR_PREFIX_FOR_DYNACONF", "DYNACONF")
 
+# By default all environment variables (filtered by `envvar_prefix`) will
+# be pulled into settings space. In case some of them are polluting the space,
+# setting this flag to `True` will change this behaviour.
+# Only "known" variables will be considered -- that is variables defined before
+# in settings files (or includes/preloads).
+IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF = get(
+    "IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF", False
+)
+
 # The default encoding to open settings files
 ENCODING_FOR_DYNACONF = get("ENCODING_FOR_DYNACONF", "utf-8")
 
diff --git a/dynaconf/loaders/env_loader.py b/dynaconf/loaders/env_loader.py
index 6f29a07..2eeddd8 100644
--- a/dynaconf/loaders/env_loader.py
+++ b/dynaconf/loaders/env_loader.py
@@ -40,6 +40,7 @@ def load_from_env(
         prefix = prefix.upper()
         env_ = f"{prefix}_"
 
+    # Load a single environment variable explicitly.
     if key:
         key = upperfy(key)
         value = environ.get(f"{env_}{key}")
@@ -50,15 +51,26 @@ def load_from_env(
                 obj[key] = parse_conf_data(
                     value, tomlfy=True, box_settings=obj
                 )
+
+    # Load environment variables in bulk (when matching).
     else:
+        # Only known variables should be loaded from environment.
+        ignore_unknown = obj.get('IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF')
+        known_keys = set(obj.store)
+
         trim_len = len(env_)
         data = {
             key[trim_len:]: parse_conf_data(
                 data, tomlfy=True, box_settings=obj
             )
             for key, data in environ.items()
-            if key.startswith(env_)
+            if key.startswith(env_) and not (
+                # Ignore environment variables that haven't been
+                # pre-defined in settings space.
+                ignore_unknown and key[trim_len:] not in known_keys
+            )
         }
+        # Update the settings space based on gathered data from environment.
         if data:
             obj.update(data, loader_identifier=identifier)
 
diff --git a/example/ignore_unknown_envvars/.env b/example/ignore_unknown_envvars/.env
new file mode 100644
index 0000000..7ef40de
--- /dev/null
+++ b/example/ignore_unknown_envvars/.env
@@ -0,0 +1,11 @@
+# Unwanted, unprefixed variable.
+SECRET=hunter1
+
+# Unwanted, prefixed variable.
+MYAPP_SECRET=hunter1
+
+# Wanted, unprefixed variable.
+VAR1=bar
+
+# Wanted, prefixed variable.
+MYAPP_VAR1=ham
\ No newline at end of file
diff --git a/example/ignore_unknown_envvars/app.py b/example/ignore_unknown_envvars/app.py
new file mode 100644
index 0000000..5d8b10f
--- /dev/null
+++ b/example/ignore_unknown_envvars/app.py
@@ -0,0 +1,25 @@
+from dynaconf import LazySettings
+
+
+# No prefix.
+settings = LazySettings(
+    settings_file="settings.toml",
+    load_dotenv=True,
+    ignore_unknown_envvars=True,
+    envvar_prefix=False,
+)
+
+assert settings.VAR1 == "bar"
+assert not settings.get("SECRET")
+
+
+# MYAPP_ prefix.
+settings = LazySettings(
+    settings_file="settings.toml",
+    load_dotenv=True,
+    ignore_unknown_envvars=True,
+    envvar_prefix="MYAPP",
+)
+
+assert settings.VAR1 == "ham"
+assert not settings.get("SECRET")
diff --git a/example/ignore_unknown_envvars/settings.toml b/example/ignore_unknown_envvars/settings.toml
new file mode 100644
index 0000000..66f9cc3
--- /dev/null
+++ b/example/ignore_unknown_envvars/settings.toml
@@ -0,0 +1,3 @@
+# Here pre-defining known variables.
+var1 = "foo"
+var2 = "qux"


Test patch:
diff --git a/tests/test_env_loader.py b/tests/test_env_loader.py
index 344e07d..3dc98b9 100644
--- a/tests/test_env_loader.py
+++ b/tests/test_env_loader.py
@@ -22,7 +22,7 @@ environ["DYNACONF_SHOULDBEFRESHINCONTEXT"] = "first"
 environ["DYNACONF_VALUE"] = "@float 42.1"
 
 # environ['FRESH_VARS_FOR_DYNACONF'] = '@json ["MUSTBEALWAYSFRESH"]'
-# settings.configure()
+# settings.configure(IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF=True)
 settings.configure(
     FRESH_VARS_FOR_DYNACONF=["MUSTBEALWAYSFRESH"],
     ROOT_PATH_FOR_DYNACONF=os.path.dirname(os.path.abspath(__file__)),
@@ -439,3 +439,53 @@ def test_load_dunder(clean_env):
         silent=True,
     )
     assert "DATABASES" not in settings
+
+
+def test_filtering_unknown_variables():
+    # Predefine some known variable.
+    settings.MYCONFIG = "bar"
+    # Enable environment filtering.
+    settings.IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF = True
+
+    # Pollute the environment.
+    environ["IGNOREME"] = "foo"
+
+    load_from_env(
+        obj=settings,
+        prefix=False,
+        key=None,
+        silent=True,
+        identifier="env_global",
+        env=False,
+    )
+
+    # Verify the filter works.
+    assert not settings.get("IGNOREME")
+    # Smoke test.
+    assert settings.get("MYCONFIG") == "bar"
+
+
+def test_filtering_unknown_variables_with_prefix():
+    # Predefine some known variable.
+    settings.MYCONFIG = "bar"
+    # Enable environment filtering.
+    settings.IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF = True
+
+    # Pollute the environment.
+    environ["APP_IGNOREME"] = "foo"
+    # Also change legitimate variable.
+    environ["APP_MYCONFIG"] = "ham"
+
+    load_from_env(
+        obj=settings,
+        prefix="APP",
+        key=None,
+        silent=True,
+        identifier="env_global",
+        env=False,
+    )
+
+    # Verify the filter works.
+    assert not settings.get("IGNOREME")
+    # Smoke test.
+    assert settings.get("MYCONFIG") == "ham"