Skip to content

Built-in Admin Models — Hybrid Approach (Schema-First + Protocol) #32

Description

@borhanst

Parent

#16 — Database Backend Abstraction for Multi-ORM Support

What to build

Define built-in admin models (User, Role, Permission, AuditLog, LoginAttempt) using a hybrid approach: schema-first for built-in models + protocol for custom user models.

The Three Layers

1. Protocol Layer — Contract Definition

Define AdminUserProtocol in fastapi_admin_kit/auth/protocol.py — the contract that any user model must satisfy to work with admin RBAC:

@runtime_checkable
class AdminUserProtocol(Protocol):
    id: Any
    email: str
    hashed_password: str
    is_active: bool
    is_superuser: bool
    
    def verify_password(self, password: str) -> bool: ...
    def hash_password(self, password: str) -> None: ...
    
    @property
    def roles(self) -> list: ...  # Must return role objects with .permissions

Similarly define AdminRoleProtocol and AdminPermissionProtocol.

2. Schema-First Layer — Built-in Model Definitions

Define schemas in fastapi_admin_kit/schemas/builtin.py:

USER_SCHEMA = Schema(
    table_name="admin_users",
    fields=[
        Field("id", type="integer", primary_key=True, auto_increment=True),
        Field("email", type="string", max_length=255, unique=True, nullable=False),
        Field("hashed_password", type="string", max_length=255),
        Field("is_active", type="boolean", default=True),
        Field("is_superuser", type="boolean", default=False),
        Field("last_login", type="datetime", nullable=True),
        Field("created_at", type="datetime", server_default="now()"),
    ],
    relations=[
        Relation("roles", target="admin_roles", type="many_to_many",
                 through="admin_user_roles"),
    ],
)

Same for ROLE_SCHEMA, PERMISSION_SCHEMA, AUDIT_LOG_SCHEMA, LOGIN_ATTEMPT_SCHEMA.

3. Materialization Layer — Backend Converts Schemas to Native Models

Each backend implements materialize(schema):

# SQLAlchemy backend
class SQLAlchemyBackend:
    def materialize(self, schema: Schema) -> type:
        """Convert schema → SQLAlchemy model class."""
        columns = []
        for f in schema.fields:
            columns.append(Column(f.name, self._map_type(f.type), ...))
        return type(schema.table_name, (Base,), {"__tablename__": schema.table_name, ...})

# Beanie backend
class MongoBackend:
    def materialize(self, schema: Schema) -> type:
        """Convert schema → Beanie Document class."""
        fields = {f.name: self._map_type(f.type) for f in schema.fields}
        return type(schema.table_name, (Document,), {**fields, ...})

# Django backend
class DjangoBackend:
    def materialize(self, schema: Schema) -> type:
        """Convert schema → Django model class."""
        fields_dict = {f.name: self._map_type(f.type) for f in schema.fields}
        return type(schema.table_name, (models.Model,), {**fields_dict, ...})

How It Connects

┌──────────────────────────────────────────────────┐
│  Protocols (AdminUserProtocol, etc.)             │
│  Contract: what fields/methods are required      │
└──────────────────────┬───────────────────────────┘
                       │
          ┌────────────┼────────────┐
          ▼            ▼            ▼
   ┌────────────┐ ┌────────────┐ ┌────────────┐
   │ Schema-    │ │ User's     │ │ User's     │
   │ First      │ │ Custom     │ │ Custom     │
   │ Built-in   │ │ SQLAlchemy │ │ Beanie     │
   │ Models     │ │ Model      │ │ Document   │
   └─────┬──────┘ └─────┬──────┘ └─────┬──────┘
         │              │              │
         ▼              ▼              ▼
   ┌─────────────────────────────────────────────┐
   │  Backend.materialize(schema)                │
   │  + Validates user model satisfies protocol  │
   └──────────────────────┬──────────────────────┘
                          ▼
                  ┌───────────────┐
                  │  RBAC System  │
                  │  Auth System  │
                  │  Audit System │
                  └───────────────┘

User Configures

# Option 1: Use built-in models (default)
admin = Admin(app=app, backend=SQLAlchemyBackend(engine=engine))

# Option 2: Custom user model (SQLAlchemy)
class MyUser(Base):
    __tablename__ = "my_users"
    id = Column(Integer, primary_key=True)
    email = Column(String(255), unique=True)
    hashed_password = Column(String(255))
    is_active = Column(Boolean, default=True)
    is_superuser = Column(Boolean, default=False)
    phone = Column(String(20))  # custom field
    roles = relationship("Role", secondary=my_user_roles, ...)

admin = Admin(app=app, backend=SQLAlchemyBackend(engine=engine), auth_model=MyUser)

# Option 3: Custom user model (Beanie/MongoDB)
class MyUser(Document):
    email: str
    hashed_password: str
    is_active: bool = True
    is_superuser: bool = False
    phone: str | None = None
    roles: list[Link[Role]] = []

admin = Admin(app=app, backend=MongoBackend(...), auth_model=MyUser)

Files to Create/Modify

New Files

  • fastapi_admin_kit/schemas/__init__.py — Schema/Field/Relation dataclasses
  • fastapi_admin_kit/schemas/builtin.py — Built-in model schemas
  • fastapi_admin_kit/schemas/types.py — Field type mappings
  • fastapi_admin_kit/auth/protocol.py — AdminUserProtocol, AdminRoleProtocol, AdminPermissionProtocol

Modified Files

  • fastapi_admin_kit/auth/models.py — Becomes generated from schema OR thin wrapper
  • fastapi_admin_kit/audit/models.py — Same
  • fastapi_admin_kit/backends/sqlalchemy.py — Add materialize() method
  • fastapi_admin_kit/admin/core.py — Accept auth_model param, validate protocol compliance

Acceptance Criteria

  • AdminUserProtocol defined with required fields and methods
  • AdminRoleProtocol and AdminPermissionProtocol defined
  • Schema, Field, Relation dataclasses in schemas/
  • Built-in model schemas in schemas/builtin.py
  • SqlAlchemyBackend.materialize() converts schema → SQLAlchemy model
  • Admin accepts auth_model parameter
  • Admin validates auth_model satisfies AdminUserProtocol
  • Default behavior unchanged — built-in models work without config
  • Custom user model works with RBAC when passed as auth_model
  • All existing auth/audit tests pass
  • New tests for protocol validation and schema materialization

Blocked by

#23 (Introspection Adapter) — needs introspection protocol to understand how models are described

Metadata

Metadata

Assignees

Labels

architectureArchitecture and design decisions

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions