""" Provides a static, file-based authority for workspace membership. This module loads and validates a JSON file defining members of a single workspace. It is intended for an initial, internal-only launch and does not provide multi-workspace support. """ import json import pathlib from dataclasses import dataclass from types import MappingProxyType from typing import Dict, List, Literal, Mapping, Optional # --- Public API --- class MembershipAuthorityError(Exception): """Custom exception for errors related to the membership authority.""" pass @dataclass(frozen=True) class MemberContext: """Represents the validated context of an authorized member.""" principal_id: str workspace_id: str role: Literal["admin", "operator", "member"] status: Literal["active", "suspended", "revoked"] # --- Internal Implementation --- @dataclass(frozen=True) class _MembershipAuthority: """Represents the entire validated membership authority configuration.""" schema_version: int workspace_id: str members: Mapping[str, MemberContext] def _validate_authority_data(data: dict) -> _MembershipAuthority: """Performs strict validation of the raw data from JSON.""" _EXPECTED_TOP_LEVEL_KEYS = {"schema_version", "workspace_id", "members"} _EXPECTED_MEMBER_KEYS = {"principal_id", "workspace_id", "role", "status"} _VALID_ROLES = {"admin", "operator", "member"} _VALID_STATUSES = {"active", "suspended", "revoked"} if not isinstance(data, dict) or set(data.keys()) != _EXPECTED_TOP_LEVEL_KEYS: raise MembershipAuthorityError(f"Authority data must be a JSON object with exact keys: {_EXPECTED_TOP_LEVEL_KEYS}") schema_version = data["schema_version"] if type(schema_version) is not int or schema_version != 1: raise MembershipAuthorityError(f"Unsupported schema_version: must be integer 1.") workspace_id = data.get("workspace_id") if not isinstance(workspace_id, str) or not workspace_id.strip(): raise MembershipAuthorityError("'workspace_id' must be a non-empty string.") if not isinstance(data.get("members"), list): raise MembershipAuthorityError("'members' must be a list.") members_map: Dict[str, MemberContext] = {} for member_data in data["members"]: if not isinstance(member_data, dict) or set(member_data.keys()) != _EXPECTED_MEMBER_KEYS: raise MembershipAuthorityError(f"Member data must be an object with exact keys: {_EXPECTED_MEMBER_KEYS}") principal_id = member_data.get("principal_id") prefix = "user:google:" if (not isinstance(principal_id, str) or not principal_id.strip() or not principal_id.startswith(prefix)): raise MembershipAuthorityError(f"Invalid principal_id format for '{principal_id}'.") suffix = principal_id[len(prefix):] if not suffix.strip(): raise MembershipAuthorityError(f"Invalid principal_id format for '{principal_id}': empty suffix.") if principal_id in members_map: raise MembershipAuthorityError(f"Duplicate principal_id found: {principal_id}") if member_data.get("workspace_id") != workspace_id: raise MembershipAuthorityError(f"Member workspace_id mismatch for {principal_id}.") role = member_data.get("role") if not isinstance(role, str) or role not in _VALID_ROLES: raise MembershipAuthorityError(f"Invalid role for {principal_id}: {role}") status = member_data.get("status") if not isinstance(status, str) or status not in _VALID_STATUSES: raise MembershipAuthorityError(f"Invalid status for {principal_id}: {status}") members_map[principal_id] = MemberContext(**member_data) return _MembershipAuthority( schema_version=schema_version, workspace_id=workspace_id, members=MappingProxyType(members_map), ) # --- Public API Functions --- def load_membership_authority( authority_path: Optional[pathlib.Path] = None, ) -> _MembershipAuthority: """ Loads and validates the membership_authority.json file. Args: authority_path: Optional path to the authority file for testing. Returns: A validated, internal _MembershipAuthority object. Raises: MembershipAuthorityError: If the file is not found, malformed, or fails validation. """ if authority_path is None: authority_path = pathlib.Path(__file__).parent / "membership_authority.json" try: with open(authority_path, "r", encoding="utf-8") as f: raw_data = json.load(f) except FileNotFoundError as e: raise MembershipAuthorityError(f"Authority file not found at {authority_path}") from e except OSError as e: raise MembershipAuthorityError(f"Authority file could not be read at {authority_path}") from e except UnicodeDecodeError as e: raise MembershipAuthorityError(f"Authority file is not valid UTF-8 at {authority_path}") from e except json.JSONDecodeError as e: raise MembershipAuthorityError(f"Authority file is not valid JSON: {e}") from e return _validate_authority_data(raw_data) def get_member_context_by_principal( principal_id: str, authority: Optional[_MembershipAuthority] = None ) -> Optional[MemberContext]: """ Retrieves the context for a given principal_id. If authority is not provided, it loads from the default module-relative file. An invalid authority file will raise MembershipAuthorityError. Returns a MemberContext only if the principal exists and their status is 'active'. Returns None for unknown, suspended, or revoked principals. Args: principal_id: The canonical principal ID to look up. authority: Optional pre-loaded authority object for testing. Returns: A MemberContext object or None. """ auth_to_check = authority if auth_to_check is None: auth_to_check = load_membership_authority() member = auth_to_check.members.get(principal_id) if member and member.status == "active": return member return None