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>
425 lines
15 KiB
Python
425 lines
15 KiB
Python
"""
|
|
Repository implementations for space composability.
|
|
|
|
Provides storage for space references and access control.
|
|
"""
|
|
|
|
import json
|
|
import sqlite3
|
|
from abc import ABC, abstractmethod
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
from datetime import datetime
|
|
|
|
from .models import (
|
|
SpaceReference,
|
|
SpaceReferenceType,
|
|
SpacePermission,
|
|
AccessLevel,
|
|
SpaceRole,
|
|
)
|
|
|
|
|
|
class ISpaceReferenceRepository(ABC):
|
|
"""Interface for space reference storage."""
|
|
|
|
@abstractmethod
|
|
def add_reference(self, reference: SpaceReference) -> SpaceReference:
|
|
"""Add a space reference."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_reference(self, reference_id: str) -> Optional[SpaceReference]:
|
|
"""Get a reference by ID."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_references_from(
|
|
self, source_space_id: str, ref_type: Optional[SpaceReferenceType] = None
|
|
) -> List[SpaceReference]:
|
|
"""Get all references from a source space."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_references_to(
|
|
self, target_space_id: str, ref_type: Optional[SpaceReferenceType] = None
|
|
) -> List[SpaceReference]:
|
|
"""Get all references to a target space."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def remove_reference(self, reference_id: str) -> bool:
|
|
"""Remove a reference."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def remove_references_from(self, source_space_id: str) -> int:
|
|
"""Remove all references from a source space."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def reference_exists(
|
|
self, source_space_id: str, target_space_id: str, ref_type: SpaceReferenceType
|
|
) -> bool:
|
|
"""Check if a specific reference exists."""
|
|
pass
|
|
|
|
|
|
class IAccessControlRepository(ABC):
|
|
"""Interface for access control storage."""
|
|
|
|
@abstractmethod
|
|
def grant_permission(self, permission: SpacePermission) -> SpacePermission:
|
|
"""Grant a permission."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def revoke_permission(
|
|
self, space_id: str, principal_type: str, principal_id: str
|
|
) -> bool:
|
|
"""Revoke a permission."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_permissions_for_space(self, space_id: str) -> List[SpacePermission]:
|
|
"""Get all permissions for a space."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_permissions_for_principal(
|
|
self, principal_type: str, principal_id: str
|
|
) -> List[SpacePermission]:
|
|
"""Get all permissions for a principal."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_permission(
|
|
self, space_id: str, principal_type: str, principal_id: str
|
|
) -> Optional[SpacePermission]:
|
|
"""Get a specific permission."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def clear_space_permissions(self, space_id: str) -> int:
|
|
"""Clear all permissions for a space."""
|
|
pass
|
|
|
|
|
|
class SqliteSpaceReferenceRepository(ISpaceReferenceRepository):
|
|
"""SQLite implementation of space reference repository."""
|
|
|
|
def __init__(self, db_path: Path):
|
|
"""Initialize the repository.
|
|
|
|
Args:
|
|
db_path: Path to the SQLite database file
|
|
"""
|
|
self._db_path = db_path
|
|
self._init_tables()
|
|
|
|
def _get_connection(self) -> sqlite3.Connection:
|
|
"""Get a database connection."""
|
|
conn = sqlite3.connect(str(self._db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def _init_tables(self) -> None:
|
|
"""Initialize database tables."""
|
|
with self._get_connection() as conn:
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS space_references (
|
|
id TEXT PRIMARY KEY,
|
|
source_space_id TEXT NOT NULL,
|
|
target_space_id TEXT NOT NULL,
|
|
reference_type TEXT NOT NULL,
|
|
alias TEXT,
|
|
metadata TEXT,
|
|
created_at TEXT NOT NULL,
|
|
UNIQUE(source_space_id, target_space_id, reference_type)
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_space_ref_source
|
|
ON space_references(source_space_id)
|
|
""")
|
|
conn.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_space_ref_target
|
|
ON space_references(target_space_id)
|
|
""")
|
|
conn.commit()
|
|
|
|
def add_reference(self, reference: SpaceReference) -> SpaceReference:
|
|
"""Add a space reference."""
|
|
with self._get_connection() as conn:
|
|
conn.execute(
|
|
"""
|
|
INSERT OR REPLACE INTO space_references
|
|
(id, source_space_id, target_space_id, reference_type, alias, metadata, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
reference.id,
|
|
reference.source_space_id,
|
|
reference.target_space_id,
|
|
reference.reference_type.value,
|
|
reference.alias,
|
|
json.dumps(reference.metadata),
|
|
reference.created_at.isoformat(),
|
|
),
|
|
)
|
|
conn.commit()
|
|
return reference
|
|
|
|
def get_reference(self, reference_id: str) -> Optional[SpaceReference]:
|
|
"""Get a reference by ID."""
|
|
with self._get_connection() as conn:
|
|
row = conn.execute(
|
|
"SELECT * FROM space_references WHERE id = ?", (reference_id,)
|
|
).fetchone()
|
|
if row:
|
|
return self._row_to_reference(row)
|
|
return None
|
|
|
|
def get_references_from(
|
|
self, source_space_id: str, ref_type: Optional[SpaceReferenceType] = None
|
|
) -> List[SpaceReference]:
|
|
"""Get all references from a source space."""
|
|
with self._get_connection() as conn:
|
|
if ref_type:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT * FROM space_references
|
|
WHERE source_space_id = ? AND reference_type = ?
|
|
ORDER BY created_at
|
|
""",
|
|
(source_space_id, ref_type.value),
|
|
).fetchall()
|
|
else:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT * FROM space_references
|
|
WHERE source_space_id = ?
|
|
ORDER BY created_at
|
|
""",
|
|
(source_space_id,),
|
|
).fetchall()
|
|
return [self._row_to_reference(row) for row in rows]
|
|
|
|
def get_references_to(
|
|
self, target_space_id: str, ref_type: Optional[SpaceReferenceType] = None
|
|
) -> List[SpaceReference]:
|
|
"""Get all references to a target space."""
|
|
with self._get_connection() as conn:
|
|
if ref_type:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT * FROM space_references
|
|
WHERE target_space_id = ? AND reference_type = ?
|
|
ORDER BY created_at
|
|
""",
|
|
(target_space_id, ref_type.value),
|
|
).fetchall()
|
|
else:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT * FROM space_references
|
|
WHERE target_space_id = ?
|
|
ORDER BY created_at
|
|
""",
|
|
(target_space_id,),
|
|
).fetchall()
|
|
return [self._row_to_reference(row) for row in rows]
|
|
|
|
def remove_reference(self, reference_id: str) -> bool:
|
|
"""Remove a reference."""
|
|
with self._get_connection() as conn:
|
|
cursor = conn.execute(
|
|
"DELETE FROM space_references WHERE id = ?", (reference_id,)
|
|
)
|
|
conn.commit()
|
|
return cursor.rowcount > 0
|
|
|
|
def remove_references_from(self, source_space_id: str) -> int:
|
|
"""Remove all references from a source space."""
|
|
with self._get_connection() as conn:
|
|
cursor = conn.execute(
|
|
"DELETE FROM space_references WHERE source_space_id = ?",
|
|
(source_space_id,),
|
|
)
|
|
conn.commit()
|
|
return cursor.rowcount
|
|
|
|
def reference_exists(
|
|
self, source_space_id: str, target_space_id: str, ref_type: SpaceReferenceType
|
|
) -> bool:
|
|
"""Check if a specific reference exists."""
|
|
with self._get_connection() as conn:
|
|
row = conn.execute(
|
|
"""
|
|
SELECT 1 FROM space_references
|
|
WHERE source_space_id = ? AND target_space_id = ? AND reference_type = ?
|
|
""",
|
|
(source_space_id, target_space_id, ref_type.value),
|
|
).fetchone()
|
|
return row is not None
|
|
|
|
def _row_to_reference(self, row: sqlite3.Row) -> SpaceReference:
|
|
"""Convert a database row to a SpaceReference."""
|
|
return SpaceReference(
|
|
id=row["id"],
|
|
source_space_id=row["source_space_id"],
|
|
target_space_id=row["target_space_id"],
|
|
reference_type=SpaceReferenceType(row["reference_type"]),
|
|
alias=row["alias"],
|
|
metadata=json.loads(row["metadata"]) if row["metadata"] else {},
|
|
created_at=datetime.fromisoformat(row["created_at"]),
|
|
)
|
|
|
|
|
|
class SqliteAccessControlRepository(IAccessControlRepository):
|
|
"""SQLite implementation of access control repository."""
|
|
|
|
def __init__(self, db_path: Path):
|
|
"""Initialize the repository.
|
|
|
|
Args:
|
|
db_path: Path to the SQLite database file
|
|
"""
|
|
self._db_path = db_path
|
|
self._init_tables()
|
|
|
|
def _get_connection(self) -> sqlite3.Connection:
|
|
"""Get a database connection."""
|
|
conn = sqlite3.connect(str(self._db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def _init_tables(self) -> None:
|
|
"""Initialize database tables."""
|
|
with self._get_connection() as conn:
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS space_permissions (
|
|
space_id TEXT NOT NULL,
|
|
principal_type TEXT NOT NULL,
|
|
principal_id TEXT NOT NULL,
|
|
access_level TEXT NOT NULL,
|
|
role TEXT,
|
|
granted_by TEXT,
|
|
granted_at TEXT NOT NULL,
|
|
expires_at TEXT,
|
|
PRIMARY KEY(space_id, principal_type, principal_id)
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_perm_principal
|
|
ON space_permissions(principal_type, principal_id)
|
|
""")
|
|
conn.commit()
|
|
|
|
def grant_permission(self, permission: SpacePermission) -> SpacePermission:
|
|
"""Grant a permission."""
|
|
with self._get_connection() as conn:
|
|
conn.execute(
|
|
"""
|
|
INSERT OR REPLACE INTO space_permissions
|
|
(space_id, principal_type, principal_id, access_level, role,
|
|
granted_by, granted_at, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
permission.space_id,
|
|
permission.principal_type,
|
|
permission.principal_id,
|
|
permission.access_level.value,
|
|
permission.role.value if permission.role else None,
|
|
permission.granted_by,
|
|
permission.granted_at.isoformat(),
|
|
permission.expires_at.isoformat() if permission.expires_at else None,
|
|
),
|
|
)
|
|
conn.commit()
|
|
return permission
|
|
|
|
def revoke_permission(
|
|
self, space_id: str, principal_type: str, principal_id: str
|
|
) -> bool:
|
|
"""Revoke a permission."""
|
|
with self._get_connection() as conn:
|
|
cursor = conn.execute(
|
|
"""
|
|
DELETE FROM space_permissions
|
|
WHERE space_id = ? AND principal_type = ? AND principal_id = ?
|
|
""",
|
|
(space_id, principal_type, principal_id),
|
|
)
|
|
conn.commit()
|
|
return cursor.rowcount > 0
|
|
|
|
def get_permissions_for_space(self, space_id: str) -> List[SpacePermission]:
|
|
"""Get all permissions for a space."""
|
|
with self._get_connection() as conn:
|
|
rows = conn.execute(
|
|
"SELECT * FROM space_permissions WHERE space_id = ?", (space_id,)
|
|
).fetchall()
|
|
return [self._row_to_permission(row) for row in rows]
|
|
|
|
def get_permissions_for_principal(
|
|
self, principal_type: str, principal_id: str
|
|
) -> List[SpacePermission]:
|
|
"""Get all permissions for a principal."""
|
|
with self._get_connection() as conn:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT * FROM space_permissions
|
|
WHERE principal_type = ? AND principal_id = ?
|
|
""",
|
|
(principal_type, principal_id),
|
|
).fetchall()
|
|
return [self._row_to_permission(row) for row in rows]
|
|
|
|
def get_permission(
|
|
self, space_id: str, principal_type: str, principal_id: str
|
|
) -> Optional[SpacePermission]:
|
|
"""Get a specific permission."""
|
|
with self._get_connection() as conn:
|
|
row = conn.execute(
|
|
"""
|
|
SELECT * FROM space_permissions
|
|
WHERE space_id = ? AND principal_type = ? AND principal_id = ?
|
|
""",
|
|
(space_id, principal_type, principal_id),
|
|
).fetchone()
|
|
if row:
|
|
return self._row_to_permission(row)
|
|
return None
|
|
|
|
def clear_space_permissions(self, space_id: str) -> int:
|
|
"""Clear all permissions for a space."""
|
|
with self._get_connection() as conn:
|
|
cursor = conn.execute(
|
|
"DELETE FROM space_permissions WHERE space_id = ?", (space_id,)
|
|
)
|
|
conn.commit()
|
|
return cursor.rowcount
|
|
|
|
def _row_to_permission(self, row: sqlite3.Row) -> SpacePermission:
|
|
"""Convert a database row to a SpacePermission."""
|
|
expires_at = row["expires_at"]
|
|
if expires_at:
|
|
expires_at = datetime.fromisoformat(expires_at)
|
|
|
|
role = row["role"]
|
|
if role:
|
|
role = SpaceRole(role)
|
|
|
|
return SpacePermission(
|
|
space_id=row["space_id"],
|
|
principal_type=row["principal_type"],
|
|
principal_id=row["principal_id"],
|
|
access_level=AccessLevel(row["access_level"]),
|
|
role=role,
|
|
granted_by=row["granted_by"],
|
|
granted_at=datetime.fromisoformat(row["granted_at"]),
|
|
expires_at=expires_at,
|
|
)
|