envdrift 4.2.1__py3-none-any.whl
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.
- envdrift/__init__.py +30 -0
- envdrift/_version.py +34 -0
- envdrift/api.py +192 -0
- envdrift/cli.py +42 -0
- envdrift/cli_commands/__init__.py +1 -0
- envdrift/cli_commands/diff.py +91 -0
- envdrift/cli_commands/encryption.py +630 -0
- envdrift/cli_commands/encryption_helpers.py +93 -0
- envdrift/cli_commands/hook.py +75 -0
- envdrift/cli_commands/init_cmd.py +117 -0
- envdrift/cli_commands/partial.py +222 -0
- envdrift/cli_commands/sync.py +1140 -0
- envdrift/cli_commands/validate.py +109 -0
- envdrift/cli_commands/vault.py +376 -0
- envdrift/cli_commands/version.py +15 -0
- envdrift/config.py +489 -0
- envdrift/constants.json +18 -0
- envdrift/core/__init__.py +30 -0
- envdrift/core/diff.py +233 -0
- envdrift/core/encryption.py +400 -0
- envdrift/core/parser.py +260 -0
- envdrift/core/partial_encryption.py +239 -0
- envdrift/core/schema.py +253 -0
- envdrift/core/validator.py +312 -0
- envdrift/encryption/__init__.py +117 -0
- envdrift/encryption/base.py +217 -0
- envdrift/encryption/dotenvx.py +236 -0
- envdrift/encryption/sops.py +458 -0
- envdrift/env_files.py +60 -0
- envdrift/integrations/__init__.py +21 -0
- envdrift/integrations/dotenvx.py +689 -0
- envdrift/integrations/precommit.py +266 -0
- envdrift/integrations/sops.py +85 -0
- envdrift/output/__init__.py +21 -0
- envdrift/output/rich.py +424 -0
- envdrift/py.typed +0 -0
- envdrift/sync/__init__.py +26 -0
- envdrift/sync/config.py +218 -0
- envdrift/sync/engine.py +383 -0
- envdrift/sync/operations.py +138 -0
- envdrift/sync/result.py +99 -0
- envdrift/vault/__init__.py +107 -0
- envdrift/vault/aws.py +282 -0
- envdrift/vault/azure.py +170 -0
- envdrift/vault/base.py +150 -0
- envdrift/vault/gcp.py +210 -0
- envdrift/vault/hashicorp.py +238 -0
- envdrift-4.2.1.dist-info/METADATA +160 -0
- envdrift-4.2.1.dist-info/RECORD +52 -0
- envdrift-4.2.1.dist-info/WHEEL +4 -0
- envdrift-4.2.1.dist-info/entry_points.txt +2 -0
- envdrift-4.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Vault client interfaces for multiple backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from envdrift.vault.base import AuthenticationError, SecretValue, VaultClient, VaultError
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VaultProvider(Enum):
|
|
15
|
+
"""Supported vault providers."""
|
|
16
|
+
|
|
17
|
+
AZURE = "azure"
|
|
18
|
+
AWS = "aws"
|
|
19
|
+
HASHICORP = "hashicorp"
|
|
20
|
+
GCP = "gcp"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_vault_client(provider: VaultProvider | str, **config) -> VaultClient:
|
|
24
|
+
"""
|
|
25
|
+
Create and return a provider-specific VaultClient configured from the provided keyword arguments.
|
|
26
|
+
|
|
27
|
+
Parameters:
|
|
28
|
+
provider (VaultProvider | str): Vault provider enum or provider name ("azure", "aws", "hashicorp", "gcp").
|
|
29
|
+
**config: Provider-specific configuration:
|
|
30
|
+
- For "azure": `vault_url` (str) — required.
|
|
31
|
+
- For "aws": `region` (str) — optional, defaults to "us-east-1".
|
|
32
|
+
- For "hashicorp": `url` (str) — required; `token` (str) — optional.
|
|
33
|
+
- For "gcp": `project_id` (str) — required.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
VaultClient: A configured client instance for the requested provider.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ImportError: If the provider's optional dependencies are not installed.
|
|
40
|
+
ValueError: If the provider is unsupported or cannot be converted to a VaultProvider.
|
|
41
|
+
"""
|
|
42
|
+
if isinstance(provider, str):
|
|
43
|
+
provider = VaultProvider(provider)
|
|
44
|
+
|
|
45
|
+
if provider == VaultProvider.AZURE:
|
|
46
|
+
try:
|
|
47
|
+
from envdrift.vault.azure import AzureKeyVaultClient
|
|
48
|
+
except ImportError as e:
|
|
49
|
+
raise ImportError(
|
|
50
|
+
"Azure vault support requires additional dependencies. "
|
|
51
|
+
"Install with: pip install envdrift[azure]"
|
|
52
|
+
) from e
|
|
53
|
+
vault_url = config.get("vault_url")
|
|
54
|
+
if not vault_url:
|
|
55
|
+
raise ValueError("Azure vault requires 'vault_url' configuration")
|
|
56
|
+
return AzureKeyVaultClient(vault_url=vault_url)
|
|
57
|
+
|
|
58
|
+
elif provider == VaultProvider.AWS:
|
|
59
|
+
try:
|
|
60
|
+
from envdrift.vault.aws import AWSSecretsManagerClient
|
|
61
|
+
except ImportError as e:
|
|
62
|
+
raise ImportError(
|
|
63
|
+
"AWS vault support requires additional dependencies. "
|
|
64
|
+
"Install with: pip install envdrift[aws]"
|
|
65
|
+
) from e
|
|
66
|
+
return AWSSecretsManagerClient(region=config.get("region", "us-east-1"))
|
|
67
|
+
|
|
68
|
+
elif provider == VaultProvider.HASHICORP:
|
|
69
|
+
try:
|
|
70
|
+
from envdrift.vault.hashicorp import HashiCorpVaultClient
|
|
71
|
+
except ImportError as e:
|
|
72
|
+
raise ImportError(
|
|
73
|
+
"HashiCorp Vault support requires additional dependencies. "
|
|
74
|
+
"Install with: pip install envdrift[hashicorp]"
|
|
75
|
+
) from e
|
|
76
|
+
url = config.get("url")
|
|
77
|
+
if not url:
|
|
78
|
+
raise ValueError("HashiCorp Vault requires 'url' configuration")
|
|
79
|
+
return HashiCorpVaultClient(
|
|
80
|
+
url=url,
|
|
81
|
+
token=config.get("token"),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
elif provider == VaultProvider.GCP:
|
|
85
|
+
try:
|
|
86
|
+
from envdrift.vault.gcp import GCPSecretManagerClient
|
|
87
|
+
except ImportError as e:
|
|
88
|
+
raise ImportError(
|
|
89
|
+
"GCP Secret Manager support requires additional dependencies. "
|
|
90
|
+
"Install with: pip install envdrift[gcp]"
|
|
91
|
+
) from e
|
|
92
|
+
project_id = config.get("project_id")
|
|
93
|
+
if not project_id:
|
|
94
|
+
raise ValueError("GCP Secret Manager requires 'project_id' configuration")
|
|
95
|
+
return GCPSecretManagerClient(project_id=project_id)
|
|
96
|
+
|
|
97
|
+
raise ValueError(f"Unsupported vault provider: {provider}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"AuthenticationError",
|
|
102
|
+
"SecretValue",
|
|
103
|
+
"VaultClient",
|
|
104
|
+
"VaultError",
|
|
105
|
+
"VaultProvider",
|
|
106
|
+
"get_vault_client",
|
|
107
|
+
]
|
envdrift/vault/aws.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""AWS Secrets Manager client implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from envdrift.vault.base import (
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
SecretNotFoundError,
|
|
10
|
+
SecretValue,
|
|
11
|
+
VaultClient,
|
|
12
|
+
VaultError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import boto3
|
|
17
|
+
from botocore.exceptions import (
|
|
18
|
+
ClientError,
|
|
19
|
+
NoCredentialsError,
|
|
20
|
+
PartialCredentialsError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
AWS_AVAILABLE = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
AWS_AVAILABLE = False
|
|
26
|
+
boto3 = None
|
|
27
|
+
ClientError = Exception
|
|
28
|
+
NoCredentialsError = Exception
|
|
29
|
+
PartialCredentialsError = Exception
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AWSSecretsManagerClient(VaultClient):
|
|
33
|
+
"""AWS Secrets Manager implementation.
|
|
34
|
+
|
|
35
|
+
Uses boto3's default credential chain which supports:
|
|
36
|
+
- Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
|
|
37
|
+
- Shared credential file (~/.aws/credentials)
|
|
38
|
+
- AWS config file (~/.aws/config)
|
|
39
|
+
- IAM role credentials (EC2, ECS, Lambda)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, region: str = "us-east-1"):
|
|
43
|
+
"""Initialize AWS Secrets Manager client.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
region: AWS region name
|
|
47
|
+
"""
|
|
48
|
+
if not AWS_AVAILABLE:
|
|
49
|
+
raise ImportError("boto3 not installed. Install with: pip install envdrift[aws]")
|
|
50
|
+
|
|
51
|
+
self.region = region
|
|
52
|
+
self._client = None
|
|
53
|
+
|
|
54
|
+
def authenticate(self) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Initialize the AWS Secrets Manager client for the configured region and verify access.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
AuthenticationError: if AWS credentials are missing or incomplete.
|
|
60
|
+
VaultError: if the Secrets Manager service returns an error.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
self._client = boto3.client(
|
|
64
|
+
"secretsmanager",
|
|
65
|
+
region_name=self.region,
|
|
66
|
+
)
|
|
67
|
+
# Test authentication using get_caller_identity via STS
|
|
68
|
+
# This is more reliable than list_secrets which requires extra permissions
|
|
69
|
+
sts = boto3.client("sts", region_name=self.region)
|
|
70
|
+
sts.get_caller_identity()
|
|
71
|
+
except (NoCredentialsError, PartialCredentialsError) as e:
|
|
72
|
+
raise AuthenticationError(f"AWS authentication failed: {e}") from e
|
|
73
|
+
except ClientError as e:
|
|
74
|
+
error_code = e.response.get("Error", {}).get("Code", "")
|
|
75
|
+
if error_code in ("AccessDenied", "InvalidClientTokenId"):
|
|
76
|
+
raise AuthenticationError(f"AWS authentication failed: {e}") from e
|
|
77
|
+
raise VaultError(f"AWS Secrets Manager error: {e}") from e
|
|
78
|
+
|
|
79
|
+
def is_authenticated(self) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Return whether the AWS Secrets Manager client has been authenticated.
|
|
82
|
+
|
|
83
|
+
This method validates credentials by calling STS get_caller_identity(),
|
|
84
|
+
which ensures that expired or revoked credentials are detected.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
`true` if the client is authenticated and credentials are valid, `false` otherwise.
|
|
88
|
+
"""
|
|
89
|
+
if self._client is None:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
# Validate credentials are still valid by calling STS
|
|
93
|
+
try:
|
|
94
|
+
sts = boto3.client("sts", region_name=self.region)
|
|
95
|
+
sts.get_caller_identity()
|
|
96
|
+
return True
|
|
97
|
+
except (NoCredentialsError, PartialCredentialsError, ClientError):
|
|
98
|
+
# Credentials are invalid/expired, reset client state
|
|
99
|
+
self._client = None
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def get_secret(self, name: str) -> SecretValue:
|
|
103
|
+
"""
|
|
104
|
+
Retrieve a secret from AWS Secrets Manager.
|
|
105
|
+
|
|
106
|
+
Parameters:
|
|
107
|
+
name (str): Secret name or ARN.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
SecretValue: Contains the secret's name, value, version, and metadata (`arn`, `created_date`, `version_stages`).
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
SecretNotFoundError: If the secret does not exist.
|
|
114
|
+
VaultError: For other AWS Secrets Manager errors.
|
|
115
|
+
"""
|
|
116
|
+
self.ensure_authenticated()
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
response = self._client.get_secret_value(SecretId=name)
|
|
120
|
+
|
|
121
|
+
# Secret can be string or binary
|
|
122
|
+
if "SecretString" in response:
|
|
123
|
+
value = response["SecretString"]
|
|
124
|
+
else:
|
|
125
|
+
# Binary secrets - try UTF-8, fall back to base64
|
|
126
|
+
try:
|
|
127
|
+
value = response["SecretBinary"].decode("utf-8")
|
|
128
|
+
except UnicodeDecodeError:
|
|
129
|
+
import base64
|
|
130
|
+
|
|
131
|
+
value = base64.b64encode(response["SecretBinary"]).decode("ascii")
|
|
132
|
+
|
|
133
|
+
created = response.get("CreatedDate")
|
|
134
|
+
created_str = str(created) if created else None
|
|
135
|
+
return SecretValue(
|
|
136
|
+
name=response.get("Name", name),
|
|
137
|
+
value=value,
|
|
138
|
+
version=response.get("VersionId"),
|
|
139
|
+
metadata={
|
|
140
|
+
"arn": response.get("ARN"),
|
|
141
|
+
"created_date": created_str,
|
|
142
|
+
"version_stages": response.get("VersionStages", []),
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
except ClientError as e:
|
|
146
|
+
error_code = e.response.get("Error", {}).get("Code", "")
|
|
147
|
+
if error_code == "ResourceNotFoundException":
|
|
148
|
+
raise SecretNotFoundError(f"Secret '{name}' not found") from e
|
|
149
|
+
raise VaultError(f"AWS Secrets Manager error: {e}") from e
|
|
150
|
+
|
|
151
|
+
def list_secrets(self, prefix: str = "") -> list[str]:
|
|
152
|
+
"""
|
|
153
|
+
List secret names stored in AWS Secrets Manager.
|
|
154
|
+
|
|
155
|
+
Parameters:
|
|
156
|
+
prefix (str): Optional name prefix to filter results; only secrets whose names start with this prefix are returned.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
List of secret names.
|
|
160
|
+
"""
|
|
161
|
+
self.ensure_authenticated()
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
secrets = []
|
|
165
|
+
paginator = self._client.get_paginator("list_secrets")
|
|
166
|
+
|
|
167
|
+
# Note: AWS ListSecrets filter with Key="name" performs exact match,
|
|
168
|
+
# so we fetch all secrets and filter client-side for prefix matching
|
|
169
|
+
for page in paginator.paginate():
|
|
170
|
+
for secret in page.get("SecretList", []):
|
|
171
|
+
name = secret.get("Name")
|
|
172
|
+
if name:
|
|
173
|
+
# Apply client-side prefix filtering
|
|
174
|
+
if not prefix or name.startswith(prefix):
|
|
175
|
+
secrets.append(name)
|
|
176
|
+
|
|
177
|
+
return sorted(secrets)
|
|
178
|
+
except ClientError as e:
|
|
179
|
+
raise VaultError(f"AWS Secrets Manager error: {e}") from e
|
|
180
|
+
|
|
181
|
+
def get_secret_json(self, name: str) -> dict:
|
|
182
|
+
"""
|
|
183
|
+
Retrieve the secret identified by `name` and parse its value as a JSON object.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
dict: Parsed JSON from the secret value.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
VaultError: If the secret value is not valid JSON.
|
|
190
|
+
"""
|
|
191
|
+
secret = self.get_secret(name)
|
|
192
|
+
try:
|
|
193
|
+
return json.loads(secret.value)
|
|
194
|
+
except json.JSONDecodeError as e:
|
|
195
|
+
raise VaultError(f"Secret '{name}' is not valid JSON: {e}") from e
|
|
196
|
+
|
|
197
|
+
def create_secret(self, name: str, value: str, description: str = "") -> SecretValue:
|
|
198
|
+
"""
|
|
199
|
+
Create a new secret in AWS Secrets Manager.
|
|
200
|
+
|
|
201
|
+
Parameters:
|
|
202
|
+
name (str): The name to assign to the secret.
|
|
203
|
+
value (str): The secret value to store.
|
|
204
|
+
description (str): Optional human-readable description for the secret.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
SecretValue: The created secret's representation, including the stored value, version identifier, and metadata (ARN).
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
VaultError: If AWS Secrets Manager returns an error while creating the secret.
|
|
211
|
+
"""
|
|
212
|
+
self.ensure_authenticated()
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
response = self._client.create_secret(
|
|
216
|
+
Name=name,
|
|
217
|
+
SecretString=value,
|
|
218
|
+
Description=description,
|
|
219
|
+
)
|
|
220
|
+
return SecretValue(
|
|
221
|
+
name=response.get("Name", name),
|
|
222
|
+
value=value,
|
|
223
|
+
version=response.get("VersionId"),
|
|
224
|
+
metadata={"arn": response.get("ARN")},
|
|
225
|
+
)
|
|
226
|
+
except ClientError as e:
|
|
227
|
+
raise VaultError(f"AWS Secrets Manager error: {e}") from e
|
|
228
|
+
|
|
229
|
+
def update_secret(self, name: str, value: str) -> SecretValue:
|
|
230
|
+
"""
|
|
231
|
+
Update an existing secret in AWS Secrets Manager.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
SecretValue: Contains the updated secret's name, value, version, and ARN metadata.
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
VaultError: If AWS Secrets Manager returns an error.
|
|
238
|
+
"""
|
|
239
|
+
self.ensure_authenticated()
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
response = self._client.put_secret_value(
|
|
243
|
+
SecretId=name,
|
|
244
|
+
SecretString=value,
|
|
245
|
+
)
|
|
246
|
+
return SecretValue(
|
|
247
|
+
name=response.get("Name", name),
|
|
248
|
+
value=value,
|
|
249
|
+
version=response.get("VersionId"),
|
|
250
|
+
metadata={"arn": response.get("ARN")},
|
|
251
|
+
)
|
|
252
|
+
except ClientError as e:
|
|
253
|
+
raise VaultError(f"AWS Secrets Manager error: {e}") from e
|
|
254
|
+
|
|
255
|
+
def set_secret(self, name: str, value: str) -> SecretValue:
|
|
256
|
+
"""
|
|
257
|
+
Create or update a secret in AWS Secrets Manager.
|
|
258
|
+
|
|
259
|
+
Attempts to update the secret first; if it doesn't exist, creates it.
|
|
260
|
+
|
|
261
|
+
Parameters:
|
|
262
|
+
name (str): The name of the secret.
|
|
263
|
+
value (str): The secret value to store.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
SecretValue: The created or updated secret's representation.
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
VaultError: If AWS Secrets Manager returns an error.
|
|
270
|
+
"""
|
|
271
|
+
self.ensure_authenticated()
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
# Try to update first (most common case)
|
|
275
|
+
return self.update_secret(name, value)
|
|
276
|
+
except VaultError as e:
|
|
277
|
+
# Extract original ClientError if available
|
|
278
|
+
if e.__cause__ and hasattr(e.__cause__, "response"):
|
|
279
|
+
error_code = e.__cause__.response.get("Error", {}).get("Code", "")
|
|
280
|
+
if error_code == "ResourceNotFoundException":
|
|
281
|
+
return self.create_secret(name, value)
|
|
282
|
+
raise
|
envdrift/vault/azure.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Azure Key Vault client implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from envdrift.vault.base import (
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
SecretNotFoundError,
|
|
8
|
+
SecretValue,
|
|
9
|
+
VaultClient,
|
|
10
|
+
VaultError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from azure.core.exceptions import (
|
|
15
|
+
ClientAuthenticationError,
|
|
16
|
+
HttpResponseError,
|
|
17
|
+
ResourceNotFoundError,
|
|
18
|
+
)
|
|
19
|
+
from azure.identity import DefaultAzureCredential
|
|
20
|
+
from azure.keyvault.secrets import SecretClient
|
|
21
|
+
|
|
22
|
+
AZURE_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
AZURE_AVAILABLE = False
|
|
25
|
+
DefaultAzureCredential = None
|
|
26
|
+
SecretClient = None
|
|
27
|
+
ResourceNotFoundError = Exception
|
|
28
|
+
ClientAuthenticationError = Exception
|
|
29
|
+
HttpResponseError = Exception
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AzureKeyVaultClient(VaultClient):
|
|
33
|
+
"""Azure Key Vault implementation.
|
|
34
|
+
|
|
35
|
+
Uses DefaultAzureCredential which supports:
|
|
36
|
+
- Environment variables (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID)
|
|
37
|
+
- Managed Identity
|
|
38
|
+
- Azure CLI credentials
|
|
39
|
+
- VS Code credentials
|
|
40
|
+
- Interactive browser login
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, vault_url: str):
|
|
44
|
+
"""
|
|
45
|
+
Create an Azure Key Vault client bound to the provided vault URL.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
vault_url (str): Vault URL (e.g., "https://my-vault.vault.azure.net/").
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ImportError: If the Azure SDK is not installed (install with `pip install envdrift[azure]`).
|
|
52
|
+
"""
|
|
53
|
+
if not AZURE_AVAILABLE:
|
|
54
|
+
raise ImportError("Azure SDK not installed. Install with: pip install envdrift[azure]")
|
|
55
|
+
|
|
56
|
+
self.vault_url = vault_url
|
|
57
|
+
self._client: SecretClient | None = None
|
|
58
|
+
self._credential = None
|
|
59
|
+
|
|
60
|
+
def authenticate(self) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Authenticate to Azure Key Vault using DefaultAzureCredential and initialize the SecretClient.
|
|
63
|
+
|
|
64
|
+
On success sets self._credential to the created credential and self._client to a ready SecretClient.
|
|
65
|
+
Raises AuthenticationError if credential acquisition fails and VaultError for HTTP-related Key Vault errors.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
self._credential = DefaultAzureCredential()
|
|
69
|
+
self._client = SecretClient(
|
|
70
|
+
vault_url=self.vault_url,
|
|
71
|
+
credential=self._credential,
|
|
72
|
+
)
|
|
73
|
+
# Test authentication by actually consuming one item from the iterator
|
|
74
|
+
# The iterator is lazy and won't authenticate until iterated
|
|
75
|
+
secrets_iter = self._client.list_properties_of_secrets()
|
|
76
|
+
next(iter(secrets_iter), None) # Consume one item to verify auth
|
|
77
|
+
except ClientAuthenticationError as e:
|
|
78
|
+
raise AuthenticationError(f"Azure authentication failed: {e}") from e
|
|
79
|
+
except HttpResponseError as e:
|
|
80
|
+
raise VaultError(f"Azure Key Vault error: {e}") from e
|
|
81
|
+
|
|
82
|
+
def is_authenticated(self) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Return whether the client has an initialized SecretClient and is ready for operations.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
`true` if the internal client is initialized, `false` otherwise.
|
|
88
|
+
"""
|
|
89
|
+
return self._client is not None
|
|
90
|
+
|
|
91
|
+
def get_secret(self, name: str) -> SecretValue:
|
|
92
|
+
"""
|
|
93
|
+
Retrieve a secret from the configured Azure Key Vault.
|
|
94
|
+
|
|
95
|
+
Parameters:
|
|
96
|
+
name (str): The name of the secret to retrieve.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
SecretValue: Contains the secret's name, value, version, and metadata (keys: "enabled", "created_on", "updated_on", "content_type").
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
SecretNotFoundError: If no secret with the given name exists in the vault.
|
|
103
|
+
VaultError: For other Azure Key Vault HTTP errors.
|
|
104
|
+
"""
|
|
105
|
+
self.ensure_authenticated()
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
secret = self._client.get_secret(name)
|
|
109
|
+
props = secret.properties
|
|
110
|
+
created = str(props.created_on) if props.created_on else None
|
|
111
|
+
updated = str(props.updated_on) if props.updated_on else None
|
|
112
|
+
return SecretValue(
|
|
113
|
+
name=name,
|
|
114
|
+
value=secret.value or "",
|
|
115
|
+
version=props.version,
|
|
116
|
+
metadata={
|
|
117
|
+
"enabled": props.enabled,
|
|
118
|
+
"created_on": created,
|
|
119
|
+
"updated_on": updated,
|
|
120
|
+
"content_type": props.content_type,
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
except ResourceNotFoundError as e:
|
|
124
|
+
raise SecretNotFoundError(f"Secret '{name}' not found in vault") from e
|
|
125
|
+
except HttpResponseError as e:
|
|
126
|
+
raise VaultError(f"Azure Key Vault error: {e}") from e
|
|
127
|
+
|
|
128
|
+
def list_secrets(self, prefix: str = "") -> list[str]:
|
|
129
|
+
"""
|
|
130
|
+
List secret names in the vault, optionally filtered by a prefix.
|
|
131
|
+
|
|
132
|
+
Parameters:
|
|
133
|
+
prefix (str): Optional string; include only secret names that start with this prefix.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
list[str]: Sorted list of secret names that match the prefix.
|
|
137
|
+
"""
|
|
138
|
+
self.ensure_authenticated()
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
secrets = []
|
|
142
|
+
for secret_properties in self._client.list_properties_of_secrets():
|
|
143
|
+
name = secret_properties.name
|
|
144
|
+
if name and (not prefix or name.startswith(prefix)):
|
|
145
|
+
secrets.append(name)
|
|
146
|
+
return sorted(secrets)
|
|
147
|
+
except HttpResponseError as e:
|
|
148
|
+
raise VaultError(f"Azure Key Vault error: {e}") from e
|
|
149
|
+
|
|
150
|
+
def set_secret(self, name: str, value: str) -> SecretValue:
|
|
151
|
+
"""
|
|
152
|
+
Store or update a secret in Azure Key Vault.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
SecretValue containing the stored secret's name, value, version, and metadata (includes `enabled`).
|
|
156
|
+
"""
|
|
157
|
+
self.ensure_authenticated()
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
secret = self._client.set_secret(name, value)
|
|
161
|
+
return SecretValue(
|
|
162
|
+
name=name,
|
|
163
|
+
value=secret.value or "",
|
|
164
|
+
version=secret.properties.version,
|
|
165
|
+
metadata={
|
|
166
|
+
"enabled": secret.properties.enabled,
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
except HttpResponseError as e:
|
|
170
|
+
raise VaultError(f"Azure Key Vault error: {e}") from e
|