Skip to content
bitzorcas
中EN

Guide

Search HTTP Contracts, Permissions, and Feature

The nine Search endpoints, manual and generated routes, timeouts, runtime permission derivation, catalog drift, neutral Feature policy, input validation, and scope.

Last updated

Search mixes five hand-written Minimal APIs with four [GenerateEndpoint] routes. Effective authorization comes from each request’s ResourceDescriptor + AuthorizationAction; a comment saying “requires manage/check” does not change the suffix evaluated at runtime.

1. Endpoint matrix

MethodRouteImplementationActionDerived permission
POST/api/search/unifiedmanual conversionViewsearch.search.view
POST/api/search/conflict/checkmanual conversionSearchsearch.conflict.search
POST/api/search/segment/wordsmanual enum checkCreatesearch.index.create
DELETE/api/search/segment/wordsmanual enum checkDeletesearch.index.delete
GET/api/search/segment/words/{segmentType}manual enum checkViewsearch.index.view
GET/api/search/conflict/records/{recordId}generatedViewsearch.conflict.view
GET/api/search/index/cataloggeneratedViewsearch.index.view
GET/api/search/index/{indexKey}/statisticsgeneratedViewsearch.index.view
POST/api/search/index/{indexKey}/rebuildgeneratedManagesearch.index.manage

The manual group requires authentication and userPolicy rate limiting. Unified, conflict, and word writes use StandardCommand; word listing uses ShortRead. The generated request records declare no Search-specific timeout policy.

2. Unified-search request

Submit a unified query
POST /api/search/unified HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9
Content-Type: application/json
{
"indexKey": "conflict",
"keyword": "Hailan Technology",
"skip": 0,
"take": 20
}

The API model defaults Skip to 0 and Take to 20. No rule rejects blank/unknown IndexKey, negative Skip, non-positive or excessive Take, long Keyword, control/query syntax, or an expensive deep window.

The handler sends the same untrimmed term to Name, Subject, and Content. It does not select culture or analyzer. Public behavior should not depend on unspecified defaults inside the external package.

3. Conflict-check request

ConflictCheckApiRequest
{
"tenantId": "client-supplied-and-ignored",
"parties": [
{
"name": "Hailan Technology Co., Ltd.",
"foreignName": "海岚科技有限公司",
"formerName": "Hailan Industries",
"creditCode": "91310000MA00000000"
}
],
"excludeCaseId": "case-2026-0042"
}

The handler replaces TenantId with the current tenant. That is secure, but retaining the ignored field in DTO/API misleads clients. Remove it or mark it explicitly deprecated and ignored.

There is no Parties rule. A runtime null collection throws; an empty list returns HasConflict=false; null Name can still arrive from JSON; duplicate Name entries overwrite each other in the dictionary; a blank Name with another term becomes a blank category key. These are unsafe implicit semantics for a high-value compliance check.

4. SegmentType and Word

The route validates that SegmentType is 0/1/2 before Mediator. An invalid value returns an anonymous 400 object rather than shared Problem Details:

{
"code": "Search.Segment.InvalidType",
"message": "Invalid segmentation type: 9"
}

Word is trimmed and rejected only when blank. There is no length, Unicode normalization, casing, control-character, or type-specific validation.

Target interactive-search rules
public static class SearchErrors
{
public static readonly Error IndexUnknown =
Error.NotFound("Search.Index.Unknown", "The search scenario is unavailable.");
public static readonly Error PageInvalid =
Error.Validation("Search.Page.Invalid", "The search window is invalid.");
public static readonly Error KeywordTooLong =
Error.Validation("Search.Keyword.TooLong", "The search term is too long.");
}
// Resolve IndexKey through an enabled catalog instead of exposing physical names for probing.
var catalog = await catalogStore.GetAsync(request.IndexKey, cancellationToken);
if (catalog is null || catalog.Status != IndexStatus.Active)
return SearchErrors.IndexUnknown;
// Large extraction belongs to an asynchronous export contract.
if (request.Skip < 0 || request.Take is < 1 or > 100)
return SearchErrors.PageInvalid;
// Normalize first, then enforce both rune and UTF-8 byte limits.
if (Normalize(request.Keyword).Length > 200)
return SearchErrors.KeywordTooLong;

