toru-vault 0.1.2__tar.gz → 0.2.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.
- {toru_vault-0.1.2/toru_vault.egg-info → toru_vault-0.2.0}/PKG-INFO +22 -17
- {toru_vault-0.1.2 → toru_vault-0.2.0}/README.md +19 -16
- {toru_vault-0.1.2 → toru_vault-0.2.0}/pyproject.toml +4 -1
- {toru_vault-0.1.2 → toru_vault-0.2.0}/setup.py +5 -3
- toru_vault-0.2.0/tests/test_env_load.py +101 -0
- toru_vault-0.2.0/tests/test_vault_encryption.py +124 -0
- toru_vault-0.2.0/tests/test_vault_jit.py +79 -0
- {toru_vault-0.1.2 → toru_vault-0.2.0}/toru_vault/__init__.py +1 -0
- {toru_vault-0.1.2 → toru_vault-0.2.0}/toru_vault/__main__.py +29 -6
- toru_vault-0.2.0/toru_vault/in_env.py +173 -0
- toru_vault-0.2.0/toru_vault/in_memory.py +379 -0
- {toru_vault-0.1.2 → toru_vault-0.2.0}/toru_vault/lazy_dict.py +10 -15
- toru_vault-0.2.0/toru_vault/vault.py +252 -0
- {toru_vault-0.1.2 → toru_vault-0.2.0/toru_vault.egg-info}/PKG-INFO +22 -17
- {toru_vault-0.1.2 → toru_vault-0.2.0}/toru_vault.egg-info/SOURCES.txt +5 -3
- {toru_vault-0.1.2 → toru_vault-0.2.0}/toru_vault.egg-info/requires.txt +3 -0
- toru_vault-0.1.2/tests/test_cli.py +0 -96
- toru_vault-0.1.2/tests/test_lazy_dict.py +0 -125
- toru_vault-0.1.2/tests/test_vault.py +0 -196
- toru_vault-0.1.2/toru_vault/vault.py +0 -532
- {toru_vault-0.1.2 → toru_vault-0.2.0}/LICENSE +0 -0
- {toru_vault-0.1.2 → toru_vault-0.2.0}/setup.cfg +0 -0
- {toru_vault-0.1.2 → toru_vault-0.2.0}/toru_vault/py.typed +0 -0
- {toru_vault-0.1.2 → toru_vault-0.2.0}/toru_vault.egg-info/dependency_links.txt +0 -0
- {toru_vault-0.1.2 → toru_vault-0.2.0}/toru_vault.egg-info/entry_points.txt +0 -0
- {toru_vault-0.1.2 → toru_vault-0.2.0}/toru_vault.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: toru-vault
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary: ToruVault: A simple Python package for managing Bitwarden secrets
|
5
5
|
Author: Toru AI
|
6
6
|
Author-email: ToruAI <mpaszynski@toruai.com>
|
@@ -15,20 +15,20 @@ Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
16
16
|
Requires-Dist: bitwarden-sdk
|
17
17
|
Requires-Dist: cryptography>=36.0.0
|
18
|
+
Provides-Extra: keyring
|
19
|
+
Requires-Dist: keyring>=23.0.0; extra == "keyring"
|
18
20
|
Dynamic: author
|
19
21
|
Dynamic: license-file
|
20
22
|
Dynamic: requires-python
|
21
23
|
|
22
|
-
|
23
|
-
<img src="img/logo.svg" alt="ToruVault Logo" width="300"/>
|
24
|
-
</p>
|
24
|
+

|
25
25
|
|
26
26
|
# ToruVault
|
27
27
|
|
28
28
|
A simple Python package for managing Bitwarden secrets with enhanced security.
|
29
29
|
|
30
30
|
|
31
|
-

|
32
32
|

|
33
33
|

