AACsearch
Архитектура

Типы ключей и модель безопасности

Четыре категории ключей AACsearch — search, connector, scoped и admin — их префиксы ss_*, хранение только хешей, HMAC + TTL + фильтр scoped-токена и порядок проверки каждой категории во время запроса.

AACsearch выдаёт по одной физической строке на учётную запись в SearchApiKey, различая их колонкой scopes[]. Исходная строка ключа никогда не хранится — только sha256-хеш. Scoped-токены — четвёртая, производная категория учётных данных, живущая целиком в bearer-строке (без строки в БД) и проверяемая через HMAC.

Четыре категории ключей

Описание

Диаграмма группирует учётные данные AACsearch в два бакета: три персистируемых типа строк в SearchApiKey (admin, ingest/connector, search) — каждый хранится только как sha256-хеш со своим префиксом и набором scopes — и производный тип scoped-токена, который никогда не персистируется, а подписан HMAC-SHA256 поверх BETTER_AUTH_SECRET. Стрелки помечают предполагаемую поверхность развёртывания (только сервер, CMS, доверенный backend, браузерный виджет) и показывают, что scoped-токены наследуются от родительского search-ключа.

graph TD
    classDef admin fill:#fde68a,stroke:#b45309,color:#78350f
    classDef ingest fill:#bae6fd,stroke:#0369a1,color:#075985
    classDef search fill:#bbf7d0,stroke:#15803d,color:#14532d
    classDef scoped fill:#ddd6fe,stroke:#6d28d9,color:#4c1d95

    subgraph Persisted["SearchApiKey rows — sha256(raw) only"]
        Admin["Admin key<br/>prefix: ss_search_<br/>scopes: [admin]<br/>full index control"]:::admin
        Ingest["Ingest / Connector key<br/>prefix: ss_connector_ (or ss_search_)<br/>scopes: [connector_write] / [ingest]<br/>writes to SearchSyncOutbox"]:::ingest
        Search["Search key<br/>prefix: ss_search_<br/>scopes: [search]<br/>read-only, origin-restricted"]:::search
    end

    subgraph Derived["Not persisted — verified by HMAC"]
        Scoped["Scoped token<br/>prefix: ss_scoped_<br/>payload: { keyId, parentRawKey, filterBy, exp }<br/>HMAC-SHA256 over BETTER_AUTH_SECRET"]:::scoped
    end

    Search -. parent of .-> Scoped

    UseAdmin["Server-side only<br/>NEVER in browser/CMS"] --- Admin
    UseIngest["CMS connector / sync job"] --- Ingest
    UseSearch["Server-rendered search<br/>(trusted backend)"] --- Search
    UseScoped["Browser widget / per-user filter<br/>(short-lived, embeddable)"] --- Scoped

Поток проверки на запрос

Описание

Диаграмма — это поток проверки на каждый запрос: по префиксу выбирается либо «HMAC-проверить-и-затем-хешировать» (для ss_scoped_*), либо прямой «хеш-и-lookup» (для ss_search_* / ss_connector_*), затем — проверка scope, allow-list origin, бакет rate-limit и, наконец, AND-объединение tenant-фильтра до запуска обработчика. Любая ошибка коротит на 401, 403 или 429.

flowchart TD
    Req["Request: Authorization: Bearer &lt;token&gt;"]
    Shape{"Prefix?"}
    Scoped["ss_scoped_*"]
    Search["ss_search_*"]
    Connector["ss_connector_*"]
    Reject["401 missing_bearer_token"]

    HMAC["1. base64url decode payload<br/>2. HMAC-SHA256(payload, BETTER_AUTH_SECRET)<br/>3. timingSafeEqual(sig, expected)<br/>4. exp &gt; now()"]
    Hash["sha256(normalizeKeyForHash(raw))"]
    Lookup["SELECT * FROM search_api_key<br/>WHERE hash = ?<br/>AND revokedAt IS NULL<br/>AND deletedAt IS NULL"]
    ScopeCheck{"Required scope<br/>in scopes[]?"}
    OriginCheck{"Origin in<br/>allowedOrigins?"}
    RateLimit{"RateLimitBucket<br/>&le; rateLimitPerMinute?"}
    Filter["Combine: userFilter && tenantId:&lt;org&gt; && (scopedFilter)"]
    Allow["Proceed to handler"]

    Req --> Shape
    Shape --> Scoped --> HMAC --> Hash
    Shape --> Search --> Hash
    Shape --> Connector --> Hash
    Shape -. unknown .-> Reject

    Hash --> Lookup --> ScopeCheck
    ScopeCheck -- no --> Reject
    ScopeCheck -- yes --> OriginCheck
    OriginCheck -- no --> Reject
    OriginCheck -- yes --> RateLimit
    RateLimit -- 429 --> Reject
    RateLimit -- ok --> Filter --> Allow

Правила хранения

СвойствоAdminIngest / ConnectorSearchScoped
Префиксss_search_*ss_connector_* (или ss_search_*)ss_search_*ss_scoped_*
Сохранён в SearchApiKey?ДаДаДаНет — производный
Хранится какsha256-хешsha256-хеш (общее пространство)sha256-хешСамодостаточный подписанный payload
Требуемый scopeadminconnector_write / ingestsearchНаследует от родителя (должен быть search)
ИстечениеНеобязательный expiresAtНеобязательный expiresAtНеобязательный expiresAtОбязательный exp (≤ 24 ч, обычно 15 мин)
Tenant-фильтр применяется?n/a (admin)Авто-инжекция при записиАвто-инжекция при поискеАвто-инжекция плюс scoped-фильтр
Допустим в браузере?НикогдаНикогдаТолько при origin-lockДа (целевое использование)
ОтзывrevokedAtrevokedAtrevokedAtДождаться exp (или ротировать родителя)

Какие Hard invariants это обеспечивает

  • #3 Хранение только хешей. Ни один путь кода не сохраняет исходный ключ. hashSearchApiKey() — единственная функция, касающаяся секрета, и она возвращает hex-дайджест.
  • #4 Scoped-токены. Любая клиентская поисковая строка использует ss_scoped_*-токен, а не долгоживущий ss_search_*. HMAC + TTL + фильтр не обсуждается.
  • #5 Изоляция тенантов. tenantId:<org> AND-объединяется в filter_by на уровне поискового адаптера, а не отправляется заголовком — это защищено от подделки.

См. также

On this page