Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
30f9a79
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 15, 2025
473d787
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 16, 2025
b89fbe2
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 16, 2025
89390ba
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 17, 2025
0c80267
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 17, 2025
7575d01
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 18, 2025
ee602f4
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 19, 2025
00f0669
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 19, 2025
0adfd4c
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 19, 2025
c2021cd
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 22, 2025
8dafb3b
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 22, 2025
51b3fe7
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 23, 2025
bb3f77a
feat(infrastructure): add SQLAlchemy async database foundation with A…
jsell-rh Dec 23, 2025
3f348b4
Merge AIHCM-121: Database foundation with SQLAlchemy and Alembic
jsell-rh Dec 23, 2025
0feb2c0
feat(shared-kernel): add authorization abstractions and SpiceDB client
jsell-rh Dec 23, 2025
9fac78f
Merge AIHCM-122: Authorization abstractions and SpiceDB client
jsell-rh Dec 23, 2025
f8fc154
Merge remote-tracking branch 'origin/main' into main
jsell-rh Dec 23, 2025
22d202e
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 23, 2025
be9570b
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 23, 2025
0b759a1
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Dec 23, 2025
8762ee8
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 5, 2026
a74c79e
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 5, 2026
86f80c8
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 6, 2026
669ba32
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 6, 2026
d1005ee
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 8, 2026
1ad4576
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 14, 2026
6e5e818
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 14, 2026
b356c55
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 16, 2026
5070998
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 16, 2026
7345f9b
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 16, 2026
6f846a0
ci: automerge mintmaker non-major upgrades if tests pass
jsell-rh Jan 19, 2026
b2756bf
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 19, 2026
b40fde0
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 19, 2026
292e933
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 19, 2026
edc18cd
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 19, 2026
1b9341b
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 19, 2026
20aaae6
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 20, 2026
56f66f5
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 21, 2026
bf1945d
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 26, 2026
ae117f0
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 26, 2026
9b64754
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 27, 2026
a3db3c1
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 27, 2026
ae5330b
fix(deploy): set postgres uid/gid to 001379999
jsell-rh Jan 27, 2026
877e3e1
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 27, 2026
c833314
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 27, 2026
2d183e5
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 27, 2026
872df8f
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 27, 2026
b907f41
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 27, 2026
97c8675
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 27, 2026
7e483f8
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 27, 2026
348e824
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 29, 2026
66ba551
refactor(api.iam): rename Role to GroupRole
jsell-rh Jan 29, 2026
3399577
refactor(api.iam): add TenantMember value object
jsell-rh Jan 29, 2026
77c65c2
refactor(api.iam): add TenantMemberAdded event and method
jsell-rh Jan 29, 2026
768d479
refactor(api.iam): add TenantMemberRemoved event and method
jsell-rh Jan 29, 2026
340f6c6
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Jan 30, 2026
f860195
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Feb 2, 2026
5e4d2d5
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Feb 5, 2026
172dd5c
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Feb 6, 2026
10543b0
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Feb 6, 2026
2853a15
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Feb 6, 2026
d677969
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Feb 6, 2026
3fa8055
Merge branch 'main' of github.com:openshift-hyperfleet/kartograph
jsell-rh Feb 6, 2026
a6a4193
feat(iam.infrastructure): add workspace repository and fix cascade co…
jsell-rh Feb 6, 2026
4e51f64
refactor(iam.infrastructure): split models.py into modular structure
jsell-rh Feb 6, 2026
f99bc35
Merge branch 'main' into jsell/feat/AIHCM-143-workspace-repo-infra
jsell-rh Feb 6, 2026
fa4c2be
fix(iam.infrastructure): fix workspace timestamp inheritance and prob…
jsell-rh Feb 6, 2026
8391435
Merge branch 'jsell/feat/AIHCM-143-workspace-repo-infra' of github.co…
jsell-rh Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/api/iam/dependencies/workspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""FastAPI dependency injection for workspace repository.

Provides workspace repository instances for route handlers
using FastAPI's dependency injection system.
"""

from typing import Annotated

from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession

from iam.dependencies.outbox import get_outbox_repository
from iam.infrastructure.workspace_repository import WorkspaceRepository
from infrastructure.database.dependencies import get_write_session
from infrastructure.outbox.repository import OutboxRepository


def get_workspace_repository(
session: Annotated[AsyncSession, Depends(get_write_session)],
outbox: Annotated[OutboxRepository, Depends(get_outbox_repository)],
) -> WorkspaceRepository:
"""Get WorkspaceRepository instance.

Args:
session: Async database session
outbox: Outbox repository for transactional outbox pattern

Returns:
WorkspaceRepository instance with outbox pattern enabled
"""
return WorkspaceRepository(session=session, outbox=outbox)
143 changes: 0 additions & 143 deletions src/api/iam/infrastructure/models.py

This file was deleted.

19 changes: 19 additions & 0 deletions src/api/iam/infrastructure/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""SQLAlchemy ORM models for IAM bounded context.

These models map to database tables and are used by repository implementations.
They store only metadata - authorization data (membership, roles) is stored in SpiceDB.
"""

from iam.infrastructure.models.api_key import APIKeyModel
from iam.infrastructure.models.group import GroupModel
from iam.infrastructure.models.tenant import TenantModel
from iam.infrastructure.models.user import UserModel
from iam.infrastructure.models.workspace import WorkspaceModel

