access

package
v0.85.0 Latest Latest
Warning

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

Go to latest
Published: Oct 13, 2026 License: MIT Imports: 15 Imported by: 2

Documentation

Overview

Package access is ContentKit's access model: the Actor; the key grammar (<tenant>:content:<id>, <tenant>:content:<id>/<version>, <tenant>:group:<id> and configured tiers); rules stored on content, versions and groups; the Gate deciding versions from one billing read per request, its listing Filter and SQL over content_facets; and the Billing dependency.

Index

Constants

View Source
const Anyone = ""

Anyone is the audience of a free row: an anonymous visitor.

View Source
const HeldLimit = 1000

HeldLimit is the keys one billing read returns per keyspace; a viewer holding more gets an incomplete Filter.

View Source
const (

	// MaxKeyBytes bounds a key: <tenant>:content:<id>/<version>.
	MaxKeyBytes = maxTenant + len(":content:") + 36 + 1 + 36
)

Variables

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

ErrNotContentKey is a string outside the key grammar.

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

ErrRule is a rule outside the grammar or naming an undeclared tier.

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: only free rules and claimed tiers open, nothing is offered.

Functions

func AudiencesSQL added in v0.83.0

func AudiencesSQL(alias string, tiers []string) (string, map[string]any, error)

AudiencesSQL is a text[] expression over the content_facets row aliased alias naming who opens it without buying it: Anyone and every one of tiers for a free row (public, or past free_after), the tiers among tiers its rule names for a tier row, nobody otherwise. Totals count rows per audience with it. Args are pgx named args prefixed audience_.

func ContentPrefix added in v0.83.0

func ContentPrefix(tenant string) string

ContentPrefix is the keyspace of a tenant's content and version keys.

func GroupCommentsModerate added in v0.83.0

func GroupCommentsModerate(kind string) string

GroupCommentsModerate: delete, hold and release comments on the group and the content it owns, and ban commenters from them.

func GroupContentWrite added in v0.83.0

func GroupContentWrite(kind string) string

GroupContentWrite: create, edit, publish and delete the content the group owns (owner_group), and see its drafts.

func GroupMediaWrite added in v0.83.0

func GroupMediaWrite(kind string) string

GroupMediaWrite: upload and edit the group's media.

func GroupOffersWrite added in v0.83.0

func GroupOffersWrite(kind string) string

GroupOffersWrite: put and withdraw the group's offers and its content's.

func GroupPermissions added in v0.83.0

func GroupPermissions(kind string) []string

GroupPermissions is every permission ContentKit checks in a group kind's AuthKit scopes.

func GroupPrefix added in v0.83.0

func GroupPrefix(tenant string) string

GroupPrefix is the keyspace of a tenant's group keys.

func GroupSettingsManage added in v0.83.0

func GroupSettingsManage(kind string) string

GroupSettingsManage: edit the group itself, delete or restore it, and put or withdraw its offers and its content's.

func Names added in v0.71.0

func Names(ps ...Permission) []string

Names is ps as strings, as an auth's role catalog declares them.

func ValidTenant added in v0.83.0

func ValidTenant(s string) bool

ValidTenant reports a tenant id usable in keys: [a-z0-9_-]{1,64}.

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 // the subject (auth.Identity.Subject); empty when Anonymous
	// Kind is the subject's kind: a person (auth.SubjectUser) or an
	// application (auth.SubjectApplication).
	Kind auth.SubjectKind
	// Invoker is who acts on the subject's behalf; zero when the subject acts
	// for itself.
	Invoker auth.Invoker
	// IP is the client's address (the request's RemoteAddr): the key of an
	// anonymous actor's reactions, votes and limits.
	IP        string
	Anonymous bool
	// Verified is the request's verified credential, kept for that request
	// alone: Can asks it. Nil for an anonymous actor.
	Verified auth.Verified
}

Actor is the caller. ContentKit's routes build it from the host's auth.Authenticator: the Verified's Identity, or anonymous when the request carries no credential.

