feat(cloud-auth): require production credentials
This commit is contained in:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from hashlib import sha256
|
||||
from hmac import compare_digest
|
||||
from collections.abc import Iterable
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
|
||||
@@ -106,6 +107,19 @@ class ConfiguredBearerAuthProvider:
|
||||
return matched_principal
|
||||
|
||||
|
||||
def create_auth_provider(
|
||||
credentials: Iterable[BearerCredential],
|
||||
*,
|
||||
allow_insecure_anonymous: bool,
|
||||
) -> AuthProvider:
|
||||
configured = list(credentials)
|
||||
if configured:
|
||||
return ConfiguredBearerAuthProvider(configured)
|
||||
if allow_insecure_anonymous:
|
||||
return NullAuthProvider()
|
||||
return ConfiguredBearerAuthProvider([])
|
||||
|
||||
|
||||
def _extract_bearer_token(request: object) -> str | None:
|
||||
headers = getattr(request, "headers", None)
|
||||
if headers is None:
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
from cloud.auth import BearerCredential
|
||||
|
||||
|
||||
EnvironmentName = Literal["local", "test", "production"]
|
||||
SUPPORTED_DATABASE_PREFIXES = (
|
||||
@@ -27,6 +30,7 @@ class CloudControlConfig:
|
||||
lease_duration_seconds: float = 60.0
|
||||
max_task_attempts: int = 3
|
||||
allow_insecure_anonymous: bool = False
|
||||
credentials: tuple[BearerCredential, ...] = ()
|
||||
|
||||
|
||||
def load_control_config(
|
||||
@@ -75,12 +79,70 @@ def load_control_config(
|
||||
values.get("CLOUD_ALLOW_INSECURE_ANONYMOUS"),
|
||||
default=False,
|
||||
),
|
||||
credentials=(
|
||||
*_parse_credentials(values.get("CLOUD_PUBLIC_CREDENTIALS_JSON")),
|
||||
*_parse_credentials(
|
||||
values.get("CLOUD_HOST_CREDENTIALS_JSON"),
|
||||
require_host_id=True,
|
||||
),
|
||||
),
|
||||
)
|
||||
validate_control_config(config)
|
||||
return config
|
||||
|
||||
|
||||
def validate_control_config(config: CloudControlConfig) -> None:
|
||||
if config.environment == "production" and config.allow_insecure_anonymous:
|
||||
raise CloudConfigurationError(
|
||||
"anonymous access cannot be enabled in production"
|
||||
)
|
||||
return config
|
||||
if config.environment == "production" and not config.credentials:
|
||||
raise CloudConfigurationError(
|
||||
"production requires at least one configured bearer credential"
|
||||
)
|
||||
|
||||
|
||||
def _parse_credentials(
|
||||
raw_value: str | None,
|
||||
*,
|
||||
require_host_id: bool = False,
|
||||
) -> tuple[BearerCredential, ...]:
|
||||
if raw_value is None or not raw_value.strip():
|
||||
return ()
|
||||
try:
|
||||
payload = json.loads(raw_value)
|
||||
if not isinstance(payload, list):
|
||||
raise TypeError
|
||||
credentials: list[BearerCredential] = []
|
||||
for item in payload:
|
||||
if not isinstance(item, dict):
|
||||
raise TypeError
|
||||
principal_id = item.get("principal_id")
|
||||
token = item.get("token")
|
||||
scopes = item.get("scopes", [])
|
||||
host_id = item.get("host_id")
|
||||
if (
|
||||
not isinstance(principal_id, str)
|
||||
or not isinstance(token, str)
|
||||
or not isinstance(scopes, list)
|
||||
or not all(isinstance(scope, str) for scope in scopes)
|
||||
or (host_id is not None and not isinstance(host_id, str))
|
||||
or (require_host_id and not isinstance(host_id, str))
|
||||
):
|
||||
raise TypeError
|
||||
credentials.append(
|
||||
BearerCredential(
|
||||
principal_id=principal_id,
|
||||
token=token,
|
||||
scopes=frozenset(scopes),
|
||||
host_id=host_id,
|
||||
)
|
||||
)
|
||||
return tuple(credentials)
|
||||
except (TypeError, ValueError, json.JSONDecodeError) as exc:
|
||||
raise CloudConfigurationError(
|
||||
"configured bearer credentials are invalid"
|
||||
) from exc
|
||||
|
||||
|
||||
def _positive_float(
|
||||
|
||||
Reference in New Issue
Block a user