Implements space composition and inheritance features: - SpaceReference model for space-to-space references (includes, extends, links_to, composed_of) - Variable inheritance through parent chain with local override - Config inheritance with source tracking - Access control models (SpacePermission, SpaceRole, AccessLevel) - InheritanceResolver for walking parent chains - AccessControlService for permission management - ComposableSpaceService integrating all composability features - Circular reference detection for EXTENDS references - SQLite repositories for references and permissions - 57 comprehensive unit tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
272 lines
9.2 KiB
Python
272 lines
9.2 KiB
Python
"""
|
|
Domain models for space composability.
|
|
|
|
Defines models for space references, inheritance, and access control.
|
|
"""
|
|
|
|
import uuid
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from typing import Dict, Any, List, Optional
|
|
from enum import Enum
|
|
|
|
|
|
class SpaceReferenceType(Enum):
|
|
"""Type of reference between spaces."""
|
|
INCLUDES = "includes" # Space A includes content from Space B
|
|
EXTENDS = "extends" # Space A extends/inherits from Space B
|
|
LINKS_TO = "links_to" # Space A has a soft link to Space B
|
|
COMPOSED_OF = "composed_of" # Space A is composed of multiple spaces
|
|
|
|
|
|
@dataclass
|
|
class SpaceReference:
|
|
"""
|
|
Represents a reference between two spaces.
|
|
|
|
Unlike parent_space_id (single inheritance), space references allow
|
|
multiple relationships between spaces for composition.
|
|
|
|
Attributes:
|
|
id: Unique reference identifier
|
|
source_space_id: The space making the reference
|
|
target_space_id: The space being referenced
|
|
reference_type: Type of reference relationship
|
|
alias: Optional alias for the referenced space
|
|
metadata: Additional reference metadata
|
|
created_at: When the reference was created
|
|
"""
|
|
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
source_space_id: str = ""
|
|
target_space_id: str = ""
|
|
reference_type: SpaceReferenceType = SpaceReferenceType.LINKS_TO
|
|
alias: Optional[str] = None
|
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
created_at: datetime = field(default_factory=datetime.now)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dictionary for serialization."""
|
|
return {
|
|
"id": self.id,
|
|
"source_space_id": self.source_space_id,
|
|
"target_space_id": self.target_space_id,
|
|
"reference_type": self.reference_type.value,
|
|
"alias": self.alias,
|
|
"metadata": self.metadata,
|
|
"created_at": self.created_at.isoformat(),
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "SpaceReference":
|
|
"""Create from dictionary."""
|
|
created_at = data.get("created_at")
|
|
if isinstance(created_at, str):
|
|
created_at = datetime.fromisoformat(created_at)
|
|
elif created_at is None:
|
|
created_at = datetime.now()
|
|
|
|
ref_type = data.get("reference_type", "links_to")
|
|
if isinstance(ref_type, str):
|
|
ref_type = SpaceReferenceType(ref_type)
|
|
|
|
return cls(
|
|
id=data.get("id", str(uuid.uuid4())),
|
|
source_space_id=data.get("source_space_id", ""),
|
|
target_space_id=data.get("target_space_id", ""),
|
|
reference_type=ref_type,
|
|
alias=data.get("alias"),
|
|
metadata=data.get("metadata", {}),
|
|
created_at=created_at,
|
|
)
|
|
|
|
|
|
class AccessLevel(Enum):
|
|
"""Access level for space permissions."""
|
|
NONE = "none" # No access
|
|
READ = "read" # Read-only access
|
|
WRITE = "write" # Read and write access
|
|
ADMIN = "admin" # Full administrative access
|
|
|
|
|
|
class SpaceRole(Enum):
|
|
"""Predefined roles for space access."""
|
|
OWNER = "owner" # Full control, can delete space
|
|
ADMIN = "admin" # Can manage members and settings
|
|
EDITOR = "editor" # Can edit documents and variables
|
|
VIEWER = "viewer" # Read-only access
|
|
GUEST = "guest" # Limited read access
|
|
|
|
|
|
@dataclass
|
|
class SpacePermission:
|
|
"""
|
|
Permission entry for a space.
|
|
|
|
Maps a principal (user/group/role) to an access level for a space.
|
|
|
|
Attributes:
|
|
space_id: The space this permission applies to
|
|
principal_type: Type of principal (user, group, role)
|
|
principal_id: Identifier for the principal
|
|
access_level: The access level granted
|
|
role: Optional predefined role
|
|
granted_by: Who granted this permission
|
|
granted_at: When permission was granted
|
|
expires_at: Optional expiration timestamp
|
|
"""
|
|
space_id: str
|
|
principal_type: str # "user", "group", "role"
|
|
principal_id: str
|
|
access_level: AccessLevel = AccessLevel.READ
|
|
role: Optional[SpaceRole] = None
|
|
granted_by: Optional[str] = None
|
|
granted_at: datetime = field(default_factory=datetime.now)
|
|
expires_at: Optional[datetime] = None
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Convert to dictionary."""
|
|
return {
|
|
"space_id": self.space_id,
|
|
"principal_type": self.principal_type,
|
|
"principal_id": self.principal_id,
|
|
"access_level": self.access_level.value,
|
|
"role": self.role.value if self.role else None,
|
|
"granted_by": self.granted_by,
|
|
"granted_at": self.granted_at.isoformat(),
|
|
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any]) -> "SpacePermission":
|
|
"""Create from dictionary."""
|
|
granted_at = data.get("granted_at")
|
|
if isinstance(granted_at, str):
|
|
granted_at = datetime.fromisoformat(granted_at)
|
|
elif granted_at is None:
|
|
granted_at = datetime.now()
|
|
|
|
expires_at = data.get("expires_at")
|
|
if isinstance(expires_at, str):
|
|
expires_at = datetime.fromisoformat(expires_at)
|
|
|
|
access_level = data.get("access_level", "read")
|
|
if isinstance(access_level, str):
|
|
access_level = AccessLevel(access_level)
|
|
|
|
role = data.get("role")
|
|
if isinstance(role, str):
|
|
role = SpaceRole(role)
|
|
|
|
return cls(
|
|
space_id=data["space_id"],
|
|
principal_type=data.get("principal_type", "user"),
|
|
principal_id=data["principal_id"],
|
|
access_level=access_level,
|
|
role=role,
|
|
granted_by=data.get("granted_by"),
|
|
granted_at=granted_at,
|
|
expires_at=expires_at,
|
|
)
|
|
|
|
def is_expired(self) -> bool:
|
|
"""Check if permission has expired."""
|
|
if self.expires_at is None:
|
|
return False
|
|
return datetime.now() > self.expires_at
|
|
|
|
def has_access(self, required_level: AccessLevel) -> bool:
|
|
"""Check if this permission grants the required access level."""
|
|
if self.is_expired():
|
|
return False
|
|
|
|
level_order = [AccessLevel.NONE, AccessLevel.READ, AccessLevel.WRITE, AccessLevel.ADMIN]
|
|
return level_order.index(self.access_level) >= level_order.index(required_level)
|
|
|
|
|
|
@dataclass
|
|
class SpaceAccess:
|
|
"""
|
|
Computed access result for a principal on a space.
|
|
|
|
Combines all applicable permissions to determine effective access.
|
|
|
|
Attributes:
|
|
space_id: The space being accessed
|
|
principal_id: The principal accessing
|
|
effective_level: Computed effective access level
|
|
roles: All roles that apply
|
|
inherited_from: Space IDs from which access was inherited
|
|
permissions: Source permissions that contributed
|
|
"""
|
|
space_id: str
|
|
principal_id: str
|
|
effective_level: AccessLevel = AccessLevel.NONE
|
|
roles: List[SpaceRole] = field(default_factory=list)
|
|
inherited_from: List[str] = field(default_factory=list)
|
|
permissions: List[SpacePermission] = field(default_factory=list)
|
|
|
|
def can_read(self) -> bool:
|
|
"""Check if principal can read."""
|
|
return self.effective_level in [AccessLevel.READ, AccessLevel.WRITE, AccessLevel.ADMIN]
|
|
|
|
def can_write(self) -> bool:
|
|
"""Check if principal can write."""
|
|
return self.effective_level in [AccessLevel.WRITE, AccessLevel.ADMIN]
|
|
|
|
def is_admin(self) -> bool:
|
|
"""Check if principal has admin access."""
|
|
return self.effective_level == AccessLevel.ADMIN
|
|
|
|
|
|
@dataclass
|
|
class InheritedVariable:
|
|
"""
|
|
A variable with its inheritance source.
|
|
|
|
Attributes:
|
|
name: Variable name
|
|
value: Variable value
|
|
source_space_id: Space where this variable is defined
|
|
inheritance_depth: How many levels up the inheritance chain (0 = local)
|
|
scope: Variable scope
|
|
"""
|
|
name: str
|
|
value: Any
|
|
source_space_id: str
|
|
inheritance_depth: int = 0
|
|
scope: str = "space"
|
|
|
|
def is_local(self) -> bool:
|
|
"""Check if variable is locally defined (not inherited)."""
|
|
return self.inheritance_depth == 0
|
|
|
|
|
|
@dataclass
|
|
class InheritedConfig:
|
|
"""
|
|
Configuration with inheritance tracking.
|
|
|
|
Attributes:
|
|
default_variant: Effective default variant
|
|
enable_caching: Effective caching setting
|
|
theme: Effective theme
|
|
history_enabled: Effective history setting
|
|
variable_scope: Effective variable scope
|
|
source_spaces: Map of config key to source space ID
|
|
"""
|
|
default_variant: str = "hierarchical"
|
|
enable_caching: bool = True
|
|
theme: Optional[str] = None
|
|
history_enabled: bool = False
|
|
variable_scope: str = "space"
|
|
source_spaces: Dict[str, str] = field(default_factory=dict)
|
|
|
|
def get_source(self, key: str) -> Optional[str]:
|
|
"""Get the source space ID for a config key."""
|
|
return self.source_spaces.get(key)
|
|
|
|
def is_inherited(self, key: str, current_space_id: str) -> bool:
|
|
"""Check if a config key is inherited from a parent."""
|
|
source = self.source_spaces.get(key)
|
|
return source is not None and source != current_space_id
|