func (Actor) Can added in v0.71.0

func (a Actor) Can(ctx context.Context, scope auth.Scope, permission string) (bool, error)

Can reports whether the actor holds exactly permission in scope, asking its Verified (auth.PermissionChecker). Anonymous, without a Verified or without the capability it holds nothing. An error never grants.

func (Actor) Person added in v0.71.0

func (a Actor) Person() bool

Person reports a signed-in person: a subject that is not an application.

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 Billing added in v0.83.0

type Billing interface {
	// Held answers q for subject in one read. A subject that cannot hold
	// keys (no customer id) answers an empty Answer, not an error.
	Held(ctx context.Context, subject string, q Query) (Answer, error)
	// Products reads the products with these keys, live or archived, each
	// with its live prices; a key without a product is absent.
	Products(ctx context.Context, keys []string) (map[string]Product, error)
	// CreateProducts creates products, in order.
	CreateProducts(ctx context.Context, products []NewProduct) ([]Product, error)
	// UpdateProducts renames, archives or restores products, in order. An
	// archived product is not sold; its buyers and subscribers keep it.
	UpdateProducts(ctx context.Context, updates []ProductUpdate) ([]Product, error)
	// CreatePrices creates prices, in order. Prices are immutable: equal terms
	// under a key answer the existing price, and other terms under the key of
	// a live price replace (archive) it.
	CreatePrices(ctx context.Context, prices []NewPrice) ([]Price, error)
	// ArchivePrices takes prices off sale; buyers and subscribers keep theirs.
	ArchivePrices(ctx context.Context, ids []string) error
	// Grant gives subjects products free and indefinitely.
	Grant(ctx context.Context, grants []Grant) error
	// Revoke takes back subjects' free grants of products; revoking what is
	// not granted changes nothing.
	Revoke(ctx context.Context, grants []Grant) error
	// StopRenewals stops every subscription to products from renewing: each
	// ends with its paid period. A stopped one stays as it is.
	StopRenewals(ctx context.Context, productIDs []string) error
}

Billing is ContentKit's billing dependency: which keys a viewer holds, the catalog edits behind offers, and free grants. adapters/openrails implements it over an embedded or a remote OpenRails. Every method is batch-shaped.

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 hold it).
	Unknown bool
	// Sell are the keys whose purchase opens it, the rule's own first (its
	// tiers or group), then the version's and the content's;
	// offers says which are on sale.
	Sell []Key
	// Requires is the key a buyer must hold first (GroupPPV), nil when held.
	Requires *Key
	// Preview is the part a viewer may read while it is locked.
	Preview *Preview
	// FreeAfter is when it opens to anyone.
	FreeAfter *time.Time
}

Decision is one item's verdict.

type Entitled added in v0.83.0

type Entitled interface {
	Entitlements() []string
}

Entitled is an optional capability of an actor's auth.Verified: the entitlement claims its verified token carries. The Gate answers a tier in kinds.Config.TokenClaims from them without a billing read, so a refund keeps such a tier until the token expires.

type Facet added in v0.83.0

type Facet struct {
	Level     Level
	Tiers     []string
	GroupID   *string
	FreeAfter *time.Time
}

Facet is a rule as the content_facets listing projection stores it: access_level, access_tiers, access_group_id and access_free_after.

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 configured tiers they hold and their keys in the tenant's content and group keyspaces. 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 keyspace, a failed read); Gate.Settle and Gate.Decide resolve those. Allows and SQL(Hide) 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) Complete added in v0.66.0

func (f *Filter) Complete() bool

Complete reports whether Contents, Versions and Groups are the viewer's whole holdings. An anonymous filter is complete; an unknown one is not.

func (*Filter) Contents added in v0.83.0

func (f *Filter) Contents() []string

Contents returns the content ids whose content key the viewer holds, sorted.

func (*Filter) Groups added in v0.83.0

