mind-castle 0.1.1__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.
- mind_castle-0.1.1/PKG-INFO +11 -0
- mind_castle-0.1.1/README.md +2 -0
- mind_castle-0.1.1/mind_castle/__init__.py +0 -0
- mind_castle-0.1.1/mind_castle/secret_store.py +20 -0
- mind_castle-0.1.1/mind_castle/secret_stores.py +122 -0
- mind_castle-0.1.1/mind_castle/secret_type.py +50 -0
- mind_castle-0.1.1/mind_castle.egg-info/PKG-INFO +11 -0
- mind_castle-0.1.1/mind_castle.egg-info/SOURCES.txt +12 -0
- mind_castle-0.1.1/mind_castle.egg-info/dependency_links.txt +1 -0
- mind_castle-0.1.1/mind_castle.egg-info/requires.txt +2 -0
- mind_castle-0.1.1/mind_castle.egg-info/top_level.txt +1 -0
- mind_castle-0.1.1/pyproject.toml +21 -0
- mind_castle-0.1.1/setup.cfg +4 -0
- mind_castle-0.1.1/tests/test_memory.py +15 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mind-castle
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A universal secrets manager
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: boto3>=1.35.38
|
|
8
|
+
Requires-Dist: sqlalchemy<3.0.0,>=2.0.0
|
|
9
|
+
|
|
10
|
+
# mind-castle
|
|
11
|
+
A universal secret store
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SecretStore():
|
|
6
|
+
'''This is an abstract class that defines the interface for a secret store.'''
|
|
7
|
+
|
|
8
|
+
def get_secret(self, key: str, default: str = None) -> Optional[str]:
|
|
9
|
+
raise NotImplementedError()
|
|
10
|
+
|
|
11
|
+
def put_secret(self, value: str) -> dict:
|
|
12
|
+
raise NotImplementedError()
|
|
13
|
+
|
|
14
|
+
def get_secret_key(self) -> str:
|
|
15
|
+
'''
|
|
16
|
+
Generates a key based on this machine's hardware and the time.
|
|
17
|
+
https://docs.python.org/3/library/uuid.html#uuid.uuid1
|
|
18
|
+
'''
|
|
19
|
+
|
|
20
|
+
return str(uuid.uuid1())
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import boto3
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from .secret_store import SecretStore
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# If you add a secret store implementation here, make sure to add it to
|
|
10
|
+
# the SecretStoreType enum and TYPE_STORE_MAP dictionary at the bottom of this file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MemorySecretStore(SecretStore):
|
|
14
|
+
'''
|
|
15
|
+
An in-memory secret store.
|
|
16
|
+
This is not persistent and will be lost when the application is restarted.
|
|
17
|
+
Use only for testing or development.
|
|
18
|
+
'''
|
|
19
|
+
|
|
20
|
+
store_type = "mem"
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.secrets = {}
|
|
24
|
+
|
|
25
|
+
def get_secret(self, key: str, default: str = None) -> Optional[str]:
|
|
26
|
+
return self.secrets.get(key, default)
|
|
27
|
+
|
|
28
|
+
def put_secret(self, value: str) -> dict:
|
|
29
|
+
key = self.get_secret_key()
|
|
30
|
+
self.secrets[key] = value
|
|
31
|
+
return {"secret_type": self.store_type, "key": key}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class JsonSecretStore(SecretStore):
|
|
35
|
+
store_type = "json"
|
|
36
|
+
filename = "secrets.json"
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
if not os.path.exists(self.filename):
|
|
40
|
+
with open(self.filename, "w") as f:
|
|
41
|
+
f.write("{}")
|
|
42
|
+
|
|
43
|
+
def get_secret(self, key: str, default: str = None) -> Optional[str]:
|
|
44
|
+
with open(self.filename, "r") as f:
|
|
45
|
+
secrets = json.load(f)
|
|
46
|
+
return secrets.get(key, default)
|
|
47
|
+
|
|
48
|
+
def put_secret(self, value: str) -> dict:
|
|
49
|
+
key = self.get_secret_key()
|
|
50
|
+
with open(self.filename, "r") as f:
|
|
51
|
+
secrets = json.load(f)
|
|
52
|
+
secrets[key] = value
|
|
53
|
+
|
|
54
|
+
with open(self.filename, "w") as f:
|
|
55
|
+
json.dump(secrets, f)
|
|
56
|
+
return {"secret_type": self.store_type, "key": key}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AWSSecretsManagerSecretStore(SecretStore):
|
|
60
|
+
'''
|
|
61
|
+
Uses AWS Secrets Manager to store secrets.
|
|
62
|
+
This assumes you have credentials in the environment or in the default AWS credentials file.
|
|
63
|
+
'''
|
|
64
|
+
store_type = "awssecretsmanager"
|
|
65
|
+
|
|
66
|
+
def __init__(self):
|
|
67
|
+
# TODO: Use this region param as a test for how to pass args to a secret store init
|
|
68
|
+
self.client = boto3.client('secretsmanager', region_name='us-east-2')
|
|
69
|
+
|
|
70
|
+
def get_secret(self, key: str, default: str = None) -> Optional[str]:
|
|
71
|
+
response = self.client.get_secret_value(SecretId=key)
|
|
72
|
+
return response.get("SecretString", default)
|
|
73
|
+
|
|
74
|
+
def put_secret(self, value: str) -> dict:
|
|
75
|
+
key = self.get_secret_key()
|
|
76
|
+
# Response is always a success. This throws an exception if it fails
|
|
77
|
+
response = self.client.create_secret(
|
|
78
|
+
Name=key,
|
|
79
|
+
ClientRequestToken=key,
|
|
80
|
+
# Description='string',
|
|
81
|
+
# KmsKeyId='string',
|
|
82
|
+
# SecretBinary=b'bytes',
|
|
83
|
+
SecretString=value,
|
|
84
|
+
# Tags=[
|
|
85
|
+
# {
|
|
86
|
+
# 'Key': 'string',
|
|
87
|
+
# 'Value': 'string'
|
|
88
|
+
# },
|
|
89
|
+
# ],
|
|
90
|
+
# AddReplicaRegions=[
|
|
91
|
+
# {
|
|
92
|
+
# 'Region': 'string',
|
|
93
|
+
# 'KmsKeyId': 'string'
|
|
94
|
+
# },
|
|
95
|
+
# ],
|
|
96
|
+
# ForceOverwriteReplicaSecret=True|False
|
|
97
|
+
)
|
|
98
|
+
return {"secret_type": self.store_type, "key": key, "arn": response.get("ARN")}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SecretStoreType(str, Enum):
|
|
102
|
+
'''List of supported secret store types'''
|
|
103
|
+
MEMORY = MemorySecretStore.store_type
|
|
104
|
+
JSON = JsonSecretStore.store_type
|
|
105
|
+
AWS_SECRETS_MANAGER = AWSSecretsManagerSecretStore.store_type
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Map of secret store types to their implementations
|
|
109
|
+
TYPE_STORE_MAP = {
|
|
110
|
+
SecretStoreType.MEMORY: MemorySecretStore,
|
|
111
|
+
SecretStoreType.JSON: JsonSecretStore,
|
|
112
|
+
SecretStoreType.AWS_SECRETS_MANAGER: AWSSecretsManagerSecretStore
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_secret_store(secret_type: SecretStoreType) -> SecretStore:
|
|
117
|
+
'''Instantiates and returns a SecretStore implementation with the given type.'''
|
|
118
|
+
|
|
119
|
+
store_class = TYPE_STORE_MAP.get(secret_type)
|
|
120
|
+
if store_class is None:
|
|
121
|
+
raise ValueError(f"Unknown secret store type '{secret_type}'")
|
|
122
|
+
return store_class()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sqlalchemy.types as types
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from .secret_stores import get_secret_store, SecretStoreType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
logger.level = logging.DEBUG
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SecretData(types.TypeDecorator):
|
|
13
|
+
'''A sqlalchemy field type that stores data in a secret store.'''
|
|
14
|
+
|
|
15
|
+
impl = types.JSON # The base data type for this field
|
|
16
|
+
cache_ok = True
|
|
17
|
+
|
|
18
|
+
def __init__(self, store_type: SecretStoreType, *args, **kwargs):
|
|
19
|
+
super().__init__(*args, **kwargs)
|
|
20
|
+
# Get an implementation from the store factory
|
|
21
|
+
# based on the given store_type
|
|
22
|
+
self.secret_type = store_type
|
|
23
|
+
self.secret_store = get_secret_store(store_type)
|
|
24
|
+
|
|
25
|
+
def process_bind_param(self, value, dialect):
|
|
26
|
+
logger.debug(f"Storing {value} of type {type(value)} as a secret.")
|
|
27
|
+
# This might not be a dict or list, but let's json dump everything to be consistent
|
|
28
|
+
stringValue = json.dumps(value, sort_keys=True)
|
|
29
|
+
|
|
30
|
+
store_params = self.secret_store.put_secret(stringValue)
|
|
31
|
+
logger.debug(f"Stored {stringValue} as {store_params}")
|
|
32
|
+
return store_params
|
|
33
|
+
|
|
34
|
+
def process_result_value(self, value, dialect):
|
|
35
|
+
if value == {}:
|
|
36
|
+
return {}
|
|
37
|
+
logger.debug(f"Restoring {value} from a secret.")
|
|
38
|
+
secret_type = value.get("secret_type")
|
|
39
|
+
key = value.get("key")
|
|
40
|
+
if secret_type is None:
|
|
41
|
+
raise ValueError(f"Secret data is missing secret_type: {value}")
|
|
42
|
+
|
|
43
|
+
# We can't assume the secret type here is the same as 'self.secret_type'
|
|
44
|
+
# because the user may have been using a different secret store type previously
|
|
45
|
+
store = get_secret_store(secret_type) if secret_type != self.secret_type else self.secret_store
|
|
46
|
+
result = store.get_secret(key)
|
|
47
|
+
logger.debug(f"Restored {result} from key {key}")
|
|
48
|
+
if result is None:
|
|
49
|
+
return {}
|
|
50
|
+
return json.loads(result)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mind-castle
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A universal secrets manager
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: boto3>=1.35.38
|
|
8
|
+
Requires-Dist: sqlalchemy<3.0.0,>=2.0.0
|
|
9
|
+
|
|
10
|
+
# mind-castle
|
|
11
|
+
A universal secret store
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
mind_castle/__init__.py
|
|
4
|
+
mind_castle/secret_store.py
|
|
5
|
+
mind_castle/secret_stores.py
|
|
6
|
+
mind_castle/secret_type.py
|
|
7
|
+
mind_castle.egg-info/PKG-INFO
|
|
8
|
+
mind_castle.egg-info/SOURCES.txt
|
|
9
|
+
mind_castle.egg-info/dependency_links.txt
|
|
10
|
+
mind_castle.egg-info/requires.txt
|
|
11
|
+
mind_castle.egg-info/top_level.txt
|
|
12
|
+
tests/test_memory.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mind_castle
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mind-castle"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "A universal secrets manager"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"boto3>=1.35.38",
|
|
9
|
+
"sqlalchemy>=2.0.0,<3.0.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[tool.pytest.ini_options]
|
|
13
|
+
pythonpath = [
|
|
14
|
+
"."
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.uv]
|
|
18
|
+
dev-dependencies = [
|
|
19
|
+
"pytest>=8.3.3",
|
|
20
|
+
]
|
|
21
|
+
package = true
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from memory_secret_store import MemorySecretStore
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_put_secret():
|
|
5
|
+
secret_store = MemorySecretStore()
|
|
6
|
+
secret_store.put_secret("some_secret_key", "some_secret_value")
|
|
7
|
+
|
|
8
|
+
assert secret_store.secrets["some_secret_key"] == "some_secret_value"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_get_secret():
|
|
12
|
+
secret_store = MemorySecretStore()
|
|
13
|
+
secret_store.put_secret("some_secret_key", "some_secret_value")
|
|
14
|
+
|
|
15
|
+
assert secret_store.get_secret("some_secret_key") == "some_secret_value"
|