Skip to content

Commit 8daddcb

Browse files
Merge pull request #316 from skoranda/yaml_environment_variable_parsing
Pull YAML configuration values from environment
2 parents 1c02692 + f9f1b5c commit 8daddcb

File tree

9 files changed

+146
-8
lines changed

9 files changed

+146
-8
lines changed

doc/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,46 @@ apt-get install libffi-dev libssl-dev xmlsec1
2929
Alternatively the application can be installed directly from PyPI (`pip install satosa`), or the [Docker image](https://hub.docker.com/r/satosa/) can be used.
3030

3131
# Configuration
32+
SATOSA is configured using YAML.
33+
3234
All default configuration files, as well as an example WSGI application for the proxy, can be found
3335
in the [example directory](../example).
3436

37+
The default YAML syntax is extended to include the capability to resolve
38+
environment variables. The following tags are used to achieve this:
39+
40+
* The `!ENV` tag
41+
42+
The `!ENV` tag is followed by a string that denotes the environment variable
43+
name. It will be replaced by the value of the environment variable with the
44+
same name.
45+
46+
In the example below `LDAP_BIND_PASSWORD` will, at runtime, be replaced with
47+
the value from the process environment variable of the same name. If the
48+
process environment has been set with `LDAP_BIND_PASSWORD=secret_password` then
49+
the configuration value for `bind_password` will be `secret_password`.
50+
51+
```
52+
bind_password: !ENV LDAP_BIND_PASSWORD
53+
```
54+
55+
* The `!ENVFILE` tag
56+
57+
The `!ENVFILE` tag is followed by a string that denotes the environment
58+
variable name. It will be replaced by the value of the environment variable
59+
with the same name.
60+
61+
In the example below `LDAP_BIND_PASSWORD_FILE` will, at runtime, be replaced
62+
with the value from the process environment variable of the same name. If the
63+
process environment has been set with
64+
`LDAP_BIND_PASSWORD_FILE=/etc/satosa/secrets/ldap.txt` then the configuration
65+
value for `bind_password` will be `secret_password`.
66+
67+
```
68+
bind_password: !ENVFILE LDAP_BIND_PASSWORD_FILE
69+
```
70+
71+
3572
## <a name="proxy_conf" style="color:#000000">SATOSA proxy configuration</a>: `proxy_conf.yaml.example`
3673
| Parameter name | Data type | Example value | Description |
3774
| -------------- | --------- | ------------- | ----------- |

example/plugins/microservices/ldap_attribute_store.yaml.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ config:
88
"":
99
ldap_url: ldaps://ldap.example.org
1010
bind_dn: cn=admin,dc=example,dc=org
11-
bind_password: xxxxxxxx
11+
# Obtain bind password from environment variable LDAP_BIND_PASSWORD.
12+
bind_password: !ENV LDAP_BIND_PASSWORD
13+
# Obtain bind password from file pointed to by
14+
# environment variable LDAP_BIND_PASSWORD_FILE.
15+
# bind_password: !ENVFILE LDAP_BIND_PASSWORD
1216
search_base: ou=People,dc=example,dc=org
1317
read_only: true
1418
auto_bind: true

src/satosa/plugin_loader.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from contextlib import contextmanager
88
from pydoc import locate
99

10-
import yaml
11-
from yaml.error import YAMLError
10+
from satosa.yaml import load as yaml_load
11+
from satosa.yaml import YAMLError
1212

1313
from .backends.base import BackendModule
1414
from .exception import SATOSAConfigurationError
@@ -143,7 +143,7 @@ def _response_micro_service_filter(cls):
143143

144144
def _load_plugin_config(config):
145145
try:
146-
return yaml.safe_load(config)
146+
return yaml_load(config)
147147
except YAMLError as exc:
148148
if hasattr(exc, 'problem_mark'):
149149
mark = exc.problem_mark

src/satosa/satosa_config.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
"""
44
import logging
55
import os
6+
import os.path
67

7-
import yaml
8+
from satosa.exception import SATOSAConfigurationError
9+
from satosa.yaml import load as yaml_load
10+
from satosa.yaml import YAMLError
811

9-
from .exception import SATOSAConfigurationError
1012

1113
logger = logging.getLogger(__name__)
1214

@@ -143,10 +145,11 @@ def _load_yaml(self, config_file):
143145
:param config_file: config to load. Can be file path or yaml string
144146
:return: Loaded config
145147
"""
148+
146149
try:
147150
with open(os.path.abspath(config_file)) as f:
148-
return yaml.safe_load(f.read())
149-
except yaml.YAMLError as exc:
151+
return yaml_load(f.read())
152+
except YAMLError as exc:
150153
logger.error("Could not parse config as YAML: {}".format(exc))
151154
if hasattr(exc, 'problem_mark'):
152155
mark = exc.problem_mark

src/satosa/yaml.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import os
2+
import re
3+
4+
from yaml import SafeLoader as _safe_loader
5+
from yaml import YAMLError
6+
from yaml import safe_load as load
7+
8+
9+
def _constructor_env_variables(loader, node):
10+
"""
11+
Extracts the environment variable from the node's value.
12+
:param yaml.Loader loader: the yaml loader
13+
:param node: the current node in the yaml
14+
:return: value of the environment variable
15+
"""
16+
raw_value = loader.construct_scalar(node)
17+
new_value = os.environ.get(raw_value)
18+
if new_value is None:
19+
msg = "Cannot construct value from {node}: {value}".format(
20+
node=node, value=new_value
21+
)
22+
raise YAMLError(msg)
23+
return new_value
24+
25+
26+
def _constructor_envfile_variables(loader, node):
27+
"""
28+
Extracts the environment variable from the node's value.
29+
:param yaml.Loader loader: the yaml loader
30+
:param node: the current node in the yaml
31+
:return: value read from file pointed to by environment variable
32+
"""
33+
raw_value = loader.construct_scalar(node)
34+
filepath = os.environ.get(raw_value)
35+
try:
36+
with open(filepath, "r") as fd:
37+
new_value = fd.read()
38+
except (TypeError, IOError) as e:
39+
msg = "Cannot construct value from {node}: {path}".format(
40+
node=node, path=filepath
41+
)
42+
raise YAMLError(msg) from e
43+
else:
44+
return new_value
45+
46+
47+
TAG_ENV = "!ENV"
48+
TAG_ENVFILE = "!ENVFILE"
49+
50+
51+
_safe_loader.add_constructor(TAG_ENV, _constructor_env_variables)
52+
_safe_loader.add_constructor(TAG_ENVFILE, _constructor_envfile_variables)

tests/satosa/test_satosa_config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import os
23
from unittest.mock import mock_open, patch
34

45
import pytest
@@ -7,6 +8,7 @@
78
from satosa.exception import SATOSAConfigurationError
89
from satosa.satosa_config import SATOSAConfig
910

11+
TEST_RESOURCE_BASE_PATH = os.path.join(os.path.dirname(__file__), "../test_resources")
1012

1113
class TestSATOSAConfig:
1214
@pytest.fixture
@@ -73,3 +75,22 @@ def test_can_read_endpoint_configs_from_file(self, satosa_config_dict, modules_k
7375

7476
with pytest.raises(SATOSAConfigurationError):
7577
SATOSAConfig(satosa_config_dict)
78+
79+
def test_can_substitute_from_environment_variable(self, monkeypatch):
80+
monkeypatch.setenv("SATOSA_COOKIE_STATE_NAME", "oatmeal_raisin")
81+
config = SATOSAConfig(
82+
os.path.join(TEST_RESOURCE_BASE_PATH, "proxy_conf_environment_test.yaml")
83+
)
84+
85+
assert config["COOKIE_STATE_NAME"] == 'oatmeal_raisin'
86+
87+
def test_can_substitute_from_environment_variable_file(self, monkeypatch):
88+
cookie_file = os.path.join(TEST_RESOURCE_BASE_PATH, 'cookie_state_name')
89+
monkeypatch.setenv("SATOSA_COOKIE_STATE_NAME_FILE", cookie_file)
90+
config = SATOSAConfig(
91+
os.path.join(
92+
TEST_RESOURCE_BASE_PATH, "proxy_conf_environment_file_test.yaml"
93+
)
94+
)
95+
96+
assert config["COOKIE_STATE_NAME"] == 'chocolate_chip'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
chocolate_chip
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
BASE: https://example.com
2+
3+
STATE_ENCRYPTION_KEY: state_encryption_key
4+
5+
INTERNAL_ATTRIBUTES: {"attributes": {}}
6+
7+
COOKIE_STATE_NAME: !ENVFILE SATOSA_COOKIE_STATE_NAME_FILE
8+
9+
BACKEND_MODULES: []
10+
FRONTEND_MODULES: []
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
BASE: https://example.com
2+
3+
STATE_ENCRYPTION_KEY: state_encryption_key
4+
5+
INTERNAL_ATTRIBUTES: {"attributes": {}}
6+
7+
COOKIE_STATE_NAME: !ENV SATOSA_COOKIE_STATE_NAME
8+
9+
BACKEND_MODULES: []
10+
FRONTEND_MODULES: []

0 commit comments

Comments
 (0)