Documentation
¶
Overview ¶
Package bless manages trust lists for projects. Identities are "blessed" to grant access or "cursed" to block it.
Trust lives in a BLESS file, one identity per line. A leading + marks a maintainer, a leading - marks a curse, and a bare identity is a contributor. An @ref line inherits another project's list.
An identity is written "kind:value", where the kind is the namespace it belongs to: "email:alice@example.com", "ssh:SHA256:abc", "github:alice". Any prefix is a kind. Written with no prefix an identity has KindNone, which is its own namespace rather than a wildcard, so a bare entry never matches a qualified lookup. A leading colon escapes a bare value that contains one, so ":SHA256:abc" is the bare identity "SHA256:abc".
Use Parse to read a file and File.Check to look up one identity, or File.Lookup when you also want the entry the role came from and its reason. Resolver follows @ref inheritance across projects through a Loader.
A file is one ordered sequence of lines. Read it with File.Entries and File.Inherits, which return copies, and change it through File.Add, File.Remove, File.Bless and File.Curse, so an edit can never move an entry away from the comment above it.
bless answers what role an identity has. It does not model what a role may do, so pushing, releasing or moderating stay the caller's vocabulary.
Index ¶
- Constants
- Variables
- type Entry
- type File
- func (f *File) Add(e Entry)
- func (f *File) AddInherit(ref string)
- func (f *File) Bless(identity Identity, reason string, asMaintainer bool)
- func (f *File) Check(identity Identity) Role
- func (f *File) Curse(identity Identity, reason string)
- func (f *File) Encode() ([]byte, error)
- func (f *File) Entries() []Entry
- func (f *File) Inherits() []Inherit
- func (f *File) Lookup(identity Identity) (Entry, bool)
- func (f *File) Remove(identity Identity) bool
- func (f *File) RemoveInherit(ref string) bool
- func (f *File) Validate() error
- type Identity
- type Inherit
- type Kind
- type Loader
- type Option
- type Resolver
- type Result
- type Role
Constants ¶
const BlessFile = "BLESS"
BlessFile is the conventional name of a project's trust file.
Variables ¶
ErrEmptyIdentity is returned by ParseIdentity when there is no value left after the kind.
ErrInvalidIdentity reports an identity that File.Encode cannot write and Parse read back unchanged.
ErrInvalidReason reports a reason that File.Encode cannot write and Parse read back unchanged.
ErrInvalidRef reports an inherit ref that is empty or contains whitespace. Whitespace usually means a trailing comment, which the format does not support: comments go on their own line.
ErrInvalidRole reports an entry whose Role is not one of the defined roles. There is no prefix to write for it, so it would encode as a plain line and read back as a contributor.
ErrMaxDepth reports that an inheritance chain was cut short by WithMaxDepth. The search is incomplete, so RoleUnknown means "not found within the limit" rather than "not trusted".
ErrMaxRefs reports that a search examined the maximum number of refs set by WithMaxRefs. The search is incomplete, so callers must fail closed.
ErrNoLoader reports a resolver built without a Loader. The search cannot start, so callers must treat the result as unknown and fail closed.
ErrNoRole reports an entry whose Role was never set. The zero Role means "not found" when looking up, so it cannot also mean "contributor" when writing.
ErrNotFound is returned by Loader when a BLESS file does not exist. This is distinct from a parse error. A missing file is not a failure, it means no trust data is available at that ref.
Functions ¶
This section is empty.
Types ¶
type Entry ¶
type Entry struct {
// Role is the trust level: maintainer (+), contributor (no prefix), or cursed (-).
Role Role
// Identity is who the entry is about, qualified by its namespace.
Identity Identity
// Reason is an optional human-readable note, mainly used for curses.
Reason string
}
Entry represents a single identity in a BLESS file.
type File ¶
type File struct {
// contains filtered or unexported fields
}
File is a BLESS file: one ordered sequence of lines, holding entries, inherit refs, comments and blanks in the order they were written.
The sequence is the whole document. There is no second view of it to keep in step, so an entry cannot drift away from the comment above it and File.Encode can write the file back with its structure intact.
Read it with File.Entries and File.Inherits, change it with File.Add, File.Remove, File.Bless and File.Curse. The zero File is an empty file, ready to use.
func Parse ¶
Parse reads a BLESS file from raw bytes. CRLF input is accepted and normalized to LF by File.Encode.
One entry per line: an optional role prefix, an identity, then an optional reason. A leading + is a maintainer, a leading - is a curse, and no prefix is a contributor. An @ref line inherits another project's list.
Format:
# Comment +alice@example.com bob@example.com -spammer@example.com AI slop email:carol@example.com ssh:SHA256:abc :SHA256:abc @~alice/other-project
An identity is "kind:value", see ParseIdentity. Written bare it has KindNone, which is its own namespace rather than a wildcard.
A comment must be on its own line. There are no trailing comments, so an @ref containing whitespace is an error rather than a ref that silently never resolves.
func (*File) Add ¶
Add appends a new entry to the end of the file. The identity kind is stored lowercase.
Entries are not sorted. A BLESS file is a document, and moving an entry would slide it under whatever comment happens to sit above its new position. Order it by hand if you want it ordered.
func (*File) AddInherit ¶
AddInherit appends an inherit ref to the end of the file.
func (*File) Bless ¶
Bless replaces every matching entry with one contributor or maintainer. asMaintainer selects the exact role, so false demotes a maintainer. reason replaces any prior reason.
func (*File) Check ¶
Check returns the role for the given identity in this file only, or RoleUnknown if it is not listed. See File.Lookup for the deciding entry and its reason.
func (*File) Curse ¶
Curse blocks an identity. Any existing blessings are removed first.
func (*File) Encode ¶
Encode serializes the trust file back to BLESS format. Every non-empty document ends with a newline. If the file was parsed with Parse, comments and blank lines are preserved. Entries added via File.Add are appended after the last line.
Whatever Encode returns, Parse accepts and reads back with the same roles, identities and reasons. A file that could not satisfy that is an error, see File.Validate.
func (*File) Entries ¶
Entries returns the file's entries in document order.
The result is a copy. Changing it does not change the file, so a caller cannot reorder entries out from under the comments that describe them. Use File.Add, File.Remove, File.Bless and File.Curse to make changes.
func (*File) Inherits ¶
Inherits returns the file's inherit refs in document order. As with File.Entries, the result is a copy.
func (*File) Lookup ¶
Lookup returns the entry that decides the given identity's role in this file, and whether one was found. It does not follow inheritance directives, use Resolver for that.
An identity may be listed more than once. The strongest claim wins, so a curse beats a blessing anywhere in the file. Among equals the first wins.
This is the only place a local claim is selected. A role and the entry it came from are chosen together, so they cannot disagree.
func (*File) Remove ¶
Remove deletes every entry matching the given identity, along with the lines they came from. Comments and blanks around them are left alone. Returns true if any entries were removed.
func (*File) RemoveInherit ¶
RemoveInherit deletes every inherit ref equal to ref, along with the lines they came from. Returns true if any were removed.
func (*File) Validate ¶
Validate reports every entry and inherit that File.Encode could not write back faithfully. Problems are joined, so one call surfaces all of them.
A file from Parse is always valid. These are mistakes only reachable by building a File in Go, where a struct literal can leave a field unset.
type Identity ¶
type Identity struct {
// Kind is the namespace. Empty means the identity was written bare.
Kind Kind
// Value is the identifier within that namespace.
Value string
}
Identity is a participant, qualified by the namespace it belongs to.
Two identities of different kinds never match, so "email:alice" and "username:alice" are different people. An identity written with no prefix has KindNone, which is its own namespace rather than a wildcard.
Identity is comparable, but == is not the same question as Identity.Matches. Two values that differ only by Unicode spelling are unequal yet match. Matches is the semantic comparison.
func Bare ¶
Bare returns an identity with no kind. It is not a wildcard, it only matches other bare identities.
func ParseIdentity ¶
ParseIdentity reads the wire form, "kind:value" or "value".
A leading colon means an empty kind whose value contains a colon, so ":SHA256:abc" is the bare identity "SHA256:abc". Without it the text before the first colon is always the kind.
The kind is lowercased. The value is left as written.
func (Identity) Matches ¶
Matches reports whether two identities are the same participant.
Kinds must agree, compared without regard to case. Values are then compared by the rule for that kind:
ssh exact bytes, no normalization
email NFC, then the local part exactly and the domain as DNS sees
it, which is IDN-aware and case-insensitive
username the RFC 8265 PRECIS UsernameCaseMapped profile, exact if rejected
bare NFC, then folded
any other exact bytes, no normalization
A fingerprint is an opaque token rather than text, and for an unrecognized kind there is no way to tell which it is, so neither is normalized. For an authorization library the dangerous direction is matching more than intended.
type Inherit ¶
type Inherit struct {
// Ref is the target to inherit trust from.
// Examples: "~alice/compiler", "~org/shared-trust"
Ref string
}
Inherit represents a trust inheritance directive (@ref in a BLESS file).
type Kind ¶
type Kind string
Kind is the namespace an identity belongs to. Its canonical form is lowercase. The set is open: any prefix before the first colon is a kind, so a new platform needs no release.
const ( // KindNone is an identity written with no prefix. Compares folded. KindNone Kind = "" // KindEmail is an email address. Per RFC 5321 the local part compares // exactly and the domain as DNS, so Unicode and punycode spellings of a // domain are one domain. KindEmail Kind = "email" // KindSSH is an SSH key fingerprint. Compares as exact bytes, because // base64 is case-significant. KindSSH Kind = "ssh" // KindUsername is a username on some platform. Compares by the RFC 8265 // PRECIS UsernameCaseMapped profile. KindUsername Kind = "username" )
Kinds bless knows the comparison rules for. Any other prefix is still a valid kind, it just compares exactly.
type Loader ¶
type Loader interface {
// LoadTrust loads a BLESS file by reference.
// The ref format is defined by the consumer:
// "~alice/my-repo" - a repo on the forge
// "./path" - a filesystem path (for CLI tools)
//
// Refs come from BLESS files and are untrusted input. A loader must reject
// refs outside its allowed paths, schemes, hosts or namespaces before using
// them in filesystem or network operations.
//
// Returns nil and [ErrNotFound] if the ref has no trust file, nil and the
// error for anything else (permission denied, I/O).
//
// A nil error must come with a non-nil file. Returning neither is a broken
// loader, which the resolver reports as an error rather than treating as a
// ref with no trust data.
//
// Resolver serializes calls to LoadTrust. A returned file must not be mutated
// while any resolution may be reading it.
LoadTrust(ref string) (*File, error)
}
Loader is the interface consumers implement to provide trust files from their storage backend. A git forge reads from git trees, a CLI tool reads from the filesystem, tests use in-memory maps.
type Option ¶
type Option func(*Resolver)
Option configures a Resolver at construction.
func WithMaxDepth ¶
WithMaxDepth limits how deep the resolver follows inheritance chains. It bounds work on long chains. Repeated refs are handled separately, so a chain that loops back terminates regardless of this setting.
A chain cut short by this limit is reported as ErrMaxDepth, including at 0, since a ref that went unread is a ref that went unread.
The default is 10. Non-positive values disable inheritance, which still checks the file at the starting ref.
func WithMaxRefs ¶
WithMaxRefs limits how many refs one resolution examines. This bounds work across broad graphs as WithMaxDepth bounds work across deep graphs. Repeated directives count each time they are examined.
A search cut short by this limit is reported as ErrMaxRefs. The default is 1000. Non-positive values allow only the starting ref.
type Resolver ¶
type Resolver struct {
// contains filtered or unexported fields
}
Resolver walks trust inheritance chains to determine an identity's role.
A Resolver is fixed once built, so one may be shared by concurrent Resolver.Resolve calls.
func (*Resolver) Resolve ¶
Resolve determines the trust role for an identity starting from the given ref.
Resolution order:
- Check the BLESS file at ref. Its own entries settle the question.
- Otherwise read every inherited list and combine them.
- Return RoleUnknown if nothing matches anywhere.
A file is sovereign over the lists it inherits. Its own entries win outright, so a project can bless someone an upstream list curses.
Among the inherited lists there is no such precedence. The strongest claim wins: cursed, then maintainer, then contributor. Inheriting a list means accepting its curses.
The outcome never depends on the order @ref lines appear in. Ties go to the shallower source, then to the lower ref name.
A missing file is an ordinary no-match: a Loader returning ErrNotFound contributes nothing and is not an error.
Any other reason the search did not finish is returned as an error: a loader failure, a chain cut short by WithMaxDepth, or a graph cut short by WithMaxRefs. The error is returned even when a role was found, because a branch that went unread could have contributed a stronger claim. Treat a non-nil error as "this answer may be wrong" and fail closed.
A ref is explored once per useful depth. A cycle terminates rather than erroring, and so does a diamond, which is the same shape from here.
type Result ¶
type Result struct {
// Role is the resolved trust level.
Role Role
// Source is the ref that provided the match (e.g. "~alice/repo").
//
// When several inherited lists match at the same role, this is the
// shallowest of them, and the lowest ref name among equals.
//
// Empty if Role is RoleUnknown.
Source string
// Entry is the entry that decided Role, so its role and reason always
// describe the claim this result acted on. Nil when Role is RoleUnknown.
Entry *Entry
}
Result is the outcome of a trust resolution.
type Role ¶
type Role int
Role represents a participant's trust level.
const ( // RoleUnknown means the identity was not found in any trust file. RoleUnknown Role = iota // RoleContributor means the identity has been blessed (no prefix in BLESS). RoleContributor // RoleMaintainer means the identity has elevated trust (+ prefix in BLESS). // What that permits is the consumer's decision, bless does not model // actions. RoleMaintainer // RoleCursed means the identity has been explicitly blocked (- prefix in BLESS). RoleCursed )
Declaration order carries no meaning. Precedence lives in [Role.rank], so these can be reordered or added to without changing how duplicates or inherited claims resolve.
func (Role) IsAllowed ¶
IsAllowed reports whether the role permits participation. Maintainers and contributors are allowed. Cursed and unknown are not.
Source Files
¶
- bless.go
- doc.go
- entry.go
- identity.go
- resolve.go
- role.go