How Scoped API Keys with Capability-Level Grants and KB Restrictions Function in WeKnora
WeKnora's TenantAPIKey model enforces machine-to-machine security by binding API keys to specific capabilities (such as retrieve or ingest) and optional knowledge-base allow-lists, ensuring that automated clients can only perform explicitly authorized actions on designated data sets.
Scoped API keys with capability-level grants and knowledge-base restrictions provide the foundation for secure automation in WeKnora. The system stores all machine-to-machine credentials in the TenantAPIKey structure, which defines both what actions a key may perform and which knowledge bases it can access. This design allows platform administrators to issue least-privilege credentials that minimize the blast radius of compromised API keys.
Core Data Structures
WeKnora persists API key metadata in internal/types/tenant_api_key.go, where three primary structures collaborate to define access boundaries.
TenantAPIKey
The TenantAPIKey struct represents the persistent storage of an API key. It captures the scope type, capability set, and optional knowledge-base restrictions.
type TenantAPIKey struct {
ScopeType string // "tenant" or "platform"
FullAccess bool // Bypasses granular checks if true
KnowledgeBaseIDs StringArray // Allow-list of KB IDs (empty = unrestricted)
Capabilities StringArray // e.g., "retrieve", "chat", "ingest"
// ... additional fields like encrypted secret, name, tenant ID
}
APIKeyCapability
APIKeyCapability enumerates the additive grants a key can receive. Defined constants include retrieve, chat, ingest, and manage_kbs, allowing fine-grained control over read, write, and administrative operations.
TenantAPIKeyScope
The TenantAPIKeyScope struct serves as the request-level projection used by middleware and RBAC checks. It mirrors the persistent fields but is constructed fresh for each HTTP request to ensure current policy enforcement.
Scope and Capability Normalization
Before enforcement, WeKnora normalizes inputs to guarantee consistent evaluation. This occurs in internal/types/tenant_api_key.go through dedicated normalization functions.
Scope normalization (NormalizeAPIKeyScopeType) coerces any input to "tenant" or "platform". Platform keys are the only ones that can bypass tenant-level RBAC checks.
Capability normalization (NormalizeAPIKeyCapability and NormalizeAPIKeyCapabilities) deduplicates capability strings and drops unknown values. This prevents injection of invalid capabilities and ensures the stored scope contains only recognized grants.
Capability Evaluation with HasCapability
During request processing, handlers verify permissions via the HasCapability method on TenantAPIKeyScope. This function checks if the normalized capability list contains the requested action.
func (s TenantAPIKeyScope) HasCapability(c APIKeyCapability) bool {
c = NormalizeAPIKeyCapability(c)
if c == "" { return false }
for _, item := range NormalizeAPIKeyCapabilities(s.Capabilities) {
if item == string(c) { return true }
}
return false
}
If the capability is absent, the handler returns 403 Forbidden, blocking the operation before it reaches business logic.
Knowledge-Base Restrictions
Beyond capabilities, keys may be knowledge-base-restricted through the KnowledgeBaseIDs field. When this array is non-empty, the key is confined to a specific subset of knowledge bases.
Two helper methods govern this restriction:
- AllowsKnowledgeBase(kbID) – Returns true when the allow-list is empty (unrestricted) or when
kbIDappears inKnowledgeBaseIDs. - IsKnowledgeBaseRestricted() – Returns true if the allow-list contains one or more entries.
These checks ensure that a key created for ingesting data into kb-123 cannot accidentally—or maliciously—access kb-456.
Enforcement in Request Handlers
HTTP middleware and route handlers rely on two authorization helpers defined in internal/types/tenant_api_key.go to enforce KB restrictions at the edge.
AuthorizeTenantAPIKeyKnowledgeBases
Invoked by any route touching one or more knowledge bases (e.g., ingest, retrieve, copy), this function verifies that every requested KB ID appears in the key's allow-list. If the key is KB-restricted and any requested ID is missing, it returns 403 Forbidden.
AuthorizeTenantAPIKeyKnowledgeTargets
Used by routes accepting explicit knowledge_ids (such as search or history), this helper rejects calls when callers supply knowledge identifiers without a verified KB or when any kb_id falls outside the allow-list.
Route definitions in router/routes_knowledge.go demonstrate typical usage:
router.POST("/knowledge/:kb_id/ingest", func(c echo.Context) error {
kbID := c.Param("kb_id")
if err := types.AuthorizeTenantAPIKeyKnowledgeBases(c.Request().Context(), kbID); err != nil {
return err // 403 if KB not allowed
}
scope, _ := types.TenantAPIKeyScopeFromContext(c.Request().Context())
if !scope.HasCapability(types.APIKeyCapabilityIngest) {
return errors.NewForbiddenError("API key lacks ingest capability")
}
// ...perform ingest logic...
})
The Complete Authorization Flow
The enforcement of scoped API keys follows a five-step pipeline for every authenticated request:
- Authentication – Middleware extracts the
X-API-Keyheader, decrypts the stored secret, and builds aTenantAPIKeyScopeinstance. - Scope Normalisation –
ScopeTypeis normalized; platform keys bypass tenant-level checks, while tenant keys continue through RBAC. - Capability Evaluation – Handlers call
HasCapabilityto validate actions like chat, ingest, or manage_kbs. - KB Restriction Verification – If
KnowledgeBaseIDsis non-empty,AuthorizeTenantAPIKeyKnowledgeBasesconfirms every touched KB is in the allow-list. - Authorization Decision – Any check failure triggers a 403 Forbidden response; otherwise, the operation proceeds.
Practical Implementation Examples
Creating and enforcing a restricted key requires coordination between storage, middleware, and handlers.
// 1. Create a scoped API key (admin UI or CLI)
key := types.TenantAPIKey{
TenantID: ptrUint64(42),
ScopeType: types.APIKeyScopeTenant,
Name: "ingest-key-for-kb-123",
FullAccess: false,
KnowledgeBaseIDs: types.StringArray{"kb-123"},
Capabilities: types.StringArray{string(types.APIKeyCapabilityIngest)},
}
db.Create(&key) // APIKey is encrypted automatically
// 2. Middleware extracts scope for each request
func apiKeyMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
keyStr := c.Request().Header.Get("X-API-Key")
// lookup → TenantAPIKey → TenantAPIKeyScope (omitted for brevity)
scope := types.TenantAPIKeyScope{
KeyID: key.ID,
ScopeType: key.ScopeType,
FullAccess: key.FullAccess,
KnowledgeBaseIDs: key.KnowledgeBaseIDs,
Capabilities: key.Capabilities,
}
ctx := types.WithTenantAPIKeyScope(c.Request().Context(), scope)
c.SetRequest(c.Request().WithContext(ctx))
return next(c)
}
}
// 3. Handler allowing only "retrieve" on a specific KB
func getDocumentHandler(c echo.Context) error {
kbID := c.Param("kb_id")
if err := types.AuthorizeTenantAPIKeyKnowledgeBases(c.Request().Context(), kbID); err != nil {
return err // 403 if KB not allowed
}
scope, _ := types.TenantAPIKeyScopeFromContext(c.Request().Context())
if !scope.HasCapability(types.APIKeyCapabilityRetrieve) {
return errors.NewForbiddenError("API key lacks retrieve capability")
}
// ...fetch and return document...
return nil
}
Summary
- TenantAPIKey stores persistent credentials with
ScopeType,Capabilities, and optionalKnowledgeBaseIDs. - Capability-level grants use
HasCapabilityto validate that a key holds the required permission for an action. - KB restrictions are enforced via
AuthorizeTenantAPIKeyKnowledgeBasesandAllowsKnowledgeBase, creating hard boundaries around data access. - Normalization functions guarantee consistent handling of scope types and capabilities during storage and runtime checks.
- Platform keys bypass tenant-level checks, while tenant keys are fully constrained by capability and knowledge-base allow-lists.
Frequently Asked Questions
What is the difference between tenant and platform scoped API keys in WeKnora?
Tenant-scoped keys operate within a single tenant's boundaries and must pass both capability and knowledge-base checks, while platform-scoped keys bypass tenant-level RBAC entirely. According to the source code in internal/types/tenant_api_key.go, platform keys are normalized to "platform" and are typically reserved for infrastructure-level automation.
How does WeKnora validate that an API key has permission to access a specific knowledge base?
The system invokes AuthorizeTenantAPIKeyKnowledgeBases from internal/types/tenant_api_key.go, which checks AllowsKnowledgeBase for every requested KB ID. If the key's KnowledgeBaseIDs array is non-empty and does not contain the requested ID, the function returns a 403 Forbidden error before the handler executes.
What happens if an API key attempts to use a capability it does not possess?
When a handler calls scope.HasCapability(capability) and the capability string is absent from the key's normalized capability list, the method returns false. The handler then returns 403 Forbidden, rejecting the request without touching business logic or data layers.
How are API key capabilities normalized before storage and validation?
NormalizeAPIKeyCapabilities deduplicates the capability array and drops unknown strings, while NormalizeAPIKeyCapability validates individual capability values against the enumerated constants. This ensures that only recognized grants like ingest or retrieve are persisted and evaluated, preventing policy pollution from invalid input.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →