Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
certifi==2023.7.22
cffi==1.15.1
cffi==1.17.1
charset-normalizer==3.1.0
exceptiongroup==1.1.1
idna==3.4
Expand All @@ -9,6 +9,6 @@ pluggy==1.0.0
pycparser==2.21
PyNaCl==1.5.0
pytest==7.3.1
requests==2.31.0
requests==2.32.0
tomli==2.0.1
urllib3==2.2.2
34 changes: 25 additions & 9 deletions src/phase/utils/secret_referencing.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import re
from typing import Dict, List
from .exceptions import EnvironmentNotFoundException
from .const import SECRET_REF_REGEX
from .phase_io import Phase

"""
Secret Referencing Syntax:

This documentation explains the syntax used for referencing secrets within the configuration.
Secrets can be referenced both locally (within the same environment) and across different environments,
with or without specifying a path.
Secrets can be referenced locally (within the same environment), across different environments,
and across different applications, with or without specifying a path.

Syntax Patterns:

Expand Down Expand Up @@ -40,8 +40,16 @@
- Secret Key: `STRIPE_KEY`
- Description: References a secret named `STRIPE_KEY` located at `/backend/payments/` in the current environment.

5. Cross-Application Reference:
Syntax: `${backend_api::production./frontend/SECRET_KEY}`
- Application: Different application (e.g., `backend_api`).
- Environment: Different environment (e.g., `production`).
- Path: Specifies a path within the environment (`/frontend/`).
- Secret Key: `SECRET_KEY`
- Description: References a secret named `SECRET_KEY` located at `/frontend/` in the `production` environment of the `backend_api` application.

Note:
The syntax allows for flexible secret management, enabling both straightforward local references and more complex cross-environment references.
The syntax allows for flexible secret management, enabling local references, cross-environment references, and cross-application references.
"""


Expand Down Expand Up @@ -74,12 +82,13 @@ def resolve_secret_reference(ref: str, secrets_dict: Dict[str, Dict[str, Dict[st
"""
Resolves a single secret reference to its actual value by fetching it from the specified environment.

The function supports both local and cross-environment secret references, allowing for flexible secret management.
The function supports local, cross-environment, and cross-application secret references, allowing for flexible secret management.
Local references are identified by the absence of a dot '.' in the reference string, implying the current environment.
Cross-environment references include an environment name, separated by a dot from the rest of the path.
Cross-application references use '::' to separate the application name from the rest of the reference.

Args:
ref (str): The secret reference string, which could be a local or cross-environment reference.
ref (str): The secret reference string, which could be a local, cross-environment, or cross-application reference.
secrets_dict (Dict[str, Dict[str, Dict[str, str]]]): A dictionary containing known secrets.
phase ('Phase'): An instance of the Phase class to fetch secrets.
current_application_name (str): The name of the current application.
Expand All @@ -88,10 +97,17 @@ def resolve_secret_reference(ref: str, secrets_dict: Dict[str, Dict[str, Dict[st
Returns:
str: The resolved secret value or the original reference if not resolved.
"""
original_ref = ref # Store the original reference
app_name = current_application_name
env_name = current_env_name
path = "/" # Default root path
key_name = ref

# Check if this is a cross-application reference
if "::" in ref:
parts = ref.split("::", 1)
app_name, ref = parts[0], parts[1]

# Parse the reference to identify environment, path, and secret key.
if "." in ref: # Cross-environment references
parts = ref.split(".", 1)
Expand All @@ -112,15 +128,15 @@ def resolve_secret_reference(ref: str, secrets_dict: Dict[str, Dict[str, Dict[st
return secrets_dict[env_name]['/'][key_name]

# If the secret is not found in secrets_dict, try to fetch it from Phase
fetched_secrets = phase.get(env_name=env_name, app_name=current_application_name, keys=[key_name], path=path)
fetched_secrets = phase.get(env_name=env_name, app_name=app_name, keys=[key_name], path=path)
for secret in fetched_secrets:
if secret["key"] == key_name:
return secret["value"]
except EnvironmentNotFoundException:
pass

# Return the reference as is if not resolved
return f"${{{ref}}}"
# Return the original secret value as is if not resolved
return f"${{{original_ref}}}"


def resolve_all_secrets(value: str, all_secrets: List[Dict[str, str]], phase: 'Phase', current_application_name: str, current_env_name: str) -> str:
Expand Down
45 changes: 43 additions & 2 deletions tests/test_secret_referencing.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@
# Mock Phase class
class MockPhase:
def get(self, env_name, app_name, keys, path):
if env_name == "prod" and path == "/frontend":
if env_name == "prod" and path == "/frontend" and app_name == "test_app":
return [{"key": "SECRET_KEY", "value": "prod_secret_value"}]
if env_name == "production" and path == "/" and app_name == "backend_api":
return [{"key": "API_KEY", "value": "backend_api_key"}]
if env_name == "staging" and path == "/auth" and app_name == "auth_service":
return [{"key": "AUTH_TOKEN", "value": "auth_service_token"}]
raise EnvironmentNotFoundException(env_name=env_name)

@pytest.fixture
Expand Down Expand Up @@ -115,4 +119,41 @@ def test_resolve_local_reference_missing_path(phase, current_application_name, c
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}"
assert resolved_value == "${invalid_format}"

# Tests for Cross-Application References
def test_resolve_cross_app_reference(phase, current_application_name, current_env_name):
ref = "backend_api::production.API_KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "backend_api_key"

def test_resolve_cross_app_reference_with_path(phase, current_application_name, current_env_name):
ref = "auth_service::staging./auth/AUTH_TOKEN"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "auth_service_token"

def test_resolve_missing_cross_app_key(phase, current_application_name, current_env_name):
ref = "another_app::dev.MISSING_KEY"
resolved_value = resolve_secret_reference(ref, secrets_dict, phase, current_application_name, current_env_name)
assert resolved_value == "${another_app::dev.MISSING_KEY}"

def test_resolve_all_secrets_with_cross_app(phase, current_application_name, current_env_name):
value = "Use this key: ${KEY}, this cross-app key: ${backend_api::production.API_KEY}, and this path key: ${/backend/payments/STRIPE_KEY}"
all_secrets = [
{"environment": "current", "path": "/", "key": "KEY", "value": "value1"},
{"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, this cross-app key: backend_api_key, and this path key: stripe_value"
assert resolved_value == expected_value

# Complex Case: Mixed references including cross-app with missing values
def test_resolve_mixed_references_with_cross_app(phase, current_application_name, current_env_name):
value = "Local: ${KEY}, Cross-Env: ${staging.DEBUG}, Cross-App: ${backend_api::production.API_KEY}, Missing Cross-App: ${missing_app::prod.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: backend_api_key, Missing Cross-App: ${missing_app::prod.KEY}"
assert resolved_value == expected_value