| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- """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)
|