{"record":{"id":"705b9647d27cfa3c","repo":"Tencent/WeKnora","slug":"storage-path-has-no-tenant-segment","errorCode":null,"errorMessage":"storage path has no tenant segment","messagePattern":"storage path has no tenant segment","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/utils/presign.go","lineNumber":118,"sourceCode":"\t// Verify signature.\n\texpected := signPayload(key, filePath, tenantID, expires)\n\treturn hmac.Equal([]byte(expected), []byte(sig))\n}\n\n// kbScopedExportsSegment is the only storage prefix served by the KB-scoped\n// file proxy. Embedded wiki/chunk images land under exports/; raw knowledge\n// uploads use {tenant}/{knowledgeID}/... and are served via\n// /knowledge/{id}/download instead.\nconst kbScopedExportsSegment = \"exports\"\n\n// ValidateStoragePathTenant ensures the tenant segment embedded in a provider://\n// storage path matches the authenticated caller's tenant. Cross-tenant access\n// for arbitrary tenant paths uses /api/v1/files/presigned with an HMAC bound to\n// the resource owner; KB-scoped shared rendering uses ValidateKBScopedStoragePath.\nfunc ValidateStoragePathTenant(filePath string, tenantID uint64) error {\n\tpathTenant := ParseTenantIDFromStoragePath(filePath)\n\tif pathTenant == 0 {\n\t\treturn fmt.Errorf(\"storage path has no tenant segment\")\n\t}\n\tif pathTenant != tenantID {\n\t\treturn fmt.Errorf(\"storage path workspace mismatch\")\n\t}\n\treturn nil\n}\n\n// ValidateKBScopedStoragePath is used by GET /knowledge-bases/:id/files. It\n// requires the path to belong to the KB owner tenant and to live under the\n// exports/ namespace used for embedded images (SaveBytes / multimodal output).\n// This prevents borrowers with shared-KB read access from using the proxy to\n// fetch arbitrary owner-tenant objects such as raw knowledge uploads.\nfunc ValidateKBScopedStoragePath(filePath string, tenantID uint64) error {\n\tif err := ValidateStoragePathTenant(filePath, tenantID); err != nil {\n\t\treturn err\n\t}\n\tif !storagePathHasExportsScope(filePath, tenantID) {\n\t\treturn fmt.Errorf(\"storage path is outside KB-scoped exports namespace\")","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/utils/presign.go#L100-L136","documentation":"ValidateStoragePathTenant found no tenant segment in the storage path: ParseTenantIDFromStoragePath returned 0. The path must embed the owning tenant's ID (e.g. /storage/<tenantID>/...) so presigned-URL validation can confirm the caller's tenant matches. A path without this segment cannot be attributed to a tenant and is rejected defensively.","triggerScenarios":"Calling ValidateStoragePathTenant (directly or via ValidateKBScopedStoragePath) with a legacy path written before tenant-scoped layout was introduced, a malformed path, or a path built manually without the tenant component.","commonSituations":"Migrating pre-multitenancy objects stored under flat keys; objects uploaded by scripts bypassing the normal upload API; misconfigured storage base path stripping the tenant prefix; test fixtures using dummy paths like 'file.pdf'.","solutions":["Re-upload or copy the object into the tenant-scoped layout /storage/<tenantID>/... using the normal upload API","Fix the code/fixture constructing the path so it includes the tenant ID segment (use the shared path-builder helper)","Check ParseTenantIDFromStoragePath's expected format and confirm your path matches (delimiters, numeric segment position)","For legacy data, run a one-time migration rewriting object keys to include the tenant segment"],"exampleFix":"// before\nkey := fmt.Sprintf(\"uploads/%s\", filename)\n// after\nkey := fmt.Sprintf(\"storage/%d/uploads/%s\", tenantID, filename)","handlingStrategy":"validation","validationCode":"func pathHasTenantSegment(p string) bool {\n    return utils.ParseTenantIDFromStoragePath(p) != 0\n}\n// guard before requesting presigned access\nif !pathHasTenantSegment(storageKey) {\n    return errors.New(\"storage key must be in /storage/<tenantID>/... layout\")\n}","typeGuard":"func isTenantScopedPath(p string) bool {\n    parts := strings.SplitN(strings.TrimPrefix(p, \"/\"), \"/\", 3)\n    if len(parts) < 2 || parts[0] != \"storage\" { return false }\n    _, err := strconv.ParseUint(parts[1], 10, 64)\n    return err == nil\n}","tryCatchPattern":"err := utils.ValidateStoragePathTenant(key, session.TenantID)\nif err != nil {\n    if strings.Contains(err.Error(), \"no tenant segment\") {\n        return fmt.Errorf(\"object %q is not tenant-scoped; re-upload or migrate it\", key)\n    }\n    return err\n}","preventionTips":["Always build storage keys with the shared path-builder that injects the tenant segment","Add a startup/CI check that test fixtures use tenant-scoped paths","Write a migration for any pre-multitenancy objects still under flat keys"],"tags":["authorization","multitenancy","storage-path","presigned-url"],"backgroundTag":"cross-tenant-storage-access","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}