Files
ZY-Agent/backend/packages/harness/deerflow/persistence/api_key/model.py
T
1445043649 52e9999a61 feat(persistence): PR8 T8.4 — ApiKeyRow ORM + 3 tests
Adds deerflow/persistence/api_key/{__init__,model}.py with ApiKeyRow:
  - id: UUID36 PK
  - service_account_id: FK service_accounts ON DELETE CASCADE
  - key_prefix: String(16), UNIQUE — global uniqueness preserved even
    after revoke so audit logs never reference an ambiguous prefix
  - key_hash: String(128) sha-256 hex (plaintext returned only once at
    create time)
  - name / scopes / rate_limit_rpm / expires_at / last_used_at /
    revoked_at — all nullable or default-providing
  - created_at: UTC
  - Index idx_api_keys_sa (service_account_id) — list keys for an SA
  - Index idx_api_keys_active (key_prefix) WHERE revoked_at IS NULL —
    partial index, dual-dialect via sqlite_where + postgresql_where,
    shrinks the hot-path lookup index by excluding revoked keys

T8.4 tests:
  - test_unique_key_prefix_enforced: column-level UNIQUE blocks two
    rows from sharing key_prefix (active or revoked alike)
  - test_active_index_declares_both_dialect_where_clauses: schema
    introspection confirms idx_api_keys_active has both
    `dialect_options.sqlite.where` and `dialect_options.postgresql.where`
    set to `revoked_at IS NULL` — guards against accidental loss of the
    dual-driver hint when the Index is edited later
  - test_cascade_on_service_account_delete: deleting the parent SA
    removes all api_keys rows

Registered in deerflow/persistence/models/__init__.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:10:32 +08:00

95 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""ORM model for API keys (credentials owned by a service account)."""
from __future__ import annotations
from datetime import UTC, datetime
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, text
from sqlalchemy.orm import Mapped, mapped_column
from deerflow.persistence.base import Base
class ApiKeyRow(Base):
__tablename__ = "api_keys"
id: Mapped[str] = mapped_column(
String(36),
primary_key=True,
comment="API key 主键,UUID 字符串(36 字符)",
)
service_account_id: Mapped[str] = mapped_column(
String(36),
ForeignKey("service_accounts.id", ondelete="CASCADE"),
nullable=False,
comment="所属 service_accountservice_account 删除时级联清掉所有 api_key",
)
key_prefix: Mapped[str] = mapped_column(
String(16),
nullable=False,
unique=True,
comment="公开 prefix(如 'dfk_live_abc12345'),可在审计日志 / UI 中打印;全局唯一,撤销后亦不复用以避免审计混淆",
)
key_hash: Mapped[str] = mapped_column(
String(128),
nullable=False,
comment="完整 token 的 sha-256 hex64 字符;预留 128 以兼容未来更长哈希),plaintext token 仅在创建时返回给调用方",
)
name: Mapped[str] = mapped_column(
String(64),
nullable=False,
comment="key 的人类可读标签(如 'ci pipeline' / 'frontend prod'),同 service_account 内不强制唯一",
)
scopes: Mapped[str] = mapped_column(
String(1024),
nullable=False,
default="",
comment="scope 列表,逗号分隔字符串(如 'threads:read,threads:write');用 String 而非 PG text[] 以保 SQLite dev 兼容,Stage 2 切纯 PG 后可平滑迁",
)
rate_limit_rpm: Mapped[int | None] = mapped_column(
Integer,
nullable=True,
comment="每分钟请求数限制;NULL 表示走该 service_account 的默认限速(Stage 1 起生效)",
)
expires_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
comment="过期时间(UTC);NULL = 不过期",
)
last_used_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
comment="最近一次成功鉴权时间(UTC);Stage 1 鉴权中间件每次更新",
)
revoked_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
comment="撤销时间(UTC);NULL = 仍然有效。被撤销的 key 不删行(保留审计),但鉴权层据此拒绝",
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(UTC),
comment="创建时间(UTC",
)
__table_args__ = (
Index("idx_api_keys_sa", "service_account_id"),
# 部分索引:只索引活跃 keyrevoked_at IS NULL),鉴权热路径走 prefix lookup
# 撤销后的 key 不进活跃索引以减小热索引大小。SQLite + Postgres 均支持
# WHERE 子句的部分索引;双驱动维护两套等价 where 表达式。
Index(
"idx_api_keys_active",
"key_prefix",
sqlite_where=text("revoked_at IS NULL"),
postgresql_where=text("revoked_at IS NULL"),
),
{
"comment": (
"API key 表(headless API 凭证)。每行属于唯一 service_accountkey_prefix 全局唯一可在日志中打印,"
"key_hash 是完整 token 的 sha-256plaintext token 只在创建时返给调用方。撤销保留行(revoked_at 非空),"
"活跃 key 走部分索引 idx_api_keys_active 加速鉴权热路径。Stage 0 仅落 schemaStage 1 起接鉴权 + 限速。"
)
},
)