|
34
34
|
|
@@ -37,8 +37,8 @@ A simple Python package for managing Bitwarden secrets with enhanced security.
|
|
37
37
|
- Load secrets from Bitwarden Secret Manager into environment variables
|
38
38
|
- Get secrets as a Python dictionary
|
39
39
|
- Filter secrets by project ID
|
40
|
-
-
|
41
|
-
-
|
40
|
+
- JIT decryption of individual secrets
|
41
|
+
- No persistent caching of decrypted values
|
42
42
|
- Secure file permissions for state storage
|
43
43
|
- Machine-specific secret protection
|
44
44
|
- Secure credential storage using OS keyring
|
@@ -51,21 +51,27 @@ A simple Python package for managing Bitwarden secrets with enhanced security.
|
|
51
51
|
# Install UV if you don't have it already
|
52
52
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
53
53
|
|
54
|
-
# Install toru-vault package
|
54
|
+
# Install toru-vault package (basic installation)
|
55
55
|
uv pip install toru-vault
|
56
56
|
|
57
|
+
# Or install with keyring support (recommended for secure storage)
|
58
|
+
uv pip install toru-vault[keyring]
|
59
|
+
|
57
60
|
# Or install in a virtual environment (recommended)
|
58
61
|
uv venv create -p python3.10 .venv
|
59
62
|
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
60
|
-
uv pip install toru-vault
|
63
|
+
uv pip install toru-vault[keyring]
|
61
64
|
```
|
62
65
|
|
63
|
-
|
64
|
-
This will automatically install all required dependencies:
|
66
|
+
This will install all required dependencies:
|
65
67
|
- bitwarden-sdk - For interfacing with Bitwarden API
|
66
|
-
- keyring - For secure credential storage
|
67
68
|
- cryptography - For encryption/decryption operations
|
68
69
|
|
70
|
+
And when installed with the keyring option:
|
71
|
+
- keyring - For secure credential storage using OS keyring
|
72
|
+
|
73
|
+
> **Note:** Keyring is now optional but recommended. Without keyring, some features like `toru-vault init` won't work, and you'll need to use the `use_keyring=False` parameter with the `get()` function to use in-memory encryption instead of the system keyring.
|
74
|
+
|
69
75
|
### From Source with UV
|
70
76
|
|
71
77
|
```bash
|
@@ -193,11 +199,10 @@ vault.env_load_all(override=True)
|
|
193
199
|
The vault package includes several security enhancements:
|
194
200
|
|
195
201
|
1. **OS Keyring Integration**: Securely stores BWS_TOKEN, ORGANIZATION_ID, and STATE_FILE in your OS keyring
|
196
|
-
2. **Memory Protection**: Secrets are encrypted in memory using Fernet encryption (AES-128)
|
197
|
-
3. **
|
198
|
-
4. **
|
199
|
-
5. **
|
200
|
-
6. **Machine-Specific Encryption**: Uses machine-specific identifiers for encryption keys
|
202
|
+
2. **Memory Protection**: Secrets are individually encrypted in memory using Fernet encryption (AES-128)
|
203
|
+
3. **JIT Decryption**: Secrets are only decrypted when explicitly accessed and never stored in decrypted form
|
204
|
+
4. **Secure File Permissions**: Sets secure permissions on state files
|
205
|
+
5. **Machine-Specific Encryption**: Uses machine-specific identifiers for encryption keys
|
201
206
|
7. **Cache Clearing**: Automatically clears secret cache on program exit
|
202
207
|
8. **Environment Variable Protection**: Doesn't override existing environment variables by default
|
203
208
|
9. **Secure Key Derivation**: Uses PBKDF2 with SHA-256 for key derivation
|
@@ -1,13 +1,11 @@
|
|
1
|
-
|
2
|
-
<img src="img/logo.svg" alt="ToruVault Logo" width="300"/>
|
3
|
-
</p>
|
1
|
+

|
4
2
|
|
5
3
|
# ToruVault
|
6
4
|
|
7
5
|
A simple Python package for managing Bitwarden secrets with enhanced security.
|
8
6
|
|
9
7
|
|
10
|
-

|
11
9
|

|
12
10
|

