"""Principal — the unified owner-identity model. Every namespaced actor in the system (a User, an Organization, a User Group) is a row in ``principals``. The kind-specific extension lives in its own table: - ``users`` — credentials, role, system flags, cluster/worker FKs - (no extension for ORG / GROUP — every column they need is on ``principals`` itself) Resources (``models``, ``model_routes``, ``clusters``, ...) record their owner via ``owner_principal_id``. Memberships connect principals to principals (a user-principal joining an org-principal, or a user- principal joining a group-principal). ACLs reference principals directly. """ from datetime import datetime from enum import Enum from typing import ClassVar, List, Optional from sqlalchemy import Enum as SQLEnum from sqlmodel import ( Column, Field, ForeignKey, Integer, SQLModel, UniqueConstraint, ) from sqlalchemy import Text from gpustack.mixins import BaseModelMixin from gpustack.schemas.common import ListParams, PaginatedList # Canonical slug of the built-in platform Org-principal. Created by the # multi-tenancy foundation migration; system / infrastructure resources # default to it. # # ``PLATFORM_PRINCIPAL_ID`` is the id we *seed* it with in a fresh DB. # It happens to be ``1`` today, but anywhere we don't have to bake the # integer into the SQL — primarily migrations and any future bootstrap # code that runs against a populated DB — we look it up by slug instead. # That's the form that survives any future renumbering (e.g. when # ``users`` and ``principals`` get unified and USER-kind principals # inherit ``users.id``, the platform Org will get a new id above # ``max(users.id)``). PLATFORM_PRINCIPAL_SLUG = 'default' PLATFORM_PRINCIPAL_ID = 1 class PrincipalType(str, Enum): """Discriminator for the kind of principal a row represents. Kept named ``PrincipalType`` (rather than ``PrincipalKind``) for continuity with the cluster-access / model-route ACL APIs that already accepted this enum on the wire. """ USER = "user" ORG = "org" GROUP = "group" class OrgRole(str, Enum): # Two-tier Org membership model: ADMIN can manage the Org's infra # (resources, members, settings); USER is a plain consumer. The # platform-wide superuser lives on `users.is_admin` and is distinct # from `OrgRole.ADMIN` — always disambiguate with `is_platform_admin` # vs `org_role == OrgRole.ADMIN` in code. ADMIN = "admin" USER = "user" class PrincipalBase(SQLModel): kind: PrincipalType = Field( sa_column=Column(SQLEnum(PrincipalType), nullable=False), ) # User-facing identifier used in URLs and ``effective_route_name``. # Globally unique among non-NULL values. Auto-set to ``user-{user.id}`` # for USER principals; user-supplied for ORG; NULL for GROUP (groups # never appear in URL prefixes). slug: Optional[str] = Field(default=None, nullable=True) name: str = Field(nullable=False) description: Optional[str] = Field( default=None, sa_column=Column(Text, nullable=True) ) # Structural parent. NULL for USER and ORG; for GROUP, points at the # owning ORG-principal so the group lives inside it. parent_principal_id: Optional[int] = Field( default=None, sa_column=Column( Integer, ForeignKey("principals.id", ondelete="CASCADE"), nullable=True, ), ) class Principal(PrincipalBase, BaseModelMixin, table=True): __tablename__ = 'principals' __table_args__ = ( UniqueConstraint('slug', name='uix_principals_slug'), # Group names must be unique within their parent org. NULL parent # (USER / ORG) doesn't participate — UNIQUE treats NULL as # distinct, so users and orgs can share names freely. UniqueConstraint( 'parent_principal_id', 'name', name='uix_principals_parent_name' ), ) id: Optional[int] = Field(default=None, primary_key=True) class PrincipalListParams(ListParams): kind: Optional[PrincipalType] = None parent_principal_id: Optional[int] = None sortable_fields: ClassVar[List[str]] = [ "name", "slug", "kind", "created_at", "updated_at", ] class PrincipalPublic(SQLModel): id: int kind: PrincipalType slug: Optional[str] = None name: str description: Optional[str] = None parent_principal_id: Optional[int] = None created_at: datetime updated_at: datetime PrincipalsPublic = PaginatedList[PrincipalPublic] class PrincipalMembershipBase(SQLModel): parent_principal_id: int = Field( sa_column=Column( Integer, ForeignKey("principals.id", ondelete="CASCADE"), nullable=False, ), ) member_principal_id: int = Field( sa_column=Column( Integer, ForeignKey("principals.id", ondelete="CASCADE"), nullable=False, ), ) # Only meaningful when parent is an ORG; NULL for GROUP memberships # (groups don't have role tiers — you're either in or out). role: Optional[OrgRole] = Field( default=None, sa_column=Column(SQLEnum(OrgRole), nullable=True), ) class PrincipalMembership(PrincipalMembershipBase, BaseModelMixin, table=True): """Membership of one principal inside another. Surrogate ``id`` PK so soft-delete + re-add doesn't collide on a composite key. At any point in time at most one row per ``(parent_principal_id, member_principal_id)`` may have ``deleted_at IS NULL``; that invariant is enforced in the route handlers (the add path looks up an active row first and re-uses any soft-deleted row by clearing ``deleted_at`` and updating ``role``). """ __tablename__ = 'principal_memberships' id: Optional[int] = Field(default=None, primary_key=True)