Skip to content

Commit 5d8fa01

Browse files
Merge branch 'main' into fix--override-exception-import
2 parents c6a3534 + 1954fd0 commit 5d8fa01

File tree

5 files changed

+72
-15
lines changed

5 files changed

+72
-15
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "phase_dev"
7-
version = "2.1.0"
7+
version = "2.1.1"
88
description = "Python SDK for Phase secrets manager"
99
readme = "README.md"
1010
requires-python = ">=3.10"

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
certifi==2023.7.22
2-
cffi==1.15.1
2+
cffi==1.17.1
33
charset-normalizer==3.1.0
44
exceptiongroup==1.1.1
55
idna==3.4
@@ -9,6 +9,6 @@ pluggy==1.0.0
99
pycparser==2.21
1010
PyNaCl==1.5.0
1111
pytest==7.3.1
12-
requests==2.31.0
12+
requests==2.32.0
1313
tomli==2.0.1
1414
urllib3==2.2.2

src/phase/utils/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
import re
33

4-
__version__ = "2.1.0"
4+
__version__ = "2.1.1"
55
__ph_version__ = "v1"
66

77

src/phase/utils/secret_referencing.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import re
21
from typing import Dict, List
32
from .exceptions import EnvironmentNotFoundException
43
from .const import SECRET_REF_REGEX
4+
from .phase_io import Phase
55

66
"""
77
Secret Referencing Syntax:
88
99
This documentation explains the syntax used for referencing secrets within the configuration.
10-
Secrets can be referenced both locally (within the same environment) and across different environments,
11-
with or without specifying a path.
10+
Secrets can be referenced locally (within the same environment), across different environments,
11+
and across different applications, with or without specifying a path.
1212
1313
Syntax Patterns:
1414
@@ -40,8 +40,16 @@
4040
- Secret Key: `STRIPE_KEY`
4141
- Description: References a secret named `STRIPE_KEY` located at `/backend/payments/` in the current environment.
4242
43+
5. Cross-Application Reference:
44+
Syntax: `${backend_api::production./frontend/SECRET_KEY}`
45+
- Application: Different application (e.g., `backend_api`).
46+
- Environment: Different environment (e.g., `production`).
47+
- Path: Specifies a path within the environment (`/frontend/`).
48+
- Secret Key: `SECRET_KEY`
49+
- Description: References a secret named `SECRET_KEY` located at `/frontend/` in the `production` environment of the `backend_api` application.
50+
4351
Note:
44-
The syntax allows for flexible secret management, enabling both straightforward local references and more complex cross-environment references.
52+
The syntax allows for flexible secret management, enabling local references, cross-environment references, and cross-application references.
4553
"""
4654

4755