__all__ = [
"APIKeyModel",
"GroupModel",
"TenantModel",
"UserModel",
"WorkspaceModel",
]
72 changes: 72 additions & 0 deletions src/api/iam/infrastructure/models/api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""SQLAlchemy ORM model for the api_keys table.

Stores API key metadata in PostgreSQL. The key_hash is the only
sensitive data stored - the plaintext secret is never persisted.
"""

from datetime import datetime

from sqlalchemy import Boolean, DateTime, ForeignKey, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column

from infrastructure.database.models import Base, TimestampMixin


class APIKeyModel(Base, TimestampMixin):
"""ORM model for api_keys table.

Stores API key metadata in PostgreSQL. The key_hash is the only
sensitive data stored - the plaintext secret is never persisted.

Notes:
- created_by_user_id is VARCHAR(255) to match users.id (external SSO IDs)
This is for audit trail only - authorization is handled by SpiceDB.
- tenant_id is VARCHAR(26) for ULID format
- key_hash is unique for authentication lookup
- prefix allows key identification without exposing the full key
- Per-user key names are unique within a tenant

Foreign Key Constraint:
- tenant_id references tenants.id with CASCADE delete
- When a tenant is deleted, all API keys are cascade deleted
- API key revocation must be handled in service layer to emit events
"""

__tablename__ = "api_keys"

id: Mapped[str] = mapped_column(String(26), primary_key=True)
created_by_user_id: Mapped[str] = mapped_column(
String(255), nullable=False, index=True
)
tenant_id: Mapped[str] = mapped_column(
String(26),
ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
key_hash: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
prefix: Mapped[str] = mapped_column(String(12), nullable=False, index=True)
expires_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False
)
last_used_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
is_revoked: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)

__table_args__ = (
UniqueConstraint(
"tenant_id",
"created_by_user_id",
"name",
name="uq_api_keys_tenant_user_name",
),
)

def __repr__(self) -> str:
"""Return string representation."""
return (
f"<APIKeyModel(id={self.id}, created_by_user_id={self.created_by_user_id}, "
f"name={self.name}, prefix={self.prefix})>"
)
43 changes: 43 additions & 0 deletions src/api/iam/infrastructure/models/group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""SQLAlchemy ORM model for the groups table.

Stores group metadata in PostgreSQL. Membership relationships are
managed through SpiceDB, not as database columns.
"""

from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column

from infrastructure.database.models import Base, TimestampMixin


class GroupModel(Base, TimestampMixin):
"""ORM model for groups table (metadata only).

Stores group metadata in PostgreSQL. Membership relationships are
managed through SpiceDB, not as database columns.

Note: Group names are NOT globally unique - per-tenant uniqueness
is enforced at the application level.

Foreign Key Constraint:
- tenant_id references tenants.id with RESTRICT delete
- Application layer must explicitly delete groups before tenant deletion
- This ensures GroupDeleted domain events are emitted for SpiceDB cleanup
"""

__tablename__ = "groups"

id: Mapped[str] = mapped_column(String(26), primary_key=True)
tenant_id: Mapped[str] = mapped_column(
String(26),
ForeignKey("tenants.id", ondelete="RESTRICT"),
nullable=False,
index=True,
)
name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)

def __repr__(self) -> str:
"""Return string representation."""
return (
f"<GroupModel(id={self.id}, tenant_id={self.tenant_id}, name={self.name})>"
)
32 changes: 32 additions & 0 deletions src/api/iam/infrastructure/models/tenant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""SQLAlchemy ORM model for the tenants table.

Stores tenant metadata in PostgreSQL. Tenants represent organizations
and are the top-level isolation boundary in the system.
"""

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from infrastructure.database.models import Base, TimestampMixin


class TenantModel(Base, TimestampMixin):
"""ORM model for tenants table.

Stores tenant metadata in PostgreSQL. Tenants represent organizations
and are the top-level isolation boundary in the system.

Note: Tenant names are globally unique across the entire system.
"""

__tablename__ = "tenants"

id: Mapped[str] = mapped_column(String(26), primary_key=True)
name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)

# Relationships
workspaces = relationship("WorkspaceModel", back_populates="tenant")

def __repr__(self) -> str:
"""Return string representation."""
return f"<TenantModel(id={self.id}, name={self.name})>"
29 changes: 29 additions & 0 deletions src/api/iam/infrastructure/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""SQLAlchemy ORM model for the users table.

Stores user metadata in PostgreSQL. Users are provisioned from SSO
and this table only stores minimal metadata for lookup and reference.
"""

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column

from infrastructure.database.models import Base, TimestampMixin


class UserModel(Base, TimestampMixin):
"""ORM model for users table (metadata only).

Stores user metadata in PostgreSQL. Users are provisioned from SSO
and this table only stores minimal metadata for lookup and reference.

Note: id is VARCHAR(255) to accommodate external SSO IDs (UUIDs, Auth0, etc.)
"""

__tablename__ = "users"

id: Mapped[str] = mapped_column(String(255), primary_key=True)
username: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)

def __repr__(self) -> str:
"""Return string representation."""
return f"<UserModel(id={self.id}, username={self.username})>"
Loading