mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-16 22:54:12 +00:00
fix(gateway): api_key owners no longer 403 on namespaces they own
The namespace-ownership middleware compared an api_key caller's RAW key against namespace_ownership.owner_id, but api_keys are stored HMAC-hashed (HashAPIKey). So every api_key-authenticated owner got a 403 on a namespace they actually own — blocking function deploy and PUT /v1/push/config. Hash the presented api_key before the ownership comparison (hashed first, raw second as a rolling-upgrade legacy fallback), mirroring the existing lookupAPIKeyNamespace pattern. The wallet path is unchanged (wallets stored raw). Security-reviewed: grants only to the correct key holder, no escalation.
This commit is contained in:
parent
b1b8ac57d5
commit
7b5587094d
@ -844,14 +844,35 @@ func (g *Gateway) authorizationMiddleware(next http.Handler) http.Handler {
|
||||
nsID := nres.Rows[0][0]
|
||||
|
||||
q := "SELECT 1 FROM namespace_ownership WHERE namespace_id = ? AND owner_type = ? AND owner_id = ? LIMIT 1"
|
||||
res, err := db.Query(internalCtx, q, nsID, ownerType, ownerID)
|
||||
|
||||
// If primary owner check fails and we have a JWT wallet with API key fallback, try the API key
|
||||
if (err != nil || res == nil || res.Count == 0) && ownerType == "wallet" && apiKeyFallback != "" {
|
||||
res, err = db.Query(internalCtx, q, nsID, "api_key", apiKeyFallback)
|
||||
// ownsBy reports whether (ot, oid) owns this namespace. API keys are
|
||||
// stored HMAC-hashed in namespace_ownership (see service.HashAPIKey),
|
||||
// while the presented value here is the RAW key — so for api_key owners
|
||||
// we check the hashed form first and the raw form second (rolling-
|
||||
// upgrade legacy), mirroring lookupAPIKeyNamespace. Without this, an
|
||||
// api_key-authenticated owner never matched and got a 403 on a
|
||||
// namespace they actually own (blocked function deploy / push config).
|
||||
ownsBy := func(ot, oid string) bool {
|
||||
hashed := ""
|
||||
if ot == "api_key" {
|
||||
hashed = g.authService.HashAPIKey(oid)
|
||||
}
|
||||
for _, c := range apiKeyOwnerCandidates(ot, oid, hashed) {
|
||||
res, qerr := db.Query(internalCtx, q, nsID, ot, c)
|
||||
if qerr == nil && res != nil && res.Count > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil || res == nil || res.Count == 0 {
|
||||
owns := ownsBy(ownerType, ownerID)
|
||||
// A JWT wallet can also fall back to its API key (also stored hashed).
|
||||
if !owns && ownerType == "wallet" && apiKeyFallback != "" {
|
||||
owns = ownsBy("api_key", apiKeyFallback)
|
||||
}
|
||||
|
||||
if !owns {
|
||||
writeError(w, http.StatusForbidden, "forbidden: not an owner of namespace")
|
||||
return
|
||||
}
|
||||
@ -860,6 +881,18 @@ func (g *Gateway) authorizationMiddleware(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// apiKeyOwnerCandidates returns the owner_id values to test for a namespace
|
||||
// ownership check. API keys are stored HMAC-hashed in namespace_ownership, so
|
||||
// for an api_key owner the hashed form is checked first and the raw key second
|
||||
// (rolling-upgrade legacy, mirroring lookupAPIKeyNamespace). For every other
|
||||
// owner type the value is used as-is.
|
||||
func apiKeyOwnerCandidates(ownerType, ownerID, hashed string) []string {
|
||||
if ownerType == "api_key" && hashed != "" && hashed != ownerID {
|
||||
return []string{hashed, ownerID}
|
||||
}
|
||||
return []string{ownerID}
|
||||
}
|
||||
|
||||
// requiresNamespaceOwnership returns true if the path should be guarded by
|
||||
// namespace ownership checks.
|
||||
func requiresNamespaceOwnership(p string) bool {
|
||||
|
||||
35
core/pkg/gateway/middleware_apikey_owner_test.go
Normal file
35
core/pkg/gateway/middleware_apikey_owner_test.go
Normal file
@ -0,0 +1,35 @@
|
||||
package gateway
|
||||
|
||||
import "testing"
|
||||
|
||||
// Bugboard follow-up: api_key ownership is stored HMAC-hashed, but the
|
||||
// ownership middleware presented the RAW key — so an api_key-authed owner got
|
||||
// a 403 on a namespace they own (blocking function deploy / push config).
|
||||
// apiKeyOwnerCandidates returns the values to check: hashed-first, raw-fallback.
|
||||
|
||||
func TestApiKeyOwnerCandidates_apiKeyChecksHashedThenRaw(t *testing.T) {
|
||||
got := apiKeyOwnerCandidates("api_key", "ak_raw", "HASHED")
|
||||
if len(got) != 2 || got[0] != "HASHED" || got[1] != "ak_raw" {
|
||||
t.Errorf("api_key: want [HASHED ak_raw] (hashed first, raw legacy fallback); got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiKeyOwnerCandidates_walletUsedAsIs(t *testing.T) {
|
||||
got := apiKeyOwnerCandidates("wallet", "0xWALLET", "")
|
||||
if len(got) != 1 || got[0] != "0xWALLET" {
|
||||
t.Errorf("wallet must be used as-is (never hashed); got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiKeyOwnerCandidates_noHashAvailableFallsBackToRaw(t *testing.T) {
|
||||
// When hashing is unavailable/disabled (HashAPIKey returns the key
|
||||
// unchanged), don't duplicate — just check the raw value once.
|
||||
got := apiKeyOwnerCandidates("api_key", "ak_raw", "ak_raw")
|
||||
if len(got) != 1 || got[0] != "ak_raw" {
|
||||
t.Errorf("no-op hash must yield a single raw candidate; got %v", got)
|
||||
}
|
||||
got2 := apiKeyOwnerCandidates("api_key", "ak_raw", "")
|
||||
if len(got2) != 1 || got2[0] != "ak_raw" {
|
||||
t.Errorf("empty hash must yield a single raw candidate; got %v", got2)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user