|
13
11
|
|
@@ -16,8 +14,8 @@ A simple Python package for managing Bitwarden secrets with enhanced security.
|
|
16
14
|
- Load secrets from Bitwarden Secret Manager into environment variables
|
17
15
|
- Get secrets as a Python dictionary
|
18
16
|
- Filter secrets by project ID
|
19
|
-
-
|
20
|
-
-
|
17
|
+
- JIT decryption of individual secrets
|
18
|
+
- No persistent caching of decrypted values
|
21
19
|
- Secure file permissions for state storage
|
22
20
|
- Machine-specific secret protection
|
23
21
|
- Secure credential storage using OS keyring
|
@@ -30,21 +28,27 @@ A simple Python package for managing Bitwarden secrets with enhanced security.
|
|
30
28
|
# Install UV if you don't have it already
|
31
29
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
32
30
|
|
33
|
-
# Install toru-vault package
|
31
|
+
# Install toru-vault package (basic installation)
|
34
32
|
uv pip install toru-vault
|
35
33
|
|
34
|
+
# Or install with keyring support (recommended for secure storage)
|
35
|
+
uv pip install toru-vault[keyring]
|
36
|
+
|
36
37
|
# Or install in a virtual environment (recommended)
|
37
38
|
uv venv create -p python3.10 .venv
|
38
39
|
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
39
|
-
uv pip install toru-vault
|
40
|
+
uv pip install toru-vault[keyring]
|
40
41
|
```
|
41
42
|
|
42
|
-
|
43
|
-
This will automatically install all required dependencies:
|
43
|
+
This will install all required dependencies:
|
44
44
|
- bitwarden-sdk - For interfacing with Bitwarden API
|
45
|
-
- keyring - For secure credential storage
|
46
45
|
- cryptography - For encryption/decryption operations
|
47
46
|
|
47
|
+
And when installed with the keyring option:
|
48
|
+
- keyring - For secure credential storage using OS keyring
|
49
|
+
|
50
|
+
> **Note:** Keyring is now optional but recommended. Without keyring, some features like `toru-vault init` won't work, and you'll need to use the `use_keyring=False` parameter with the `get()` function to use in-memory encryption instead of the system keyring.
|
51
|
+
|
48
52
|
### From Source with UV
|
49
53
|
|
50
54
|
```bash
|
@@ -172,11 +176,10 @@ vault.env_load_all(override=True)
|
|
172
176
|
The vault package includes several security enhancements:
|
173
177
|
|
174
178
|
1. **OS Keyring Integration**: Securely stores BWS_TOKEN, ORGANIZATION_ID, and STATE_FILE in your OS keyring
|
175
|
-
2. **Memory Protection**: Secrets are encrypted in memory using Fernet encryption (AES-128)
|
176
|
-
3. **
|
177
|
-
4. **
|
178
|
-
5. **
|
179
|
-
6. **Machine-Specific Encryption**: Uses machine-specific identifiers for encryption keys
|
179
|
+
2. **Memory Protection**: Secrets are individually encrypted in memory using Fernet encryption (AES-128)
|
180
|
+
3. **JIT Decryption**: Secrets are only decrypted when explicitly accessed and never stored in decrypted form
|
181
|
+
4. **Secure File Permissions**: Sets secure permissions on state files
|
182
|
+
5. **Machine-Specific Encryption**: Uses machine-specific identifiers for encryption keys
|
180
183
|
7. **Cache Clearing**: Automatically clears secret cache on program exit
|
181
184
|
8. **Environment Variable Protection**: Doesn't override existing environment variables by default
|
182
185
|
9. **Secure Key Derivation**: Uses PBKDF2 with SHA-256 for key derivation
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "toru-vault"
|
7
|
-
version = "0.
|
7
|
+
version = "0.2.0"
|
8
8
|
description = "ToruVault: A simple Python package for managing Bitwarden secrets"
|
9
9
|
readme = "README.md"
|
10
10
|
authors = [
|
@@ -22,6 +22,9 @@ dependencies = [
|
|
22
22
|
"cryptography>=36.0.0",
|
23
23
|
]
|
24
24
|
|
25
|
+
[project.optional-dependencies]
|
26
|
+
keyring = ["keyring>=23.0.0"]
|
27
|
+
|
25
28
|
[project.urls]
|
26
29
|
Homepage = "https://github.com/ToruAI/ToruVault"
|
27
30
|
Issues = "https://github.com/ToruAI/ToruVault/issues"
|
@@ -1,14 +1,16 @@
|
|
1
|
-
from setuptools import setup
|
1
|
+
from setuptools import setup
|
2
2
|
|
3
3
|
setup(
|
4
4
|
name="toru-vault",
|
5
|
-
version='0.
|
5
|
+
version='0.2.0',
|
6
6
|
packages=["toru_vault"],
|
7
7
|
install_requires=[
|
8
8
|
"bitwarden-sdk",
|
9
|
-
"keyring>=23.0.0",
|
10
9
|
"cryptography>=36.0.0",
|
11
10
|
],
|
11
|
+
extras_require={
|
12
|
+
"keyring": ["keyring>=23.0.0"],
|
13
|
+
},
|
12
14
|
description="ToruVault: A simple Python package for managing Bitwarden secrets",
|
13
15
|
author="Toru AI",
|
14
16
|
author_email="dev@toruai.com",
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import os
|
2
|
+
import pytest
|
3
|
+
from unittest.mock import patch, MagicMock
|
4
|
+
|
5
|
+
from toru_vault.vault import env_load, env_load_all
|
6
|
+
|
7
|
+
class TestEnvLoad:
|
8
|
+
|
9
|
+
def test_env_load_single_project(self, mock_env_vars, mock_initialize_client, mock_bitwarden_client):
|
10
|
+
"""Test loading secrets from a single project into environment variables"""
|
11
|
+
# Clear any existing test environment variables
|
12
|
+
if "TEST_SECRET1" in os.environ:
|
13
|
+
del os.environ["TEST_SECRET1"]
|
14
|
+
if "TEST_SECRET2" in os.environ:
|
15
|
+
del os.environ["TEST_SECRET2"]
|
16
|
+
|
17
|
+
# Call the function with a specific project
|
18
|
+
env_load(project_id="project1")
|
19
|
+
|
20
|
+
# Verify Bitwarden client was initialized
|
21
|
+
mock_initialize_client.assert_called_once()
|
22
|
+
|
23
|
+
# Verify secrets synchronization was performed
|
24
|
+
mock_bitwarden_client.secrets().sync.assert_called_once()
|
25
|
+
|
26
|
+
# Verify environment variables were set
|
27
|
+
assert os.environ["TEST_SECRET1"] == "test_value1"
|
28
|
+
assert os.environ["TEST_SECRET2"] == "test_value2"
|
29
|
+
|
30
|
+
# Test overriding
|
31
|
+
os.environ["TEST_SECRET1"] = "existing_value"
|
32
|
+
|
33
|
+
# Call with override=False, shouldn't change existing value
|
34
|
+
env_load(project_id="project1", override=False)
|
35
|
+
assert os.environ["TEST_SECRET1"] == "existing_value"
|
36
|
+
|
37
|
+
# Call with override=True, should change existing value
|
38
|
+
env_load(project_id="project1", override=True)
|
39
|
+
assert os.environ["TEST_SECRET1"] == "test_value1"
|
40
|
+
|
41
|
+
def test_env_load_all(self, mock_env_vars, mock_initialize_client, mock_bitwarden_client):
|
42
|
+
"""Test loading secrets from all projects into environment variables"""
|
43
|
+
# Clear any existing test environment variables
|
44
|
+
if "TEST_SECRET1" in os.environ:
|
45
|
+
del os.environ["TEST_SECRET1"]
|
46
|
+
if "TEST_SECRET2" in os.environ:
|
47
|
+
del os.environ["TEST_SECRET2"]
|
48
|
+
|
49
|
+
# Call the function to load all secrets
|
50
|
+
env_load_all()
|
51
|
+
|
52
|
+
# Verify Bitwarden client was initialized
|
53
|
+
mock_initialize_client.assert_called_once()
|
54
|
+
|
55
|
+
# Verify secrets synchronization was performed
|
56
|
+
mock_bitwarden_client.secrets().sync.assert_called_once()
|
57
|
+
|
58
|
+
# Verify secrets were fetched with list and get_by_ids
|
59
|
+
mock_bitwarden_client.secrets().list.assert_called_once()
|
60
|
+
mock_bitwarden_client.secrets().get_by_ids.assert_called_once()
|
61
|
+
|
62
|
+
# Verify environment variables were set
|
63
|
+
assert os.environ["TEST_SECRET1"] == "test_value1"
|
64
|
+
assert os.environ["TEST_SECRET2"] == "test_value2"
|
65
|
+
|
66
|
+
# Test overriding
|
67
|
+
os.environ["TEST_SECRET1"] = "existing_value"
|
68
|
+
|
69
|
+
# Call with override=False, shouldn't change existing value
|
70
|
+
env_load_all(override=False)
|
71
|
+
assert os.environ["TEST_SECRET1"] == "existing_value"
|
72
|
+
|
73
|
+
# Call with override=True, should change existing value
|
74
|
+
env_load_all(override=True)
|
75
|
+
assert os.environ["TEST_SECRET1"] == "test_value1"
|
76
|
+
|
77
|
+
def test_env_load_missing_organization_id(self, mock_env_vars):
|
78
|
+
"""Test env_load behavior when organization_id is missing"""
|
79
|
+
# Remove organization ID from environment
|
80
|
+
if "ORGANIZATION_ID" in os.environ:
|
81
|
+
del os.environ["ORGANIZATION_ID"]
|
82
|
+
|
83
|
+
# Should not raise exception, just return without doing anything
|
84
|
+
env_load(project_id="project1")
|
85
|
+
|
86
|
+
# Verify no environment variables were set
|
87
|
+
assert "TEST_SECRET1" not in os.environ
|
88
|
+
assert "TEST_SECRET2" not in os.environ
|
89
|
+
|
90
|
+
def test_env_load_all_missing_organization_id(self, mock_env_vars):
|
91
|
+
"""Test env_load_all behavior when organization_id is missing"""
|
92
|
+
# Remove organization ID from environment
|
93
|
+
if "ORGANIZATION_ID" in os.environ:
|
94
|
+
del os.environ["ORGANIZATION_ID"]
|
95
|
+
|
96
|
+
# Should not raise exception, just return without doing anything
|
97
|
+
env_load_all()
|
98
|
+
|
99
|
+
# Verify no environment variables were set
|
100
|
+
assert "TEST_SECRET1" not in os.environ
|
101
|
+
assert "TEST_SECRET2" not in os.environ
|
@@ -0,0 +1,124 @@
|
|
1
|
+
import pytest
|
2
|
+
import os
|
3
|
+
from unittest.mock import patch, MagicMock
|
4
|
+
|
5
|
+
from toru_vault.in_memory import (
|
6
|
+
_encrypt_secret,
|
7
|
+
_decrypt_secret,
|
8
|
+
_encrypt_secrets,
|
9
|
+
_decrypt_secrets,
|
10
|
+
create_secrets_dict
|
11
|
+
)
|
12
|
+
|
13
|
+
class TestEncryption:
|
14
|
+
"""Tests for encryption/decryption implementation"""
|
15
|
+
|
16
|
+
def test_single_secret_encryption_decryption(self):
|
17
|
+
"""Test individual secret encryption and decryption"""
|
18
|
+
secret_value = "super-sensitive-value"
|
19
|
+
|
20
|
+
# Encrypt the secret
|
21
|
+
encrypted = _encrypt_secret(secret_value)
|
22
|
+
assert encrypted is not None
|
23
|
+
assert ":" in encrypted # Should contain salt and encrypted data
|
24
|
+
|
25
|
+
# Should not be the same as original
|
26
|
+
assert encrypted != secret_value
|
27
|
+
|
28
|
+
# Decrypt the secret
|
29
|
+
decrypted = _decrypt_secret(encrypted)
|
30
|
+
assert decrypted == secret_value
|
31
|
+
|
32
|
+
def test_multiple_secrets_encryption_decryption(self):
|
33
|
+
"""Test encryption and decryption of multiple secrets"""
|
34
|
+
secrets = {
|
35
|
+
"API_KEY": "secret-api-key",
|
36
|
+
"PASSWORD": "secret-password",
|
37
|
+
"TOKEN": "secret-token"
|
38
|
+
}
|
39
|
+
|
40
|
+
# Encrypt dictionary of secrets
|
41
|
+
encrypted_secrets = _encrypt_secrets(secrets)
|
42
|
+
assert encrypted_secrets is not None
|
43
|
+
|
44
|
+
# Ensure each value is encrypted
|
45
|
+
for key, value in encrypted_secrets.items():
|
46
|
+
assert ":" in value # Should contain salt and encrypted data
|
47
|
+
assert value != secrets[key] # Should not match original value
|
48
|
+
|
49
|
+
# Decrypt all secrets
|
50
|
+
decrypted_secrets = _decrypt_secrets(encrypted_secrets)
|
51
|
+
assert decrypted_secrets is not None
|
52
|
+
|
53
|
+
# Ensure decrypted values match original
|
54
|
+
for key, value in secrets.items():
|
55
|
+
assert decrypted_secrets[key] == value
|
56
|
+
|
57
|
+
def test_decryption_in_memory(self):
|
58
|
+
"""Test decryption in non-keyring mode"""
|
59
|
+
# Create test secrets
|
60
|
+
test_secrets = {
|
61
|
+
"API_KEY": "test-api-key",
|
62
|
+
"DB_PASSWORD": "test-db-password"
|
63
|
+
}
|
64
|
+
|
65
|
+
# Create secret keys set
|
66
|
+
secret_keys = set(test_secrets.keys())
|
67
|
+
|
68
|
+
# Create LazySecretsDict with in-memory decryption
|
69
|
+
secrets_dict = create_secrets_dict(
|
70
|
+
secret_keys,
|
71
|
+
"test_org_id",
|
72
|
+
"test_project_id",
|
73
|
+
test_secrets, # Direct use of secrets
|
74
|
+
False # No keyring
|
75
|
+
)
|
76
|
+
|
77
|
+
# Access each secret and verify correct values
|
78
|
+
for key, expected_value in test_secrets.items():
|
79
|
+
assert secrets_dict[key] == expected_value
|
80
|
+
|
81
|
+
@patch("keyring.get_password")
|
82
|
+
@patch("keyring.set_password")
|
83
|
+
def test_decryption_with_keyring(self, mock_set_password, mock_get_password):
|
84
|
+
"""Test decryption with keyring enabled"""
|
85
|
+
# Create test secrets
|
86
|
+
test_secrets = {
|
87
|
+
"API_KEY": "test-api-key",
|
88
|
+
"DB_PASSWORD": "test-db-password"
|
89
|
+
}
|
90
|
+
|
91
|
+
# Mock keyring to return encrypted values
|
92
|
+
def mock_get_side_effect(service_name, key):
|
93
|
+
# Return encrypted version of the test secret
|
94
|
+
if key in test_secrets:
|
95
|
+
return _encrypt_secret(test_secrets[key])
|
96
|
+
return None
|
97
|
+
|
98
|
+
mock_get_password.side_effect = mock_get_side_effect
|
99
|
+
|
100
|
+
# Create secret keys set
|
101
|
+
secret_keys = set(test_secrets.keys())
|
102
|
+
organization_id = "test_org_id"
|
103
|
+
project_id = "test_project_id"
|
104
|
+
|
105
|
+
# Create LazySecretsDict with keyring decryption
|
106
|
+
secrets_dict = create_secrets_dict(
|
107
|
+
secret_keys,
|
108
|
+
organization_id,
|
109
|
+
project_id,
|
110
|
+
test_secrets, # Used to initialize keyring
|
111
|
+
True # Use keyring
|
112
|
+
)
|
113
|
+
|
114
|
+
# Verify secrets were encrypted and stored in keyring
|
115
|
+
assert mock_set_password.call_count == len(test_secrets)
|
116
|
+
|
117
|
+
# Access each secret and verify decryption
|
118
|
+
for key, expected_value in test_secrets.items():
|
119
|
+
# Value should be decrypted from keyring when accessed
|
120
|
+
assert secrets_dict[key] == expected_value
|
121
|
+
|
122
|
+
|
123
|
+
|
124
|
+
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import pytest
|
2
|
+
import os
|
3
|
+
from unittest.mock import patch, MagicMock
|
4
|
+
|
5
|
+
class TestVaultDecryption:
|
6
|
+
"""Test encryption/decryption with vault API functions"""
|
7
|
+
|
8
|
+
def test_get_in_memory(self, mock_initialize_client, mock_env_vars):
|
9
|
+
"""Test retrieval with get() using in-memory mode"""
|
10
|
+
from toru_vault import vault
|
11
|
+
|
12
|
+
# Get secrets with in-memory storage (no keyring)
|
13
|
+
secrets = vault.get("project1", use_keyring=False)
|
14
|
+
|
15
|
+
# Verify we got the expected keys
|
16
|
+
assert "TEST_SECRET1" in secrets
|
17
|
+
assert "TEST_SECRET2" in secrets
|
18
|
+
|
19
|
+
# Access secrets and verify values
|
20
|
+
assert secrets["TEST_SECRET1"] == "test_value1"
|
21
|
+
assert secrets["TEST_SECRET2"] == "test_value2"
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
@patch("keyring.get_password")
|
26
|
+
@patch("keyring.set_password")
|
27
|
+
def test_get_with_keyring(self, mock_set, mock_get, mock_initialize_client, mock_env_vars):
|
28
|
+
"""Test decryption with get() using keyring"""
|
29
|
+
from toru_vault import vault
|
30
|
+
from toru_vault.in_memory import _encrypt_secret, _decrypt_secret
|
31
|
+
|
32
|
+
# Setup keyring mock to simulate stored encrypted values
|
33
|
+
def mock_get_side_effect(service_name, key):
|
34
|
+
if key == "TEST_SECRET1":
|
35
|
+
return _encrypt_secret("test_value1")
|
36
|
+
elif key == "TEST_SECRET2":
|
37
|
+
return _encrypt_secret("test_value2")
|
38
|
+
return None
|
39
|
+
|
40
|
+
mock_get.side_effect = mock_get_side_effect
|
41
|
+
|
42
|
+
# Get secrets using keyring
|
43
|
+
secrets = vault.get("project1", use_keyring=True)
|
44
|
+
|
45
|
+
# Access both secrets sequentially to test individual decryption
|
46
|
+
value1 = secrets["TEST_SECRET1"]
|
47
|
+
assert value1 == "test_value1"
|
48
|
+
|
49
|
+
value2 = secrets["TEST_SECRET2"]
|
50
|
+
assert value2 == "test_value2"
|
51
|
+
|
52
|
+
# Verify we called get_password at least twice, plus once for org_id lookup
|
53
|
+
# Expected number: 2 secret lookups + 1 for ORGANIZATION_ID
|
54
|
+
assert mock_get.call_count >= 3
|
55
|
+
|
56
|
+
# Tests for env_load and env_load_all removed as they already exist in TestEnvLoad
|
57
|
+
|
58
|
+
def test_get_all(self, mock_initialize_client, mock_env_vars):
|
59
|
+
"""Test decryption with get_all()"""
|
60
|
+
with patch("toru_vault.vault.get") as mock_get:
|
61
|
+
from toru_vault import vault
|
62
|
+
|
63
|
+
# Setup mock get to return test secrets
|
64
|
+
mock_get.return_value = {
|
65
|
+
"TEST_SECRET1": "test_value1",
|
66
|
+
"TEST_SECRET2": "test_value2"
|
67
|
+
}
|
68
|
+
|
69
|
+
# Call get_all
|
70
|
+
all_secrets = vault.get_all(use_keyring=False)
|
71
|
+
|
72
|
+
# Verify get was called for the project
|
73
|
+
mock_get.assert_called_once_with("project1", use_keyring=False)
|
74
|
+
|
75
|
+
# Verify expected secrets are in result
|
76
|
+
assert "TEST_SECRET1" in all_secrets
|
77
|
+
assert all_secrets["TEST_SECRET1"] == "test_value1"
|
78
|
+
assert "TEST_SECRET2" in all_secrets
|
79
|
+
assert all_secrets["TEST_SECRET2"] == "test_value2"
|
@@ -7,9 +7,32 @@ import os
|
|
7
7
|
import sys
|
8
8
|
import getpass
|
9
9
|
|
10
|
-
from .vault import (_initialize_client,
|
11
|
-
_KEYRING_BWS_TOKEN_KEY, _KEYRING_ORG_ID_KEY,
|
12
|
-
_KEYRING_AVAILABLE)
|
10
|
+
from .vault import (_initialize_client, _get_from_keyring_or_env,
|
11
|
+
_KEYRING_SERVICE_NAME, _KEYRING_BWS_TOKEN_KEY, _KEYRING_ORG_ID_KEY,
|
12
|
+
_KEYRING_STATE_FILE_KEY, _KEYRING_AVAILABLE)
|
13
|
+
|
14
|
+
|
15
|
+
def _set_to_keyring(key, value):
|
16
|
+
"""
|
17
|
+
Set a value to keyring
|
18
|
+
|
19
|
+
Args:
|
20
|
+
key (str): Key in keyring
|
21
|
+
value (str): Value to store
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
bool: True if successful, False otherwise
|
25
|
+
"""
|
26
|
+
if not _KEYRING_AVAILABLE:
|
27
|
+
return False
|
28
|
+
|
29
|
+
try:
|
30
|
+
import keyring
|
31
|
+
keyring.set_password(_KEYRING_SERVICE_NAME, key, value)
|
32
|
+
return True
|
33
|
+
except Exception as e:
|
34
|
+
print(f"Failed to set {key} to keyring: {e}")
|
35
|
+
return False
|
13
36
|
|
14
37
|
|
15
38
|
def list_projects(organization_id=None):
|
@@ -103,21 +126,21 @@ def init_vault():
|
|
103
126
|
# Store in keyring
|
104
127
|
if _KEYRING_AVAILABLE:
|
105
128
|
if existing_token != token or not existing_token:
|
106
|
-
if
|
129
|
+
if _set_to_keyring(_KEYRING_BWS_TOKEN_KEY, token):
|
107
130
|
print("BWS_TOKEN stored in keyring")
|
108
131
|
else:
|
109
132
|
print("Failed to store BWS_TOKEN in keyring")
|
110
133
|
return False
|
111
134
|
|
112
135
|
if existing_org_id != org_id or not existing_org_id:
|
113
|
-
if
|
136
|
+
if _set_to_keyring(_KEYRING_ORG_ID_KEY, org_id):
|
114
137
|
print("ORGANIZATION_ID stored in keyring")
|
115
138
|
else:
|
116
139
|
print("Failed to store ORGANIZATION_ID in keyring")
|
117
140
|
return False
|
118
141
|
|
119
142
|
if existing_state_file != state_file or not existing_state_file:
|
120
|
-
if
|
143
|
+
if _set_to_keyring(_KEYRING_STATE_FILE_KEY, state_file):
|
121
144
|
print("STATE_FILE stored in keyring")
|
122
145
|
else:
|
123
146
|
print("Failed to store STATE_FILE in keyring")
|