172 lines
7.5 KiB
Python
172 lines
7.5 KiB
Python
# #region profile_service [C:5] [TYPE Module] [SEMANTICS sqlalchemy, profile, preference, superset, lookup]
|
|
#
|
|
# @BRIEF Composite facade orchestrating profile preference persistence, Superset account lookup,
|
|
# security badges, and deterministic actor matching by delegating to focused sub-services.
|
|
# @LAYER Domain
|
|
# @RELATION DEPENDS_ON -> [UserDashboardPreference]
|
|
# @RELATION DEPENDS_ON -> [ProfilePreferenceResponse]
|
|
# @RELATION DEPENDS_ON -> [SupersetClient]
|
|
# @RELATION DEPENDS_ON -> [AuthRepositoryModule]
|
|
# @RELATION DEPENDS_ON -> [User]
|
|
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy.orm.Session]
|
|
# @RELATION DEPENDS_ON -> [ProfilePreferenceService]
|
|
# @RELATION DEPENDS_ON -> [SupersetLookupService]
|
|
# @RELATION DEPENDS_ON -> [SecurityBadgeService]
|
|
# @RELATION DEPENDS_ON -> [ProfileUtils]
|
|
#
|
|
# @INVARIANT Profile ID needs to be unique per-user session
|
|
#
|
|
# @TEST_CONTRACT ProfilePreferenceUpdateRequest -> ProfilePreferenceResponse
|
|
# @TEST_FIXTURE valid_profile_update -> {"user_id":"u-1","superset_username":"John_Doe","show_only_my_dashboards":true}
|
|
# @TEST_EDGE enable_without_username -> toggle=true with empty username returns validation error
|
|
# @TEST_EDGE cross_user_mutation -> attempt to update another user preference returns forbidden
|
|
# @TEST_EDGE lookup_env_not_found -> unknown environment_id returns not found
|
|
# @TEST_INVARIANT normalization_consistency -> VERIFIED_BY: [valid_profile_update, enable_without_username]
|
|
# @DATA_CONTRACT Profile_id -> ProfileInfo; session_id -> valid UUID
|
|
# @PRE Session is active and valid
|
|
# @POST Profile with updated fields populated and
|
|
# @SIDE_EFFECT Database read/write operations
|
|
# @RATIONALE Decomposed from monolithic 770-line ProfileService into three focused services
|
|
# (ProfilePreferenceService, SupersetLookupService, SecurityBadgeService) plus pure
|
|
# utility functions (ProfileUtils) to satisfy INV_7. This module is a thin facade
|
|
# that preserves the public API contract for all existing callers.
|
|
# @REJECTED Keeping all three domains in a single ProfileService class was rejected — it violated
|
|
# INV_7 (770 lines vs 150 max), mixed I/O patterns (DB writes + HTTP calls), and created
|
|
# unnecessary coupling between preference persistence and Superset network operations.
|
|
|
|
from collections.abc import Iterable
|
|
from typing import Any
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ..models.auth import User
|
|
from ..schemas.profile import (
|
|
ProfilePreferenceResponse,
|
|
ProfilePreferenceUpdateRequest,
|
|
SupersetAccountLookupRequest,
|
|
SupersetAccountLookupResponse,
|
|
)
|
|
from .profile_preference_service import ProfilePreferenceService
|
|
from .profile_utils import (
|
|
EnvironmentNotFoundError,
|
|
ProfileAuthorizationError,
|
|
ProfileValidationError,
|
|
normalize_owner_tokens,
|
|
normalize_username,
|
|
)
|
|
from .security_badge_service import SecurityBadgeService
|
|
from .superset_lookup_service import SupersetLookupService
|
|
|
|
# Re-export exception classes for backward-compatible imports
|
|
__all__ = [
|
|
"EnvironmentNotFoundError",
|
|
"ProfileAuthorizationError",
|
|
"ProfilePreferenceService",
|
|
"ProfileService",
|
|
"ProfileValidationError",
|
|
"SecurityBadgeService",
|
|
"SupersetLookupService",
|
|
]
|
|
|
|
|
|
# #region ProfileService [C:4] [TYPE Class] [SEMANTICS profile,facade,delegate,coordinator]
|
|
# @BRIEF Facade that composes ProfilePreferenceService, SupersetLookupService, and
|
|
# SecurityBadgeService into a single interface for backward compatibility.
|
|
# @RELATION DEPENDS_ON -> [ProfilePreferenceService]
|
|
# @RELATION DEPENDS_ON -> [SupersetLookupService]
|
|
# @RELATION DEPENDS_ON -> [SecurityBadgeService]
|
|
# @RELATION DEPENDS_ON -> [ProfileUtils]
|
|
# @PRE Caller provides authenticated User context for external service methods.
|
|
# @POST Delegates to sub-services and returns normalized profile/lookup responses.
|
|
# @SIDE_EFFECT Writes preference records and encrypted tokens; performs external account lookups when requested.
|
|
# @DATA_CONTRACT Input[User,ProfilePreferenceUpdateRequest|SupersetAccountLookupRequest] -> Output[ProfilePreferenceResponse|SupersetAccountLookupResponse|bool]
|
|
# @INVARIANT Profile data integrity maintained, cache consistency with database state
|
|
# @RATIONALE Thin coordinator — all sub-10-line delegating methods. Actual business logic lives
|
|
# in the injected sub-services.
|
|
class ProfileService:
|
|
"""Facade composing preference, lookup, and security services."""
|
|
|
|
# region init [TYPE Function]
|
|
# @BRIEF Initialize facade and create sub-services.
|
|
# @PRE db session is active and config_manager supports get_environments().
|
|
# @POST All sub-services are initialized and ready.
|
|
def __init__(self, db: Session, config_manager: Any, plugin_loader: Any = None):
|
|
self.preference_service = ProfilePreferenceService(db, config_manager, plugin_loader)
|
|
self.lookup_service = SupersetLookupService(config_manager)
|
|
self.security_badge_service = SecurityBadgeService(plugin_loader)
|
|
self.db = db
|
|
self.config_manager = config_manager
|
|
self.plugin_loader = plugin_loader
|
|
|
|
# endregion init
|
|
|
|
# region get_my_preference [TYPE Function]
|
|
# @BRIEF Delegate to ProfilePreferenceService.
|
|
def get_my_preference(self, current_user: User) -> ProfilePreferenceResponse:
|
|
return self.preference_service.get_my_preference(current_user)
|
|
|
|
# endregion get_my_preference
|
|
|
|
# region get_dashboard_filter_binding [TYPE Function]
|
|
# @BRIEF Delegate to ProfilePreferenceService.
|
|
def get_dashboard_filter_binding(self, current_user: User) -> dict:
|
|
return self.preference_service.get_dashboard_filter_binding(current_user)
|
|
|
|
# endregion get_dashboard_filter_binding
|
|
|
|
# region update_my_preference [TYPE Function]
|
|
# @BRIEF Delegate to ProfilePreferenceService.
|
|
def update_my_preference(
|
|
self,
|
|
current_user: User,
|
|
payload: ProfilePreferenceUpdateRequest,
|
|
target_user_id: str | None = None,
|
|
) -> ProfilePreferenceResponse:
|
|
return self.preference_service.update_my_preference(
|
|
current_user, payload, target_user_id
|
|
)
|
|
|
|
# endregion update_my_preference
|
|
|
|
# region lookup_superset_accounts [TYPE Function]
|
|
# @BRIEF Delegate to SupersetLookupService.
|
|
# @SIDE_EFFECT Асинхронный HTTP-вызов к Superset API.
|
|
async def lookup_superset_accounts(
|
|
self,
|
|
current_user: User,
|
|
request: SupersetAccountLookupRequest,
|
|
) -> SupersetAccountLookupResponse:
|
|
return await self.lookup_service.lookup_superset_accounts(current_user, request)
|
|
|
|
# endregion lookup_superset_accounts
|
|
|
|
# region matches_dashboard_actor [TYPE Function]
|
|
# @BRIEF Apply trim+case-insensitive actor match across owners OR modified_by.
|
|
# @PRE bound_username can be empty; owners may contain mixed payload.
|
|
# @POST Returns True when normalized username matches owners or modified_by.
|
|
def matches_dashboard_actor(
|
|
self,
|
|
bound_username: str | None,
|
|
owners: Iterable[Any] | None,
|
|
modified_by: str | None,
|
|
) -> bool:
|
|
normalized_actor = normalize_username(bound_username)
|
|
if not normalized_actor:
|
|
return False
|
|
|
|
owner_tokens = normalize_owner_tokens(owners)
|
|
modified_token = normalize_username(modified_by)
|
|
|
|
if normalized_actor in owner_tokens:
|
|
return True
|
|
if modified_token and normalized_actor == modified_token:
|
|
return True
|
|
return False
|
|
|
|
# endregion matches_dashboard_actor
|
|
|
|
|
|
# #endregion ProfileService
|
|
|
|
# #endregion profile_service
|