Files
markitect-main/markitect/spaces/composability/models.py
tegwick 727ce4d3c5 feat(spaces): implement Phase 7 Composability
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>
2026-02-08 17:41:40 +01:00

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