aws-chain-secrets 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aws_chain_secrets-1.0.0/LICENSE +21 -0
- aws_chain_secrets-1.0.0/PKG-INFO +133 -0
- aws_chain_secrets-1.0.0/README.rst +115 -0
- aws_chain_secrets-1.0.0/aws_chain_secrets/__init__.py +1 -0
- aws_chain_secrets-1.0.0/aws_chain_secrets/caster.py +67 -0
- aws_chain_secrets-1.0.0/aws_chain_secrets/client.py +44 -0
- aws_chain_secrets-1.0.0/aws_chain_secrets/exceptions.py +42 -0
- aws_chain_secrets-1.0.0/aws_chain_secrets/py.typed +0 -0
- aws_chain_secrets-1.0.0/aws_chain_secrets/secrets_manager.py +117 -0
- aws_chain_secrets-1.0.0/aws_chain_secrets.egg-info/PKG-INFO +133 -0
- aws_chain_secrets-1.0.0/aws_chain_secrets.egg-info/SOURCES.txt +16 -0
- aws_chain_secrets-1.0.0/aws_chain_secrets.egg-info/dependency_links.txt +1 -0
- aws_chain_secrets-1.0.0/aws_chain_secrets.egg-info/requires.txt +1 -0
- aws_chain_secrets-1.0.0/aws_chain_secrets.egg-info/top_level.txt +2 -0
- aws_chain_secrets-1.0.0/setup.cfg +4 -0
- aws_chain_secrets-1.0.0/setup.py +29 -0
- aws_chain_secrets-1.0.0/tests/__init__.py +0 -0
- aws_chain_secrets-1.0.0/tests/test_caster.py +21 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Hyouk Oh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: aws_chain_secrets
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: This aims to enable the use of one or more secrets from AWS SecretsManager.
|
|
5
|
+
Home-page: https://github.com/h5kure/aws-chain-secrets
|
|
6
|
+
Author: Hyouk Oh
|
|
7
|
+
Author-email: h.5.kure@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
Description-Content-Type: text/x-rst
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
|
|
19
|
+
AWS chain secrets
|
|
20
|
+
===================
|
|
21
|
+
|
|
22
|
+
This package aims to make using AWS SecretsManager more comfortable.
|
|
23
|
+
|
|
24
|
+
Introduce
|
|
25
|
+
---------
|
|
26
|
+
This aims to enable the use of one or more secrets from AWS SecretsManager.
|
|
27
|
+
|
|
28
|
+
If more than one secret is used, the secret specified last takes precedence.
|
|
29
|
+
|
|
30
|
+
For example, if you use secrets named A and B in that order, overlapping entries from A and B will later use
|
|
31
|
+
the value of the specified B entry.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
.. code-block:: python
|
|
35
|
+
|
|
36
|
+
>>> A = {"foo": 1, "bar": 2}
|
|
37
|
+
>>> B = {"bar": 3}
|
|
38
|
+
>>> secrets("bar", casting_type=int)
|
|
39
|
+
3
|
|
40
|
+
|
|
41
|
+
Install
|
|
42
|
+
-------
|
|
43
|
+
|
|
44
|
+
.. code-block:: bash
|
|
45
|
+
|
|
46
|
+
$ python -m pip install aws_chain_secrets
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
How to use
|
|
50
|
+
----------
|
|
51
|
+
|
|
52
|
+
.. note::
|
|
53
|
+
|
|
54
|
+
AWS credential is required.
|
|
55
|
+
|
|
56
|
+
If your credentials installed or set to environment, you can omit credentials when initialize `SecretsManager`.
|
|
57
|
+
|
|
58
|
+
connect and fetch manually
|
|
59
|
+
""""""""""""""""""""""""""
|
|
60
|
+
|
|
61
|
+
.. code-block:: python
|
|
62
|
+
|
|
63
|
+
from aws_chain_secrets import SecretsManager
|
|
64
|
+
secrets = SecretsManager('ap-northeast-2', 'secret_name_1', 'secret_name_2', ...)
|
|
65
|
+
secrets.connect()
|
|
66
|
+
secrets.fetch()
|
|
67
|
+
secrets(key="A", default=None, casting_type=int)
|
|
68
|
+
secrets.disconnect()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
.. note::
|
|
72
|
+
|
|
73
|
+
The secret assigned last takes precedence over secrets assigned earlier.
|
|
74
|
+
|
|
75
|
+
In this case, `secret_name_2` has a higher priority than `secret_name_1`.
|
|
76
|
+
|
|
77
|
+
This works likes `dict.update` (`secret_name_1.update(secret_name_2)`).
|
|
78
|
+
|
|
79
|
+
use with context manager
|
|
80
|
+
""""""""""""""""""""""""
|
|
81
|
+
|
|
82
|
+
You can omit `connect()`, `fetch()` and `disconnect()` when using context manager.
|
|
83
|
+
|
|
84
|
+
.. code-block:: python
|
|
85
|
+
|
|
86
|
+
from aws_chain_secrets import SecretsManager
|
|
87
|
+
with SecretsManager('ap-northeast-2', 'secret_name_1', 'secret_name_2', ...) as secrets:
|
|
88
|
+
secrets(key="A", default=None, casting_type=int)
|
|
89
|
+
|
|
90
|
+
type casting
|
|
91
|
+
""""""""""""
|
|
92
|
+
|
|
93
|
+
All secret values retrieved from AWS SecretsManager are string types by default.
|
|
94
|
+
|
|
95
|
+
You can change it to any type you want.
|
|
96
|
+
|
|
97
|
+
.. code-block:: python
|
|
98
|
+
|
|
99
|
+
>>> secrets(key="A", casting_type=int)
|
|
100
|
+
1
|
|
101
|
+
>>> secrets(key="SOME_ARRAY", casting_type=list)
|
|
102
|
+
['1', '2', '3', '4']
|
|
103
|
+
>>> secrets(key="SOME_ARRAY", casting_type=list[int])
|
|
104
|
+
[1, 2, 3, 4]
|
|
105
|
+
|
|
106
|
+
default value
|
|
107
|
+
"""""""""""""
|
|
108
|
+
|
|
109
|
+
You can specify a default value, just like when using Python `dict`'s `get`.
|
|
110
|
+
|
|
111
|
+
.. code-block:: python
|
|
112
|
+
|
|
113
|
+
# if the key value of A does not exist, 0 specified as default is returned.
|
|
114
|
+
>>> secrets(key="A", default=0)
|
|
115
|
+
0
|
|
116
|
+
|
|
117
|
+
change value
|
|
118
|
+
""""""""""""
|
|
119
|
+
|
|
120
|
+
You can change (or add) the value of a specified secret by name.
|
|
121
|
+
|
|
122
|
+
.. code-block:: python
|
|
123
|
+
|
|
124
|
+
>>> secrets.set('secret_name_1', 'key', 'value')
|
|
125
|
+
|
|
126
|
+
update remote (AWS secrets manager) data from local
|
|
127
|
+
"""""""""""""""""""""""""""""""""""""""""""""""""""
|
|
128
|
+
|
|
129
|
+
You can update (upload) the values of a specified secret by name from local to remote.
|
|
130
|
+
|
|
131
|
+
.. code-block:: python
|
|
132
|
+
|
|
133
|
+
>>> secrets.update('secret_name_1')
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
AWS chain secrets
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
This package aims to make using AWS SecretsManager more comfortable.
|
|
5
|
+
|
|
6
|
+
Introduce
|
|
7
|
+
---------
|
|
8
|
+
This aims to enable the use of one or more secrets from AWS SecretsManager.
|
|
9
|
+
|
|
10
|
+
If more than one secret is used, the secret specified last takes precedence.
|
|
11
|
+
|
|
12
|
+
For example, if you use secrets named A and B in that order, overlapping entries from A and B will later use
|
|
13
|
+
the value of the specified B entry.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
.. code-block:: python
|
|
17
|
+
|
|
18
|
+
>>> A = {"foo": 1, "bar": 2}
|
|
19
|
+
>>> B = {"bar": 3}
|
|
20
|
+
>>> secrets("bar", casting_type=int)
|
|
21
|
+
3
|
|
22
|
+
|
|
23
|
+
Install
|
|
24
|
+
-------
|
|
25
|
+
|
|
26
|
+
.. code-block:: bash
|
|
27
|
+
|
|
28
|
+
$ python -m pip install aws_chain_secrets
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
How to use
|
|
32
|
+
----------
|
|
33
|
+
|
|
34
|
+
.. note::
|
|
35
|
+
|
|
36
|
+
AWS credential is required.
|
|
37
|
+
|
|
38
|
+
If your credentials installed or set to environment, you can omit credentials when initialize `SecretsManager`.
|
|
39
|
+
|
|
40
|
+
connect and fetch manually
|
|
41
|
+
""""""""""""""""""""""""""
|
|
42
|
+
|
|
43
|
+
.. code-block:: python
|
|
44
|
+
|
|
45
|
+
from aws_chain_secrets import SecretsManager
|
|
46
|
+
secrets = SecretsManager('ap-northeast-2', 'secret_name_1', 'secret_name_2', ...)
|
|
47
|
+
secrets.connect()
|
|
48
|
+
secrets.fetch()
|
|
49
|
+
secrets(key="A", default=None, casting_type=int)
|
|
50
|
+
secrets.disconnect()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
.. note::
|
|
54
|
+
|
|
55
|
+
The secret assigned last takes precedence over secrets assigned earlier.
|
|
56
|
+
|
|
57
|
+
In this case, `secret_name_2` has a higher priority than `secret_name_1`.
|
|
58
|
+
|
|
59
|
+
This works likes `dict.update` (`secret_name_1.update(secret_name_2)`).
|
|
60
|
+
|
|
61
|
+
use with context manager
|
|
62
|
+
""""""""""""""""""""""""
|
|
63
|
+
|
|
64
|
+
You can omit `connect()`, `fetch()` and `disconnect()` when using context manager.
|
|
65
|
+
|
|
66
|
+
.. code-block:: python
|
|
67
|
+
|
|
68
|
+
from aws_chain_secrets import SecretsManager
|
|
69
|
+
with SecretsManager('ap-northeast-2', 'secret_name_1', 'secret_name_2', ...) as secrets:
|
|
70
|
+
secrets(key="A", default=None, casting_type=int)
|
|
71
|
+
|
|
72
|
+
type casting
|
|
73
|
+
""""""""""""
|
|
74
|
+
|
|
75
|
+
All secret values retrieved from AWS SecretsManager are string types by default.
|
|
76
|
+
|
|
77
|
+
You can change it to any type you want.
|
|
78
|
+
|
|
79
|
+
.. code-block:: python
|
|
80
|
+
|
|
81
|
+
>>> secrets(key="A", casting_type=int)
|
|
82
|
+
1
|
|
83
|
+
>>> secrets(key="SOME_ARRAY", casting_type=list)
|
|
84
|
+
['1', '2', '3', '4']
|
|
85
|
+
>>> secrets(key="SOME_ARRAY", casting_type=list[int])
|
|
86
|
+
[1, 2, 3, 4]
|
|
87
|
+
|
|
88
|
+
default value
|
|
89
|
+
"""""""""""""
|
|
90
|
+
|
|
91
|
+
You can specify a default value, just like when using Python `dict`'s `get`.
|
|
92
|
+
|
|
93
|
+
.. code-block:: python
|
|
94
|
+
|
|
95
|
+
# if the key value of A does not exist, 0 specified as default is returned.
|
|
96
|
+
>>> secrets(key="A", default=0)
|
|
97
|
+
0
|
|
98
|
+
|
|
99
|
+
change value
|
|
100
|
+
""""""""""""
|
|
101
|
+
|
|
102
|
+
You can change (or add) the value of a specified secret by name.
|
|
103
|
+
|
|
104
|
+
.. code-block:: python
|
|
105
|
+
|
|
106
|
+
>>> secrets.set('secret_name_1', 'key', 'value')
|
|
107
|
+
|
|
108
|
+
update remote (AWS secrets manager) data from local
|
|
109
|
+
"""""""""""""""""""""""""""""""""""""""""""""""""""
|
|
110
|
+
|
|
111
|
+
You can update (upload) the values of a specified secret by name from local to remote.
|
|
112
|
+
|
|
113
|
+
.. code-block:: python
|
|
114
|
+
|
|
115
|
+
>>> secrets.update('secret_name_1')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .secrets_manager import SecretsManager
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import string
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from types import GenericAlias
|
|
5
|
+
from typing import Any, Type, get_origin, get_args
|
|
6
|
+
|
|
7
|
+
TRUE_STRINGS = ('y', 'yes', 't', 'true', 'on', '1')
|
|
8
|
+
FALSE_STRINGS = ('', 'n', 'no', 'f', 'false', 'off', '0')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def string_to_boolean(value: str) -> bool:
|
|
12
|
+
"""
|
|
13
|
+
Convert a string representation of truth to True or False.
|
|
14
|
+
|
|
15
|
+
:param value: a string representation of truth
|
|
16
|
+
:return: True if value represents truth, False if value represents false, otherwise raise ValueError
|
|
17
|
+
:raises: Raises ValueError if 'value' is anything else
|
|
18
|
+
"""
|
|
19
|
+
value = value.lower()
|
|
20
|
+
if value in TRUE_STRINGS:
|
|
21
|
+
return True
|
|
22
|
+
if value in FALSE_STRINGS:
|
|
23
|
+
return False
|
|
24
|
+
raise ValueError(f"invalid truth value {repr(value)}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cast_boolean(value: Any) -> bool:
|
|
28
|
+
value = str(value)
|
|
29
|
+
return string_to_boolean(value)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cast(value: str, casting_type: Type = None) -> Any:
|
|
33
|
+
"""
|
|
34
|
+
string value to typed value
|
|
35
|
+
:param value: original string
|
|
36
|
+
:param casting_type: expected type to be
|
|
37
|
+
:return: typed value
|
|
38
|
+
"""
|
|
39
|
+
if casting_type is None:
|
|
40
|
+
return value
|
|
41
|
+
if casting_type is bool:
|
|
42
|
+
return cast_boolean(value)
|
|
43
|
+
if casting_type in (set, list, tuple, bytes, bytearray):
|
|
44
|
+
return Sequence(wrapper=casting_type)(value)
|
|
45
|
+
if casting_type is dict:
|
|
46
|
+
return json.loads(value)
|
|
47
|
+
if isinstance(casting_type, GenericAlias):
|
|
48
|
+
origin = get_origin(casting_type)
|
|
49
|
+
args = get_args(casting_type)
|
|
50
|
+
if origin in (set, list, tuple, bytes, bytearray):
|
|
51
|
+
return Sequence(cast_type=args[0] if args else str, wrapper=origin)(value)
|
|
52
|
+
raise NotImplementedError(f"cannot cast to {repr(casting_type)}")
|
|
53
|
+
return casting_type(value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Sequence:
|
|
58
|
+
"""
|
|
59
|
+
Helper to cast Sequence (list, tuple, bytes, bytearray) and Set(set, frozenset)
|
|
60
|
+
"""
|
|
61
|
+
cast_type: Type = str
|
|
62
|
+
delimiter: str = ','
|
|
63
|
+
strip: str = string.whitespace
|
|
64
|
+
wrapper: Type = list
|
|
65
|
+
|
|
66
|
+
def __call__(self, value: str):
|
|
67
|
+
return self.wrapper(cast(v.strip(self.strip), self.cast_type) for v in value.split(self.delimiter))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import boto3
|
|
4
|
+
from botocore.client import BaseClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Client:
|
|
8
|
+
"""
|
|
9
|
+
wrapper for AWS client
|
|
10
|
+
"""
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
service_name: str,
|
|
14
|
+
region_name: str,
|
|
15
|
+
aws_access_key_id: str = None,
|
|
16
|
+
aws_secret_access_key: str = None
|
|
17
|
+
):
|
|
18
|
+
self.service_name = service_name
|
|
19
|
+
self.region_name = region_name
|
|
20
|
+
self.aws_access_key_id = aws_access_key_id
|
|
21
|
+
self.aws_secret_access_key = aws_secret_access_key
|
|
22
|
+
self._client: Optional[BaseClient] = None
|
|
23
|
+
|
|
24
|
+
def __enter__(self):
|
|
25
|
+
self.connect()
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
29
|
+
self.disconnect()
|
|
30
|
+
|
|
31
|
+
def _create_client(self) -> BaseClient:
|
|
32
|
+
return boto3.session.Session().client(
|
|
33
|
+
service_name=self.service_name,
|
|
34
|
+
region_name=self.region_name,
|
|
35
|
+
aws_access_key_id=self.aws_access_key_id,
|
|
36
|
+
aws_secret_access_key=self.aws_secret_access_key
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def connect(self):
|
|
40
|
+
self._client = self._create_client()
|
|
41
|
+
|
|
42
|
+
def disconnect(self):
|
|
43
|
+
self._client.close()
|
|
44
|
+
del self._client
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class SecretsManagerException(Exception):
|
|
4
|
+
DEFAULT_MESSAGE = "Unknown error occurred."
|
|
5
|
+
|
|
6
|
+
def __init__(self, message: str = None):
|
|
7
|
+
super().__init__(message or self.DEFAULT_MESSAGE)
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def from_code(cls, code: str, message: str = None) -> 'SecretsManagerException':
|
|
11
|
+
if code == 'DecryptionFailureException':
|
|
12
|
+
return DecryptionFailureException(message)
|
|
13
|
+
elif code == 'InternalServiceErrorException':
|
|
14
|
+
return InternalServiceErrorException(message)
|
|
15
|
+
elif code == 'InvalidParameterException':
|
|
16
|
+
return InvalidParameterException(message)
|
|
17
|
+
elif code == 'InvalidRequestException':
|
|
18
|
+
return InvalidRequestException(message)
|
|
19
|
+
elif code == 'ResourceNotFoundException':
|
|
20
|
+
return ResourceNotFoundException(message)
|
|
21
|
+
else:
|
|
22
|
+
return cls(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DecryptionFailureException(SecretsManagerException):
|
|
26
|
+
DEFAULT_MESSAGE = "Secrets Manager can't decrypt the protected secret text using the provided KMS key."
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InternalServiceErrorException(SecretsManagerException):
|
|
30
|
+
DEFAULT_MESSAGE = "An error occurred on the server side."
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InvalidParameterException(SecretsManagerException):
|
|
34
|
+
DEFAULT_MESSAGE = "You provided an invalid value for a parameter.",
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InvalidRequestException(SecretsManagerException):
|
|
38
|
+
DEFAULT_MESSAGE = "You provided a parameter value that is not valid for the current state of the resource."
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ResourceNotFoundException(SecretsManagerException):
|
|
42
|
+
DEFAULT_MESSAGE = "We can't find the resource that you asked for."
|
|
File without changes
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
from collections import ChainMap
|
|
4
|
+
from typing import Any, Type
|
|
5
|
+
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
from aws_chain_secrets.caster import cast
|
|
9
|
+
from aws_chain_secrets.client import Client
|
|
10
|
+
from aws_chain_secrets.exceptions import SecretsManagerException
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Null:
|
|
14
|
+
"""
|
|
15
|
+
class to represent null type
|
|
16
|
+
"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
null = Null()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SecretsManager(Client):
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
region_name: str,
|
|
27
|
+
*secret_names: str,
|
|
28
|
+
aws_access_key_id: str = None,
|
|
29
|
+
aws_secret_access_key: str = None
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
initial setup
|
|
33
|
+
:param region_name: AWS region name
|
|
34
|
+
:param secret_names: These are the secret names from which to read settings. The last name has priority.
|
|
35
|
+
"""
|
|
36
|
+
self.secret_names = secret_names
|
|
37
|
+
super().__init__(
|
|
38
|
+
"secretsmanager",
|
|
39
|
+
region_name,
|
|
40
|
+
aws_access_key_id,
|
|
41
|
+
aws_secret_access_key
|
|
42
|
+
)
|
|
43
|
+
self._secrets = {secret_name: {} for secret_name in secret_names}
|
|
44
|
+
self._combined = ChainMap(*reversed(self._secrets.values()))
|
|
45
|
+
|
|
46
|
+
def __call__(self, key, *, default: Any = null, casting_type: Type = str):
|
|
47
|
+
return self.get(key, default=default, casting_type=casting_type)
|
|
48
|
+
|
|
49
|
+
def __enter__(self):
|
|
50
|
+
super().__enter__()
|
|
51
|
+
self.fetch()
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def get(self, key, *, default: Any = null, casting_type: Type = str):
|
|
55
|
+
"""
|
|
56
|
+
get secret value
|
|
57
|
+
:param key: key value to search
|
|
58
|
+
:param default: default value if key not exists in secrets
|
|
59
|
+
:param casting_type: casting type to convert value
|
|
60
|
+
:return: Any secret value specified by key
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
value = self._combined[key]
|
|
64
|
+
except KeyError:
|
|
65
|
+
if isinstance(default, Null):
|
|
66
|
+
raise KeyError(f"{repr(key)} not found. Please check secret value in AWS Secrets Manager.")
|
|
67
|
+
return default
|
|
68
|
+
return cast(value, casting_type)
|
|
69
|
+
|
|
70
|
+
def set(self, secret_name: str, key, value):
|
|
71
|
+
"""
|
|
72
|
+
set a `key: value` to the specified secret
|
|
73
|
+
:param secret_name: secret_name of AWS Secrets Manager
|
|
74
|
+
:param key: key of map
|
|
75
|
+
:param value: value of map
|
|
76
|
+
"""
|
|
77
|
+
self._secrets[secret_name][key] = value
|
|
78
|
+
|
|
79
|
+
def keys(self):
|
|
80
|
+
return self._combined.keys()
|
|
81
|
+
|
|
82
|
+
def values(self):
|
|
83
|
+
return self._combined.values()
|
|
84
|
+
|
|
85
|
+
def items(self):
|
|
86
|
+
return self._combined.items()
|
|
87
|
+
|
|
88
|
+
def fetch(self):
|
|
89
|
+
"""
|
|
90
|
+
get secrets from AWS and update
|
|
91
|
+
"""
|
|
92
|
+
for secret_name in self.secret_names:
|
|
93
|
+
self._secrets[secret_name].clear()
|
|
94
|
+
self._secrets[secret_name].update(self._get_secrets(secret_name))
|
|
95
|
+
|
|
96
|
+
def update(self, secret_name: str):
|
|
97
|
+
"""
|
|
98
|
+
update specified secrets from local to AWS
|
|
99
|
+
:param secret_name: secret name of AWS Secrets Manager
|
|
100
|
+
"""
|
|
101
|
+
self._client.update_secret(SecretId=secret_name, SecretString=json.dumps(self._secrets[secret_name]))
|
|
102
|
+
|
|
103
|
+
def _get_secrets(self, secret_name: str) -> dict:
|
|
104
|
+
"""
|
|
105
|
+
get secrets from AWS
|
|
106
|
+
:param secret_name: secret name of AWS Secrets Manager
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
response = self._client.get_secret_value(SecretId=secret_name)
|
|
110
|
+
except ClientError as e:
|
|
111
|
+
raise SecretsManagerException.from_code(e.response['Error']['Code'])
|
|
112
|
+
else:
|
|
113
|
+
if 'SecretString' in response:
|
|
114
|
+
return json.loads(response['SecretString'])
|
|
115
|
+
return json.loads(base64.b64decode(response['SecretBinary']))
|
|
116
|
+
|
|
117
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: aws-chain-secrets
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: This aims to enable the use of one or more secrets from AWS SecretsManager.
|
|
5
|
+
Home-page: https://github.com/h5kure/aws-chain-secrets
|
|
6
|
+
Author: Hyouk Oh
|
|
7
|
+
Author-email: h.5.kure@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
Description-Content-Type: text/x-rst
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
|
|
19
|
+
AWS chain secrets
|
|
20
|
+
===================
|
|
21
|
+
|
|
22
|
+
This package aims to make using AWS SecretsManager more comfortable.
|
|
23
|
+
|
|
24
|
+
Introduce
|
|
25
|
+
---------
|
|
26
|
+
This aims to enable the use of one or more secrets from AWS SecretsManager.
|
|
27
|
+
|
|
28
|
+
If more than one secret is used, the secret specified last takes precedence.
|
|
29
|
+
|
|
30
|
+
For example, if you use secrets named A and B in that order, overlapping entries from A and B will later use
|
|
31
|
+
the value of the specified B entry.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
.. code-block:: python
|
|
35
|
+
|
|
36
|
+
>>> A = {"foo": 1, "bar": 2}
|
|
37
|
+
>>> B = {"bar": 3}
|
|
38
|
+
>>> secrets("bar", casting_type=int)
|
|
39
|
+
3
|
|
40
|
+
|
|
41
|
+
Install
|
|
42
|
+
-------
|
|
43
|
+
|
|
44
|
+
.. code-block:: bash
|
|
45
|
+
|
|
46
|
+
$ python -m pip install aws_chain_secrets
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
How to use
|
|
50
|
+
----------
|
|
51
|
+
|
|
52
|
+
.. note::
|
|
53
|
+
|
|
54
|
+
AWS credential is required.
|
|
55
|
+
|
|
56
|
+
If your credentials installed or set to environment, you can omit credentials when initialize `SecretsManager`.
|
|
57
|
+
|
|
58
|
+
connect and fetch manually
|
|
59
|
+
""""""""""""""""""""""""""
|
|
60
|
+
|
|
61
|
+
.. code-block:: python
|
|
62
|
+
|
|
63
|
+
from aws_chain_secrets import SecretsManager
|
|
64
|
+
secrets = SecretsManager('ap-northeast-2', 'secret_name_1', 'secret_name_2', ...)
|
|
65
|
+
secrets.connect()
|
|
66
|
+
secrets.fetch()
|
|
67
|
+
secrets(key="A", default=None, casting_type=int)
|
|
68
|
+
secrets.disconnect()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
.. note::
|
|
72
|
+
|
|
73
|
+
The secret assigned last takes precedence over secrets assigned earlier.
|
|
74
|
+
|
|
75
|
+
In this case, `secret_name_2` has a higher priority than `secret_name_1`.
|
|
76
|
+
|
|
77
|
+
This works likes `dict.update` (`secret_name_1.update(secret_name_2)`).
|
|
78
|
+
|
|
79
|
+
use with context manager
|
|
80
|
+
""""""""""""""""""""""""
|
|
81
|
+
|
|
82
|
+
You can omit `connect()`, `fetch()` and `disconnect()` when using context manager.
|
|
83
|
+
|
|
84
|
+
.. code-block:: python
|
|
85
|
+
|
|
86
|
+
from aws_chain_secrets import SecretsManager
|
|
87
|
+
with SecretsManager('ap-northeast-2', 'secret_name_1', 'secret_name_2', ...) as secrets:
|
|
88
|
+
secrets(key="A", default=None, casting_type=int)
|
|
89
|
+
|
|
90
|
+
type casting
|
|
91
|
+
""""""""""""
|
|
92
|
+
|
|
93
|
+
All secret values retrieved from AWS SecretsManager are string types by default.
|
|
94
|
+
|
|
95
|
+
You can change it to any type you want.
|
|
96
|
+
|
|
97
|
+
.. code-block:: python
|
|
98
|
+
|
|
99
|
+
>>> secrets(key="A", casting_type=int)
|
|
100
|
+
1
|
|
101
|
+
>>> secrets(key="SOME_ARRAY", casting_type=list)
|
|
102
|
+
['1', '2', '3', '4']
|
|
103
|
+
>>> secrets(key="SOME_ARRAY", casting_type=list[int])
|
|
104
|
+
[1, 2, 3, 4]
|
|
105
|
+
|
|
106
|
+
default value
|
|
107
|
+
"""""""""""""
|
|
108
|
+
|
|
109
|
+
You can specify a default value, just like when using Python `dict`'s `get`.
|
|
110
|
+
|
|
111
|
+
.. code-block:: python
|
|
112
|
+
|
|
113
|
+
# if the key value of A does not exist, 0 specified as default is returned.
|
|
114
|
+
>>> secrets(key="A", default=0)
|
|
115
|
+
0
|
|
116
|
+
|
|
117
|
+
change value
|
|
118
|
+
""""""""""""
|
|
119
|
+
|
|
120
|
+
You can change (or add) the value of a specified secret by name.
|
|
121
|
+
|
|
122
|
+
.. code-block:: python
|
|
123
|
+
|
|
124
|
+
>>> secrets.set('secret_name_1', 'key', 'value')
|
|
125
|
+
|
|
126
|
+
update remote (AWS secrets manager) data from local
|
|
127
|
+
"""""""""""""""""""""""""""""""""""""""""""""""""""
|
|
128
|
+
|
|
129
|
+
You can update (upload) the values of a specified secret by name from local to remote.
|
|
130
|
+
|
|
131
|
+
.. code-block:: python
|
|
132
|
+
|
|
133
|
+
>>> secrets.update('secret_name_1')
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.rst
|
|
3
|
+
setup.py
|
|
4
|
+
aws_chain_secrets/__init__.py
|
|
5
|
+
aws_chain_secrets/caster.py
|
|
6
|
+
aws_chain_secrets/client.py
|
|
7
|
+
aws_chain_secrets/exceptions.py
|
|
8
|
+
aws_chain_secrets/py.typed
|
|
9
|
+
aws_chain_secrets/secrets_manager.py
|
|
10
|
+
aws_chain_secrets.egg-info/PKG-INFO
|
|
11
|
+
aws_chain_secrets.egg-info/SOURCES.txt
|
|
12
|
+
aws_chain_secrets.egg-info/dependency_links.txt
|
|
13
|
+
aws_chain_secrets.egg-info/requires.txt
|
|
14
|
+
aws_chain_secrets.egg-info/top_level.txt
|
|
15
|
+
tests/__init__.py
|
|
16
|
+
tests/test_caster.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
boto3
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import setuptools
|
|
2
|
+
|
|
3
|
+
with open("README.rst", "r", encoding='utf-8') as readme:
|
|
4
|
+
long_description = readme.read()
|
|
5
|
+
|
|
6
|
+
setuptools.setup(
|
|
7
|
+
name="aws_chain_secrets",
|
|
8
|
+
version='1.0.0',
|
|
9
|
+
author="Hyouk Oh",
|
|
10
|
+
author_email="h.5.kure@gmail.com",
|
|
11
|
+
description="This aims to enable the use of one or more secrets from AWS SecretsManager.",
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/x-rst",
|
|
14
|
+
url="https://github.com/h5kure/aws-chain-secrets",
|
|
15
|
+
packages=setuptools.find_packages(),
|
|
16
|
+
package_data={"": ["py.typed"]},
|
|
17
|
+
include_package_data=True,
|
|
18
|
+
classifiers=[
|
|
19
|
+
"Programming Language :: Python :: 3.7",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
],
|
|
27
|
+
python_requires='>=3.7',
|
|
28
|
+
install_requires=['boto3']
|
|
29
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from aws_chain_secrets import caster
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CasterTestCase(unittest.TestCase):
|
|
7
|
+
def test_int(self):
|
|
8
|
+
self.assertEqual(1, caster.cast('1', int))
|
|
9
|
+
|
|
10
|
+
def test_bool(self):
|
|
11
|
+
self.assertTrue(caster.cast('true', bool))
|
|
12
|
+
|
|
13
|
+
def test_list(self):
|
|
14
|
+
self.assertEqual(['1', '2', '3', '4'], caster.cast('1,2,3,4', list))
|
|
15
|
+
self.assertEqual([1, 2, 3, 4], caster.cast('1, 2, 3, 4', list[int]))
|
|
16
|
+
|
|
17
|
+
def test_set(self):
|
|
18
|
+
self.assertEqual({1, 2, 3, 4}, caster.cast('1,2,3,4', set[int]))
|
|
19
|
+
|
|
20
|
+
def test_dict(self):
|
|
21
|
+
self.assertEqual({'foo': 'bar'}, caster.cast('{"foo": "bar"}', dict))
|