-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathsecret_referencing.py
More file actions
341 lines (284 loc) · 17.5 KB
/
secret_referencing.py
File metadata and controls
341 lines (284 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
import pytest
from unittest.mock import Mock, patch
import phase_cli.utils.secret_referencing as sr
from phase_cli.utils.secret_referencing import resolve_secret_reference, resolve_all_secrets, EnvironmentNotFoundException
from phase_cli.utils.const import SECRET_REF_REGEX
# Mock data for secrets
secrets_dict = {
"current": {
"/": {
"KEY": "value1"
},
"/backend/payments": {
"STRIPE_KEY": "stripe_value"
}
},
"staging": {
"/": {
"DEBUG": "staging_debug_value"
}
},
"prod": {
"/frontend": {
"SECRET_KEY": "prod_secret_value"
}
}
}
# Mock Phase class
class MockPhase:
def get(self, env_name, app_name, keys, path):
# Handle cross-application references
if app_name == "other_app" and env_name == "dev" and path == "/":
return [
{"key": "API_KEY", "value": "other_app_api_key"},
{"key": "POSTGRESQL_USER", "value": "pg_user"},
{"key": "POSTGRESQL_PASSWORD", "value": "pg_password"},
{"key": "POSTGRESQL_DB", "value": "db"},
{"key": "POSTGRESQL_HOST", "value": "localhost"},
{"key": "POSTGRESQL_URL", "value": "postgresql://${/creds/POSTGRESQL_USER}:${/creds/POSTGRESQL_PASSWORD}@${POSTGRESQL_HOST}/${POSTGRESQL_DB}"},
{"key": "A", "value": "${B}"},
{"key": "B", "value": "${C}"},
{"key": "C", "value": "${A}"},
]
elif app_name == "other_app" and env_name == "dev" and path == "/creds":
return [
{"key": "POSTGRESQL_USER", "value": "pg_user"},
{"key": "POSTGRESQL_PASSWORD", "value": "pg_password"},
]
elif app_name == "other_app" and env_name == "prod" and path == "/config":
return [{"key": "DB_PASSWORD", "value": "other_app_db_password"}]
elif app_name == "backend_api" and env_name == "production" and path == "/frontend":
return [{"key": "SECRET_KEY", "value": "backend_api_secret_key"}]
# Handle regular environment references
elif env_name == "prod" and path == "/frontend":
return [{"key": "SECRET_KEY", "value": "prod_secret_value"}]
raise EnvironmentNotFoundException(env_name=env_name)
@pytest.fixture
def phase():
return MockPhase()
@pytest.fixture
def current_env_name():
return "current"
@pytest.fixture
def current_application_name():
return "test_app"
@pytest.fixture(autouse=True)
def clear_cache():
sr._SECRETS_CACHE.clear()
def test_resolve_local_reference_root(phase, current_application_name, current_env_name):
ref = "KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "value1"
def test_resolve_local_reference_path(phase, current_application_name, current_env_name):
ref = "/backend/payments/STRIPE_KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "stripe_value"
def test_resolve_cross_environment_root(phase, current_application_name, current_env_name):
ref = "staging.DEBUG"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "staging_debug_value"
def test_resolve_cross_environment_path(phase, current_application_name, current_env_name):
ref = "prod./frontend/SECRET_KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "prod_secret_value"
def test_resolve_all_secrets(phase, current_application_name, current_env_name):
value = "Use this key: ${KEY}, and this staging key: ${staging.DEBUG}, and this path key: ${/backend/payments/STRIPE_KEY}"
all_secrets = [
{"environment": "current", "path": "/", "key": "KEY", "value": "value1"},
{"environment": "staging", "path": "/", "key": "DEBUG", "value": "staging_debug_value"},
{"environment": "current", "path": "/backend/payments", "key": "STRIPE_KEY", "value": "stripe_value"}
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
expected_value = "Use this key: value1, and this staging key: staging_debug_value, and this path key: stripe_value"
assert resolved_value == expected_value
# Edge Case: Missing key in the current environment
def test_resolve_missing_local_key(phase, current_application_name, current_env_name):
ref = "MISSING_KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "${MISSING_KEY}"
# Edge Case: Missing key in a cross environment reference
def test_resolve_missing_cross_env_key(phase, current_application_name, current_env_name):
ref = "prod.MISSING_KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "${prod.MISSING_KEY}"
# Edge Case: Missing path in a cross environment reference
def test_resolve_missing_cross_env_path(phase, current_application_name, current_env_name):
ref = "prod./missing_path/SECRET_KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "${prod./missing_path/SECRET_KEY}"
# Complex Case: Mixed references with missing values
def test_resolve_mixed_references_with_missing(phase, current_application_name, current_env_name):
value = "Local: ${KEY}, Missing Local: ${MISSING_KEY}, Cross: ${staging.DEBUG}, Missing Cross: ${prod.MISSING_KEY}"
all_secrets = [
{"environment": "current", "path": "/", "key": "KEY", "value": "value1"},
{"environment": "staging", "path": "/", "key": "DEBUG", "value": "staging_debug_value"}
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
expected_value = "Local: value1, Missing Local: ${MISSING_KEY}, Cross: staging_debug_value, Missing Cross: ${prod.MISSING_KEY}"
assert resolved_value == expected_value
# Edge Case: Local reference with missing path
def test_resolve_local_reference_missing_path(phase, current_application_name, current_env_name):
ref = "/missing_path/KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "${/missing_path/KEY}"
# Edge Case: Invalid reference format
def test_resolve_invalid_reference_format(phase, current_application_name, current_env_name):
ref = "invalid_format"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "${invalid_format}"
# Cross Application: Basic root path reference
def test_resolve_cross_application_root(phase, current_application_name, current_env_name):
ref = "other_app::dev.API_KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "other_app_api_key"
# Cross Application: Reference with specific path
def test_resolve_cross_application_path(phase, current_application_name, current_env_name):
ref = "other_app::prod./config/DB_PASSWORD"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "other_app_db_password"
# Cross Application: Missing key
def test_resolve_cross_application_missing_key(phase, current_application_name, current_env_name):
ref = "other_app::dev.MISSING_KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "${other_app::dev.MISSING_KEY}"
# Cross Application: Missing environment
def test_resolve_cross_application_missing_env(phase, current_application_name, current_env_name):
ref = "other_app::missing_env.API_KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "${other_app::missing_env.API_KEY}"
# Cross Application: Mixed references with cross-application
def test_resolve_mixed_references_with_cross_app(phase, current_application_name, current_env_name):
value = "Local: ${KEY}, Cross Env: ${staging.DEBUG}, Cross App: ${other_app::dev.API_KEY}, Missing Cross App: ${other_app::dev.MISSING_KEY}"
all_secrets = [
{"environment": "current", "path": "/", "key": "KEY", "value": "value1"},
{"environment": "staging", "path": "/", "key": "DEBUG", "value": "staging_debug_value"}
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
expected_value = "Local: value1, Cross Env: staging_debug_value, Cross App: other_app_api_key, Missing Cross App: ${other_app::dev.MISSING_KEY}"
assert resolved_value == expected_value
# Cross Application: Complex example with frontend path
def test_resolve_cross_app_frontend_example(phase, current_application_name, current_env_name):
ref = "backend_api::production./frontend/SECRET_KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "backend_api_secret_key"
def test_recursive_cross_app_resolution(phase, current_application_name, current_env_name):
# App A value referencing App B value which itself contains references
value = "DB=${other_app::dev.POSTGRESQL_URL}"
all_secrets = [
{"environment": current_env_name, "path": "/", "key": "POSTGRESQL_URL", "value": "postgresql://${other_app::dev.POSTGRESQL_USER}:${other_app::dev.POSTGRESQL_PASSWORD}@${other_app::dev.POSTGRESQL_HOST}/${other_app::dev.POSTGRESQL_DB}"}
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "DB=postgresql://pg_user:pg_password@localhost/db"
def test_partial_env_case_insensitive_variants(phase, current_application_name, current_env_name):
value = "X=${development.DEBUG};Y=${DEV.DEBUG};Z=${DeVeLoPmEnT.DEBUG}"
all_secrets = [
{"environment": "Development", "path": "/", "key": "DEBUG", "value": "true"},
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "X=true;Y=true;Z=true"
def test_partial_env_substring_variants(phase, current_application_name, current_env_name):
value = "A=${deve.DEBUG};B=${lop.DEBUG}"
all_secrets = [
{"environment": "Development", "path": "/", "key": "DEBUG", "value": "on"},
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "A=on;B=on"
def test_partial_env_ambiguous_prefers_shortest(phase, current_application_name, current_env_name):
value = "Z=${de.DEBUG}"
all_secrets = [
{"environment": "dev", "path": "/", "key": "DEBUG", "value": "a"},
{"environment": "development", "path": "/", "key": "DEBUG", "value": "b"},
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "Z=a"
def test_ambiguous_exact_wins(phase, current_application_name, current_env_name):
value = "Z=${dev.DEBUG}"
all_secrets = [
{"environment": "dev", "path": "/", "key": "DEBUG", "value": "a"},
{"environment": "development", "path": "/", "key": "DEBUG", "value": "b"},
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "Z=a"
def test_recursive_local_multi_layer(phase, current_application_name, current_env_name):
value = "CONN=${/db/URL}"
all_secrets = [
{"environment": current_env_name, "path": "/db", "key": "USER", "value": "u"},
{"environment": current_env_name, "path": "/db", "key": "PASS", "value": "p"},
{"environment": current_env_name, "path": "/db", "key": "HOST", "value": "h"},
{"environment": current_env_name, "path": "/db", "key": "DB", "value": "d"},
{"environment": current_env_name, "path": "/db", "key": "URL", "value": "postgresql://${/db/USER}:${/db/PASS}@${/db/HOST}/${/db/DB}"},
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "CONN=postgresql://u:p@h/d"
def test_recursive_missing_inner_reference(phase, current_application_name, current_env_name):
value = "CONN=${/db/URL}"
all_secrets = [
{"environment": current_env_name, "path": "/db", "key": "USER", "value": "u"},
{"environment": current_env_name, "path": "/db", "key": "PASS", "value": "p"},
{"environment": current_env_name, "path": "/db", "key": "URL", "value": "postgresql://${/db/USER}:${/db/PASS}@${/db/HOST}/${/db/DB}"},
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert "${/db/HOST}" in resolved_value and "${/db/DB}" in resolved_value
def test_cycle_self_reference_cross_app(phase, current_application_name, current_env_name):
value = "X=${other_app::dev.A}"
all_secrets = []
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value.startswith("X=${")
assert "A" in resolved_value
def test_cycle_multi_secret_loop_cross_app(phase, current_application_name, current_env_name):
value = "X=${other_app::dev.A} Y=${other_app::dev.B} Z=${other_app::dev.C}"
all_secrets = []
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert "${" in resolved_value # at least one unresolved placeholder due to cycle
def test_cycle_across_env_case_variants_local(phase, current_application_name, current_env_name):
value = "X=${Development.DEBUG}"
all_secrets = [
{"environment": "Development", "path": "/", "key": "DEBUG", "value": "${development.DEBUG}"},
{"environment": "development", "path": "/", "key": "DEBUG", "value": "${Development.DEBUG}"},
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "X=${Development.DEBUG}" or resolved_value == "X=${development.DEBUG}"
def test_multiple_occurrences_same_reference(phase, current_application_name, current_env_name):
value = "A=${KEY};B=${KEY}"
all_secrets = [
{"environment": current_env_name, "path": "/", "key": "KEY", "value": "v"},
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "A=v;B=v"
# =============================================================================
# Syntax preservation tests to prevent referencing syntax overalp with third party platforms like Railway with ${{...}}
# =============================================================================
def test_railway_syntax_preserved(phase, current_application_name, current_env_name):
"""Railway-style ${{...}} syntax should NOT be treated as a secret reference."""
value = "Some value with ${{RAILWAY_REF}}"
all_secrets = []
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "Some value with ${{RAILWAY_REF}}"
def test_railway_syntax_with_env_preserved(phase, current_application_name, current_env_name):
"""Railway-style ${{env.key}} should NOT be treated as cross-env reference."""
value = "${{production.DATABASE_URL}}"
all_secrets = []
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "${{production.DATABASE_URL}}"
def test_mixed_railway_and_phase_refs(phase, current_application_name, current_env_name):
"""Mix of ${{...}} Railway and ${...} Phase refs - only Phase should be resolved."""
value = "Railway: ${{RAILWAY_TOKEN}}, Phase: ${KEY}"
all_secrets = [
{"environment": current_env_name, "path": "/", "key": "KEY", "value": "secret_value"},
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "Railway: ${{RAILWAY_TOKEN}}, Phase: secret_value"
def test_secret_value_containing_railway_syntax(phase, current_application_name, current_env_name):
"""Secret values containing ${{...}} should preserve the Railway syntax after resolution."""
value = "${CONFIG}"
all_secrets = [
{"environment": current_env_name, "path": "/", "key": "CONFIG", "value": "url=${{RAILWAY.STATIC_URL}}"},
]
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "url=${{RAILWAY.STATIC_URL}}"
def test_github_actions_syntax_preserved(phase, current_application_name, current_env_name):
"""GitHub Actions ${{ secrets.X }} syntax should be preserved like Railway."""
value = "${{ secrets.GITHUB_TOKEN }}"
all_secrets = []
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
assert resolved_value == "${{ secrets.GITHUB_TOKEN }}"