gooddata-flight-server 1.34.1.dev1__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 gooddata-flight-server might be problematic. Click here for more details.
- gooddata_flight_server/__init__.py +23 -0
- gooddata_flight_server/_version.py +7 -0
- gooddata_flight_server/cli.py +137 -0
- gooddata_flight_server/config/__init__.py +1 -0
- gooddata_flight_server/config/config.py +536 -0
- gooddata_flight_server/errors/__init__.py +1 -0
- gooddata_flight_server/errors/error_code.py +209 -0
- gooddata_flight_server/errors/error_info.py +475 -0
- gooddata_flight_server/exceptions.py +16 -0
- gooddata_flight_server/health/__init__.py +1 -0
- gooddata_flight_server/health/health_check_http_server.py +103 -0
- gooddata_flight_server/health/server_health_monitor.py +83 -0
- gooddata_flight_server/metrics.py +16 -0
- gooddata_flight_server/py.typed +1 -0
- gooddata_flight_server/server/__init__.py +1 -0
- gooddata_flight_server/server/auth/__init__.py +1 -0
- gooddata_flight_server/server/auth/auth_middleware.py +83 -0
- gooddata_flight_server/server/auth/token_verifier.py +62 -0
- gooddata_flight_server/server/auth/token_verifier_factory.py +55 -0
- gooddata_flight_server/server/auth/token_verifier_impl.py +41 -0
- gooddata_flight_server/server/base.py +63 -0
- gooddata_flight_server/server/default.logging.ini +28 -0
- gooddata_flight_server/server/flight_rpc/__init__.py +1 -0
- gooddata_flight_server/server/flight_rpc/flight_middleware.py +162 -0
- gooddata_flight_server/server/flight_rpc/flight_server.py +228 -0
- gooddata_flight_server/server/flight_rpc/flight_service.py +279 -0
- gooddata_flight_server/server/flight_rpc/server_methods.py +200 -0
- gooddata_flight_server/server/server_base.py +321 -0
- gooddata_flight_server/server/server_main.py +116 -0
- gooddata_flight_server/tasks/__init__.py +1 -0
- gooddata_flight_server/tasks/base.py +21 -0
- gooddata_flight_server/tasks/metrics.py +115 -0
- gooddata_flight_server/tasks/task.py +193 -0
- gooddata_flight_server/tasks/task_error.py +60 -0
- gooddata_flight_server/tasks/task_executor.py +96 -0
- gooddata_flight_server/tasks/task_result.py +363 -0
- gooddata_flight_server/tasks/temporal_container.py +247 -0
- gooddata_flight_server/tasks/thread_task_executor.py +639 -0
- gooddata_flight_server/utils/__init__.py +1 -0
- gooddata_flight_server/utils/libc_utils.py +35 -0
- gooddata_flight_server/utils/logging.py +158 -0
- gooddata_flight_server/utils/methods_discovery.py +98 -0
- gooddata_flight_server/utils/otel_tracing.py +142 -0
- gooddata_flight_server-1.34.1.dev1.data/scripts/gooddata-flight-server +10 -0
- gooddata_flight_server-1.34.1.dev1.dist-info/LICENSE.txt +7 -0
- gooddata_flight_server-1.34.1.dev1.dist-info/METADATA +749 -0
- gooddata_flight_server-1.34.1.dev1.dist-info/RECORD +49 -0
- gooddata_flight_server-1.34.1.dev1.dist-info/WHEEL +5 -0
- gooddata_flight_server-1.34.1.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
2
|
+
from functools import partial
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
5
|
+
from threading import Thread
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
from gooddata_flight_server.health.server_health_monitor import (
|
|
11
|
+
ModuleHealthStatus,
|
|
12
|
+
ServerHealthMonitor,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
LIVENESS_ENDPOINT_PATH = "/live"
|
|
16
|
+
READINESS_ENDPOINT_PATH = "/ready"
|
|
17
|
+
|
|
18
|
+
SERVER_MODULE_DEBUG_NAME = "server"
|
|
19
|
+
|
|
20
|
+
LOGGER = structlog.get_logger("gooddata_flight_server.health_check_http_server")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _HealthCheckHandler(BaseHTTPRequestHandler):
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
*args: Any,
|
|
27
|
+
server_health_monitor: ServerHealthMonitor,
|
|
28
|
+
**kwargs: Any,
|
|
29
|
+
):
|
|
30
|
+
self._server_health_monitor = server_health_monitor
|
|
31
|
+
super().__init__(*args, **kwargs)
|
|
32
|
+
|
|
33
|
+
def log_message(self, format: str, *args: list) -> None:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def do_GET(self) -> None: # noqa: N802
|
|
37
|
+
if self.path == READINESS_ENDPOINT_PATH:
|
|
38
|
+
self.send_response(HTTPStatus.NO_CONTENT if self.is_ready() else HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
39
|
+
elif self.path == LIVENESS_ENDPOINT_PATH:
|
|
40
|
+
self.send_response(HTTPStatus.NO_CONTENT if self.is_alive() else HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
41
|
+
else:
|
|
42
|
+
self.send_response(HTTPStatus.NOT_FOUND)
|
|
43
|
+
self.end_headers()
|
|
44
|
+
|
|
45
|
+
def is_ready(self) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Readiness check inspecting flight server startup state.
|
|
48
|
+
|
|
49
|
+
:return: True if instance is ready otherwise False
|
|
50
|
+
"""
|
|
51
|
+
LOGGER.debug("checking_flight_server_ready")
|
|
52
|
+
if (
|
|
53
|
+
SERVER_MODULE_DEBUG_NAME in self._server_health_monitor.module_statuses
|
|
54
|
+
and self._server_health_monitor.module_statuses[SERVER_MODULE_DEBUG_NAME] == ModuleHealthStatus.OK
|
|
55
|
+
):
|
|
56
|
+
LOGGER.debug("flight_server_ready")
|
|
57
|
+
return True
|
|
58
|
+
else:
|
|
59
|
+
LOGGER.warning("flight_server_not_ready")
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def is_alive(self) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
Liveness check inspecting all running modules statuses.
|
|
65
|
+
|
|
66
|
+
:return: True if server is healthy otherwise False
|
|
67
|
+
"""
|
|
68
|
+
LOGGER.debug("checking_flight_server_healthy")
|
|
69
|
+
|
|
70
|
+
for (
|
|
71
|
+
module,
|
|
72
|
+
status,
|
|
73
|
+
) in self._server_health_monitor.module_statuses.items():
|
|
74
|
+
if status == ModuleHealthStatus.NOT_OK:
|
|
75
|
+
LOGGER.warning("unhealthy_module", module=module)
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
LOGGER.debug("flight_server_healthy")
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class HealthCheckHttpServer:
|
|
83
|
+
"""
|
|
84
|
+
HTTP server implementation for health checks mainly usable in k8s environment for readiness
|
|
85
|
+
and liveness probes. Exposes $LIVENESS_ENDPOINT_PATH and $READINESS_ENDPOINT_PATH returning HTTP 200 in
|
|
86
|
+
case of success otherwise HTTP 500.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
host: str,
|
|
92
|
+
port: int,
|
|
93
|
+
server_health_monitor: ServerHealthMonitor,
|
|
94
|
+
):
|
|
95
|
+
handler = partial(_HealthCheckHandler, server_health_monitor=server_health_monitor)
|
|
96
|
+
httpd = HTTPServer((host, port), handler)
|
|
97
|
+
|
|
98
|
+
def serve_forever(httpd_instance: HTTPServer) -> None:
|
|
99
|
+
with httpd_instance:
|
|
100
|
+
LOGGER.info("health_check_started", host=host, port=port)
|
|
101
|
+
httpd_instance.serve_forever()
|
|
102
|
+
|
|
103
|
+
Thread(target=serve_forever, args=(httpd,), daemon=True).start()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
2
|
+
import enum
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
|
|
8
|
+
from gooddata_flight_server.metrics import ServerMetrics
|
|
9
|
+
from gooddata_flight_server.utils.libc_utils import LibcUtils
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModuleHealthStatus(enum.Enum):
|
|
13
|
+
"""
|
|
14
|
+
This enum lists health status of a module
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
OK = "ok"
|
|
18
|
+
NOT_OK = "not_ok"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ServerHealthMonitor:
|
|
22
|
+
"""
|
|
23
|
+
Server health monitor and maintenance.
|
|
24
|
+
|
|
25
|
+
The monitor includes a thread doing regular maintenance - namely periodically performing
|
|
26
|
+
malloc trim() to make system throw away the garbage. This is essential to survive in runtime environments
|
|
27
|
+
that impose memory (RSS) limits and kill the server if it exceeds it - the malloc does not
|
|
28
|
+
free the used memory back to the system; the RSS keeps growing and growing until the server
|
|
29
|
+
gets killed. The trim() call makes malloc drop all unneeded allocations.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
trim_interval: int = 30,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._logger = structlog.get_logger("gooddata_flight_server.maintenance")
|
|
37
|
+
self._libc = LibcUtils()
|
|
38
|
+
self._trim_interval = trim_interval
|
|
39
|
+
self._module_statuses: dict[str, ModuleHealthStatus] = {}
|
|
40
|
+
|
|
41
|
+
self._thread = threading.Thread(
|
|
42
|
+
name="gooddata_flight_server.maintenance",
|
|
43
|
+
target=self._maintenance,
|
|
44
|
+
daemon=True,
|
|
45
|
+
)
|
|
46
|
+
self._thread.start()
|
|
47
|
+
|
|
48
|
+
self._logger.info("server_health_monitor_started")
|
|
49
|
+
|
|
50
|
+
def _maintenance(self) -> None:
|
|
51
|
+
last_trim = time.time()
|
|
52
|
+
|
|
53
|
+
while True:
|
|
54
|
+
if time.time() - last_trim > self._trim_interval:
|
|
55
|
+
try:
|
|
56
|
+
trim_start = time.perf_counter()
|
|
57
|
+
self._libc.malloc_trim()
|
|
58
|
+
duration = time.perf_counter() - trim_start
|
|
59
|
+
|
|
60
|
+
ServerMetrics.TRIM_SUMMARY.observe(duration)
|
|
61
|
+
self._logger.debug(
|
|
62
|
+
"server_maintenance",
|
|
63
|
+
op="malloc_trim",
|
|
64
|
+
duration=duration,
|
|
65
|
+
)
|
|
66
|
+
except Exception:
|
|
67
|
+
ServerMetrics.TRIM_ERROR_COUNT.inc()
|
|
68
|
+
self._logger.error("malloc_trim_failed", exc_info=True)
|
|
69
|
+
|
|
70
|
+
last_trim = time.time()
|
|
71
|
+
|
|
72
|
+
time.sleep(1.0)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def module_statuses(self) -> dict[str, ModuleHealthStatus]:
|
|
76
|
+
return self._module_statuses
|
|
77
|
+
|
|
78
|
+
def set_module_status(self, name: str, status: ModuleHealthStatus) -> None:
|
|
79
|
+
"""
|
|
80
|
+
:param name: name of the module to which the status belongs
|
|
81
|
+
:param status: health status of the module
|
|
82
|
+
"""
|
|
83
|
+
self._module_statuses[name] = status
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
2
|
+
|
|
3
|
+
from prometheus_client import Counter, Summary
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# TODO: metric prefix should be configurable
|
|
7
|
+
class ServerMetrics:
|
|
8
|
+
TRIM_SUMMARY = Summary(
|
|
9
|
+
"gdfs_malloc_trim",
|
|
10
|
+
"Summary of malloc trim call durations.",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
TRIM_ERROR_COUNT = Counter(
|
|
14
|
+
"gdfs_malloc_trim_error",
|
|
15
|
+
"Number of times malloc_trim has failed. Repeated failures means big trouble incoming.",
|
|
16
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Mark package as supporting typing. See https://www.python.org/dev/peps/pep-0561/ for details.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
import pyarrow.flight
|
|
5
|
+
import structlog
|
|
6
|
+
|
|
7
|
+
from gooddata_flight_server.server.auth.token_verifier import (
|
|
8
|
+
TokenVerificationStrategy,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TokenAuthMiddleware(pyarrow.flight.ServerMiddleware):
|
|
13
|
+
MiddlewareName = "auth_token"
|
|
14
|
+
|
|
15
|
+
def __init__(self, token: str, token_data: Any) -> None:
|
|
16
|
+
super().__init__()
|
|
17
|
+
|
|
18
|
+
self._token = token
|
|
19
|
+
self._token_data = token_data
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def token_data(self) -> Any:
|
|
23
|
+
return self._token_data
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_DEFAULT_AUTH_TOKEN_HEADER = "Authorization"
|
|
27
|
+
_LOGGER = structlog.get_logger("gooddata_flight_server.auth")
|
|
28
|
+
_BEARER_END_IDX = 7
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TokenAuthMiddlewareFactory(pyarrow.flight.ServerMiddlewareFactory):
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
token_header_name: Optional[str],
|
|
35
|
+
strategy: TokenVerificationStrategy,
|
|
36
|
+
):
|
|
37
|
+
super().__init__()
|
|
38
|
+
|
|
39
|
+
self._token_header_name = token_header_name
|
|
40
|
+
self._strategy = strategy
|
|
41
|
+
|
|
42
|
+
def _extract_token(self, headers: dict[str, list[str]]) -> str:
|
|
43
|
+
def _auth_header_value(lookup: str) -> str:
|
|
44
|
+
_lookup = lookup.lower()
|
|
45
|
+
values = [value for header, values in headers.items() if header.lower() == _lookup for value in values]
|
|
46
|
+
|
|
47
|
+
if len(values) > 1:
|
|
48
|
+
raise pyarrow.flight.FlightUnauthenticatedError(
|
|
49
|
+
f"Authentication failed because the authentication header '{lookup}' is specified multiple times."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if not len(values):
|
|
53
|
+
raise pyarrow.flight.FlightUnauthenticatedError(
|
|
54
|
+
"Authentication failed because the authentication header bearing the token was not included "
|
|
55
|
+
"on the call."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return values[0]
|
|
59
|
+
|
|
60
|
+
if self._token_header_name is None:
|
|
61
|
+
token = _auth_header_value(_DEFAULT_AUTH_TOKEN_HEADER)
|
|
62
|
+
if not token.startswith("Bearer "):
|
|
63
|
+
raise pyarrow.flight.FlightUnauthenticatedError(
|
|
64
|
+
"Authentication failed because the 'Authorization' header does not start with 'Bearer '"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return token[_BEARER_END_IDX:].strip()
|
|
68
|
+
|
|
69
|
+
token = _auth_header_value(self._token_header_name)
|
|
70
|
+
return token.strip()
|
|
71
|
+
|
|
72
|
+
def start_call(self, info: pyarrow.flight.CallInfo, headers: dict[str, list[str]]) -> Optional[TokenAuthMiddleware]:
|
|
73
|
+
try:
|
|
74
|
+
token = self._extract_token(headers)
|
|
75
|
+
result = self._strategy.verify(call_info=info, token=token)
|
|
76
|
+
|
|
77
|
+
return TokenAuthMiddleware(token=token, token_data=result)
|
|
78
|
+
except pyarrow.flight.FlightUnauthenticatedError as e:
|
|
79
|
+
_LOGGER.info("authentication_failed", reason=str(e))
|
|
80
|
+
raise
|
|
81
|
+
except pyarrow.flight.FlightUnauthorizedError as e:
|
|
82
|
+
_LOGGER.info("authorization_failed", reason=str(e))
|
|
83
|
+
raise
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
2
|
+
import abc
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pyarrow.flight
|
|
6
|
+
|
|
7
|
+
from gooddata_flight_server.server.base import ServerContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TokenVerificationStrategy(abc.ABC):
|
|
11
|
+
"""
|
|
12
|
+
Token verification strategy is used by server's token authentication
|
|
13
|
+
middleware to perform the actual verification:
|
|
14
|
+
|
|
15
|
+
- The middleware is responsible for extracting the token
|
|
16
|
+
- The strategy is responsible for verifying that the token is valid and
|
|
17
|
+
if applicable return any information carried in the token.
|
|
18
|
+
- The middleware is responsible for holding onto value returned by the
|
|
19
|
+
verifier and make it available to call handling code.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@abc.abstractmethod
|
|
23
|
+
def verify(self, call_info: pyarrow.flight.CallInfo, token: str) -> Any:
|
|
24
|
+
"""
|
|
25
|
+
Perform token verification.
|
|
26
|
+
|
|
27
|
+
- If the token is not valid, this method should raise either pyarrow.flight.FlightUnauthenticated
|
|
28
|
+
- If the token is valid but fails authorization for current call, this method
|
|
29
|
+
should raise pyarrow.Flight.FlightUnauthorized
|
|
30
|
+
|
|
31
|
+
Otherwise, the method should return either nothing or a custom value describing
|
|
32
|
+
the contents of the token - as it sees fit.
|
|
33
|
+
|
|
34
|
+
For example a JWT Token Verification strategy may extract claims and return them in a
|
|
35
|
+
dictionary.
|
|
36
|
+
|
|
37
|
+
:param call_info: current call info
|
|
38
|
+
:param token: token to verify
|
|
39
|
+
:return: either nothing or a custom value with info extracted from the token
|
|
40
|
+
:raises pyarrow.flight.FlightUnauthenticated - when the token is invalid / fails validation
|
|
41
|
+
:raises pyarrow.flight.FlightUnauthorized - when the token is valid but the caller holding
|
|
42
|
+
the token is not authorized to make the call
|
|
43
|
+
"""
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def create(cls, ctx: ServerContext) -> "TokenVerificationStrategy":
|
|
48
|
+
"""
|
|
49
|
+
This method is called exactly once during the server startup to obtain
|
|
50
|
+
a singleton of the concrete verification strategy to be used by
|
|
51
|
+
the server.
|
|
52
|
+
|
|
53
|
+
A typical use case here is to inspect the settings included in the server
|
|
54
|
+
context and obtain any essential configuration from the `settings`.
|
|
55
|
+
|
|
56
|
+
The strategy _may_ use this opportunity to perform any long-running
|
|
57
|
+
and blocking one-time initialization.
|
|
58
|
+
|
|
59
|
+
:param ctx: server context where the verification strategy will be used
|
|
60
|
+
:return: an instance of the strategy.
|
|
61
|
+
"""
|
|
62
|
+
return cls()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
import structlog
|
|
5
|
+
|
|
6
|
+
from gooddata_flight_server.exceptions import ServerStartupInterrupted
|
|
7
|
+
from gooddata_flight_server.server.auth.token_verifier import TokenVerificationStrategy
|
|
8
|
+
from gooddata_flight_server.server.auth.token_verifier_impl import EnumeratedTokenVerification
|
|
9
|
+
from gooddata_flight_server.server.base import ServerContext
|
|
10
|
+
|
|
11
|
+
_LOGGER = structlog.get_logger("gooddata_flight_server.auth")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _import_verification_strategy(module_name: str) -> type[TokenVerificationStrategy]:
|
|
15
|
+
_LOGGER.info("load_token_verification", module_name=module_name)
|
|
16
|
+
module = importlib.import_module(module_name)
|
|
17
|
+
|
|
18
|
+
for member in module.__dict__.values():
|
|
19
|
+
if not isinstance(member, type) or not issubclass(member, TokenVerificationStrategy):
|
|
20
|
+
# filter out module members which are not classes that implement the
|
|
21
|
+
# TokenVerificationStrategy interface
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
if member == TokenVerificationStrategy:
|
|
25
|
+
# the TokenVerificationStrategy class is likely imported in the module -
|
|
26
|
+
# don't want that to interfere
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
return member
|
|
30
|
+
|
|
31
|
+
raise ServerStartupInterrupted(
|
|
32
|
+
f"The module '{module_name}' specified in 'token_verification' setting does not "
|
|
33
|
+
f"include an implementation of {TokenVerificationStrategy.__name__}."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _find_token_verification_class(ctx: ServerContext) -> type[TokenVerificationStrategy]:
|
|
38
|
+
# config reader must ensure that there is always some value here
|
|
39
|
+
assert ctx.config.token_verification is not None
|
|
40
|
+
|
|
41
|
+
if ctx.config.token_verification == "EnumeratedTokenVerification":
|
|
42
|
+
return EnumeratedTokenVerification
|
|
43
|
+
|
|
44
|
+
return _import_verification_strategy(ctx.config.token_verification)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def create_token_verification_strategy(ctx: ServerContext) -> TokenVerificationStrategy:
|
|
48
|
+
try:
|
|
49
|
+
cls = _find_token_verification_class(ctx)
|
|
50
|
+
|
|
51
|
+
_LOGGER.info("auth_token_strategy_init", cls=cls.__name__)
|
|
52
|
+
return cls.create(ctx)
|
|
53
|
+
except Exception:
|
|
54
|
+
_LOGGER.critical("auth_token_init_failed", exc_info=True)
|
|
55
|
+
raise
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import pyarrow.flight
|
|
5
|
+
from dynaconf import ValidationError
|
|
6
|
+
|
|
7
|
+
from gooddata_flight_server.server.auth.token_verifier import TokenVerificationStrategy
|
|
8
|
+
from gooddata_flight_server.server.base import ServerContext
|
|
9
|
+
|
|
10
|
+
_TOKEN_ENUMERATION_SECTION = "enumerated_tokens"
|
|
11
|
+
_TOKEN_SETTING = "tokens"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EnumeratedTokenVerification(TokenVerificationStrategy):
|
|
15
|
+
"""
|
|
16
|
+
A simple token verification strategy that successfully verifies
|
|
17
|
+
a token if it matches one of the tokens specified in the settings.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, allowed_tokens: set[str]) -> None:
|
|
21
|
+
self._tokens = allowed_tokens
|
|
22
|
+
|
|
23
|
+
def verify(self, call_info: pyarrow.flight.CallInfo, token: str) -> Any:
|
|
24
|
+
if token not in self._tokens:
|
|
25
|
+
raise pyarrow.flight.FlightUnauthenticatedError("Authentication token is not valid.")
|
|
26
|
+
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def create(cls, ctx: ServerContext) -> "TokenVerificationStrategy":
|
|
31
|
+
tokens = list(ctx.settings.get(f"{_TOKEN_ENUMERATION_SECTION}.{_TOKEN_SETTING}") or [])
|
|
32
|
+
if not len(tokens):
|
|
33
|
+
raise ValidationError(
|
|
34
|
+
f"The 'EnumeratedTokenVerification' requires that you configure "
|
|
35
|
+
f"which tokens are allowed to use. You have to include section "
|
|
36
|
+
f"[{EnumeratedTokenVerification}] with a '{_TOKEN_SETTING}' setting that contains "
|
|
37
|
+
f"list of tokens. Alternatively, you can specify environment variable "
|
|
38
|
+
f"GOODDATA_FLIGHT_ENUMERATED_TOKENS__TOKENS."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return EnumeratedTokenVerification(set(tokens))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
import pyarrow.flight
|
|
6
|
+
from dynaconf import Dynaconf
|
|
7
|
+
|
|
8
|
+
from gooddata_flight_server.config.config import ServerConfig
|
|
9
|
+
from gooddata_flight_server.health.server_health_monitor import (
|
|
10
|
+
ServerHealthMonitor,
|
|
11
|
+
)
|
|
12
|
+
from gooddata_flight_server.server.flight_rpc.server_methods import (
|
|
13
|
+
FlightServerMethods,
|
|
14
|
+
)
|
|
15
|
+
from gooddata_flight_server.tasks.task_executor import TaskExecutor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ServerContext:
|
|
20
|
+
"""
|
|
21
|
+
Server's context.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
settings: Dynaconf
|
|
25
|
+
"""
|
|
26
|
+
All settings parsed from configuration files and/or environment variables provided at server startup.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
config: ServerConfig
|
|
30
|
+
"""
|
|
31
|
+
Server's configuration
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
location: pyarrow.flight.Location
|
|
35
|
+
"""
|
|
36
|
+
Server's Flight RPC location - this location connectable by clients.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
health: ServerHealthMonitor
|
|
40
|
+
"""
|
|
41
|
+
Server's health monitor. Components may register their health into the monitor.
|
|
42
|
+
|
|
43
|
+
This monitor is integrated with health check server where the overall status
|
|
44
|
+
is reported using liveness/readiness endpoints.
|
|
45
|
+
|
|
46
|
+
If a component is unhealthy, the whole server will be reported as such.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
task_executor: TaskExecutor
|
|
50
|
+
"""
|
|
51
|
+
Task Executor can and should be used to implement long running Tasks which generate
|
|
52
|
+
Flight data.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FlightServerMethodsFactory(Protocol):
|
|
57
|
+
"""
|
|
58
|
+
Factory function for server methods. This can be provided to the
|
|
59
|
+
GoodDataFlightServer - the server will invoke the function at the right
|
|
60
|
+
point and then integrate the FlightServerMethods into its Flight RPC service.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __call__(self, ctx: ServerContext) -> FlightServerMethods: ...
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
2
|
+
[loggers]
|
|
3
|
+
keys = root, gooddata_flight_server
|
|
4
|
+
|
|
5
|
+
[handlers]
|
|
6
|
+
keys = gooddata_flight_server_stream_handler
|
|
7
|
+
|
|
8
|
+
[formatters]
|
|
9
|
+
keys = gooddata_flight_server_formatter
|
|
10
|
+
|
|
11
|
+
[logger_root]
|
|
12
|
+
level = INFO
|
|
13
|
+
handlers = gooddata_flight_server_stream_handler
|
|
14
|
+
|
|
15
|
+
[logger_gooddata_flight_server]
|
|
16
|
+
level = INFO
|
|
17
|
+
qualname = gooddata_flight_server
|
|
18
|
+
handlers =
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
[handler_gooddata_flight_server_stream_handler]
|
|
22
|
+
class = StreamHandler
|
|
23
|
+
level = DEBUG
|
|
24
|
+
formatter = gooddata_flight_server_formatter
|
|
25
|
+
args = (sys.stderr,)
|
|
26
|
+
|
|
27
|
+
[formatter_gooddata_flight_server_formatter]
|
|
28
|
+
format = %(message)s
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|