Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 1 addition & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,8 @@ jobs:
run: |
poetry run pytest
- name: SonarCloud
uses: SonarSource/sonarcloud-github-action@master
uses: SonarSource/sonarcloud-github-action@4006f663ecaf1f8093e8e4abb9227f6041f52216
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: SonarQube Quality Gate check
uses: sonarsource/sonarqube-quality-gate-action@master
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ temp/
coverage.xml

setup.py

# Docker test artifacts
Dockerfile.test
9 changes: 9 additions & 0 deletions connect/eaas/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,12 @@
PROXIED_CONNECT_API_ENDPOINTS_PUBLIC_PREFIX,
PROXIED_CONNECT_API_ENDPOINTS_FILES_PREFIX,
)


EGRESS_PROXY_DEFAULT_MAX_RETRIES = 3
EGRESS_PROXY_DEFAULT_PATH = 'proxy'
EGRESS_PROXY_X_CONNECT_TARGET_URL_HEADER = 'X-Connect-Target-URL'
EGRESS_PROXY_USER_AGENT_HEADER = 'User-Agent'
EGRESS_PROXY_TLS_CLIENT_CERT_ENV_VAR = 'TLS_CLIENT_CERT'
EGRESS_PROXY_TLS_CLIENT_KEY_ENV_VAR = 'TLS_CLIENT_KEY'
EGRESS_PROXY_TLS_CA_CERT_ENV_VAR = 'TLS_CA_CERT'
141 changes: 141 additions & 0 deletions connect/eaas/core/egress_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import json
import os
import tempfile

from cnct import ConnectClient

from connect.eaas.core.constants import (
EGRESS_PROXY_DEFAULT_MAX_RETRIES,
EGRESS_PROXY_DEFAULT_PATH,
EGRESS_PROXY_TLS_CA_CERT_ENV_VAR,
EGRESS_PROXY_TLS_CLIENT_CERT_ENV_VAR,
EGRESS_PROXY_TLS_CLIENT_KEY_ENV_VAR,
EGRESS_PROXY_USER_AGENT_HEADER,
EGRESS_PROXY_X_CONNECT_TARGET_URL_HEADER,
)
from connect.eaas.core.models import EgressProxy, EgressProxyCertificates


class EgressProxyClient(ConnectClient):
"""Client for interacting with the Vendor Proxy API."""

PROXY_PATH = EGRESS_PROXY_DEFAULT_PATH

def __init__(
self,
proxy: EgressProxy,
certificates: EgressProxyCertificates,
):
self.proxy = proxy
self.cert_file = self._create_temp_cert_file(certificates.client_cert)
self.key_file = self._create_temp_cert_file(certificates.client_key)
self.ca_file = self._create_temp_cert_file(certificates.ca_cert)

super().__init__(
endpoint=self.proxy.url,
api_key=None,
max_retries=EGRESS_PROXY_DEFAULT_MAX_RETRIES,
use_specs=False,
)

@staticmethod
def _create_temp_cert_file(cert_content):
"""Create a temporary file with certificate content."""
temp_file = tempfile.NamedTemporaryFile(
mode='w',
delete=False,
suffix='.pem',
)
temp_file.write(cert_content)
temp_file.close()
return temp_file.name

@classmethod
def require_proxy(cls, account_id: str):
"""
Check if a proxy is required for the given account ID.

Args:
account_id: The account ID to check (e.g., 'PA-063-101')
Returns:
dict | None: Proxy configuration dictionary if it exists
for the account, None otherwise.
"""
egress_config = json.loads(os.getenv('EGRESS_PROXIES_CONFIG') or '{}')
return egress_config.get(account_id)

@classmethod
def from_env(cls, account_id: str):
"""
Create a VendorProxyClient instance from environment variables.

Args:
account_id: The account ID to get proxy config for
(e.g., 'PA-063-101')

Environment variables:
EGRESS_PROXIES_CONFIG: JSON string with proxy configurations
TLS_CLIENT_KEY: PEM-encoded private key
TLS_CLIENT_CERT: PEM-encoded client certificate
TLS_CA_CERT: PEM-encoded CA certificate
"""
# Load proxy configuration
proxy_config = cls.require_proxy(account_id)

if not proxy_config:
raise ValueError(
f"No proxy configuration found for account {account_id}",
)

proxy = EgressProxy(owner_id=account_id, **proxy_config)

if not all(key in os.environ for key in (
EGRESS_PROXY_TLS_CLIENT_CERT_ENV_VAR,
EGRESS_PROXY_TLS_CLIENT_KEY_ENV_VAR,
EGRESS_PROXY_TLS_CA_CERT_ENV_VAR,
)):
raise ValueError("Missing TLS certificate environment variables")

certificates = EgressProxyCertificates(
client_cert=os.environ[EGRESS_PROXY_TLS_CLIENT_CERT_ENV_VAR],
client_key=os.environ[EGRESS_PROXY_TLS_CLIENT_KEY_ENV_VAR],
ca_cert=os.environ[EGRESS_PROXY_TLS_CA_CERT_ENV_VAR],
)

return cls(proxy=proxy, certificates=certificates)

def send_proxied_request(self, *, target_url, target_method, **kwargs):
"""Send a request to the Vendor Proxy API."""
kwargs['json'] = kwargs.pop('payload', None) or None
return self.execute(
target_method,
self.PROXY_PATH,
target_url=target_url,
**kwargs,
)

def _prepare_call_kwargs(self, kwargs):
target_url = kwargs.pop('target_url')
kwargs = super()._prepare_call_kwargs(kwargs)
headers = self._update_headers(target_url, kwargs['headers'])
self._validate_headers(headers)
kwargs['headers'] = headers
kwargs.setdefault('cert', (self.cert_file, self.key_file))
kwargs.setdefault('verify', self.ca_file)
return kwargs

def _update_headers(self, target_url, headers):
_, rest = headers.get(EGRESS_PROXY_USER_AGENT_HEADER).split('/', 1)
headers[EGRESS_PROXY_USER_AGENT_HEADER] = (
f'connect-egress-proxy-{self.proxy.id}/{rest}'
)
headers[EGRESS_PROXY_X_CONNECT_TARGET_URL_HEADER] = target_url
headers.pop('Authorization', None)
return headers

def _validate_headers(self, headers):
for header in self.proxy.headers:
if header['name'] not in headers and header.get('required', False):
raise ValueError(
f"Missing required header: '{header['name']}'",
)
17 changes: 16 additions & 1 deletion connect/eaas/core/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
from dataclasses import dataclass
from dataclasses import dataclass, field


@dataclass
class Context:
extension_id: str
environment_id: str
environment_type: str


@dataclass
class EgressProxy:
id: str
url: str
owner_id: str
headers: list[dict] = field(default_factory=list)


@dataclass
class EgressProxyCertificates:
client_cert: str
client_key: str
ca_cert: str
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
sonar.projectName=Connect EaaS Core
sonar.projectKey=connect-eaas-core
sonar.projectKey=cloudblue_connect-eaas-core
sonar.organization=cloudbluesonarcube

sonar.language=py
Expand Down
Loading