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
- Variables
- func AudiencesSQL(alias string, tiers []string) (string, map[string]any, error)
- func ContentPrefix(tenant string) string
- func GroupCommentsModerate(kind string) string
- func GroupContentWrite(kind string) string
- func GroupMediaWrite(kind string) string
- func GroupOffersWrite(kind string) string
- func GroupPermissions(kind string) []string
- func GroupPrefix(tenant string) string
- func GroupSettingsManage(kind string) string
- func Names(ps ...Permission) []string
- func ValidTenant(s string) bool
- func WithMemo(ctx context.Context) context.Context
- type Actor
- type Answer
- type Billing
- type Decision
- type Entitled
- type Facet
- type Filter
- func (f *Filter) Allows(it Item) (allowed, known bool)
- func (f *Filter) Anonymous() bool
- func (f *Filter) Complete() bool
- func (f *Filter) Contents() []string
- func (f *Filter) Groups() []string
- func (f *Filter) SQL(alias string, mode Mode) (string, map[string]any, error)
- func (f *Filter) Tier(name string) bool
- func (f *Filter) Unknown() bool
- func (f *Filter) Versions() map[string]string
- 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) Holds(ctx context.Context, actor Actor, keys []Key) ([]bool, error)
- func (g *Gate) Settle(ctx context.Context, actor Actor, f *Filter, items []Item) (keep []bool, short bool, err error)
- func (g *Gate) Tenant() string
- func (g *Gate) Tiers() []string
- type GateConfig
- type Grant
- type HeldKeys
- type Item
- type Key
- type KeyKind
- type Level
- type Mode
- type NewPrice
- type NewProduct
- type Permission
- type Preview
- type Price
- type Product
- type ProductUpdate
- type Query
- type Rule
Constants ¶
const Anyone = ""
Anyone is the audience of a free row: an anonymous visitor.
const HeldLimit = 1000
HeldLimit is the keys one billing read returns per keyspace; a viewer holding more gets an incomplete Filter.
const ( // MaxKeyBytes bounds a key: <tenant>:content:<id>/<version>. MaxKeyBytes = maxTenant + len(":content:") + 36 + 1 + 36 )
Variables ¶
var ErrNotContentKey = errors.New("access: not a content key")
ErrNotContentKey is a string outside the key grammar.
var ErrRule = errors.New("access: invalid rule")
ErrRule is a rule outside the grammar or naming an undeclared tier.
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
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
ContentPrefix is the keyspace of a tenant's content and version keys.
func GroupCommentsModerate ¶ added in v0.83.0
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
GroupContentWrite: create, edit, publish and delete the content the group owns (owner_group), and see its drafts.
func GroupMediaWrite ¶ added in v0.83.0
GroupMediaWrite: upload and edit the group's media.
func GroupOffersWrite ¶ added in v0.83.0
GroupOffersWrite: put and withdraw the group's offers and its content's.
func GroupPermissions ¶ added in v0.83.0
GroupPermissions is every permission ContentKit checks in a group kind's AuthKit scopes.
func GroupPrefix ¶ added in v0.83.0
GroupPrefix is the keyspace of a tenant's group keys.
func GroupSettingsManage ¶ added in v0.83.0
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
ValidTenant reports a tenant id usable in keys: [a-z0-9_-]{1,64}.
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 // 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.
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
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
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
Anonymous reports a filter of an anonymous viewer: it holds nothing.
func (*Filter) Complete ¶ added in v0.66.0
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
Contents returns the content ids whose content key the viewer holds, sorted.
func (*Filter) Groups ¶ added in v0.83.0
Groups returns the group ids whose key the viewer holds, sorted.
func (*Filter) SQL ¶ added in v0.66.0
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
Tier reports whether the viewer holds the configured tier name.
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 (*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. 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
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
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.
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 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 {
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
ContentKey is the key of content id: it opens every version.
func ParseKey ¶ added in v0.66.0
ParseKey parses a key string. Anything outside the grammar is ErrNotContentKey, a bare string that is not a valid tier name included.
func VersionKey ¶ added in v0.83.0
VersionKey is the key of one version of content.
func (Key) MarshalText ¶ added in v0.66.0
MarshalText refuses the zero Key.
func (*Key) UnmarshalText ¶ added in v0.66.0
UnmarshalText parses with ParseKey.
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 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
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 (Rule) Free ¶ added in v0.83.0
Free reports whether r opens to anyone at now: public, or past FreeAfter.
func (*Rule) UnmarshalJSON ¶ added in v0.83.0
UnmarshalJSON refuses unknown fields; Validate checks the rest.