mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 22:01:59 -06:00
Implement admin enrollment APIs
This commit is contained in:
@@ -23,6 +23,7 @@ __all__ = [
|
||||
"SchedulerService",
|
||||
"GitHubService",
|
||||
"GitHubTokenPayload",
|
||||
"EnrollmentAdminService",
|
||||
]
|
||||
|
||||
_LAZY_TARGETS: Dict[str, Tuple[str, str]] = {
|
||||
@@ -43,6 +44,10 @@ _LAZY_TARGETS: Dict[str, Tuple[str, str]] = {
|
||||
"SchedulerService": ("Data.Engine.services.jobs.scheduler_service", "SchedulerService"),
|
||||
"GitHubService": ("Data.Engine.services.github.github_service", "GitHubService"),
|
||||
"GitHubTokenPayload": ("Data.Engine.services.github.github_service", "GitHubTokenPayload"),
|
||||
"EnrollmentAdminService": (
|
||||
"Data.Engine.services.enrollment.admin_service",
|
||||
"EnrollmentAdminService",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from Data.Engine.services.auth import (
|
||||
)
|
||||
from Data.Engine.services.crypto.signing import ScriptSigner, load_signer
|
||||
from Data.Engine.services.enrollment import EnrollmentService
|
||||
from Data.Engine.services.enrollment.admin_service import EnrollmentAdminService
|
||||
from Data.Engine.services.enrollment.nonce_cache import NonceCache
|
||||
from Data.Engine.services.github import GitHubService
|
||||
from Data.Engine.services.jobs import SchedulerService
|
||||
@@ -44,6 +45,7 @@ class EngineServiceContainer:
|
||||
device_auth: DeviceAuthService
|
||||
token_service: TokenService
|
||||
enrollment_service: EnrollmentService
|
||||
enrollment_admin_service: EnrollmentAdminService
|
||||
jwt_service: JWTService
|
||||
dpop_validator: DPoPValidator
|
||||
agent_realtime: AgentRealtimeService
|
||||
@@ -93,6 +95,12 @@ def build_service_container(
|
||||
logger=log.getChild("enrollment"),
|
||||
)
|
||||
|
||||
enrollment_admin_service = EnrollmentAdminService(
|
||||
repository=enrollment_repo,
|
||||
user_repository=user_repo,
|
||||
logger=log.getChild("enrollment_admin"),
|
||||
)
|
||||
|
||||
device_auth = DeviceAuthService(
|
||||
device_repository=device_repo,
|
||||
jwt_service=jwt_service,
|
||||
@@ -139,6 +147,7 @@ def build_service_container(
|
||||
device_auth=device_auth,
|
||||
token_service=token_service,
|
||||
enrollment_service=enrollment_service,
|
||||
enrollment_admin_service=enrollment_admin_service,
|
||||
jwt_service=jwt_service,
|
||||
dpop_validator=dpop_validator,
|
||||
agent_realtime=agent_realtime,
|
||||
|
||||
@@ -2,20 +2,54 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .enrollment_service import (
|
||||
EnrollmentRequestResult,
|
||||
EnrollmentService,
|
||||
EnrollmentStatus,
|
||||
EnrollmentTokenBundle,
|
||||
PollingResult,
|
||||
)
|
||||
from Data.Engine.domain.device_enrollment import EnrollmentValidationError
|
||||
from importlib import import_module
|
||||
from typing import Any
|
||||
|
||||
__all__ = [
|
||||
"EnrollmentRequestResult",
|
||||
"EnrollmentService",
|
||||
"EnrollmentRequestResult",
|
||||
"EnrollmentStatus",
|
||||
"EnrollmentTokenBundle",
|
||||
"EnrollmentValidationError",
|
||||
"PollingResult",
|
||||
"EnrollmentValidationError",
|
||||
"EnrollmentAdminService",
|
||||
]
|
||||
|
||||
_LAZY: dict[str, tuple[str, str]] = {
|
||||
"EnrollmentService": ("Data.Engine.services.enrollment.enrollment_service", "EnrollmentService"),
|
||||
"EnrollmentRequestResult": (
|
||||
"Data.Engine.services.enrollment.enrollment_service",
|
||||
"EnrollmentRequestResult",
|
||||
),
|
||||
"EnrollmentStatus": ("Data.Engine.services.enrollment.enrollment_service", "EnrollmentStatus"),
|
||||
"EnrollmentTokenBundle": (
|
||||
"Data.Engine.services.enrollment.enrollment_service",
|
||||
"EnrollmentTokenBundle",
|
||||
),
|
||||
"PollingResult": ("Data.Engine.services.enrollment.enrollment_service", "PollingResult"),
|
||||
"EnrollmentValidationError": (
|
||||
"Data.Engine.domain.device_enrollment",
|
||||
"EnrollmentValidationError",
|
||||
),
|
||||
"EnrollmentAdminService": (
|
||||
"Data.Engine.services.enrollment.admin_service",
|
||||
"EnrollmentAdminService",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
try:
|
||||
module_name, attribute = _LAZY[name]
|
||||
except KeyError as exc: # pragma: no cover - defensive
|
||||
raise AttributeError(name) from exc
|
||||
|
||||
module = import_module(module_name)
|
||||
value = getattr(module, attribute)
|
||||
globals()[name] = value
|
||||
return value
|
||||
|
||||
|
||||
def __dir__() -> list[str]: # pragma: no cover - interactive helper
|
||||
return sorted(set(__all__))
|
||||
|
||||
|
||||
113
Data/Engine/services/enrollment/admin_service.py
Normal file
113
Data/Engine/services/enrollment/admin_service.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Administrative helpers for enrollment workflows."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import secrets
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from Data.Engine.domain.enrollment_admin import DeviceApprovalRecord, EnrollmentCodeRecord
|
||||
from Data.Engine.repositories.sqlite.enrollment_repository import SQLiteEnrollmentRepository
|
||||
from Data.Engine.repositories.sqlite.user_repository import SQLiteUserRepository
|
||||
|
||||
__all__ = ["EnrollmentAdminService"]
|
||||
|
||||
|
||||
class EnrollmentAdminService:
|
||||
"""Expose administrative enrollment operations."""
|
||||
|
||||
_VALID_TTL_HOURS = {1, 3, 6, 12, 24}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
repository: SQLiteEnrollmentRepository,
|
||||
user_repository: SQLiteUserRepository,
|
||||
logger: Optional[logging.Logger] = None,
|
||||
clock: Optional[Callable[[], datetime]] = None,
|
||||
) -> None:
|
||||
self._repository = repository
|
||||
self._users = user_repository
|
||||
self._log = logger or logging.getLogger("borealis.engine.services.enrollment_admin")
|
||||
self._clock = clock or (lambda: datetime.now(tz=timezone.utc))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Enrollment install codes
|
||||
# ------------------------------------------------------------------
|
||||
def list_install_codes(self, *, status: Optional[str] = None) -> List[EnrollmentCodeRecord]:
|
||||
return self._repository.list_install_codes(status=status, now=self._clock())
|
||||
|
||||
def create_install_code(
|
||||
self,
|
||||
*,
|
||||
ttl_hours: int,
|
||||
max_uses: int,
|
||||
created_by: Optional[str],
|
||||
) -> EnrollmentCodeRecord:
|
||||
if ttl_hours not in self._VALID_TTL_HOURS:
|
||||
raise ValueError("invalid_ttl")
|
||||
|
||||
normalized_max = self._normalize_max_uses(max_uses)
|
||||
|
||||
now = self._clock()
|
||||
expires_at = now + timedelta(hours=ttl_hours)
|
||||
record_id = str(uuid.uuid4())
|
||||
code = self._generate_install_code()
|
||||
|
||||
created_by_identifier = None
|
||||
if created_by:
|
||||
created_by_identifier = self._users.resolve_identifier(created_by)
|
||||
if not created_by_identifier:
|
||||
created_by_identifier = created_by.strip() or None
|
||||
|
||||
record = self._repository.insert_install_code(
|
||||
record_id=record_id,
|
||||
code=code,
|
||||
expires_at=expires_at,
|
||||
created_by=created_by_identifier,
|
||||
max_uses=normalized_max,
|
||||
)
|
||||
|
||||
self._log.info(
|
||||
"install code created id=%s ttl=%sh max_uses=%s",
|
||||
record.record_id,
|
||||
ttl_hours,
|
||||
normalized_max,
|
||||
)
|
||||
|
||||
return record
|
||||
|
||||
def delete_install_code(self, record_id: str) -> bool:
|
||||
deleted = self._repository.delete_install_code_if_unused(record_id)
|
||||
if deleted:
|
||||
self._log.info("install code deleted id=%s", record_id)
|
||||
return deleted
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Device approvals
|
||||
# ------------------------------------------------------------------
|
||||
def list_device_approvals(self, *, status: Optional[str] = None) -> List[DeviceApprovalRecord]:
|
||||
return self._repository.list_device_approvals(status=status)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
@staticmethod
|
||||
def _generate_install_code() -> str:
|
||||
raw = secrets.token_hex(16).upper()
|
||||
return "-".join(raw[i : i + 4] for i in range(0, len(raw), 4))
|
||||
|
||||
@staticmethod
|
||||
def _normalize_max_uses(value: int) -> int:
|
||||
try:
|
||||
count = int(value)
|
||||
except Exception:
|
||||
count = 2
|
||||
if count < 1:
|
||||
return 1
|
||||
if count > 10:
|
||||
return 10
|
||||
return count
|
||||
|
||||
Reference in New Issue
Block a user