-
Notifications
You must be signed in to change notification settings - Fork 1
feat(api.iam): create workspace domain objects #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…lembic - Add SQLAlchemy 2.0 with asyncpg for async database operations - Add Alembic for schema migrations - Add python-ulid for ULID support instead of UUID - Create read/write engine separation with connection pooling - Create FastAPI dependency injection for database sessions - Create SQLAlchemy declarative base with timestamp mixin - Initialize Alembic with async migration support - Create initial migration for teams table (ULID primary key) - Add comprehensive unit tests for engines and dependencies - Configure Alembic to use settings module for database URL - Enable ruff post-write hook for migration formatting Refs: AIHCM-121
- Add authzed library for SpiceDB integration - Add python-ulid for ULID support - Create ResourceType, RelationType, Permission enums (using Group not Team) - Create AuthorizationProvider protocol for swappable implementations - Implement SpiceDBClient with async methods for relationships and permissions - Create SpiceDB schema (.zed) with Tenant→Workspace→Group hierarchy - Create AuthorizationProbe for domain-oriented observability - Move ObservationContext to shared_kernel (fix architectural boundary) - Add 35 unit tests for types and probes - All 410 tests passing Refs: AIHCM-122
Resolved conflicts in authorization files by accepting remote changes: - shared_kernel/authorization/types.py (docstring fix) - shared_kernel/authorization/spicedb/client.py (_parse_reference helper)
Implements AIHCM-142 - Workspace Domain Model & Value Objects - Add Workspace aggregate with DDD patterns following existing Tenant/Group patterns - Add WorkspaceCreated and WorkspaceDeleted domain events - Add WorkspaceId value object (already existed in value_objects.py) - Implement business rules: - Workspace names must be 1-255 characters - Root workspace cannot have a parent_workspace_id - Cannot have is_root=True with parent_workspace_id set - Add factory methods: create() for child workspaces, create_root() for root workspaces - Add mark_for_deletion() method with domain event emission - Add SpiceDB event handlers for workspace events in translator - Add comprehensive TDD tests (19 test cases) This is domain layer only - no infrastructure, repository, service, or API layers. Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
- Change workspace name max length from 255 to 512 characters - Make parent_workspace_id required in Workspace.create() since it is only used for child workspaces (use create_root() for root workspaces) - Split monolithic events.py into domain/events/ package with separate modules per aggregate (tenant, group, workspace, api_key) while maintaining full backward compatibility via __init__.py re-exports Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
WalkthroughThis PR adds a Workspace aggregate (factory methods, validation, deletion signaling, event collection) and exports it from iam.domain.aggregates. It restructures IAM domain events into a new events package with per-entity modules (tenant, group, workspace, api_key) and a central events.init providing a DomainEvent union. The outbox translator and worker are extended to translate and apply WorkspaceCreated/WorkspaceDeleted and a new DeleteRelationshipsByFilter operation. The SpiceDB client, authorization protocols, and observability probes gained filter-based deletion support. Unit tests for the workspace aggregate, translator, worker, and SpiceDB client validations were added/updated. Sequence Diagram(s)sequenceDiagram
participant Client
participant WorkspaceAggregate as "Workspace Aggregate"
participant DomainEvents as "Domain Events"
participant Translator as "IAMEventTranslator"
participant OutboxWorker as "Outbox Worker"
participant SpiceDB as "SpiceDB / AuthZ"
Client->>WorkspaceAggregate: create(name, tenant_id[, parent_id])
activate WorkspaceAggregate
WorkspaceAggregate->>WorkspaceAggregate: validate, gen id, timestamps
WorkspaceAggregate->>DomainEvents: append WorkspaceCreated
WorkspaceAggregate-->>Client: return workspace
deactivate WorkspaceAggregate
Client->>WorkspaceAggregate: mark_for_deletion()
WorkspaceAggregate->>DomainEvents: append WorkspaceDeleted
Note over DomainEvents,Translator: outbox translation pipeline
DomainEvents->>Translator: translate(event)
Translator->>OutboxWorker: emit SpiceDB operations (WriteRelationship / DeleteRelationshipsByFilter / DeleteRelationship)
OutboxWorker->>SpiceDB: apply operations (write/delete or delete_by_filter)
SpiceDB-->>OutboxWorker: ack / error
OutboxWorker-->>Translator: observability callbacks (success/failure)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
…-hyperfleet/kartograph into jsell/feat/workspace-aggregate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@src/api/iam/domain/aggregates/workspace.py`:
- Around line 91-112: The WorkspaceCreated event uses a new datetime instead of
the captured now, causing timestamp drift; update the aggregate factory in
Workspace.create (or the classmethod constructing workspace) to pass the
previously captured now variable as occurred_at when appending WorkspaceCreated
(use occurred_at=now) and remove the unnecessary conditional that checks
parent_workspace_id.value since parent_workspace_id is required—directly use
parent_workspace_id.value for the event payload.
- Around line 136-155: The Workspace.create_root flow creates a now timestamp
but then uses datetime.now(UTC) again for the WorkspaceCreated event; change the
event to reuse the previously computed now (the local variable now) so the
aggregate's created_at/updated_at and the WorkspaceCreated.occurred_at are
identical — update the call that appends WorkspaceCreated (referencing
WorkspaceCreated and workspace._pending_events.append in the create_root/cls
constructor flow) to pass occurred_at=now instead of datetime.now(UTC).
In `@src/api/iam/domain/events/tenant.py`:
- Around line 72-86: Fix the small typo in the TenantMemberRemoved docstring:
update the occurred_at attribute description in the class TenantMemberRemoved
(docstring text "When this even occurred (UTC)") to read "When this event
occurred (UTC)". Ensure the rest of the docstring formatting and attribute order
(tenant_id, user_id, removed_by, occurred_at) remains unchanged.
- Around line 53-70: Fix the typo in the docstring of the TenantMemberAdded
dataclass: update the "occurred_at" description from "When this even occurred
(UTC)" to "When this event occurred (UTC)" within the TenantMemberAdded class
documentation so the docstring correctly describes the occurred_at field.
In `@src/api/iam/infrastructure/outbox/translator.py`:
- Around line 326-362: Update the workspace translators to use
RelationType.PARENT (not RelationType.TENANT) and emit/delete the correct
relationships in _translate_workspace_created and _translate_workspace_deleted:
when creating, add a parent relation from workspace to tenant
(workspace:<id>#parent@tenant:<tenant_id>) if no parent_workspace_id, or to a
parent workspace (workspace:<id>#parent@workspace:<parent_id>) when
payload["parent_workspace_id"] is present; additionally, if payload["is_root"]
is True add a WriteRelationship for tenant root_workspace
(tenant:<tenant_id>#root_workspace@workspace:<id>); mirror these three deletes
in _translate_workspace_deleted. Locate and update the methods
_translate_workspace_created and _translate_workspace_deleted and replace
RelationType.TENANT with RelationType.PARENT where appropriate and add the extra
WriteRelationship/DeleteRelationship entries for parent_workspace_id and is_root
handling.
🧹 Nitpick comments (1)
src/api/iam/domain/aggregates/workspace.py (1)
52-55: Minor redundancy in name validation.
not namealready catches empty strings (and None if somehow passed), makinglen(name) < 1redundant.♻️ Suggested simplification
def _validate_name(self, name: str) -> None: """Validate workspace name length.""" - if not name or len(name) < 1 or len(name) > 512: + if not name or len(name) > 512: raise ValueError("Workspace name must be between 1 and 512 characters")
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
… snapshot pattern Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Extend the SpiceDB client and outbox pattern with filter-based relationship deletion using SpiceDB's DeleteRelationships API. This enables bulk deletion of relationships matching a filter without knowing specific subject IDs. Used to clean up tenant#root_workspace relationships during tenant deletion, replacing the previous workaround that relied on application-layer cascading. Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/api/shared_kernel/authorization/spicedb/client.py`:
- Around line 15-25: Add an explicit input validation before constructing a
SubjectFilter: if subject_id (optional_subject_id) is provided but subject_type
is missing, raise a clear error (e.g., a SpiceDBPermissionError or ValueError)
rather than attempting to build SubjectFilter and letting the authzed client
throw; update the logic around SubjectFilter creation in client.py (where
SubjectFilter is instantiated) to check "if subject_id and not subject_type" and
raise the validation error with a descriptive message so invalid inputs are
caught early.
🧹 Nitpick comments (2)
src/api/iam/domain/aggregates/workspace.py (2)
52-55: Minor: Redundant validation condition.The check
not name or len(name) < 1is redundant—not nameis true whennameis empty (""), and an empty string haslen(name) == 0, which is already< 1. Simplify tolen(name) < 1 or len(name) > 512.♻️ Proposed simplification
def _validate_name(self, name: str) -> None: """Validate workspace name length.""" - if not name or len(name) < 1 or len(name) > 512: + if len(name) < 1 or len(name) > 512: raise ValueError("Workspace name must be between 1 and 512 characters")
156-175: Consider capturing timestamp for consistency with factory methods.Unlike
create()andcreate_root(),mark_for_deletion()callsdatetime.now(UTC)directly in the event construction. While this doesn't cause functional issues (no entity timestamp updates here), capturingnowfirst would maintain a consistent pattern across all methods and simplify future changes ifupdated_attracking is added for deletions.♻️ Optional consistency improvement
def mark_for_deletion(self) -> None: """Mark the workspace for deletion and record the WorkspaceDeleted event. ... """ + now = datetime.now(UTC) self._pending_events.append( WorkspaceDeleted( workspace_id=self.id.value, tenant_id=self.tenant_id.value, parent_workspace_id=self.parent_workspace_id.value if self.parent_workspace_id else None, is_root=self.is_root, - occurred_at=datetime.now(UTC), + occurred_at=now, ) )
… filter deletion Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Summary by CodeRabbit
New Features
Infrastructure
Tests