access

package
v0.66.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 9 Imported by: 0

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

View Source
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
)
View Source
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.

View Source
const (

	// MaxKeyBytes bounds a key: members:<64>:<64>:<36>.
	MaxKeyBytes = len(membersNamespace) + 1 + maxComponent + 1 + maxComponent + 1 + 36
)

Variables

View Source
var ErrNotContentKey = errors.New("access: not a content key")

ErrNotContentKey is a string or reference outside the key grammar.

View Source
var ErrRule = errors.New("access: invalid rule")

ErrRule is a rule with an unknown level or a missing tier or scope.

View Source
var ErrUnavailable = errors.New("access: entitlements unavailable")

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

func MembersPrefix(tenant, kind string) string

MembersPrefix is the keyspace of a scope kind: "members:<tenant>:<kind>:".

func OwnPrefix added in v0.66.0

func OwnPrefix(tenant, kind string) string

OwnPrefix is the keyspace of a kind's owned items: "content:<tenant>:<kind>:".

func WithMemo added in v0.66.0

func WithMemo(ctx context.Context) context.Context

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

type Entitlements interface {
	Held(ctx context.Context, subject string, q Query) (Answer, error)
}

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

func (f *Filter) Allows(it Item) (allowed, known bool)

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

func (f *Filter) Anonymous() bool

Anonymous reports a filter of an anonymous viewer: it holds nothing.

func (*Filter) Branches added in v0.66.0

func (f *Filter) Branches(c Columns) ([]Branch, map[string]any, error)

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

func (f *Filter) Complete(kind string) bool

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

func (f *Filter) Members(kind string) []string

Members returns the scope ids (uuid text) of kind the viewer is a member of, sorted; for `= ANY(@ids::uuid[])`.

func (*Filter) Owned added in v0.66.0

func (f *Filter) Owned(kind string) []string

Owned returns the item ids of kind the viewer owns, sorted.

func (*Filter) SQL added in v0.66.0

func (f *Filter) SQL(c Columns) (string, map[string]any, error)

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))

func (*Filter) Tier added in v0.66.0

func (f *Filter) Tier(name string) bool

Tier reports whether the viewer holds the declared tier name.

func (*Filter) Unknown added in v0.66.0

func (f *Filter) Unknown() bool

Unknown reports a failed billing read: only public items (and tiers the Claims shortcut vouches for) are allowed, and nothing is for sale.

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 NewGate added in v0.66.0

func NewGate(c GateConfig) (*Gate, error)

NewGate validates c.

func (*Gate) Decide added in v0.66.0

func (g *Gate) Decide(ctx context.Context, actor Actor, items []Item) ([]Decision, error)

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

func (g *Gate) Filter(ctx context.Context, actor Actor) (*Filter, error)

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.

func (*Gate) TierKeys added in v0.66.0

func (g *Gate) TierKeys() []string

TierKeys returns the declared tiers, sorted: the allowlist of tier names a host may show from token claims.

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

type HeldKeys struct {
	Keys      []string
	Truncated bool // more than Query.Limit keys are held
}

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 MakeTier added in v0.66.0

func MakeTier(name string) (Key, error)

MakeTier returns a tier key: ^[a-z][a-z0-9_-]{0,62}$.

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

func ParseKey(s string) (Key, error)

ParseKey parses a key string. Anything outside the grammar is ErrNotContentKey, including a bare string that is not a valid tier name.

func Tier added in v0.66.0

func Tier(name string) Key

Tier is MakeTier, panicking on an invalid name.

func (Key) Kind added in v0.66.0

func (k Key) Kind() KeyKind

Kind returns what the key grants; 0 for the zero Key.

func (Key) MarshalText added in v0.66.0

func (k Key) MarshalText() ([]byte, error)

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) String added in v0.66.0

func (k Key) String() string

String is the key as OpenRails stores it; "" for the zero Key.

func (Key) Tier added in v0.66.0

func (k Key) Tier() (string, bool)

Tier returns a tier key's name.

func (*Key) UnmarshalText added in v0.66.0

func (k *Key) UnmarshalText(b []byte) error

UnmarshalText parses with ParseKey.

type KeyKind added in v0.66.0

type KeyKind uint8

KeyKind says what an entitlement key grants.

const (
	KeyOwn     KeyKind = iota + 1 // content:<tenant>:<kind>:<id>: owning one item; unlocks it at any level
	KeyMembers                    // members:<tenant>:<kind>:<id>: membership of a scope (channel, series)
	KeyTier                       // <tier>: a merchant-wide tier such as premium
)

type Level added in v0.66.0

type Level string

Level is an item's access level.

const (
	Public       Level = "public"      // anyone
	TierLevel    Level = "tier"        // holders of any of Rule.Tiers
	MembersLevel Level = "members"     // members of Rule.Scope
	PPV          Level = "ppv"         // owners of the item
	MembersPPV   Level = "members_ppv" // owners; buying it requires membership of Rule.Scope
)

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".

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL