mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
zip's registration is a verb method on the group now, so an operation reads as the group answering it rather than a function taking the group, and a route that is not a typed operation says which method it answers with. The package-level form is gone, along with the Router interface it existed to accept any target through. Co-authored-by: Hanzo Dev <[email protected]>
82 lines
2.6 KiB
Go
82 lines
2.6 KiB
Go
package apis_test
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/tests"
|
|
)
|
|
|
|
// TestAnonymousOwnerRuleDoesNotLeak pins the behaviour of the most common rule
|
|
// anyone writes — `owner = @request.auth.id` — for a caller who is not signed
|
|
// in.
|
|
//
|
|
// Postgres answers this by excluding every row: `owner = NULL` is NULL, and a
|
|
// NULL predicate is not true, so an anonymous caller sees nothing. A Supabase
|
|
// user porting a policy expects exactly that, and the rows they are protecting
|
|
// are precisely the ones this rule is written to protect.
|
|
//
|
|
// Base has been answering the opposite for rows whose owner is empty or NULL.
|
|
// `@request.auth.id` resolves to the identifier NULL when there is no auth
|
|
// (core/record_field_resolver_runner.go), the filter compiler counts the literal
|
|
// "null" as an empty identifier (tools/search/filter.go), and equality against
|
|
// an empty identifier is rewritten to `(owner = ” OR owner IS NULL)` so that a
|
|
// missing value can be compared. Each of those three is reasonable alone. Read
|
|
// together they hand every unowned row to anybody who asks.
|
|
//
|
|
// The row with a real owner is here to prove the rule still filters — a fix that
|
|
// returns nothing at all would pass a test that only checked the leak.
|
|
func TestAnonymousOwnerRuleDoesNotLeak(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
setup := func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
c := core.NewBaseCollection("rlsprobe")
|
|
c.Fields.Add(&core.TextField{Name: "owner"})
|
|
c.Fields.Add(&core.TextField{Name: "marker"})
|
|
rule := "owner = @request.auth.id"
|
|
c.ListRule = &rule
|
|
if err := app.Save(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Never written to, so `owner` holds the zero value — the shape any row
|
|
// takes before somebody claims it.
|
|
unowned := core.NewRecord(c)
|
|
unowned.Set("marker", "UNOWNED_ROW")
|
|
if err := app.Save(unowned); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
owned := core.NewRecord(c)
|
|
owned.Set("owner", "4q1xlclmfloku33")
|
|
owned.Set("marker", "OWNED_ROW")
|
|
if err := app.Save(owned); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
scenarios := []tests.ApiScenario{
|
|
{
|
|
Name: "anonymous caller sees no rows, not the unowned ones",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections/rlsprobe/records",
|
|
BeforeTestFunc: setup,
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"totalItems":0`,
|
|
`"items":[]`,
|
|
},
|
|
NotExpectedContent: []string{
|
|
"UNOWNED_ROW",
|
|
"OWNED_ROW",
|
|
},
|
|
ExpectedEvents: map[string]int{"*": 0, "OnRecordsListRequest": 1},
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
scenario.Test(t)
|
|
}
|
|
}
|