Files
base/apis/schema_auth.go
zeekay 1575be07b1 A bound filter value stays a value, and org authority holds only in the org's Base
Filter params were pasted in one at a time, so a value spelling another
param's placeholder was rewritten inside its own quotes and the second
value's quotes closed the first one's string: a bound value could add
conditions to the filter. They are now substituted in one pass over the
filter as written.

An org admin administers, and a member reads the schema of, only a Base
opened for its org (StoreKeyTenant): a Base deployed for no org belongs to
whoever deployed it. The org plugin names the org of every Base it opens.

A saved user's record never carries org authority: @request.auth.admin and
@request.auth.role answer only for the record a verified token mints, and
the two fields are system and hidden.

The emptiness figures of an aggregate are columns of its one query, so a
grouped request counts the groups it shows and runs one query however
many figures it asks for.

A stream the host routed to its org's Base is served by that live Base,
not the instance its grant was minted on.
2026-10-18 22:21:27 -07:00

94 lines
3.7 KiB
Go

package apis
import (
"strings"
"github.com/hanzoai/base/core"
"github.com/hanzoai/base/tools/hook"
)
// DefaultRequireSchemaAuthMiddlewareId names the middleware that admits a
// schema change.
const DefaultRequireSchemaAuthMiddlewareId = "baseRequireSchemaAuth"
// DefaultRequireSchemaReadAuthMiddlewareId names the middleware that admits a
// schema read.
const DefaultRequireSchemaReadAuthMiddlewareId = "baseRequireSchemaReadAuth"
// IsOrgAdmin reports whether a request administers the org it acts in: an org
// admin's token or the org's own secret key, in a Base opened for that org.
//
// It is the org's own authority over its own Base and nothing wider. A
// superuser is a different fact ([core.RequestEvent.HasSuperuserAuth]) and is
// never reached through this one.
func IsOrgAdmin(e *core.RequestEvent) bool {
admin, _ := e.Get(RequestEventKeyOrgAdmin).(bool)
return admin && servesOrg(e)
}
// servesOrg reports whether the Base serving a request was opened for the org
// the request acts in (StoreKeyTenant), which is what makes that org's
// authority mean anything here. A Base that names no org belongs to whoever
// deployed it, so an admin of some org IAM knows administers nothing in it.
func servesOrg(e *core.RequestEvent) bool {
org, _ := e.Get(RequestEventKeyOrg).(string)
tenant, _ := e.App.Store().Get(StoreKeyTenant).(string)
return org != "" && tenant == org
}
// RequireSchemaAuth admits a superuser, or an org admin in its own org's Base.
//
// It decides who may reach a schema route at all. Which collections an org
// admin may then change is [SchemaManageable]'s answer, asked by the route.
func RequireSchemaAuth() *hook.Handler[*core.RequestEvent] {
return &hook.Handler[*core.RequestEvent]{
Id: DefaultRequireSchemaAuthMiddlewareId,
Func: func(e *core.RequestEvent) error {
if e.HasSuperuserAuth() || IsOrgAdmin(e) {
return e.Next()
}
if sub, _ := e.Get(RequestEventKeySub).(string); e.Auth == nil && sub == "" {
return e.UnauthorizedError("The request requires an org admin's or a superuser's authorization.", nil)
}
return e.ForbiddenError("Only an admin of this org can change its schema.", nil)
},
}
}
// RequireSchemaReadAuth admits whoever may READ a Base's schema: a superuser, an
// org admin, or any member acting in the org whose Base it is. A member reads
// the org's own collections only (the routes hide Base's own tables from one).
func RequireSchemaReadAuth() *hook.Handler[*core.RequestEvent] {
return &hook.Handler[*core.RequestEvent]{
Id: DefaultRequireSchemaReadAuthMiddlewareId,
Func: func(e *core.RequestEvent) error {
if e.HasSuperuserAuth() || IsOrgAdmin(e) || IsOrgMember(e) {
return e.Next()
}
if sub, _ := e.Get(RequestEventKeySub).(string); e.Auth == nil && sub == "" {
return e.UnauthorizedError("The request requires an org member's or a superuser's authorization.", nil)
}
return e.ForbiddenError("Only a member of this org can read its schema.", nil)
},
}
}
// IsOrgMember reports whether a request acts in an org as one of its members,
// in that org's own Base: a person IAM vouched for, or the org's own key. A Base
// that serves no org has no members, so a locally signed-in user is never one.
func IsOrgMember(e *core.RequestEvent) bool {
if !servesOrg(e) {
return false
}
sub, _ := e.Get(RequestEventKeySub).(string)
return e.Auth != nil || sub != ""
}
// SchemaManageable reports whether an org admin may change collection: an
// ordinary base collection of the org's own. Auth and view collections, system
// collections and the underscore-named tables Base keeps for itself stay a
// superuser's.
func SchemaManageable(c *core.Collection) bool {
return c != nil && c.IsBase() && !c.System && !strings.HasPrefix(c.Name, "_")
}