Documentation
¶
Overview ¶
Package access is the gating vocabulary shared by content interactions and media: the authenticated Actor, the ContentResolver port and its Resolution; and the paywall model over billing entitlements: the key grammar (content:, members:, tiers), item rules, the Gate, its listing Filter with a SQL predicate, and the per-request memo. It has no dependencies beyond contentref; billing is the Entitlements port.
Index ¶
- Constants
- Variables
- func MembersPrefix(tenant, kind string) string
- func OwnPrefix(tenant, kind string) string
- func WithMemo(ctx context.Context) context.Context
- type Actor
- type Answer
- type Branch
- type Columns
- type ContentResolver
- type Decision
- type Entitlements
- type Fact
- type Facts
- type Filter
- func (f *Filter) Allows(it Item) (allowed, known bool)
- func (f *Filter) Anonymous() bool
- func (f *Filter) Branches(c Columns) ([]Branch, map[string]any, error)
- func (f *Filter) Complete(kind string) bool
- func (f *Filter) Members(kind string) []string
- func (f *Filter) Owned(kind string) []string
- func (f *Filter) SQL(c Columns) (string, map[string]any, error)
- func (f *Filter) Tier(name string) bool
- func (f *Filter) Unknown() bool
- type Gate
- func (g *Gate) Decide(ctx context.Context, actor Actor, items []Item) ([]Decision, error)
- func (g *Gate) Filter(ctx context.Context, actor Actor) (*Filter, error)
- func (g *Gate) Resolver(f Facts) ContentResolver
- func (g *Gate) Settle(ctx context.Context, actor Actor, f *Filter, items []Item) (keep []bool, short bool, err error)
- func (g *Gate) TierKeys() []string
- type GateConfig
- type HeldKeys
- type Item
- type Key
- func MakeMembers(scope contentref.ContentRef) (Key, error)
- func MakeOwn(item contentref.ContentRef) (Key, error)
- func MakeTier(name string) (Key, error)
- func Members(scope contentref.ContentRef) Key
- func Own(item contentref.ContentRef) Key
- func ParseKey(s string) (Key, error)
- func Tier(name string) Key
- type KeyKind
- type Level
- type Query
- type Resolution
- type Rule
Constants ¶
const ( // MaxPrefixes bounds Query.Prefixes (and so a Gate's member and owned kinds). MaxPrefixes = 10 // DefaultHeldLimit is the keys read per keyspace when GateConfig.HeldLimit is 0. DefaultHeldLimit = 1000 // MaxHeldLimit bounds GateConfig.HeldLimit. MaxHeldLimit = 10000 )
const ( ClassPublic = "public" // public rows ClassTier = "tier" // rows of a held tier ClassMembers = "members" // rows of a scope the viewer is a member of ClassOwned = "owned" // rows the viewer owns ClassCandidates = "candidates" // rows an incomplete keyspace may unlock: Gate.Settle decides them )
Branch classes.
const ( // MaxKeyBytes bounds a key: members:<64>:<64>:<36>. MaxKeyBytes = len(membersNamespace) + 1 + maxComponent + 1 + maxComponent + 1 + 36 )
Variables ¶
var ErrNotContentKey = errors.New("access: not a content key")
ErrNotContentKey is a string or reference outside the key grammar.
var ErrRule = errors.New("access: invalid rule")
ErrRule is a rule with an unknown level or a missing tier or scope.
ErrUnavailable wraps a failed billing read. The result it comes with is the fail-closed one: public items only, nothing for sale.
Functions ¶
func MembersPrefix ¶ added in v0.66.0
MembersPrefix is the keyspace of a scope kind: "members:<tenant>:<kind>:".
func OwnPrefix ¶ added in v0.66.0
OwnPrefix is the keyspace of a kind's owned items: "content:<tenant>:<kind>:".
func WithMemo ¶ added in v0.66.0
WithMemo returns ctx with a request memo: within it a viewer's Filter is read once and Decide answers from it (or from keys already read), so Filter then Decide is one billing read. ContentKit's content and media handlers install it; a host installs it in its own middleware. A ctx that already has one is returned unchanged.
Types ¶
type Actor ¶
type Actor struct {
ID string // stable subject id (uuid text); empty when Anonymous
Kind string // opaque: "user" | "service" | "delegated" | ...
IP string // anon fallback key for reactions / poll votes
Anonymous bool
}
Actor is the already-authenticated caller. ContentKit never authenticates.
type Answer ¶ added in v0.66.0
type Answer struct {
Keys map[string]bool // every requested key; absent means not held
Held map[string]HeldKeys // one entry per requested prefix
}
Answer answers a Query.
type Branch ¶ added in v0.66.0
type Branch struct{ Class, SQL string }
Branch is one access class's predicate.
type Columns ¶ added in v0.66.0
type Columns struct {
ID string // the item's uuid column: "p.id"
Kind string // the item kind, a declared OwnedKinds entry: "post"
Level string // an expression yielding the host's level value: "p.access_policy"
Levels map[string]Level // host value -> Level; rows of an unmapped value are never admitted
// Tier is an expression yielding a TierLevel row's tier name; TierName
// instead names the one declared tier every TierLevel value means.
Tier string
TierName string
// Scope is the membership scope's uuid column ("p.channel_id") and
// ScopeKind its declared MemberKinds entry ("channel"); required when a
// value maps to MembersLevel.
Scope string
ScopeKind string
ArgPrefix string // named-arg prefix; default "ck_"
}
Columns names a listing's access columns. Every string is trusted host SQL; never build one from user input.
type ContentResolver ¶
type ContentResolver interface {
Resolve(ctx context.Context, refs []contentref.ContentRef, actor Actor) (map[contentref.ContentKey]Resolution, error)
}
ContentResolver is the one mandatory content hook and the whole gating surface: it says whether each ContentRef exists, is visible and is accessible to the actor. It is batch-first: callers pass every ref a request needs in one call (a single item is a batch of one). The map is keyed by each requested ref's Key; a ref missing from it denies. An error fails the whole batch and denies every ref.
type Decision ¶ added in v0.66.0
type Decision struct {
Allowed bool
// Unknown: the billing read failed, so the item is denied and nothing is
// offered (the viewer may already own it).
Unknown bool
// Sell is what a paywall offers when not allowed (Rule.Sells).
Sell []Key
// Requires is the key a buyer must hold first (MembersPPV without the
// membership); nil when held or not needed.
Requires *Key
}
Decision is one item's verdict.
type Entitlements ¶ added in v0.66.0
Entitlements is ContentKit's only billing read: which keys subject holds now. One Held call is one read (adapters/openrails: one CheckEntitlements). A subject that cannot hold keys answers an empty Answer, not an error.
type Fact ¶ added in v0.66.0
type Fact struct {
// Ref is the canonical reference (see Resolution.Ref); zero keeps the
// requested one. Access is decided on its work.
Ref contentref.ContentRef
Visible bool // published and not deleted
Editor bool // may edit it (its creator, staff)
Bypass bool // host policy grants access regardless of the rule: staff, an operator, a channel reader
Withheld bool // not accessible to anyone yet (unreleased, held)
Owner string // see Resolution.Owner
Rule Rule
}
Fact is the host's knowledge of one item for one viewer.
type Facts ¶ added in v0.66.0
type Facts interface {
Facts(ctx context.Context, refs []contentref.ContentRef, actor Actor) (map[contentref.ContentKey]Fact, error)
}
Facts answers facts for a batch of refs in one query; an omitted ref denies.
type Filter ¶ added in v0.66.0
type Filter struct {
// contains filtered or unexported fields
}
Filter is one viewer's access as one billing read left it: the declared tiers they hold and their keys under each declared keyspace. It does no I/O: listings push it into SQL (Filter.SQL), loops call Allows. A Filter is immutable and safe for concurrent use.
func (*Filter) Allows ¶ added in v0.66.0
Allows decides it from the filter alone. known is false when the answer needs a key the filter did not read in full (a truncated or undeclared keyspace, a failed read); Gate.Settle and Gate.Decide resolve those. Allows and Filter.SQL agree: the SQL admits exactly the rows Allows allows or does not know, and on an Unknown filter only the allowed ones.
func (*Filter) Anonymous ¶ added in v0.66.0
Anonymous reports a filter of an anonymous viewer: it holds nothing.
func (*Filter) Branches ¶ added in v0.66.0
Branches splits SQL by access class for a UNION listing: run each branch with the keyset and LIMIT on its own index, then merge (the recipe in HOST_INTEGRATION "Listing with access"). Branches that can match nothing for this viewer are left out; none means an empty page. All share one args map.
func (*Filter) Complete ¶ added in v0.66.0
Complete reports whether Owned and Members of kind are the viewer's whole holdings: kind is a declared owned or member kind whose keyspaces were read without truncation. An anonymous filter is complete; an unknown one is not.
func (*Filter) Members ¶ added in v0.66.0
Members returns the scope ids (uuid text) of kind the viewer is a member of, sorted; for `= ANY(@ids::uuid[])`.
func (*Filter) SQL ¶ added in v0.66.0
SQL returns one boolean predicate over c admitting the rows the viewer may see, plus, while a keyspace is truncated, that keyspace's candidates (Gate.Settle decides them). An Unknown filter admits public rows only. The text depends only on c, so one prepared statement serves every viewer; args are pgx named args (pgx.NamedArgs), the form search.Eligibility takes:
(p.access_policy = ANY(@ck_open::text[]) OR (p.access_policy = ANY(@ck_members_levels::text[]) AND (p.channel_id = ANY(@ck_members::uuid[]) OR NOT @ck_members_complete::boolean)) OR (p.id = ANY(@ck_owned::uuid[]) AND p.access_policy = ANY(@ck_levels::text[])) OR (p.access_policy = ANY(@ck_paid_levels::text[]) AND NOT @ck_owned_complete::boolean))
type Gate ¶ added in v0.66.0
type Gate struct {
// contains filtered or unexported fields
}
Gate decides access to content from one billing read per viewer and request.
func (*Gate) Decide ¶ added in v0.66.0
Decide decides items for actor in order. It answers from the request's memoized Filter and keys where they suffice and reads every other key in one billing read (at most Own, Members and the tiers per item). A failed read denies the items it would have decided (Unknown) and returns them with an error wrapping ErrUnavailable; an invalid item (another tenant, a versioned ref, an invalid rule or undeclared tier) fails the call.
func (*Gate) Filter ¶ added in v0.66.0
Filter reads the viewer's access in one billing read: the declared tiers and every declared keyspace. Within WithMemo it is read once per request. An anonymous viewer costs no read, nor does a viewer whose every declared tier is claimed when no keyspace is declared. On a failed read Filter returns the fail-closed filter (Unknown) and an error wrapping ErrUnavailable: serve that filter (public rows only) or answer 503.
func (*Gate) Resolver ¶ added in v0.66.0
func (g *Gate) Resolver(f Facts) ContentResolver
Resolver turns host facts into the ContentResolver content and media use: one Facts call and at most one Decide per batch. Accessible is Visible && !Withheld && (Editor || Bypass || allowed). A failed billing read denies the items it decides; it does not fail the batch.
func (*Gate) Settle ¶ added in v0.66.0
func (g *Gate) Settle(ctx context.Context, actor Actor, f *Filter, items []Item) (keep []bool, short bool, err error)
Settle completes f's verdicts for a page: items f decides keep its answer, the rest (an incomplete keyspace's candidates) are decided in one exact read. keep[i] reports items[i] accessible; short reports a dropped item, so a hide-mode page may hold fewer rows than asked. It costs no read when f decides every item, so call it on every hide-mode page. A failed read drops the undecided items and returns an error wrapping ErrUnavailable.
type GateConfig ¶ added in v0.66.0
type GateConfig struct {
Entitlements Entitlements // required
Tenant string // required: the tenant of every item and scope
// Tiers are the declared tier names, the only bare keys ContentKit
// interprets. Rules may name no other.
Tiers []string
// MemberKinds are the scope kinds listings filter on ("channel") and
// OwnedKinds the item kinds ("post"): Filter reads each keyspace.
MemberKinds []string
OwnedKinds []string
// HeldLimit is the keys read per keyspace (default DefaultHeldLimit, at
// most MaxHeldLimit); a viewer holding more gets an incomplete Filter.
HeldLimit int
// Claims, optional, returns the verified token's entitlement claims. A
// declared tier among them is held without a billing read; an absent one
// is read live. It is never consulted for own or members keys. A refund
// then keeps tier access until the token expires.
Claims func(context.Context) []string
Logger *slog.Logger
}
GateConfig configures a Gate.
type HeldKeys ¶ added in v0.66.0
HeldKeys is the keys a subject holds under one prefix, in byte order.
type Item ¶ added in v0.66.0
type Item struct {
Ref contentref.ContentRef
Rule Rule
}
Item is one work and its rule.
type Key ¶ added in v0.66.0
type Key struct {
// contains filtered or unexported fields
}
Key is an entitlement key ContentKit gives meaning to. OpenRails stores keys opaquely; construct them here, never by hand. Keys are work-level: a version shares its work's key. The zero Key is invalid.
func MakeMembers ¶ added in v0.66.0
func MakeMembers(scope contentref.ContentRef) (Key, error)
MakeMembers returns the membership key of scope (a channel, series or collection), under the same rules as MakeOwn.
func MakeOwn ¶ added in v0.66.0
func MakeOwn(item contentref.ContentRef) (Key, error)
MakeOwn returns the key that owns item. It refuses an invalid or versioned reference and a tenant or kind outside [a-z0-9_-]{1,64}.
func Members ¶ added in v0.66.0
func Members(scope contentref.ContentRef) Key
Members is MakeMembers, panicking on an invalid reference.
func Own ¶ added in v0.66.0
func Own(item contentref.ContentRef) Key
Own is MakeOwn for constants and tests: it panics on an invalid reference.
func ParseKey ¶ added in v0.66.0
ParseKey parses a key string. Anything outside the grammar is ErrNotContentKey, including a bare string that is not a valid tier name.
func (Key) MarshalText ¶ added in v0.66.0
MarshalText refuses the zero Key.
func (Key) Ref ¶ added in v0.66.0
func (k Key) Ref() (contentref.ContentRef, bool)
Ref returns the work an own or members key names.
func (*Key) UnmarshalText ¶ added in v0.66.0
UnmarshalText parses with ParseKey.
type Query ¶ added in v0.66.0
type Query struct {
Keys []string // exact keys; an implementation over a bounded API chunks them
Prefixes []string // at most MaxPrefixes, each ending in ':'
Limit int // keys per prefix
}
Query is one billing read: exact keys and keyspace prefixes.
type Resolution ¶
type Resolution struct {
// Ref is the canonical reference every row is stored and read under (an
// alias or slug resolves to it). A zero Ref keeps the requested one, which
// content then refuses unless lower case; a Ref of another tenant is an
// error.
Ref contentref.ContentRef
// Visible = published and not soft-deleted. Media's public files (covers,
// previews) need only Visible to anonymous viewers.
Visible bool
// Accessible = the actor may consume it: an opaque host verdict
// (entitlement, purchase, ACL, flag). ContentKit imposes no access model.
// For media it is all or nothing: every private file of the item, or none.
Accessible bool
// Editor = the actor may edit the item (its creator, staff): media
// returns edit metadata and editor views only for editors, and gives a
// visible item's editor its private files whatever Accessible says.
Editor bool
// Owner is the actor id that owns the content (its creator), "" when no
// single user does. A comment ban in that owner's scope applies to it.
Owner string
}
Resolution is the host's verdict about a content reference for one actor.
func ResolveOne ¶ added in v0.34.0
func ResolveOne(ctx context.Context, r ContentResolver, ref contentref.ContentRef, actor Actor) (Resolution, error)
ResolveOne resolves a single ref as a batch of one; an omitted ref yields the zero (denying) Resolution.
func (Resolution) Full ¶
func (r Resolution) Full() bool
Full reports access to the item: everything private is served.
type Rule ¶ added in v0.66.0
type Rule struct {
Level Level
Tiers []string // TierLevel: any of these declared tiers
Scope contentref.ContentRef // MembersLevel, MembersPPV: the scope's work reference
}
Rule is an item's access rule, a host fact. Owning the item (Own) unlocks it at every level.
func (Rule) Sells ¶ added in v0.66.0
func (r Rule) Sells(item contentref.ContentRef) (sell []Key, requires *Key, err error)
Sells returns what a paywall offers for item under r: the tiers, the membership or the item itself. requires is the key a buyer must hold first (MembersPPV: the membership). Public sells nothing.
func (Rule) Unlocks ¶ added in v0.66.0
func (r Rule) Unlocks(item contentref.ContentRef) ([]Key, error)
Unlocks returns the keys any one of which unlocks item under r: Own(item) always, plus the rule's tiers or membership. A Public item needs none.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package accesstest provides an in-memory access.Entitlements and a counting wrapper, so host tests can assert "one billing read per page".
|
Package accesstest provides an in-memory access.Entitlements and a counting wrapper, so host tests can assert "one billing read per page". |