func (f *Filter) Groups() []string

Groups returns the group ids whose key the viewer holds, sorted.

func (*Filter) SQL added in v0.66.0

func (f *Filter) SQL(alias string, mode Mode) (string, map[string]any, error)

SQL returns a boolean predicate over the content_facets row aliased alias, with its pgx named args (all prefixed access_). Hide admits the rows the viewer opens: free, a held tier the rule names, a held key of the rule's group or of a group holding the row (group_ids), a held content or version key; plus, while a keyspace is truncated, its candidates, which Gate.Settle decides. An Unknown filter admits no candidates. Lock admits every row. The text depends only on alias and mode, so one prepared statement serves every viewer. A members-only listing (listing = members) combines it: listing = 'listed' OR (listing = 'members' AND <SQL(Hide)>).

func (*Filter) Tier added in v0.66.0

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

Tier reports whether the viewer holds the configured tier name.

func (*Filter) Unknown added in v0.66.0

func (f *Filter) Unknown() bool

Unknown reports a failed billing read: only free rules and claimed tiers open, and nothing is for sale.

func (*Filter) Versions added in v0.83.0

func (f *Filter) Versions() map[string]string

Versions returns the held version keys: version id -> content id.

type Gate added in v0.66.0

type Gate struct {
	// contains filtered or unexported fields
}

Gate decides access to versions from one billing read per viewer and request (see WithMemo).

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. A failed read denies what it would have decided (Unknown) and returns an error wrapping ErrUnavailable. An item with an invalid id or rule is denied with nothing offered and logged.

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 configured tiers a token claim does not answer and the tenant's content and group keyspaces. Within WithMemo it is read once per request. An anonymous viewer costs no read. On a failed read Filter returns the fail-closed filter (Unknown) and an error wrapping ErrUnavailable: serve that filter or answer 503.

func (*Gate) Holds added in v0.83.0

func (g *Gate) Holds(ctx context.Context, actor Actor, keys []Key) ([]bool, error)

Holds reports, per key in order, whether actor holds it: from the request's memoized Filter and keys where they suffice, the rest in one billing read. An anonymous actor holds nothing and costs no read. A failed read answers false for what it would have decided and returns an error wrapping ErrUnavailable.

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 hide-mode page: items f decides keep its answer, the rest (candidates of a truncated keyspace) are decided in one exact read. keep[i] reports items[i] open; short reports a dropped item, so the page may hold fewer rows than asked. It costs no read when f decides every item. A failed read drops the undecided items and returns an error wrapping ErrUnavailable.

func (*Gate) Tenant added in v0.83.0

func (g *Gate) Tenant() string

Tenant is the gate's tenant.

func (*Gate) Tiers added in v0.83.0

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

Tiers returns the configured tiers, sorted.

type GateConfig added in v0.66.0

type GateConfig struct {
	// Tenant is the tenant of every key: [a-z0-9_-]{1,64}.
	Tenant string
	// Kinds supplies the configured tiers, the only bare keys ContentKit
	// interprets, and the token claims answering them without a billing read.
	Kinds kinds.Config
	// Billing answers held keys; nil holds none for anyone, so only free
	// rules and claimed tiers open.
	Billing Billing
	Logger  *slog.Logger
}

GateConfig configures a Gate from the engine's config.

type Grant added in v0.83.0

type Grant struct {
	Subject   string
	ProductID string
}

Grant names a subject and a product given free.

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 {
	Content string
	Version string
	// Rule is the version's effective rule (Effective).
	Rule Rule
	// Groups are the groups holding the content or version (content_facets
	// group_ids): a held key of any of them opens it.
	Groups []string
}

Item is one version to decide (or a content item as a whole, Version "").

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. The zero Key is invalid.

func ContentKey added in v0.83.0

func ContentKey(tenant, id string) (Key, error)

ContentKey is the key of content id: it opens every version.

func GroupKey added in v0.83.0

func GroupKey(tenant, id string) (Key, error)

GroupKey is the key of group id.

func ParseKey added in v0.66.0

func ParseKey(s string) (Key, error)

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

func TierKey added in v0.83.0

func TierKey(name string) (Key, error)

TierKey is a tier's key, its bare name: ^[a-z][a-z0-9_-]{0,62}$.

func VersionKey added in v0.83.0

func VersionKey(tenant, content, version string) (Key, error)

VersionKey is the key of one version of content.

func (Key) Content added in v0.83.0

func (k Key) Content() string

Content is the content id of a content or version key.

func (Key) Group added in v0.83.0

func (k Key) Group() string

Group is the group id of a group key.

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) 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) Tenant added in v0.83.0

func (k Key) Tenant() string

Tenant is the tenant of a content, version or group key.

func (Key) Tier added in v0.66.0

func (k Key) Tier() string

Tier is the name of a tier key.

func (*Key) UnmarshalText added in v0.66.0

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

UnmarshalText parses with ParseKey.

func (Key) Version added in v0.83.0

func (k Key) Version() string

Version is the version id of a version key.

type KeyKind added in v0.66.0

type KeyKind uint8

KeyKind says what an entitlement key grants.

const (
	KeyContent KeyKind = iota + 1 // <tenant>:content:<id>: every version of one content item
	KeyVersion                    // <tenant>:content:<id>/<version>: one version
	KeyGroup                      // <tenant>:group:<id>: every member of a group, and content whose rule names it
	KeyTier                       // <tier>: a tier from config, such as premium
)

type Level added in v0.66.0

type Level string

Level is a rule's access level.

const (
	Public     Level = "public"    // anyone
	TierLevel  Level = "tier"      // holders of any of Rule.Tiers
	GroupLevel Level = "group"     // holders of Rule.Group's key: members-only content
	PPV        Level = "ppv"       // buyers of the content or version
	GroupPPV   Level = "group_ppv" // buyers; buying needs Rule.Group's key first
)

type Mode added in v0.83.0

type Mode string

Mode is a listing's access mode.

const (
	// Hide lists only what the viewer opens (a purchased library).
	Hide Mode = "hide"
	// Lock lists everything; Decide marks what stays locked.
	Lock Mode = "lock"
)

type NewPrice added in v0.83.0

type NewPrice struct {
	ProductID     string
	Key           string
	Amount        int64
	Currency      string
	AccessHours   *int
	IntervalHours *int
	// PSPs are the payment providers selling it.
	PSPs []string
}

NewPrice creates a price on a product.

type NewProduct added in v0.83.0

type NewProduct struct {
	Key                  string
	DisplayName          string
	Entitlements         []string
	RequiredEntitlements []string
}

NewProduct creates a product.

type Permission added in v0.71.0

type Permission string

Permission is a ContentKit staff permission: a full root:<resource>:<action> name, held in the root scope of the host's auth (auth.Root) and checked there by the Verified's Can. ContentKit names them; a host declares them in its auth's roles from these constants (AuthKit: rbac.Root.Declare) and never spells one out.

const (
	// ContentWrite: create, edit, publish, delete and restore any content
	// item, and see everything.
	ContentWrite Permission = "root:content:write"
	// ContentViewRestricted: see every content item, whatever its state,
	// listing, site scope or access, and the site's content statistics.
	ContentViewRestricted Permission = "root:content:view-restricted"
	// PollsWrite: create, edit and delete polls, their options and images,
	// and see scheduled and inactive ones.
	PollsWrite Permission = "root:polls:write"
	// CommentsModerate: edit, delete and restore anyone's comments, and list
	// every comment.
	CommentsModerate Permission = "root:comments:moderate"
	// CommentsBan: ban users from commenting anywhere in the tenant.
	CommentsBan Permission = "root:comments:ban"
	// ModerationReview: list and resolve held comments and content.
	ModerationReview Permission = "root:moderation:review"
	// GroupsWrite: create, edit, merge and delete groups, and set content's
	// memberships.
	GroupsWrite Permission = "root:groups:write"
	// OffersWrite: put and withdraw offers on any content, version or group;
	// creators and group owners need it for nobody else's.
	OffersWrite Permission = "root:offers:write"
	// NotificationsPublish: publish notices to users' inboxes (a host service).
	NotificationsPublish Permission = "root:notifications:publish"
	// ImportWrite: bulk-import content, groups, interactions and signals (a
	// legacy importer).
	ImportWrite Permission = "root:import:write"
)

func Permissions added in v0.71.0

func Permissions() []Permission

Permissions is every staff permission.

func (Permission) String added in v0.71.0

func (p Permission) String() string

type Preview added in v0.83.0

type Preview struct {
	Pages    int `json:"pages,omitempty"`
	Segments int `json:"segments,omitempty"`
}

Preview is the free part of locked content: its first Pages pages or first Segments segments (chapters).

type Price added in v0.83.0

type Price struct {
	ID        string
	ProductID string
	Key       string
	Amount    int64
	Currency  string
	// AccessHours is how long a purchase opens it; nil is forever.
	AccessHours *int
	// IntervalHours renews it every so many hours; nil is a one-time charge.
	IntervalHours *int
	Archived      bool
}

Price is one way to buy a product: Amount micros of Currency.

type Product added in v0.83.0

type Product struct {
	ID           string
	Key          string
	DisplayName  string
	Entitlements []string
	// RequiredEntitlements are the keys a buyer must already hold: OpenRails
	// refuses anyone else's order (entitlement_required).
	RequiredEntitlements []string
	Archived             bool
	Prices               []Price // its live prices
}

Product is a billing product: ContentKit sells each access key as one product whose key and only entitlement is that key.

type ProductUpdate added in v0.83.0

type ProductUpdate struct {
	ID          string
	DisplayName *string
	Archived    *bool
	// RequiredEntitlements replaces them; an empty list clears them.
	RequiredEntitlements *[]string
}

ProductUpdate changes a product's set fields.

type Query added in v0.66.0

type Query struct {
	Keys     []string // exact keys
	Prefixes []string // each ending in ':'
	Limit    int      // keys per prefix; 0 is HeldLimit
}

Query is one entitlement read: exact keys and keyspace prefixes.

type Rule added in v0.66.0

type Rule struct {
	Level Level    `json:"level"`
	Tiers []string `json:"tiers,omitempty"`    // TierLevel: any of these configured tiers
	Group string   `json:"group_id,omitempty"` // GroupLevel, GroupPPV
	// Preview is the part a viewer who does not open it may still read.
	Preview *Preview `json:"preview,omitempty"`
	// FreeAfter makes it public from then on.
	FreeAfter *time.Time `json:"free_after,omitempty"`
}

Rule is the access rule stored as JSON on content (content_items.access), on a version as its override (content_versions.access) and on a sellable group (content_groups.access). Whatever the level, a version also opens to holders of its version key, its content key and the key of any group holding it.

func Effective added in v0.83.0

func Effective(content Rule, override *Rule) Rule

Effective is a version's rule: its override, else its content's.

func (Rule) Facet added in v0.83.0

func (r Rule) Facet() Facet

Facet projects r onto content_facets' access columns.

func (Rule) Free added in v0.83.0

func (r Rule) Free(now time.Time) bool

Free reports whether r opens to anyone at now: public, or past FreeAfter.

func (*Rule) UnmarshalJSON added in v0.83.0

func (r *Rule) UnmarshalJSON(b []byte) error

UnmarshalJSON refuses unknown fields; Validate checks the rest.

func (Rule) Validate added in v0.83.0

func (r Rule) Validate(cfg kinds.Config) error

Validate checks r against cfg: a known level with exactly its own fields, configured tiers, a valid group id, and options only on a rule that locks.

Jump to

Keyboard shortcuts

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