Skip to content
This repository was archived by the owner on Mar 31, 2025. It is now read-only.

Commit 7399cd4

Browse files
authored
Merge pull request #24 from color/int-2190-clrenv-secrets-repr
Keep secrets out of ClrEnv repr
2 parents 47049b0 + be8dd97 commit 7399cd4

File tree

5 files changed

+100
-12
lines changed

5 files changed

+100
-12
lines changed

clrenv/evaluate.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141

4242
from .path import environment_paths
4343
from .read import EnvReader
44-
from .types import LeafValue, NestedMapping, check_valid_leaf_value
44+
from .types import LeafValue, NestedMapping, Secret, check_valid_leaf_value
4545

4646
logger = logging.getLogger(__name__)
4747

@@ -185,6 +185,9 @@ def _evaluate_key(self, key: str) -> Union[LeafValue, Mapping, None]:
185185
if value is None and key in self._sub_keys:
186186
return {}
187187

188+
if isinstance(value, Secret):
189+
return value.value
190+
188191
return value
189192

190193
def _sub_key_path(self, key: str) -> Tuple[str, ...]:

clrenv/read.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
import os
1616
from collections import abc, deque
1717
from pathlib import Path
18-
from typing import Any, Deque, Iterable, Mapping, Optional, Tuple
18+
from typing import Any, Deque, Iterable, Mapping, Optional, Tuple, Union
1919

2020
import boto3
2121
from botocore.exceptions import EndpointConnectionError # type: ignore
2222

2323
from .deepmerge import deepmerge
2424
from .path import environment_paths
25-
from .types import MutableNestedMapping, NestedMapping, check_valid_leaf_value
25+
from .types import MutableNestedMapping, NestedMapping, Secret, check_valid_leaf_value
2626

2727
logger = logging.getLogger(__name__)
2828

@@ -108,19 +108,19 @@ def read(self) -> NestedMapping:
108108

109109
return result
110110

111-
def postprocess_str(self, value: str) -> str:
111+
def postprocess_str(self, value: str) -> Union[str, Secret]:
112112
"""Post process string values."""
113113
# Expand environmental variables in the form of $FOO or ${FOO}.
114114
value = os.path.expandvars(value)
115115
# If value is a path starting with ~, expand.
116116
if value.startswith("~"):
117-
value = os.path.expanduser(value)
117+
return os.path.expanduser(value)
118118
# Substitute from clrypt keyfile.
119119
elif value.startswith("^keyfile "):
120-
value = self.evaluate_clrypt_key(value[9:])
120+
return Secret(source=value, value=self.evaluate_clrypt_key(value[9:]))
121121
# Substitute from aws ssm parameter store.
122122
elif value.startswith("^parameter "):
123-
value = self.evaluate_ssm_parameter(value[11:])
123+
return Secret(source=value, value=self.evaluate_ssm_parameter(value[11:]))
124124

125125
return value
126126

clrenv/tests/test_evaluate.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import functools
2+
from types import SimpleNamespace
23

4+
import botocore.exceptions # type: ignore
35
import pytest
46
import yaml
57

@@ -182,3 +184,67 @@ def test_underscored_keys(default_env):
182184
default_env["__env"] # pylint: disable=pointless-statement
183185
with pytest.raises(AttributeError):
184186
getattr(default_env.__unknown)
187+
188+
189+
def test_clrypt_secret(tmp_path, monkeypatch):
190+
"""
191+
A keyfile secret should be accessible via clrenv.
192+
"""
193+
env_path = tmp_path / "env"
194+
env_path.write_text(yaml.dump({"base": {"foo": "^keyfile aaa"}}))
195+
196+
import clrypt
197+
198+
def mock_read(group, name):
199+
assert group == "keys"
200+
assert name == "keys"
201+
return {"aaa": "bbb"}
202+
203+
monkeypatch.setattr(clrypt, "read_file_as_dict", mock_read)
204+
205+
env = clrenv.evaluate.RootClrEnv([env_path])
206+
207+
# Check that the repr does not leak the secret, but the secret is still accessible
208+
assert '^keyfile aaa' in repr(env)
209+
assert 'bbb' not in repr(env)
210+
assert env.foo == "bbb"
211+
212+
def test_ssm_secret(tmp_path, monkeypatch):
213+
"""
214+
A SSM secret should be accessible via clrenv.
215+
"""
216+
env_path = tmp_path / "env"
217+
env_path.write_text(yaml.dump({"base": {"foo": "^parameter aaa"}}))
218+
219+
monkeypatch.setenv("CLRENV_OFFLINE_DEV", "")
220+
import boto3
221+
222+
class MockClient:
223+
exceptions = SimpleNamespace()
224+
exceptions.ParameterNotFound = botocore.exceptions.ClientError
225+
226+
def get_parameter(self, Name=None, WithDecryption=None):
227+
assert WithDecryption is True
228+
assert Name in ("aaa", "endpoint_error", "param_error")
229+
if Name == "aaa":
230+
return {"Parameter": {"Value": "bbb"}}
231+
elif Name == "endpoint_error":
232+
raise botocore.exceptions.EndpointConnectionError(endpoint_url="url")
233+
elif Name == "param_error":
234+
raise MockClient.exceptions.ParameterNotFound(
235+
error_response={}, operation_name=""
236+
)
237+
238+
def mock_client(name):
239+
assert name == "ssm"
240+
return MockClient()
241+
242+
monkeypatch.setattr(boto3, "client", mock_client)
243+
244+
env = clrenv.evaluate.RootClrEnv([env_path])
245+
246+
# Check that the repr does not leak the secret, but the secret is still accessible
247+
assert '^parameter aaa' in repr(env)
248+
assert 'bbb' not in repr(env)
249+
assert env.foo == "bbb"
250+

