mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 22:21:58 -06:00
Add Engine authentication services and builders
This commit is contained in:
@@ -2,4 +2,22 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__: list[str] = []
|
||||
from .device_auth import (
|
||||
DeviceAuthRequest,
|
||||
DeviceAuthRequestBuilder,
|
||||
RefreshTokenRequest,
|
||||
RefreshTokenRequestBuilder,
|
||||
)
|
||||
from .device_enrollment import (
|
||||
EnrollmentRequestBuilder,
|
||||
ProofChallengeBuilder,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DeviceAuthRequest",
|
||||
"DeviceAuthRequestBuilder",
|
||||
"RefreshTokenRequest",
|
||||
"RefreshTokenRequestBuilder",
|
||||
"EnrollmentRequestBuilder",
|
||||
"ProofChallengeBuilder",
|
||||
]
|
||||
|
||||
165
Data/Engine/builders/device_auth.py
Normal file
165
Data/Engine/builders/device_auth.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""Builders for device authentication and token refresh inputs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from Data.Engine.domain.device_auth import (
|
||||
DeviceAuthErrorCode,
|
||||
DeviceAuthFailure,
|
||||
DeviceGuid,
|
||||
sanitize_service_context,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DeviceAuthRequest",
|
||||
"DeviceAuthRequestBuilder",
|
||||
"RefreshTokenRequest",
|
||||
"RefreshTokenRequestBuilder",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DeviceAuthRequest:
|
||||
"""Normalized authentication inputs derived from an HTTP request."""
|
||||
|
||||
access_token: str
|
||||
http_method: str
|
||||
htu: str
|
||||
service_context: Optional[str]
|
||||
dpop_proof: Optional[str]
|
||||
|
||||
|
||||
class DeviceAuthRequestBuilder:
|
||||
"""Validate and normalize HTTP headers for device authentication."""
|
||||
|
||||
_authorization: Optional[str]
|
||||
_http_method: Optional[str]
|
||||
_htu: Optional[str]
|
||||
_service_context: Optional[str]
|
||||
_dpop_proof: Optional[str]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._authorization = None
|
||||
self._http_method = None
|
||||
self._htu = None
|
||||
self._service_context = None
|
||||
self._dpop_proof = None
|
||||
|
||||
def with_authorization(self, header_value: Optional[str]) -> "DeviceAuthRequestBuilder":
|
||||
if header_value is None:
|
||||
self._authorization = None
|
||||
else:
|
||||
self._authorization = header_value.strip()
|
||||
return self
|
||||
|
||||
def with_http_method(self, method: Optional[str]) -> "DeviceAuthRequestBuilder":
|
||||
self._http_method = (method or "").strip().upper()
|
||||
return self
|
||||
|
||||
def with_htu(self, url: Optional[str]) -> "DeviceAuthRequestBuilder":
|
||||
self._htu = (url or "").strip()
|
||||
return self
|
||||
|
||||
def with_service_context(self, header_value: Optional[str]) -> "DeviceAuthRequestBuilder":
|
||||
self._service_context = sanitize_service_context(header_value)
|
||||
return self
|
||||
|
||||
def with_dpop_proof(self, proof: Optional[str]) -> "DeviceAuthRequestBuilder":
|
||||
self._dpop_proof = (proof or "").strip() or None
|
||||
return self
|
||||
|
||||
def build(self) -> DeviceAuthRequest:
|
||||
token = self._parse_authorization(self._authorization)
|
||||
method = (self._http_method or "").strip().upper()
|
||||
if not method:
|
||||
raise DeviceAuthFailure(DeviceAuthErrorCode.INVALID_TOKEN, detail="missing HTTP method")
|
||||
url = (self._htu or "").strip()
|
||||
if not url:
|
||||
raise DeviceAuthFailure(DeviceAuthErrorCode.INVALID_TOKEN, detail="missing request URL")
|
||||
return DeviceAuthRequest(
|
||||
access_token=token,
|
||||
http_method=method,
|
||||
htu=url,
|
||||
service_context=self._service_context,
|
||||
dpop_proof=self._dpop_proof,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_authorization(header_value: Optional[str]) -> str:
|
||||
header = (header_value or "").strip()
|
||||
if not header:
|
||||
raise DeviceAuthFailure(DeviceAuthErrorCode.MISSING_AUTHORIZATION)
|
||||
prefix = "Bearer "
|
||||
if not header.startswith(prefix):
|
||||
raise DeviceAuthFailure(DeviceAuthErrorCode.MISSING_AUTHORIZATION)
|
||||
token = header[len(prefix) :].strip()
|
||||
if not token:
|
||||
raise DeviceAuthFailure(DeviceAuthErrorCode.MISSING_AUTHORIZATION)
|
||||
return token
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RefreshTokenRequest:
|
||||
"""Validated refresh token payload supplied by an agent."""
|
||||
|
||||
guid: DeviceGuid
|
||||
refresh_token: str
|
||||
http_method: str
|
||||
htu: str
|
||||
dpop_proof: Optional[str]
|
||||
|
||||
|
||||
class RefreshTokenRequestBuilder:
|
||||
"""Helper to normalize refresh token JSON payloads."""
|
||||
|
||||
_guid: Optional[str]
|
||||
_refresh_token: Optional[str]
|
||||
_http_method: Optional[str]
|
||||
_htu: Optional[str]
|
||||
_dpop_proof: Optional[str]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._guid = None
|
||||
self._refresh_token = None
|
||||
self._http_method = None
|
||||
self._htu = None
|
||||
self._dpop_proof = None
|
||||
|
||||
def with_payload(self, payload: Optional[dict[str, object]]) -> "RefreshTokenRequestBuilder":
|
||||
payload = payload or {}
|
||||
self._guid = str(payload.get("guid") or "").strip()
|
||||
self._refresh_token = str(payload.get("refresh_token") or "").strip()
|
||||
return self
|
||||
|
||||
def with_http_method(self, method: Optional[str]) -> "RefreshTokenRequestBuilder":
|
||||
self._http_method = (method or "").strip().upper()
|
||||
return self
|
||||
|
||||
def with_htu(self, url: Optional[str]) -> "RefreshTokenRequestBuilder":
|
||||
self._htu = (url or "").strip()
|
||||
return self
|
||||
|
||||
def with_dpop_proof(self, proof: Optional[str]) -> "RefreshTokenRequestBuilder":
|
||||
self._dpop_proof = (proof or "").strip() or None
|
||||
return self
|
||||
|
||||
def build(self) -> RefreshTokenRequest:
|
||||
if not self._guid:
|
||||
raise DeviceAuthFailure(DeviceAuthErrorCode.INVALID_CLAIMS, detail="missing guid")
|
||||
if not self._refresh_token:
|
||||
raise DeviceAuthFailure(DeviceAuthErrorCode.INVALID_CLAIMS, detail="missing refresh token")
|
||||
method = (self._http_method or "").strip().upper()
|
||||
if not method:
|
||||
raise DeviceAuthFailure(DeviceAuthErrorCode.INVALID_TOKEN, detail="missing HTTP method")
|
||||
url = (self._htu or "").strip()
|
||||
if not url:
|
||||
raise DeviceAuthFailure(DeviceAuthErrorCode.INVALID_TOKEN, detail="missing request URL")
|
||||
return RefreshTokenRequest(
|
||||
guid=DeviceGuid(self._guid),
|
||||
refresh_token=self._refresh_token,
|
||||
http_method=method,
|
||||
htu=url,
|
||||
dpop_proof=self._dpop_proof,
|
||||
)
|
||||
100
Data/Engine/builders/device_enrollment.py
Normal file
100
Data/Engine/builders/device_enrollment.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Builder utilities for device enrollment payloads."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from Data.Engine.domain.device_auth import DeviceFingerprint
|
||||
from Data.Engine.domain.device_enrollment import EnrollmentRequest, ProofChallenge
|
||||
|
||||
__all__ = [
|
||||
"EnrollmentRequestBuilder",
|
||||
"ProofChallengeBuilder",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _EnrollmentPayload:
|
||||
hostname: str
|
||||
enrollment_code: str
|
||||
fingerprint: str
|
||||
client_nonce: bytes
|
||||
server_nonce: bytes
|
||||
|
||||
|
||||
class EnrollmentRequestBuilder:
|
||||
"""Normalize agent enrollment JSON payloads into domain objects."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._payload: Optional[_EnrollmentPayload] = None
|
||||
|
||||
def with_payload(self, payload: Optional[dict[str, object]]) -> "EnrollmentRequestBuilder":
|
||||
payload = payload or {}
|
||||
hostname = str(payload.get("hostname") or "").strip()
|
||||
enrollment_code = str(payload.get("enrollment_code") or "").strip()
|
||||
fingerprint = str(payload.get("fingerprint") or "").strip()
|
||||
client_nonce = self._coerce_bytes(payload.get("client_nonce"))
|
||||
server_nonce = self._coerce_bytes(payload.get("server_nonce"))
|
||||
self._payload = _EnrollmentPayload(
|
||||
hostname=hostname,
|
||||
enrollment_code=enrollment_code,
|
||||
fingerprint=fingerprint,
|
||||
client_nonce=client_nonce,
|
||||
server_nonce=server_nonce,
|
||||
)
|
||||
return self
|
||||
|
||||
def build(self) -> EnrollmentRequest:
|
||||
if not self._payload:
|
||||
raise ValueError("payload has not been provided")
|
||||
return EnrollmentRequest.from_payload(
|
||||
hostname=self._payload.hostname,
|
||||
enrollment_code=self._payload.enrollment_code,
|
||||
fingerprint=self._payload.fingerprint,
|
||||
client_nonce=self._payload.client_nonce,
|
||||
server_nonce=self._payload.server_nonce,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _coerce_bytes(value: object) -> bytes:
|
||||
if isinstance(value, (bytes, bytearray)):
|
||||
return bytes(value)
|
||||
if isinstance(value, str):
|
||||
return value.encode("utf-8")
|
||||
raise ValueError("nonce values must be bytes or base strings")
|
||||
|
||||
|
||||
class ProofChallengeBuilder:
|
||||
"""Construct proof challenges during enrollment approval."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._server_nonce: Optional[bytes] = None
|
||||
self._client_nonce: Optional[bytes] = None
|
||||
self._fingerprint: Optional[DeviceFingerprint] = None
|
||||
|
||||
def with_server_nonce(self, nonce: Optional[bytes]) -> "ProofChallengeBuilder":
|
||||
self._server_nonce = bytes(nonce or b"")
|
||||
return self
|
||||
|
||||
def with_client_nonce(self, nonce: Optional[bytes]) -> "ProofChallengeBuilder":
|
||||
self._client_nonce = bytes(nonce or b"")
|
||||
return self
|
||||
|
||||
def with_fingerprint(self, fingerprint: Optional[str]) -> "ProofChallengeBuilder":
|
||||
if fingerprint:
|
||||
self._fingerprint = DeviceFingerprint(fingerprint)
|
||||
else:
|
||||
self._fingerprint = None
|
||||
return self
|
||||
|
||||
def build(self) -> ProofChallenge:
|
||||
if self._server_nonce is None or self._client_nonce is None:
|
||||
raise ValueError("both server and client nonces are required")
|
||||
if not self._fingerprint:
|
||||
raise ValueError("fingerprint is required")
|
||||
return ProofChallenge(
|
||||
client_nonce=self._client_nonce,
|
||||
server_nonce=self._server_nonce,
|
||||
fingerprint=self._fingerprint,
|
||||
)
|
||||
Reference in New Issue
Block a user