from enum import Enum from datetime import datetime from typing import ClassVar, Optional, List, TYPE_CHECKING from sqlalchemy import Column, ForeignKey, Integer, UniqueConstraint from sqlmodel import Field, SQLModel, Text, JSON, Relationship from gpustack.mixins import BaseModelMixin from gpustack.schemas.common import ListParams, PaginatedList, UTCDateTime from gpustack.schemas.principals import PLATFORM_PRINCIPAL_ID if TYPE_CHECKING: from gpustack.schemas.users import User class PermissionScope(str, Enum): """ Permission scope for API key access control. Currently supports coarse-grained scopes. Future extensions may include: - management.readonly: Read-only API access (GET requests only) - management.write: Full API write access - inference.chat: Chat completion endpoints only - inference.embeddings: Embeddings endpoints only - inference.completions: Completions endpoints only """ ALL = "*" MANAGEMENT = "management" INFERENCE = "inference" class ApiKeyUpdate(SQLModel): allowed_model_names: Optional[List[str]] = Field( default=None, sa_column=Column(JSON, nullable=True), ) description: Optional[str] = Field( default=None, sa_column=Column(Text, nullable=True) ) scope: List[PermissionScope] = Field( default=[PermissionScope.ALL], sa_column=Column(JSON, nullable=False), ) class ApiKeyBase(ApiKeyUpdate): name: str class ApiKey(ApiKeyBase, BaseModelMixin, table=True): __tablename__ = 'api_keys' __table_args__ = ( UniqueConstraint( 'user_id', 'owner_principal_id', 'name', name='uix_user_org_name' ), ) id: Optional[int] = Field(default=None, primary_key=True) access_key: str = Field(unique=True, index=True) hashed_secret_key: str = Field(unique=True) user_id: int = Field(foreign_key='users.id', nullable=False) owner_principal_id: int = Field( default=PLATFORM_PRINCIPAL_ID, sa_column=Column( Integer, ForeignKey("principals.id", ondelete="CASCADE"), nullable=False, ), ) expires_at: Optional[datetime] = Field(sa_column=Column(UTCDateTime), default=None) user: Optional["User"] = Relationship( back_populates="api_keys", sa_relationship_kwargs={"lazy": "noload"}, ) is_custom: bool = Field(default=False, nullable=False) @property def user_name(self) -> Optional[str]: return self.user.username if self.user else None class ApiKeyListParams(ListParams): sortable_fields: ClassVar[List[str]] = [ "name", "expires_at", "created_at", "updated_at", ] class ApiKeyCreate(ApiKeyBase): expires_in: Optional[int] = None custom: Optional[str] = None class ApiKeyPublic(ApiKeyBase): id: int user_name: Optional[str] = None value: Optional[str] = None # only available when creating masked_value: Optional[str] = None # partial characters for identification is_custom: bool created_at: datetime updated_at: datetime expires_at: Optional[datetime] = None ApiKeysPublic = PaginatedList[ApiKeyPublic]