5. Permission drift

The catalog owns:

  • search.search.view;
  • search.conflict.search;
  • search.conflict.view;
  • search.index.manage;
  • search.index.view.

View endpoints, conflict check, and rebuild all align with the catalog. Only segmentation writes still drift:

Runtime suffix derivation
// Conflict check derives the same suffix as the catalog.
new ResourceDescriptor("search", "conflict");
AuthorizationAction.Search; // => search.conflict.search
// Rebuild declares Manage and matches the catalog; word mutations declare Create and Delete.
new ResourceDescriptor("search", "index");
AuthorizationAction.Manage; // => search.index.manage
AuthorizationAction.Create; // => search.index.create
AuthorizationAction.Delete; // => search.index.delete

AuthorizationAction has Manage but not Check. The remaining fix is to standardize word create/delete on AuthorizationAction.Manage, or split them deliberately and migrate the catalog. Change catalog, grants, seed, cache, upgrade compatibility, and tests together.

6. Feature catalog is not an entitlement gate

SearchFeatures.Index = "search.index" is cataloged with DefaultEnabled=false. The central FeaturePolicyEvaluator.ModuleFeatureMap contains tickets/chat/workflow only. Search requests therefore receive Neutral without reading tenant entitlements.

If query, conflict, and index administration are separate commercial capabilities, one Feature may also be too coarse. Whatever the chosen granularity, enforce it through the shared evaluator or an explicit use-case gate and test enabled, disabled, and unavailable states.

7. Catalog versus statistics scope

Statistics calls the engine with current TenantId. Catalog is a global table and returns DirectoryName, EntityType, DocumentCount, LastRebuiltAt, and Version without tenant filtering. A tenant principal with index.view may see physical/operational metadata.

Decide whether the catalog is platform-admin only, a redacted tenant view, or an internal health surface. Query permission and physical directory visibility should not share a vague permission by accident.

8. Per-index authorization

UnifiedSearch accepts any IndexKey but always describes resource search/search. A caller with unified view may probe conflict or future sensitive indexes.

Options include a per-index permission, catalog-defined RequiredPermission/DataClassification, authorization attributes carrying IndexKey, or exclusive dedicated endpoints for high-sensitivity scenarios. Every model still needs owner-level authorization for hits.

9. Error contract

Stable module errors are limited to:

  • SegmentWord.EmptyWord;
  • ConflictRecord.PendingCasesModule.

There is no stable IndexUnknown, QueryInvalid, WindowTooLarge, ProviderUnavailable, ProviderTimeout, or RebuildConflict classification. Raw provider exceptions reach the global exception layer.

10. Endpoint test matrix

  1. Every catalog grant versus actual action yields fixed 200/403.
  2. Feature enabled, disabled, and unavailable.
  3. Authentication, rate limit, and timeout.
  4. IndexKey allowlist and per-scenario permission.
  5. Skip, Take, and Keyword bounds.
  6. Null/empty/duplicate/Unicode/large Parties.
  7. Request TenantId cannot affect effective tenant.
  8. Invalid SegmentType uses shared Problem Details.
  9. Manual/generated OpenAPI consistency.
  10. Provider unavailability is never returned as zero hits.

11. Review commands

Terminal window
# Conflict and rebuild now match the catalog; word create/delete still drift from manage/view.
rg -n "AuthorizationAction\.|search\.(search|conflict|index)\." \
src/Platform/Search src/Hosts/BitzOrcas.Api/Endpoints/SearchEndpoints.cs -g '*.cs'
# Search currently has no central runtime feature mapping.
rg -n "search\.index|ModuleFeatureMap|\[\"search\"\]" src tests -g '*.cs'
# Target state: explicit rules for every high-risk Search request.
rg -n "IRequestRule<.*(UnifiedSearch|CheckConflict|RebuildIndex|Segment)" \
src/Platform/Search tests -g '*.cs'

Back to Search · Unified search and conflict · Testing and GA

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%