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>
This commit is contained in:
424
markitect/spaces/composability/repository.py
Normal file
424
markitect/spaces/composability/repository.py
Normal file
@@ -0,0 +1,424 @@
|
||||
"""
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user