{"record":{"id":"1cd896ee24f7c9d5","repo":"Tencent/WeKnora","slug":"rbac-resource-not-found","errorCode":null,"errorMessage":"rbac: resource not found","messagePattern":"rbac: resource not found","errorType":"error_code","errorClass":null,"httpStatus":404,"severity":"warning","filePath":"internal/middleware/rbac.go","lineNumber":23,"sourceCode":"\t\"errors\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/Tencent/WeKnora/internal/config\"\n\t\"github.com/Tencent/WeKnora/internal/logger\"\n\t\"github.com/Tencent/WeKnora/internal/types\"\n\t\"github.com/gin-gonic/gin\"\n)\n\n// ErrResourceNotFound is the sentinel a CreatorLookup returns when the\n// :id on the request does not match any row the lookup can see (either\n// the row is genuinely missing or its tenant doesn't match). When a\n// lookup returns this error, RequireOwnershipOrRole intentionally lets\n// the request proceed so the downstream handler can respond with its\n// own 404 — middleware-level 403 would hide real \"URL is wrong\" failures\n// behind a permissions error, which breaks client diagnostics and\n// operator dashboards.\nvar ErrResourceNotFound = errors.New(\"rbac: resource not found\")\n\n// CreatorLookup resolves the creator user ID for the resource targeted\n// by the current request, based on whatever is on the gin.Context (URL\n// params, query, body). Implementations live next to the handlers they\n// guard, e.g. handler.kbCreatorLookup(c) reads \":id\" and returns\n// KnowledgeBase.CreatorID.\n//\n// Return value contract:\n//   - (creatorID, nil) where creatorID != \"\"  -> the resource has a\n//     recorded owner; ownership match grants access.\n//   - (\"\", nil)                                -> \"tenant-owned\": no\n//     human creator was recorded (legacy row or built-in resource);\n//     only callers whose role meets the bar may proceed.\n//   - (\"\", ErrResourceNotFound)                -> the :id does not\n//     resolve to any row visible to this caller's tenant. Middleware\n//     proceeds to the handler so the handler can return 404 instead\n//     of masking it as 403.\n//   - (\"\", other error)                        -> transient or","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/middleware/rbac.go#L5-L41","documentation":"ErrResourceNotFound is the sentinel returned by RBAC CreatorLookups when the targeted resource row does not exist or belongs to a different tenant. RequireOwnershipOrRole deliberately lets requests carrying this error proceed so the downstream handler can return its own 404 — a middleware 403 would mask genuine 'wrong URL' failures. Note the similarly-named datasource.ErrResourceNotFound ('resource not found in source system') used by connectors like Notion on HTTP 404; they are distinct sentinels.","triggerScenarios":"A CreatorLookup (KBCreatorLookup, KBCreatorLookupFromKbIDParam, AgentCreatorLookup) queries the DB for the resource named by the URL param and finds no row (nonexistent ID) or a row with a mismatched tenant ID. Also returned by doRequest on 404 paths.","commonSituations":"Client requests a deleted or never-existing knowledge base/agent ID; ID from another tenant; stale links cached after resource deletion; Notion connector fetching a page that was deleted in the source system (datasource variant).","solutions":["Verify the resource ID in the request URL is correct and still exists (query the DB or list endpoint).","Confirm the request is made against the correct tenant/workspace — cross-tenant lookups produce the same sentinel.","If deleted resources should 404 explicitly, ensure the downstream handler maps this case to a NotFoundError response (it will, since middleware lets it through).","Use errors.Is(err, middleware.ErrResourceNotFound) to distinguish not-found from ErrOwnershipForbidden when handling."],"exampleFix":"// before: treating any eval error as forbidden\nif evalErr != nil {\n    return errors.NewForbiddenError(\"no permission\")\n}\n// after: distinguish not-found from forbidden\nif goerrors.Is(evalErr, middleware.ErrResourceNotFound) {\n    return errors.NewNotFoundError(\"knowledge base not found\")\n}\nif goerrors.Is(evalErr, middleware.ErrOwnershipForbidden) {\n    return errors.NewForbiddenError(\"No permission to operate on this knowledge base\")\n}","handlingStrategy":"type-guard","validationCode":"// before mutating, confirm the resource exists in your tenant\nkb, err := client.GetKnowledgeBase(ctx, id)\nif errors.Is(err, ErrNotFound) || kb == nil {\n    return fmt.Errorf(\"knowledge base %s does not exist in this tenant\", id)\n}","typeGuard":"func isRBACNotFound(err error) bool {\n    return errors.Is(err, middleware.ErrResourceNotFound)\n}\n// note: do not confuse with datasource.ErrResourceNotFound (connector 404s)","tryCatchPattern":"if err != nil {\n    switch {\n    case errors.Is(err, middleware.ErrResourceNotFound):\n        // let the handler 404 — the URL/resource itself is wrong\n        return errors.NewNotFoundError(\"not found\")\n    case errors.Is(err, middleware.ErrOwnershipForbidden):\n        return errors.NewForbiddenError(\"no permission\")\n    }\n    return err\n}","preventionTips":["Always match sentinels with errors.Is, not string comparison.","Distinguish middleware.ErrResourceNotFound from datasource.ErrResourceNotFound.","Treat this error as a client-side wrong-ID/tenant problem, not a permissions problem.","Purge cached links after deleting resources."],"tags":["rbac","not-found","authorization"],"backgroundTag":"resource-not-found","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}