sovereign 0.19.3__py3-none-any.whl → 1.0.0b148__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.
Potentially problematic release.
This version of sovereign might be problematic. Click here for more details.
- sovereign/__init__.py +13 -81
- sovereign/app.py +59 -48
- sovereign/cache/__init__.py +172 -0
- sovereign/cache/backends/__init__.py +110 -0
- sovereign/cache/backends/s3.py +143 -0
- sovereign/cache/filesystem.py +73 -0
- sovereign/cache/types.py +15 -0
- sovereign/configuration.py +573 -0
- sovereign/constants.py +1 -0
- sovereign/context.py +271 -104
- sovereign/dynamic_config/__init__.py +113 -0
- sovereign/dynamic_config/deser.py +78 -0
- sovereign/dynamic_config/loaders.py +120 -0
- sovereign/events.py +49 -0
- sovereign/logging/access_logger.py +85 -0
- sovereign/logging/application_logger.py +54 -0
- sovereign/logging/base_logger.py +41 -0
- sovereign/logging/bootstrapper.py +36 -0
- sovereign/logging/types.py +10 -0
- sovereign/middlewares.py +8 -7
- sovereign/modifiers/lib.py +1 -0
- sovereign/rendering.py +192 -0
- sovereign/response_class.py +18 -0
- sovereign/server.py +93 -35
- sovereign/sources/file.py +1 -1
- sovereign/sources/inline.py +1 -0
- sovereign/sources/lib.py +1 -0
- sovereign/sources/poller.py +296 -53
- sovereign/statistics.py +17 -20
- sovereign/templates/base.html +59 -46
- sovereign/templates/resources.html +203 -102
- sovereign/testing/loaders.py +8 -0
- sovereign/{modifiers/test.py → testing/modifiers.py} +0 -2
- sovereign/tracing.py +102 -0
- sovereign/types.py +299 -0
- sovereign/utils/auth.py +26 -13
- sovereign/utils/crypto/__init__.py +0 -0
- sovereign/utils/crypto/crypto.py +135 -0
- sovereign/utils/crypto/suites/__init__.py +21 -0
- sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
- sovereign/utils/crypto/suites/base_cipher.py +21 -0
- sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
- sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
- sovereign/utils/dictupdate.py +2 -1
- sovereign/utils/eds.py +37 -21
- sovereign/utils/mock.py +54 -16
- sovereign/utils/resources.py +17 -0
- sovereign/utils/version_info.py +8 -0
- sovereign/views/__init__.py +4 -0
- sovereign/views/api.py +61 -0
- sovereign/views/crypto.py +46 -15
- sovereign/views/discovery.py +37 -116
- sovereign/views/healthchecks.py +87 -18
- sovereign/views/interface.py +112 -112
- sovereign/worker.py +204 -0
- {sovereign-0.19.3.dist-info → sovereign-1.0.0b148.dist-info}/METADATA +79 -76
- sovereign-1.0.0b148.dist-info/RECORD +77 -0
- {sovereign-0.19.3.dist-info → sovereign-1.0.0b148.dist-info}/WHEEL +1 -1
- sovereign-1.0.0b148.dist-info/entry_points.txt +38 -0
- sovereign_files/__init__.py +0 -0
- sovereign_files/static/darkmode.js +51 -0
- sovereign_files/static/node_expression.js +42 -0
- sovereign_files/static/panel.js +76 -0
- sovereign_files/static/resources.css +246 -0
- sovereign_files/static/resources.js +642 -0
- sovereign_files/static/sass/style.scss +33 -0
- sovereign_files/static/style.css +16143 -0
- sovereign_files/static/style.css.map +1 -0
- sovereign/config_loader.py +0 -225
- sovereign/discovery.py +0 -175
- sovereign/logs.py +0 -131
- sovereign/schemas.py +0 -780
- sovereign/static/sass/style.scss +0 -27
- sovereign/static/style.css +0 -13553
- sovereign/templates/ul_filter.html +0 -22
- sovereign/utils/crypto.py +0 -103
- sovereign/views/admin.py +0 -120
- sovereign-0.19.3.dist-info/LICENSE.txt +0 -13
- sovereign-0.19.3.dist-info/RECORD +0 -47
- sovereign-0.19.3.dist-info/entry_points.txt +0 -10
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .base_cipher import CipherSuite
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DisabledCipher(CipherSuite):
|
|
7
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
def __str__(self) -> str:
|
|
11
|
+
return "disabled"
|
|
12
|
+
|
|
13
|
+
def encrypt(self, _: str) -> str:
|
|
14
|
+
return "Unavailable (No Secret Key)"
|
|
15
|
+
|
|
16
|
+
def decrypt(self, _: str) -> str:
|
|
17
|
+
return "Unavailable (No Secret Key)"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def key_available(self) -> bool:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def generate_key(cls) -> bytes:
|
|
25
|
+
return b"Unavailable (No key to generate)"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from cryptography.fernet import Fernet
|
|
5
|
+
|
|
6
|
+
from .base_cipher import CipherSuite
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FernetCipher(CipherSuite):
|
|
10
|
+
def __str__(self) -> str:
|
|
11
|
+
return "fernet"
|
|
12
|
+
|
|
13
|
+
def __init__(self, secret_key: str):
|
|
14
|
+
self.fernet = Fernet(secret_key)
|
|
15
|
+
|
|
16
|
+
def encrypt(self, data: str) -> str:
|
|
17
|
+
return self.fernet.encrypt(data.encode()).decode("utf-8")
|
|
18
|
+
|
|
19
|
+
def decrypt(self, data: str) -> str:
|
|
20
|
+
return self.fernet.decrypt(data).decode("utf-8")
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def key_available(self) -> bool:
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def generate_key(cls) -> bytes:
|
|
28
|
+
# Generate 256 bit length key
|
|
29
|
+
return base64.urlsafe_b64encode(os.urandom(32))
|
sovereign/utils/dictupdate.py
CHANGED
sovereign/utils/eds.py
CHANGED
|
@@ -2,8 +2,8 @@ import random
|
|
|
2
2
|
from typing import Dict, Any, Optional, List
|
|
3
3
|
from copy import deepcopy
|
|
4
4
|
from starlette.exceptions import HTTPException
|
|
5
|
-
from sovereign import config
|
|
6
|
-
from sovereign.
|
|
5
|
+
from sovereign.configuration import config
|
|
6
|
+
from sovereign.types import DiscoveryRequest
|
|
7
7
|
from sovereign.utils.templates import resolve
|
|
8
8
|
|
|
9
9
|
HARD_FAIL_ON_DNS_FAILURE = config.legacy_fields.dns_hard_fail
|
|
@@ -26,12 +26,16 @@ def _upstream_kwargs(
|
|
|
26
26
|
if hard_fail:
|
|
27
27
|
raise
|
|
28
28
|
ip_addresses = [upstream["address"]]
|
|
29
|
-
|
|
29
|
+
ret = {
|
|
30
30
|
"addrs": ip_addresses,
|
|
31
31
|
"port": upstream["port"],
|
|
32
32
|
"region": default_region or upstream.get("region", "unknown"),
|
|
33
33
|
"zone": proxy_region,
|
|
34
34
|
}
|
|
35
|
+
if "health_check_config" in upstream:
|
|
36
|
+
ret["health_check_config"] = upstream["health_check_config"]
|
|
37
|
+
|
|
38
|
+
return ret
|
|
35
39
|
|
|
36
40
|
|
|
37
41
|
def total_zones(endpoints: List[Dict[str, Dict[str, Any]]]) -> int:
|
|
@@ -84,14 +88,21 @@ def locality_lb_endpoints(
|
|
|
84
88
|
return ret
|
|
85
89
|
|
|
86
90
|
|
|
87
|
-
def lb_endpoints(
|
|
91
|
+
def lb_endpoints(
|
|
92
|
+
addrs: List[str],
|
|
93
|
+
port: int,
|
|
94
|
+
region: str,
|
|
95
|
+
zone: str,
|
|
96
|
+
health_check_config: Optional[Dict[str, Any]] = None,
|
|
97
|
+
) -> Dict[str, Any]:
|
|
88
98
|
"""
|
|
89
99
|
Creates an envoy endpoint.LbEndpoints proto
|
|
90
100
|
|
|
91
|
-
:param addrs:
|
|
92
|
-
:param port:
|
|
93
|
-
:param region:
|
|
94
|
-
:param zone:
|
|
101
|
+
:param addrs: The IP addresses or hostname(s) of the upstream.
|
|
102
|
+
:param port: The port that the upstream should be accessed on.
|
|
103
|
+
:param region: The region of the upstream.
|
|
104
|
+
:param zone: The region of the proxy asking for the endpoint configuration.
|
|
105
|
+
:param health_check_config: Optional health check config for the upstream.
|
|
95
106
|
"""
|
|
96
107
|
if PRIORITY_MAPPING is None:
|
|
97
108
|
raise RuntimeError(
|
|
@@ -100,20 +111,25 @@ def lb_endpoints(addrs: List[str], port: int, region: str, zone: str) -> Dict[st
|
|
|
100
111
|
)
|
|
101
112
|
node_priorities = PRIORITY_MAPPING.get(zone, {})
|
|
102
113
|
priority = node_priorities.get(region, 10)
|
|
114
|
+
|
|
115
|
+
endpoints = []
|
|
116
|
+
for addr in addrs:
|
|
117
|
+
endpoint = {
|
|
118
|
+
"endpoint": {
|
|
119
|
+
"address": {
|
|
120
|
+
"socket_address": {
|
|
121
|
+
"address": addr,
|
|
122
|
+
"port_value": port,
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if health_check_config:
|
|
128
|
+
endpoint["endpoint"]["health_check_config"] = health_check_config
|
|
129
|
+
endpoints.append(endpoint)
|
|
130
|
+
|
|
103
131
|
return {
|
|
104
132
|
"priority": priority,
|
|
105
133
|
"locality": {"zone": region},
|
|
106
|
-
"lb_endpoints":
|
|
107
|
-
{
|
|
108
|
-
"endpoint": {
|
|
109
|
-
"address": {
|
|
110
|
-
"socket_address": {
|
|
111
|
-
"address": addr,
|
|
112
|
-
"port_value": port,
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
for addr in addrs
|
|
118
|
-
],
|
|
134
|
+
"lb_endpoints": endpoints,
|
|
119
135
|
}
|
sovereign/utils/mock.py
CHANGED
|
@@ -1,36 +1,74 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import ast
|
|
1
3
|
from typing import Optional, Dict, List
|
|
2
4
|
from random import randint
|
|
3
|
-
from sovereign.
|
|
5
|
+
from sovereign.types import DiscoveryRequest, Node, Locality, Status
|
|
6
|
+
|
|
7
|
+
scrub = re.compile(r"[^a-zA-Z_\.]")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NodeExpressionError(Exception):
|
|
11
|
+
pass
|
|
4
12
|
|
|
5
13
|
|
|
6
14
|
def mock_discovery_request(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
api_version: Optional[str] = "V3",
|
|
16
|
+
resource_type: Optional[str] = None,
|
|
17
|
+
resource_names: Optional[List[str] | str] = None,
|
|
18
|
+
region: Optional[str] = "none",
|
|
19
|
+
version: Optional[str] = "<envoy_version>",
|
|
11
20
|
metadata: Optional[Dict[str, str]] = None,
|
|
12
21
|
error_message: Optional[str] = None,
|
|
22
|
+
expressions: Optional[list[str]] = None,
|
|
13
23
|
) -> DiscoveryRequest:
|
|
14
24
|
if resource_names is None:
|
|
15
|
-
resource_names =
|
|
16
|
-
|
|
17
|
-
resource_names =
|
|
25
|
+
resource_names = []
|
|
26
|
+
if isinstance(resource_names, str):
|
|
27
|
+
resource_names = [resource_names]
|
|
28
|
+
if expressions is None:
|
|
29
|
+
expressions = []
|
|
30
|
+
base_node = Node(
|
|
31
|
+
id="sovereign-interface",
|
|
32
|
+
cluster="*",
|
|
33
|
+
build_version=f"<randomHash>/{version}/Clean/RELEASE",
|
|
34
|
+
locality=Locality(zone=region),
|
|
35
|
+
).model_dump()
|
|
36
|
+
set_node_expressions(base_node, expressions)
|
|
18
37
|
request = DiscoveryRequest(
|
|
19
38
|
type_url=None,
|
|
20
|
-
node=Node(
|
|
21
|
-
id="mock",
|
|
22
|
-
cluster=service_cluster or "",
|
|
23
|
-
build_version=f"e5f864a82d4f27110359daa2fbdcb12d99e415b9/{version}/Clean/RELEASE",
|
|
24
|
-
locality=Locality(zone=region, region="example", sub_zone="a"),
|
|
25
|
-
),
|
|
39
|
+
node=Node.model_validate(base_node),
|
|
26
40
|
version_info=str(randint(100000, 1000000000)),
|
|
27
41
|
resource_names=resource_names,
|
|
28
|
-
|
|
29
|
-
desired_controlplane="
|
|
42
|
+
is_internal_request=True,
|
|
43
|
+
desired_controlplane="__sovereign__",
|
|
30
44
|
error_detail=Status(code=200, message="None", details=["None"]),
|
|
45
|
+
api_version=api_version,
|
|
46
|
+
resource_type=resource_type,
|
|
31
47
|
)
|
|
32
48
|
if isinstance(metadata, dict):
|
|
33
49
|
request.node.metadata = metadata
|
|
34
50
|
if error_message:
|
|
35
51
|
request.error_detail = Status(code=666, message=error_message, details=["foo"])
|
|
36
52
|
return request
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def set_node_expressions(node, expressions):
|
|
56
|
+
for expr in expressions:
|
|
57
|
+
try:
|
|
58
|
+
field, value = re.split(r"\s*=\s*", expr, maxsplit=1)
|
|
59
|
+
value = f'"{value}"'
|
|
60
|
+
except ValueError:
|
|
61
|
+
raise NodeExpressionError(f"Invalid node filter format: {expr}")
|
|
62
|
+
|
|
63
|
+
field = scrub.sub("", field)
|
|
64
|
+
parts = field.split(".")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
value = ast.literal_eval(value)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
raise NodeExpressionError(f"Invalid node filter value: {value}") from e
|
|
70
|
+
|
|
71
|
+
current = node
|
|
72
|
+
for part in parts[:-1]:
|
|
73
|
+
current = current.setdefault(part, {})
|
|
74
|
+
current[parts[-1]] = value
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from functools import cache
|
|
2
|
+
import importlib.resources as res
|
|
3
|
+
from importlib.resources.abc import Traversable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@cache
|
|
7
|
+
def get_package(name: str) -> Traversable:
|
|
8
|
+
return res.files(name)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_package_file(package_name: str, filename: str) -> Traversable:
|
|
12
|
+
return get_package(package_name).joinpath(filename)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_package_file_bytes(package_name: str, filename: str) -> bytes:
|
|
16
|
+
file = get_package_file(package_name, filename)
|
|
17
|
+
return file.read_bytes()
|
sovereign/utils/version_info.py
CHANGED
|
@@ -11,3 +11,11 @@ def compute_hash(*args: Any) -> str:
|
|
|
11
11
|
zlib.crc32(data) & 0xFFFFFFFF
|
|
12
12
|
) # same numeric value across all py versions & platforms
|
|
13
13
|
return str(version_info)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def compute_hash_int(*args: Any) -> int:
|
|
17
|
+
data: bytes = repr(args).encode()
|
|
18
|
+
version_info = (
|
|
19
|
+
zlib.crc32(data) & 0xFFFFFFFF
|
|
20
|
+
) # same numeric value across all py versions & platforms
|
|
21
|
+
return version_info
|
sovereign/views/__init__.py
CHANGED
sovereign/views/api.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Query, Path
|
|
5
|
+
from fastapi.responses import Response
|
|
6
|
+
|
|
7
|
+
from sovereign.views import reader
|
|
8
|
+
from sovereign.configuration import ConfiguredResourceTypes
|
|
9
|
+
from sovereign.utils.mock import mock_discovery_request
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _traverse(data, prefix, expressions):
|
|
15
|
+
for key, value in data.items():
|
|
16
|
+
path = f"{prefix}.{key}" if prefix else key
|
|
17
|
+
if isinstance(value, dict):
|
|
18
|
+
yield from _traverse(value, path, expressions)
|
|
19
|
+
else:
|
|
20
|
+
yield f"{path}={value}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def expand_metadata_to_expr(m):
|
|
24
|
+
exprs = []
|
|
25
|
+
yield from _traverse(m, "", exprs)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@router.get("/resources/{resource_type}", summary="Get resources for a given type")
|
|
29
|
+
async def resource(
|
|
30
|
+
resource_type: ConfiguredResourceTypes = Path(title="xDS Resource type"),
|
|
31
|
+
resource_name: Optional[str] = Query(None, title="Resource name"),
|
|
32
|
+
api_version: Optional[str] = Query("v3", title="Envoy API version"),
|
|
33
|
+
service_cluster: Optional[str] = Query("*", title="Envoy Service cluster"),
|
|
34
|
+
region: Optional[str] = Query(None, title="Locality Zone"),
|
|
35
|
+
version: Optional[str] = Query(None, title="Envoy Semantic Version"),
|
|
36
|
+
metadata: Optional[str] = Query(None, title="Envoy node metadata to filter by"),
|
|
37
|
+
) -> Response:
|
|
38
|
+
expressions = [f"cluster={service_cluster}"]
|
|
39
|
+
try:
|
|
40
|
+
data = {"metadata": json.loads(metadata or "{}")}
|
|
41
|
+
for expr in expand_metadata_to_expr(data):
|
|
42
|
+
expressions.append(expr)
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
kwargs = {
|
|
46
|
+
"api_version": api_version,
|
|
47
|
+
"resource_type": ConfiguredResourceTypes(resource_type).value,
|
|
48
|
+
"resource_names": resource_name,
|
|
49
|
+
"version": version,
|
|
50
|
+
"region": region,
|
|
51
|
+
"expressions": expressions,
|
|
52
|
+
}
|
|
53
|
+
req = mock_discovery_request(**{k: v for k, v in kwargs.items() if v is not None}) # type: ignore
|
|
54
|
+
response = await reader.blocking_read(req)
|
|
55
|
+
if content := getattr(response, "text", None):
|
|
56
|
+
return Response(content, media_type="application/json")
|
|
57
|
+
else:
|
|
58
|
+
return Response(
|
|
59
|
+
json.dumps({"title": "No resources found", "status": 404}),
|
|
60
|
+
media_type="application/json+problem",
|
|
61
|
+
)
|
sovereign/views/crypto.py
CHANGED
|
@@ -1,31 +1,38 @@
|
|
|
1
|
-
from typing import Dict
|
|
2
|
-
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
3
|
from fastapi import APIRouter, Body
|
|
4
4
|
from fastapi.responses import JSONResponse
|
|
5
|
-
from
|
|
6
|
-
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from sovereign import logs, server_cipher_container
|
|
8
|
+
from sovereign.response_class import json_response_class
|
|
9
|
+
from sovereign.configuration import EncryptionConfig
|
|
10
|
+
from sovereign.utils.crypto.crypto import CipherContainer
|
|
11
|
+
from sovereign.utils.crypto.suites import EncryptionType
|
|
7
12
|
|
|
8
13
|
router = APIRouter()
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
class EncryptionRequest(BaseModel):
|
|
12
17
|
data: str = Field(..., title="Text to be encrypted", min_length=1, max_length=65535)
|
|
13
|
-
key: str = Field(
|
|
18
|
+
key: Optional[str] = Field(
|
|
14
19
|
None,
|
|
15
|
-
title="Optional
|
|
20
|
+
title="Optional encryption key to use to encrypt",
|
|
16
21
|
min_length=44,
|
|
17
22
|
max_length=44,
|
|
18
23
|
)
|
|
24
|
+
encryption_type: str = Field(default="fernet", title="Encryption type to be used")
|
|
19
25
|
|
|
20
26
|
|
|
21
27
|
class DecryptionRequest(BaseModel):
|
|
22
28
|
data: str = Field(..., title="Text to be decrypted", min_length=1, max_length=65535)
|
|
23
29
|
key: str = Field(
|
|
24
30
|
...,
|
|
25
|
-
title="
|
|
31
|
+
title="Encryption key to use to decrypt",
|
|
26
32
|
min_length=44,
|
|
27
33
|
max_length=44,
|
|
28
34
|
)
|
|
35
|
+
encryption_type: str = Field(default="fernet", title="Encryption type to be used")
|
|
29
36
|
|
|
30
37
|
|
|
31
38
|
class DecryptableRequest(BaseModel):
|
|
@@ -37,17 +44,37 @@ class DecryptableRequest(BaseModel):
|
|
|
37
44
|
summary="Decrypt provided encrypted data using a provided key",
|
|
38
45
|
response_class=json_response_class,
|
|
39
46
|
)
|
|
40
|
-
async def _decrypt(request: DecryptionRequest = Body(None)) ->
|
|
41
|
-
|
|
47
|
+
async def _decrypt(request: DecryptionRequest = Body(None)) -> dict[str, Any]:
|
|
48
|
+
user_cipher_container = CipherContainer.from_encryption_configs(
|
|
49
|
+
encryption_configs=[
|
|
50
|
+
EncryptionConfig(
|
|
51
|
+
encryption_key=request.key,
|
|
52
|
+
encryption_type=EncryptionType(request.encryption_type),
|
|
53
|
+
)
|
|
54
|
+
],
|
|
55
|
+
logger=logs.application_logger.logger,
|
|
56
|
+
)
|
|
57
|
+
return {**user_cipher_container.decrypt_with_type(request.data)}
|
|
42
58
|
|
|
43
59
|
|
|
44
60
|
@router.post(
|
|
45
61
|
"/encrypt",
|
|
46
|
-
summary="Encrypt provided data using this servers key",
|
|
62
|
+
summary="Encrypt provided data using this servers key or provided key",
|
|
47
63
|
response_class=json_response_class,
|
|
48
64
|
)
|
|
49
|
-
async def _encrypt(request: EncryptionRequest = Body(None)) ->
|
|
50
|
-
|
|
65
|
+
async def _encrypt(request: EncryptionRequest = Body(None)) -> dict[str, Any]:
|
|
66
|
+
if request.key:
|
|
67
|
+
user_cipher_container = CipherContainer.from_encryption_configs(
|
|
68
|
+
encryption_configs=[
|
|
69
|
+
EncryptionConfig(
|
|
70
|
+
encryption_key=request.key,
|
|
71
|
+
encryption_type=EncryptionType(request.encryption_type),
|
|
72
|
+
)
|
|
73
|
+
],
|
|
74
|
+
logger=logs.application_logger.logger,
|
|
75
|
+
)
|
|
76
|
+
return {**user_cipher_container.encrypt(request.data)}
|
|
77
|
+
return {**server_cipher_container.encrypt(request.data)}
|
|
51
78
|
|
|
52
79
|
|
|
53
80
|
@router.post(
|
|
@@ -56,7 +83,7 @@ async def _encrypt(request: EncryptionRequest = Body(None)) -> Dict[str, str]:
|
|
|
56
83
|
response_class=json_response_class,
|
|
57
84
|
)
|
|
58
85
|
async def _decryptable(request: DecryptableRequest = Body(None)) -> JSONResponse:
|
|
59
|
-
|
|
86
|
+
server_cipher_container.decrypt(request.data)
|
|
60
87
|
return json_response_class({})
|
|
61
88
|
|
|
62
89
|
|
|
@@ -65,5 +92,9 @@ async def _decryptable(request: DecryptableRequest = Body(None)) -> JSONResponse
|
|
|
65
92
|
summary="Generate a new asymmetric encryption key",
|
|
66
93
|
response_class=json_response_class,
|
|
67
94
|
)
|
|
68
|
-
def _generate_key() -> Dict[str, str]:
|
|
69
|
-
|
|
95
|
+
def _generate_key(encryption_type: str = "fernet") -> Dict[str, str]:
|
|
96
|
+
cipher_suite = CipherContainer.get_cipher_suite(EncryptionType(encryption_type))
|
|
97
|
+
return {
|
|
98
|
+
"key": cipher_suite.generate_key().decode(),
|
|
99
|
+
"encryption_type": encryption_type,
|
|
100
|
+
}
|
sovereign/views/discovery.py
CHANGED
|
@@ -1,63 +1,20 @@
|
|
|
1
|
-
from typing import Dict
|
|
2
|
-
|
|
3
1
|
from fastapi import Body, Header
|
|
4
|
-
from fastapi.routing import APIRouter
|
|
5
2
|
from fastapi.responses import Response
|
|
3
|
+
from fastapi.routing import APIRouter
|
|
6
4
|
|
|
7
|
-
from sovereign import
|
|
5
|
+
from sovereign import cache, logs
|
|
6
|
+
from sovereign.views import reader
|
|
7
|
+
from sovereign.cache.types import Entry
|
|
8
8
|
from sovereign.utils.auth import authenticate
|
|
9
|
-
from sovereign.
|
|
10
|
-
from sovereign.schemas import (
|
|
9
|
+
from sovereign.types import (
|
|
11
10
|
DiscoveryRequest,
|
|
12
11
|
DiscoveryResponse,
|
|
13
|
-
ProcessedTemplate,
|
|
14
12
|
)
|
|
15
13
|
|
|
16
|
-
discovery_cache = config.discovery_cache
|
|
17
|
-
|
|
18
|
-
if discovery_cache.enabled:
|
|
19
|
-
from cashews import cache
|
|
20
|
-
|
|
21
|
-
cache.setup(
|
|
22
|
-
f"{discovery_cache.protocol}{discovery_cache.host}:{discovery_cache.port}",
|
|
23
|
-
password=discovery_cache.password.get_secret_value(),
|
|
24
|
-
client_side=discovery_cache.client_side,
|
|
25
|
-
wait_for_connection_timeout=discovery_cache.wait_for_connection_timeout,
|
|
26
|
-
socket_connect_timeout=discovery_cache.socket_connect_timeout,
|
|
27
|
-
socket_timeout=discovery_cache.socket_timeout,
|
|
28
|
-
max_connections=discovery_cache.max_connections,
|
|
29
|
-
retry_on_timeout=discovery_cache.retry_on_timeout,
|
|
30
|
-
suppress=discovery_cache.suppress,
|
|
31
|
-
socket_keepalive=discovery_cache.socket_keepalive,
|
|
32
|
-
enable=discovery_cache.enabled,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
router = APIRouter()
|
|
36
|
-
|
|
37
|
-
type_urls = {
|
|
38
|
-
"v2": {
|
|
39
|
-
"listeners": "type.googleapis.com/envoy.api.v2.Listener",
|
|
40
|
-
"clusters": "type.googleapis.com/envoy.api.v2.Cluster",
|
|
41
|
-
"endpoints": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
|
|
42
|
-
"secrets": "type.googleapis.com/envoy.api.v2.auth.Secret",
|
|
43
|
-
"routes": "type.googleapis.com/envoy.api.v2.RouteConfiguration",
|
|
44
|
-
"scoped-routes": "type.googleapis.com/envoy.api.v2.ScopedRouteConfiguration",
|
|
45
|
-
},
|
|
46
|
-
"v3": {
|
|
47
|
-
"listeners": "type.googleapis.com/envoy.config.listener.v3.Listener",
|
|
48
|
-
"clusters": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
|
|
49
|
-
"endpoints": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
|
|
50
|
-
"secrets": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret",
|
|
51
|
-
"routes": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
|
|
52
|
-
"scoped-routes": "type.googleapis.com/envoy.config.route.v3.ScopedRouteConfiguration",
|
|
53
|
-
"runtime": "type.googleapis.com/envoy.service.runtime.v3.Runtime",
|
|
54
|
-
},
|
|
55
|
-
}
|
|
56
|
-
|
|
57
14
|
|
|
58
15
|
def response_headers(
|
|
59
|
-
discovery_request: DiscoveryRequest, response:
|
|
60
|
-
) ->
|
|
16
|
+
discovery_request: DiscoveryRequest, response: Entry, xds: str
|
|
17
|
+
) -> dict[str, str]:
|
|
61
18
|
return {
|
|
62
19
|
"X-Sovereign-Client-Build": discovery_request.envoy_version,
|
|
63
20
|
"X-Sovereign-Client-Version": discovery_request.version_info,
|
|
@@ -68,6 +25,9 @@ def response_headers(
|
|
|
68
25
|
}
|
|
69
26
|
|
|
70
27
|
|
|
28
|
+
router = APIRouter()
|
|
29
|
+
|
|
30
|
+
|
|
71
31
|
@router.post(
|
|
72
32
|
"/{version}/discovery:{xds_type}",
|
|
73
33
|
summary="Envoy Discovery Service Endpoint",
|
|
@@ -81,76 +41,37 @@ def response_headers(
|
|
|
81
41
|
async def discovery_response(
|
|
82
42
|
version: str,
|
|
83
43
|
xds_type: str,
|
|
84
|
-
|
|
44
|
+
xds_req: DiscoveryRequest = Body(...),
|
|
85
45
|
host: str = Header("no_host_provided"),
|
|
86
46
|
) -> Response:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
if discovery_request.error_detail:
|
|
98
|
-
logs.queue_log_fields(XDS_ERROR_DETAIL=discovery_request.error_detail.message)
|
|
99
|
-
headers = response_headers(discovery_request, response, xds_type)
|
|
100
|
-
|
|
101
|
-
if response.version == discovery_request.version_info:
|
|
102
|
-
return not_modified(headers)
|
|
103
|
-
elif getattr(response, "resources", None) == []:
|
|
104
|
-
return Response(status_code=404, headers=headers)
|
|
105
|
-
elif response.version != discovery_request.version_info:
|
|
106
|
-
return Response(
|
|
107
|
-
response.rendered, headers=headers, media_type="application/json"
|
|
47
|
+
authenticate(xds_req)
|
|
48
|
+
|
|
49
|
+
# Pack additional info into the request
|
|
50
|
+
xds_req.desired_controlplane = host
|
|
51
|
+
xds_req.resource_type = xds_type
|
|
52
|
+
xds_req.api_version = version
|
|
53
|
+
if xds_req.error_detail:
|
|
54
|
+
logs.access_logger.queue_log_fields(
|
|
55
|
+
XDS_ERROR_DETAIL=xds_req.error_detail.message
|
|
108
56
|
)
|
|
109
|
-
|
|
110
|
-
|
|
57
|
+
logs.access_logger.queue_log_fields(
|
|
58
|
+
XDS_RESOURCES=xds_req.resource_names,
|
|
59
|
+
XDS_ENVOY_VERSION=xds_req.envoy_version,
|
|
60
|
+
XDS_CLIENT_VERSION=xds_req.version_info,
|
|
61
|
+
)
|
|
111
62
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
resource_type: str,
|
|
116
|
-
skip_auth: bool = False,
|
|
117
|
-
) -> ProcessedTemplate:
|
|
118
|
-
if not skip_auth:
|
|
119
|
-
authenticate(req)
|
|
120
|
-
if discovery_cache.enabled:
|
|
121
|
-
logs.queue_log_fields(CACHE_XDS_HIT=False)
|
|
122
|
-
cache_key = compute_hash(
|
|
123
|
-
[
|
|
124
|
-
api_version,
|
|
125
|
-
resource_type,
|
|
126
|
-
req.envoy_version,
|
|
127
|
-
req.resource_names,
|
|
128
|
-
req.desired_controlplane,
|
|
129
|
-
req.hide_private_keys,
|
|
130
|
-
req.type_url,
|
|
131
|
-
req.node.cluster,
|
|
132
|
-
req.node.locality,
|
|
133
|
-
req.node.metadata.get("auth", None),
|
|
134
|
-
req.node.metadata.get("num_cpus", None),
|
|
135
|
-
]
|
|
136
|
-
)
|
|
137
|
-
if template := await cache.get(key=cache_key, default=None):
|
|
138
|
-
logs.queue_log_fields(CACHE_XDS_HIT=True)
|
|
139
|
-
return template # type: ignore[no-any-return]
|
|
140
|
-
template = discovery.response(req, resource_type)
|
|
141
|
-
type_url = type_urls.get(api_version, {}).get(resource_type)
|
|
142
|
-
if type_url is not None:
|
|
143
|
-
for resource in template.resources:
|
|
144
|
-
if not resource.get("@type"):
|
|
145
|
-
resource["@type"] = type_url
|
|
146
|
-
if discovery_cache.enabled:
|
|
147
|
-
await cache.set(
|
|
148
|
-
key=cache_key,
|
|
149
|
-
value=template,
|
|
150
|
-
expire=discovery_cache.ttl,
|
|
63
|
+
def handle_response(entry: cache.Entry):
|
|
64
|
+
logs.access_logger.queue_log_fields(
|
|
65
|
+
XDS_SERVER_VERSION=entry.version,
|
|
151
66
|
)
|
|
152
|
-
|
|
67
|
+
headers = response_headers(xds_req, entry, xds_type)
|
|
68
|
+
if entry.len == 0:
|
|
69
|
+
return Response(status_code=404, headers=headers)
|
|
70
|
+
if entry.version == xds_req.version_info:
|
|
71
|
+
return Response(status_code=304, headers=headers)
|
|
72
|
+
return Response(entry.text, media_type="application/json", headers=headers)
|
|
153
73
|
|
|
74
|
+
if entry := await reader.blocking_read(xds_req):
|
|
75
|
+
return handle_response(entry)
|
|
154
76
|
|
|
155
|
-
|
|
156
|
-
return Response(status_code=304, headers=headers)
|
|
77
|
+
return Response(content="Something went wrong", status_code=500)
|