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>
826 lines
26 KiB
Python
826 lines
26 KiB
Python
"""
|
|
Service layer for space composability.
|
|
|
|
Provides inheritance resolution, reference management, and access control.
|
|
"""
|
|
|
|
from typing import List, Optional, Dict, Any, Set
|
|
from dataclasses import dataclass
|
|
|
|
from ..models import InformationSpace, SpaceVariable, SpaceConfig
|
|
from ..repositories.interfaces import ISpaceRepository, IVariableRepository
|
|
from .models import (
|
|
SpaceReference,
|
|
SpaceReferenceType,
|
|
SpacePermission,
|
|
AccessLevel,
|
|
SpaceRole,
|
|
SpaceAccess,
|
|
InheritedVariable,
|
|
InheritedConfig,
|
|
)
|
|
from .repository import ISpaceReferenceRepository, IAccessControlRepository
|
|
|
|
|
|
class CircularReferenceError(Exception):
|
|
"""Raised when a circular reference is detected."""
|
|
pass
|
|
|
|
|
|
class InheritanceResolver:
|
|
"""
|
|
Resolves inherited values through the space hierarchy.
|
|
|
|
Handles variable inheritance, config inheritance, and reference traversal.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
space_repo: ISpaceRepository,
|
|
variable_repo: IVariableRepository,
|
|
reference_repo: Optional[ISpaceReferenceRepository] = None,
|
|
max_depth: int = 10,
|
|
):
|
|
"""
|
|
Initialize the resolver.
|
|
|
|
Args:
|
|
space_repo: Repository for space lookups
|
|
variable_repo: Repository for variable lookups
|
|
reference_repo: Optional repository for reference lookups
|
|
max_depth: Maximum inheritance depth to prevent infinite loops
|
|
"""
|
|
self._space_repo = space_repo
|
|
self._variable_repo = variable_repo
|
|
self._reference_repo = reference_repo
|
|
self._max_depth = max_depth
|
|
|
|
def get_parent_chain(
|
|
self, space_id: str, visited: Optional[Set[str]] = None
|
|
) -> List[InformationSpace]:
|
|
"""
|
|
Get the chain of parent spaces.
|
|
|
|
Args:
|
|
space_id: Starting space ID
|
|
visited: Set of already-visited space IDs (for cycle detection)
|
|
|
|
Returns:
|
|
List of parent spaces, from immediate parent to root
|
|
|
|
Raises:
|
|
CircularReferenceError: If a cycle is detected
|
|
"""
|
|
if visited is None:
|
|
visited = set()
|
|
|
|
if space_id in visited:
|
|
raise CircularReferenceError(
|
|
f"Circular reference detected: space {space_id} already in chain"
|
|
)
|
|
|
|
visited.add(space_id)
|
|
space = self._space_repo.get_by_id(space_id)
|
|
|
|
if not space or not space.parent_space_id:
|
|
return []
|
|
|
|
if len(visited) > self._max_depth:
|
|
raise CircularReferenceError(
|
|
f"Maximum inheritance depth ({self._max_depth}) exceeded"
|
|
)
|
|
|
|
parent = self._space_repo.get_by_id(space.parent_space_id)
|
|
if not parent:
|
|
return []
|
|
|
|
return [parent] + self.get_parent_chain(parent.id, visited)
|
|
|
|
def resolve_variable(
|
|
self, space_id: str, name: str
|
|
) -> Optional[InheritedVariable]:
|
|
"""
|
|
Resolve a variable, checking parent spaces if not found locally.
|
|
|
|
Args:
|
|
space_id: The space to start resolution from
|
|
name: Variable name to resolve
|
|
|
|
Returns:
|
|
InheritedVariable with value and source, or None if not found
|
|
"""
|
|
# Check local space first
|
|
local_var = self._variable_repo.get_variable(space_id, name)
|
|
if local_var:
|
|
return InheritedVariable(
|
|
name=name,
|
|
value=local_var.value,
|
|
source_space_id=space_id,
|
|
inheritance_depth=0,
|
|
scope=local_var.scope,
|
|
)
|
|
|
|
# Walk up parent chain
|
|
try:
|
|
parents = self.get_parent_chain(space_id)
|
|
except CircularReferenceError:
|
|
return None
|
|
|
|
for depth, parent in enumerate(parents, start=1):
|
|
parent_var = self._variable_repo.get_variable(parent.id, name)
|
|
if parent_var:
|
|
return InheritedVariable(
|
|
name=name,
|
|
value=parent_var.value,
|
|
source_space_id=parent.id,
|
|
inheritance_depth=depth,
|
|
scope=parent_var.scope,
|
|
)
|
|
|
|
return None
|
|
|
|
def resolve_all_variables(
|
|
self, space_id: str, scope: Optional[str] = None
|
|
) -> Dict[str, InheritedVariable]:
|
|
"""
|
|
Resolve all variables for a space, including inherited ones.
|
|
|
|
Variables defined locally override inherited ones with the same name.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
scope: Optional scope filter
|
|
|
|
Returns:
|
|
Dictionary of variable name to InheritedVariable
|
|
"""
|
|
result: Dict[str, InheritedVariable] = {}
|
|
|
|
# Start from the furthest ancestor and work down
|
|
try:
|
|
parents = self.get_parent_chain(space_id)
|
|
except CircularReferenceError:
|
|
parents = []
|
|
|
|
# Build list from root to current: [grandparent, parent, current]
|
|
# parents is [parent, grandparent] so reverse it
|
|
current_space = self._space_repo.get_by_id(space_id)
|
|
all_spaces = list(reversed(parents))
|
|
if current_space:
|
|
all_spaces.append(current_space)
|
|
|
|
# Process from root to current so local values override inherited ones
|
|
# Index 0 is root (highest depth), last index is current (depth 0)
|
|
total = len(all_spaces)
|
|
for i, space in enumerate(all_spaces):
|
|
# depth_from_current: current=0, parent=1, grandparent=2, etc.
|
|
depth_from_current = total - 1 - i
|
|
variables = self._variable_repo.list_variables(space.id, scope)
|
|
for var in variables:
|
|
result[var.name] = InheritedVariable(
|
|
name=var.name,
|
|
value=var.value,
|
|
source_space_id=space.id,
|
|
inheritance_depth=depth_from_current,
|
|
scope=var.scope,
|
|
)
|
|
|
|
return result
|
|
|
|
def resolve_config(self, space_id: str) -> InheritedConfig:
|
|
"""
|
|
Resolve the effective configuration for a space.
|
|
|
|
Config values are inherited from parents unless overridden locally.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
|
|
Returns:
|
|
InheritedConfig with effective values and sources
|
|
"""
|
|
space = self._space_repo.get_by_id(space_id)
|
|
if not space:
|
|
return InheritedConfig()
|
|
|
|
# Start with defaults
|
|
result = InheritedConfig(
|
|
default_variant="hierarchical",
|
|
enable_caching=True,
|
|
theme=None,
|
|
history_enabled=False,
|
|
variable_scope="space",
|
|
source_spaces={},
|
|
)
|
|
|
|
# Get parent chain
|
|
try:
|
|
parents = self.get_parent_chain(space_id)
|
|
except CircularReferenceError:
|
|
parents = []
|
|
|
|
# Process from root to current (reversed order)
|
|
all_spaces = list(reversed(parents)) + [space]
|
|
|
|
for s in all_spaces:
|
|
config = s.config
|
|
if config.default_variant:
|
|
result.default_variant = config.default_variant
|
|
result.source_spaces["default_variant"] = s.id
|
|
|
|
if config.enable_caching is not None:
|
|
result.enable_caching = config.enable_caching
|
|
result.source_spaces["enable_caching"] = s.id
|
|
|
|
if config.theme:
|
|
result.theme = config.theme
|
|
result.source_spaces["theme"] = s.id
|
|
|
|
if config.history_enabled is not None:
|
|
result.history_enabled = config.history_enabled
|
|
result.source_spaces["history_enabled"] = s.id
|
|
|
|
if config.variable_scope:
|
|
result.variable_scope = config.variable_scope
|
|
result.source_spaces["variable_scope"] = s.id
|
|
|
|
return result
|
|
|
|
|
|
class AccessControlService:
|
|
"""
|
|
Service for managing space access control.
|
|
|
|
Handles permission grants, checks, and inheritance.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
permission_repo: IAccessControlRepository,
|
|
space_repo: ISpaceRepository,
|
|
inheritance_resolver: Optional[InheritanceResolver] = None,
|
|
):
|
|
"""
|
|
Initialize the service.
|
|
|
|
Args:
|
|
permission_repo: Repository for permission storage
|
|
space_repo: Repository for space lookups
|
|
inheritance_resolver: Optional resolver for inherited permissions
|
|
"""
|
|
self._permission_repo = permission_repo
|
|
self._space_repo = space_repo
|
|
self._inheritance_resolver = inheritance_resolver
|
|
|
|
def grant_permission(
|
|
self,
|
|
space_id: str,
|
|
principal_type: str,
|
|
principal_id: str,
|
|
access_level: AccessLevel,
|
|
role: Optional[SpaceRole] = None,
|
|
granted_by: Optional[str] = None,
|
|
) -> SpacePermission:
|
|
"""
|
|
Grant a permission to a principal.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
principal_type: Type of principal (user, group, role)
|
|
principal_id: Principal identifier
|
|
access_level: Access level to grant
|
|
role: Optional role assignment
|
|
granted_by: Who is granting the permission
|
|
|
|
Returns:
|
|
The created permission
|
|
"""
|
|
permission = SpacePermission(
|
|
space_id=space_id,
|
|
principal_type=principal_type,
|
|
principal_id=principal_id,
|
|
access_level=access_level,
|
|
role=role,
|
|
granted_by=granted_by,
|
|
)
|
|
return self._permission_repo.grant_permission(permission)
|
|
|
|
def revoke_permission(
|
|
self, space_id: str, principal_type: str, principal_id: str
|
|
) -> bool:
|
|
"""
|
|
Revoke a permission.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
principal_type: Type of principal
|
|
principal_id: Principal identifier
|
|
|
|
Returns:
|
|
True if revoked, False if not found
|
|
"""
|
|
return self._permission_repo.revoke_permission(
|
|
space_id, principal_type, principal_id
|
|
)
|
|
|
|
def check_access(
|
|
self,
|
|
space_id: str,
|
|
principal_type: str,
|
|
principal_id: str,
|
|
required_level: AccessLevel = AccessLevel.READ,
|
|
) -> bool:
|
|
"""
|
|
Check if a principal has the required access level.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
principal_type: Type of principal
|
|
principal_id: Principal identifier
|
|
required_level: Required access level
|
|
|
|
Returns:
|
|
True if access is granted
|
|
"""
|
|
access = self.get_effective_access(space_id, principal_type, principal_id)
|
|
if access.effective_level == AccessLevel.NONE:
|
|
return False
|
|
# Compare using level order, not string values
|
|
level_order = [AccessLevel.NONE, AccessLevel.READ, AccessLevel.WRITE, AccessLevel.ADMIN]
|
|
return level_order.index(access.effective_level) >= level_order.index(required_level)
|
|
|
|
def get_effective_access(
|
|
self, space_id: str, principal_type: str, principal_id: str
|
|
) -> SpaceAccess:
|
|
"""
|
|
Get the effective access for a principal on a space.
|
|
|
|
Considers direct permissions and inherited permissions from parent spaces.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
principal_type: Type of principal
|
|
principal_id: Principal identifier
|
|
|
|
Returns:
|
|
SpaceAccess with computed effective access
|
|
"""
|
|
result = SpaceAccess(
|
|
space_id=space_id,
|
|
principal_id=principal_id,
|
|
effective_level=AccessLevel.NONE,
|
|
roles=[],
|
|
inherited_from=[],
|
|
permissions=[],
|
|
)
|
|
|
|
# Check direct permission
|
|
direct = self._permission_repo.get_permission(
|
|
space_id, principal_type, principal_id
|
|
)
|
|
if direct and not direct.is_expired():
|
|
result.permissions.append(direct)
|
|
result.effective_level = direct.access_level
|
|
if direct.role:
|
|
result.roles.append(direct.role)
|
|
|
|
# Check inherited permissions if we have a resolver
|
|
if self._inheritance_resolver:
|
|
try:
|
|
parents = self._inheritance_resolver.get_parent_chain(space_id)
|
|
except CircularReferenceError:
|
|
parents = []
|
|
|
|
for parent in parents:
|
|
parent_perm = self._permission_repo.get_permission(
|
|
parent.id, principal_type, principal_id
|
|
)
|
|
if parent_perm and not parent_perm.is_expired():
|
|
result.permissions.append(parent_perm)
|
|
result.inherited_from.append(parent.id)
|
|
|
|
# Take the highest access level
|
|
level_order = [
|
|
AccessLevel.NONE,
|
|
AccessLevel.READ,
|
|
AccessLevel.WRITE,
|
|
AccessLevel.ADMIN,
|
|
]
|
|
if level_order.index(parent_perm.access_level) > level_order.index(
|
|
result.effective_level
|
|
):
|
|
result.effective_level = parent_perm.access_level
|
|
|
|
if parent_perm.role and parent_perm.role not in result.roles:
|
|
result.roles.append(parent_perm.role)
|
|
|
|
return result
|
|
|
|
def get_space_permissions(self, space_id: str) -> List[SpacePermission]:
|
|
"""
|
|
Get all permissions for a space.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
|
|
Returns:
|
|
List of permissions
|
|
"""
|
|
return self._permission_repo.get_permissions_for_space(space_id)
|
|
|
|
def get_principal_permissions(
|
|
self, principal_type: str, principal_id: str
|
|
) -> List[SpacePermission]:
|
|
"""
|
|
Get all permissions for a principal.
|
|
|
|
Args:
|
|
principal_type: Type of principal
|
|
principal_id: Principal identifier
|
|
|
|
Returns:
|
|
List of permissions
|
|
"""
|
|
return self._permission_repo.get_permissions_for_principal(
|
|
principal_type, principal_id
|
|
)
|
|
|
|
|
|
class ComposableSpaceService:
|
|
"""
|
|
Extended space service with composability features.
|
|
|
|
Provides space references, inheritance resolution, and access control
|
|
on top of the base SpaceService functionality.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
space_repo: ISpaceRepository,
|
|
variable_repo: IVariableRepository,
|
|
reference_repo: ISpaceReferenceRepository,
|
|
permission_repo: IAccessControlRepository,
|
|
max_inheritance_depth: int = 10,
|
|
):
|
|
"""
|
|
Initialize the service.
|
|
|
|
Args:
|
|
space_repo: Repository for spaces
|
|
variable_repo: Repository for variables
|
|
reference_repo: Repository for space references
|
|
permission_repo: Repository for permissions
|
|
max_inheritance_depth: Maximum inheritance chain depth
|
|
"""
|
|
self._space_repo = space_repo
|
|
self._variable_repo = variable_repo
|
|
self._reference_repo = reference_repo
|
|
self._permission_repo = permission_repo
|
|
|
|
self._inheritance_resolver = InheritanceResolver(
|
|
space_repo=space_repo,
|
|
variable_repo=variable_repo,
|
|
reference_repo=reference_repo,
|
|
max_depth=max_inheritance_depth,
|
|
)
|
|
|
|
self._access_service = AccessControlService(
|
|
permission_repo=permission_repo,
|
|
space_repo=space_repo,
|
|
inheritance_resolver=self._inheritance_resolver,
|
|
)
|
|
|
|
# =========================================================================
|
|
# Space References
|
|
# =========================================================================
|
|
|
|
def add_reference(
|
|
self,
|
|
source_space_id: str,
|
|
target_space_id: str,
|
|
reference_type: SpaceReferenceType = SpaceReferenceType.LINKS_TO,
|
|
alias: Optional[str] = None,
|
|
metadata: Optional[Dict[str, Any]] = None,
|
|
) -> SpaceReference:
|
|
"""
|
|
Add a reference from one space to another.
|
|
|
|
Args:
|
|
source_space_id: The space making the reference
|
|
target_space_id: The space being referenced
|
|
reference_type: Type of reference
|
|
alias: Optional alias for the target space
|
|
metadata: Optional metadata
|
|
|
|
Returns:
|
|
The created reference
|
|
|
|
Raises:
|
|
ValueError: If spaces don't exist or reference would create a cycle
|
|
CircularReferenceError: If reference would create a circular dependency
|
|
"""
|
|
# Validate spaces exist
|
|
if not self._space_repo.exists(source_space_id):
|
|
raise ValueError(f"Source space '{source_space_id}' not found")
|
|
if not self._space_repo.exists(target_space_id):
|
|
raise ValueError(f"Target space '{target_space_id}' not found")
|
|
|
|
# Check for self-reference
|
|
if source_space_id == target_space_id:
|
|
raise ValueError("Cannot reference self")
|
|
|
|
# Check for circular reference (for EXTENDS type)
|
|
if reference_type == SpaceReferenceType.EXTENDS:
|
|
self._check_circular_reference(source_space_id, target_space_id)
|
|
|
|
reference = SpaceReference(
|
|
source_space_id=source_space_id,
|
|
target_space_id=target_space_id,
|
|
reference_type=reference_type,
|
|
alias=alias,
|
|
metadata=metadata or {},
|
|
)
|
|
|
|
return self._reference_repo.add_reference(reference)
|
|
|
|
def _check_circular_reference(
|
|
self, source_id: str, target_id: str, visited: Optional[Set[str]] = None
|
|
) -> None:
|
|
"""Check if adding a reference would create a cycle."""
|
|
if visited is None:
|
|
visited = {source_id}
|
|
|
|
if target_id in visited:
|
|
raise CircularReferenceError(
|
|
f"Adding reference would create cycle: {target_id} already in chain"
|
|
)
|
|
|
|
visited.add(target_id)
|
|
|
|
# Check existing references from target
|
|
refs = self._reference_repo.get_references_from(
|
|
target_id, SpaceReferenceType.EXTENDS
|
|
)
|
|
for ref in refs:
|
|
self._check_circular_reference(source_id, ref.target_space_id, visited)
|
|
|
|
# Also check parent_space_id chain
|
|
target_space = self._space_repo.get_by_id(target_id)
|
|
if target_space and target_space.parent_space_id:
|
|
if target_space.parent_space_id in visited:
|
|
raise CircularReferenceError(
|
|
f"Adding reference would create cycle through parent: "
|
|
f"{target_space.parent_space_id}"
|
|
)
|
|
self._check_circular_reference(
|
|
source_id, target_space.parent_space_id, visited
|
|
)
|
|
|
|
def remove_reference(self, reference_id: str) -> bool:
|
|
"""
|
|
Remove a space reference.
|
|
|
|
Args:
|
|
reference_id: The reference ID
|
|
|
|
Returns:
|
|
True if removed, False if not found
|
|
"""
|
|
return self._reference_repo.remove_reference(reference_id)
|
|
|
|
def get_references_from(
|
|
self,
|
|
space_id: str,
|
|
reference_type: Optional[SpaceReferenceType] = None,
|
|
) -> List[SpaceReference]:
|
|
"""
|
|
Get all references from a space.
|
|
|
|
Args:
|
|
space_id: The source space ID
|
|
reference_type: Optional filter by type
|
|
|
|
Returns:
|
|
List of references
|
|
"""
|
|
return self._reference_repo.get_references_from(space_id, reference_type)
|
|
|
|
def get_references_to(
|
|
self,
|
|
space_id: str,
|
|
reference_type: Optional[SpaceReferenceType] = None,
|
|
) -> List[SpaceReference]:
|
|
"""
|
|
Get all references to a space.
|
|
|
|
Args:
|
|
space_id: The target space ID
|
|
reference_type: Optional filter by type
|
|
|
|
Returns:
|
|
List of references
|
|
"""
|
|
return self._reference_repo.get_references_to(space_id, reference_type)
|
|
|
|
def get_composed_spaces(self, space_id: str) -> List[InformationSpace]:
|
|
"""
|
|
Get all spaces that compose this space.
|
|
|
|
Includes parent, extended spaces, and included spaces.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
|
|
Returns:
|
|
List of composed spaces
|
|
"""
|
|
result: List[InformationSpace] = []
|
|
seen: Set[str] = {space_id}
|
|
|
|
space = self._space_repo.get_by_id(space_id)
|
|
if not space:
|
|
return []
|
|
|
|
# Add parent
|
|
if space.parent_space_id:
|
|
parent = self._space_repo.get_by_id(space.parent_space_id)
|
|
if parent and parent.id not in seen:
|
|
result.append(parent)
|
|
seen.add(parent.id)
|
|
|
|
# Add extended spaces
|
|
extends_refs = self._reference_repo.get_references_from(
|
|
space_id, SpaceReferenceType.EXTENDS
|
|
)
|
|
for ref in extends_refs:
|
|
if ref.target_space_id not in seen:
|
|
target = self._space_repo.get_by_id(ref.target_space_id)
|
|
if target:
|
|
result.append(target)
|
|
seen.add(ref.target_space_id)
|
|
|
|
# Add included spaces
|
|
includes_refs = self._reference_repo.get_references_from(
|
|
space_id, SpaceReferenceType.INCLUDES
|
|
)
|
|
for ref in includes_refs:
|
|
if ref.target_space_id not in seen:
|
|
target = self._space_repo.get_by_id(ref.target_space_id)
|
|
if target:
|
|
result.append(target)
|
|
seen.add(ref.target_space_id)
|
|
|
|
return result
|
|
|
|
# =========================================================================
|
|
# Inheritance Resolution
|
|
# =========================================================================
|
|
|
|
def resolve_variable(
|
|
self, space_id: str, name: str
|
|
) -> Optional[InheritedVariable]:
|
|
"""
|
|
Resolve a variable with inheritance.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
name: Variable name
|
|
|
|
Returns:
|
|
InheritedVariable or None if not found
|
|
"""
|
|
return self._inheritance_resolver.resolve_variable(space_id, name)
|
|
|
|
def resolve_all_variables(
|
|
self, space_id: str, scope: Optional[str] = None
|
|
) -> Dict[str, InheritedVariable]:
|
|
"""
|
|
Resolve all variables including inherited ones.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
scope: Optional scope filter
|
|
|
|
Returns:
|
|
Dictionary of variable name to InheritedVariable
|
|
"""
|
|
return self._inheritance_resolver.resolve_all_variables(space_id, scope)
|
|
|
|
def resolve_config(self, space_id: str) -> InheritedConfig:
|
|
"""
|
|
Resolve effective configuration with inheritance.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
|
|
Returns:
|
|
InheritedConfig with effective values
|
|
"""
|
|
return self._inheritance_resolver.resolve_config(space_id)
|
|
|
|
def get_parent_chain(self, space_id: str) -> List[InformationSpace]:
|
|
"""
|
|
Get the parent space chain.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
|
|
Returns:
|
|
List of parent spaces from immediate parent to root
|
|
"""
|
|
return self._inheritance_resolver.get_parent_chain(space_id)
|
|
|
|
# =========================================================================
|
|
# Access Control
|
|
# =========================================================================
|
|
|
|
def grant_access(
|
|
self,
|
|
space_id: str,
|
|
principal_type: str,
|
|
principal_id: str,
|
|
access_level: AccessLevel,
|
|
role: Optional[SpaceRole] = None,
|
|
granted_by: Optional[str] = None,
|
|
) -> SpacePermission:
|
|
"""
|
|
Grant access to a space.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
principal_type: Type of principal (user, group)
|
|
principal_id: Principal identifier
|
|
access_level: Access level to grant
|
|
role: Optional role
|
|
granted_by: Who is granting access
|
|
|
|
Returns:
|
|
The created permission
|
|
"""
|
|
return self._access_service.grant_permission(
|
|
space_id, principal_type, principal_id, access_level, role, granted_by
|
|
)
|
|
|
|
def revoke_access(
|
|
self, space_id: str, principal_type: str, principal_id: str
|
|
) -> bool:
|
|
"""
|
|
Revoke access to a space.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
principal_type: Type of principal
|
|
principal_id: Principal identifier
|
|
|
|
Returns:
|
|
True if revoked
|
|
"""
|
|
return self._access_service.revoke_permission(
|
|
space_id, principal_type, principal_id
|
|
)
|
|
|
|
def check_access(
|
|
self,
|
|
space_id: str,
|
|
principal_type: str,
|
|
principal_id: str,
|
|
required_level: AccessLevel = AccessLevel.READ,
|
|
) -> bool:
|
|
"""
|
|
Check if a principal has required access.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
principal_type: Type of principal
|
|
principal_id: Principal identifier
|
|
required_level: Required access level
|
|
|
|
Returns:
|
|
True if access is granted
|
|
"""
|
|
return self._access_service.check_access(
|
|
space_id, principal_type, principal_id, required_level
|
|
)
|
|
|
|
def get_effective_access(
|
|
self, space_id: str, principal_type: str, principal_id: str
|
|
) -> SpaceAccess:
|
|
"""
|
|
Get effective access including inheritance.
|
|
|
|
Args:
|
|
space_id: The space ID
|
|
principal_type: Type of principal
|
|
principal_id: Principal identifier
|
|
|
|
Returns:
|
|
SpaceAccess with computed access
|
|
"""
|
|
return self._access_service.get_effective_access(
|
|
space_id, principal_type, principal_id
|
|
)
|
|
|
|
def get_space_permissions(self, space_id: str) -> List[SpacePermission]:
|
|
"""Get all permissions for a space."""
|
|
return self._access_service.get_space_permissions(space_id)
|