@@ -74,12 +82,13 @@ def resolve_secret_reference(ref: str, secrets_dict: Dict[str, Dict[str, Dict[st
7482
"""
7583
Resolves a single secret reference to its actual value by fetching it from the specified environment.
7684
77-
The function supports both local and cross-environment secret references, allowing for flexible secret management.
85+
The function supports local, cross-environment, and cross-application secret references, allowing for flexible secret management.
7886
Local references are identified by the absence of a dot '.' in the reference string, implying the current environment.
7987
Cross-environment references include an environment name, separated by a dot from the rest of the path.
88+
Cross-application references use '::' to separate the application name from the rest of the reference.
8089
8190
Args:
82-
ref (str): The secret reference string, which could be a local or cross-environment reference.
91+
ref (str): The secret reference string, which could be a local, cross-environment, or cross-application reference.
8392
secrets_dict (Dict[str, Dict[str, Dict[str, str]]]): A dictionary containing known secrets.
8493
phase ('Phase'): An instance of the Phase class to fetch secrets.
8594
current_application_name (str): The name of the current application.
@@ -88,10 +97,17 @@ def resolve_secret_reference(ref: str, secrets_dict: Dict[str, Dict[str, Dict[st
8897
Returns:
8998
str: The resolved secret value or the original reference if not resolved.
9099
"""
100+
original_ref = ref # Store the original reference
101+
app_name = current_application_name
91102
env_name = current_env_name
92103
path = "/" # Default root path
93104
key_name = ref
94105

106+
# Check if this is a cross-application reference
107+
if "::" in ref:
108+
parts = ref.split("::", 1)
109+
app_name, ref = parts[0], parts[1]
110+
95111
# Parse the reference to identify environment, path, and secret key.
96112
if "." in ref: # Cross-environment references
97113
parts = ref.split(".", 1)
@@ -112,15 +128,15 @@ def resolve_secret_reference(ref: str, secrets_dict: Dict[str, Dict[str, Dict[st
112128
return secrets_dict[env_name]['/'][key_name]
113129

114130
# If the secret is not found in secrets_dict, try to fetch it from Phase
115-
fetched_secrets = phase.get(env_name=env_name, app_name=current_application_name, keys=[key_name], path=path)
131+
fetched_secrets = phase.get(env_name=env_name, app_name=app_name, keys=[key_name], path=path)
116132
for secret in fetched_secrets:
117133
if secret["key"] == key_name:
118134
return secret["value"]
119135
except EnvironmentNotFoundException:
120136
pass
121137

122-
# Return the reference as is if not resolved
123-
return f"${{{ref}}}"
138+
# Return the original secret value as is if not resolved
139+
return f"${{{original_ref}}}"
124140

125141

126142
def resolve_all_secrets(value: str, all_secrets: List[Dict[str, str]], phase: 'Phase', current_application_name: str, current_env_name: str) -> str:

tests/test_secret_referencing.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@
2929
# Mock Phase class
3030
class MockPhase:
3131
def get(self, env_name, app_name, keys, path):
32-
if env_name == "prod" and path == "/frontend":
32+
if env_name == "prod" and path == "/frontend" and app_name == "test_app":
3333
return [{"key": "SECRET_KEY", "value": "prod_secret_value"}]
34+
if env_name == "production" and path == "/" and app_name == "backend_api":
35+
return [{"key": "API_KEY", "value": "backend_api_key"}]
36+
if env_name == "staging" and path == "/auth" and app_name == "auth_service":
37+
return [{"key": "AUTH_TOKEN", "value": "auth_service_token"}]
3438
raise EnvironmentNotFoundException(env_name=env_name)
3539

3640
@pytest.fixture
@@ -115,4 +119,41 @@ def test_resolve_local_reference_missing_path(phase, current_application_name, c
115119
def test_resolve_invalid_reference_format(phase, current_application_name, current_env_name):
116120
ref = "invalid_format"
117121
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
118-
assert resolved_value == "${invalid_format}"
122+
assert resolved_value == "${invalid_format}"
123+
124+
# Tests for Cross-Application References
125+
def test_resolve_cross_app_reference(phase, current_application_name, current_env_name):
126+
ref = "backend_api::production.API_KEY"
127+
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
128+
assert resolved_value == "backend_api_key"
129+
130+
def test_resolve_cross_app_reference_with_path(phase, current_application_name, current_env_name):
131+
ref = "auth_service::staging./auth/AUTH_TOKEN"
132+
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
133+
assert resolved_value == "auth_service_token"
134+
135+
def test_resolve_missing_cross_app_key(phase, current_application_name, current_env_name):
136+
ref = "another_app::dev.MISSING_KEY"
137+
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
138+
assert resolved_value == "${another_app::dev.MISSING_KEY}"
139+
140+
def test_resolve_all_secrets_with_cross_app(phase, current_application_name, current_env_name):
141+
value = "Use this key: ${KEY}, this cross-app key: ${backend_api::production.API_KEY}, and this path key: ${/backend/payments/STRIPE_KEY}"
142+
all_secrets = [
143+
{"environment": "current", "path": "/", "key": "KEY", "value": "value1"},
144+
{"environment": "current", "path": "/backend/payments", "key": "STRIPE_KEY", "value": "stripe_value"}
145+
]
146+
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
147+
expected_value = "Use this key: value1, this cross-app key: backend_api_key, and this path key: stripe_value"
148+
assert resolved_value == expected_value
149+
150+
# Complex Case: Mixed references including cross-app with missing values
151+
def test_resolve_mixed_references_with_cross_app(phase, current_application_name, current_env_name):
152+
value = "Local: ${KEY}, Cross-Env: ${staging.DEBUG}, Cross-App: ${backend_api::production.API_KEY}, Missing Cross-App: ${missing_app::prod.KEY}"
153+
all_secrets = [
154+
{"environment": "current", "path": "/", "key": "KEY", "value": "value1"},
155+
{"environment": "staging", "path": "/", "key": "DEBUG", "value": "staging_debug_value"}
156+
]
157+
resolved_value = resolve_all_secrets(value, all_secrets, phase, current_application_name, current_env_name)
158+
expected_value = "Local: value1, Cross-Env: staging_debug_value, Cross-App: backend_api_key, Missing Cross-App: ${missing_app::prod.KEY}"
159+
assert resolved_value == expected_value

0 commit comments

Comments
 (0)