clrenv/tests/test_read.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import yaml
77

88
import clrenv
9+
from clrenv.types import Secret
910

1011

1112
@pytest.fixture(autouse=True)
@@ -75,7 +76,8 @@ def mock_read(group, name):
7576
env_path = tmp_path / "env"
7677
env_path.write_text(yaml.dump({"base": {"foo": "^keyfile aaa"}}))
7778
env = clrenv.read.EnvReader([env_path]).read()
78-
assert env["foo"] == "bbb"
79+
assert isinstance(env["foo"], Secret)
80+
assert env["foo"].value == "bbb"
7981

8082

8183
def test_ssm(tmp_path, monkeypatch):
@@ -107,7 +109,8 @@ def mock_client(name):
107109
env_path = tmp_path / "env"
108110
env_path.write_text(yaml.dump({"base": {"foo": "^parameter aaa"}}))
109111
env = clrenv.read.EnvReader([env_path]).read()
110-
assert env["foo"] == "bbb"
112+
assert isinstance(env["foo"], Secret)
113+
assert env["foo"].value == "bbb"
111114

112115
env_path.write_text(yaml.dump({"base": {"foo": "^parameter endpoint_error"}}))
113116
with pytest.raises(botocore.exceptions.EndpointConnectionError):
@@ -124,4 +127,5 @@ def test_offline_parameter_flag(tmp_path, monkeypatch):
124127
env_path = tmp_path / "env"
125128
env_path.write_text(yaml.dump({"base": {"foo": "^parameter aaa"}}))
126129
env = clrenv.read.EnvReader([env_path]).read()
127-
assert env["foo"] == "CLRENV_OFFLINE_PLACEHOLDER"
130+
assert isinstance(env["foo"], Secret)
131+
assert env["foo"].value == "CLRENV_OFFLINE_PLACEHOLDER"

clrenv/types.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
1-
from typing import Any, List, Mapping, MutableMapping, Union
1+
from typing import Any, List, Mapping, MutableMapping, NamedTuple, Union
22

33
"""Defines type annotations for use in clrenv.
44
55
Ideally we would only allow str leaf values so that they can be migrated to an env
66
var based system. For now we must supports more complex values.
77
"""
88

9+
class Secret(NamedTuple):
10+
source: str
11+
value: str
12+
13+
def __repr__(self) -> str:
14+
"""
15+
Return a Secret's representation without exposing the secret.
16+
17+
In the event that a Secret's (or, more generally, a ClrEnv object's)
18+
representation is logged, this will prevent us from leaking secrets into
19+
plaintext.
20+
"""
21+
return f"Secret(source='{self.source}')"
22+
23+
924
# Type that can be read or set as values of leaf nodes.
10-
LeafValue = Union[bool, int, float, str, List[Union[bool, int, float, str]]]
25+
LeafValue = Union[bool, int, float, str, List[Union[bool, int, float, str]], Secret]
1126
# Type of a non leaf node.
1227
NestedMapping = Mapping[str, Union[LeafValue, Mapping[str, Any]]]
1328
MutableNestedMapping = MutableMapping[str, Union[LeafValue, MutableMapping[str, Any]]]

0 commit comments

Comments
 (0)