Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

permbook source pure

Package permbook is a bounded, authorization-carrying facade over gno.land/p/nt/groups/v0. It lets a realm define nam...

Overview

Package permbook is a bounded, authorization-carrying facade over gno.land/p/nt/groups/v0. It lets a realm define named permissions, grant them to addresses, revoke them, and ask "does this address currently hold this permission" without iterating anything.

It holds no coins, imports no banker, stores no callbacks, and owns no package-level state. The importing realm allocates a *Book and keeps it private.

What this package adds, and what it does not

It adds exactly four things to groups, and deliberately nothing else:

  1. an ADMIN bound to the book, with a two-step handoff;
  2. BOUNDS on permission count, holders per permission, and name shape;
  3. permission semantics instead of membership semantics — no base set, no metadata slot, and an empty permission is pruned rather than kept;
  4. a cross-realm surface that is safe by CONSTRUCTION — the *groups.Group is unexported and no method returns a mutable handle to it.

Everything else — the B+-tree registry, the member sets, ordered iteration, the readonly views — comes from groups. This package writes no data-structure code.

The query

Example
1if !book.Has("withdraw", who) {
2	panic("not authorized")
3}

Has costs two independent B+-tree descents: O(log P) to find the permission, O(log H) to find the holder. It is NOT a function of how many permissions `who` already holds. That matters: every other named-permission implementation surveyed answers this by iterating the subject's permissions, which makes the most-privileged address the most expensive to check — the wrong asymptotic for an authorization check on a hot path.

Authorization

Mutators take (_ int, rlm realm) and identify the principal as rlm.Previous().Address(), after asserting rlm.IsCurrent(). The consuming realm threads its own cur:

Example
1func Grant(cur realm, perm string, addr address) {
2	if err := book.Grant(0, cur, perm, addr); err != nil {
3		panic(err)
4	}
5}

IsCurrent() rejects a stale or stashed realm value, so a hostile realm cannot replay an old cur to impersonate the admin. This is the same shape as p/nt/ownable/v0 and p/nt/ownable/v0/exts/authorizable.

Lifecycle of a permission

A permission comes into existence on its first grant and ceases to exist when its last holder is revoked. There is no create step and no reserved name. Has returns false either way, so the distinction is invisible to a caller and the book does not accumulate empty buckets.

Example
1(absent) ──Grant──> held by 1..MaxHoldersPerPermission ──Revoke last──> (absent)
23                              └── DropPermission ──> (absent)

Admin handoff is two-step

NominateAdmin records a nominee; only AcceptAdmin, called by that nominee, moves the admin. A one-step transfer to a well-formed-but-unowned address is permanently fatal — address.IsValid only checks bech32 form, so a mistyped address passes validation and leaves a book nobody can ever grant or revoke on again. This is the Y4 finding from the audit of Cosmic Bull's own r/permission_registry, carried forward.

Consumer contract — the parts this package CANNOT enforce

  1. THREAD A LIVE cur. rlm.IsCurrent() proves the realm value came from a live crossing frame, and rlm.Previous().Address() is then the principal that crossed into your realm. A consumer that wraps a permbook mutator in a NON-crossing exported helper resolves its importer's caller instead of its importer — Class-2 designation forgery, in the consumer. Call permbook mutators from your own crossing entrypoints, passing that entrypoint's own cur.

  2. DO NOT LEAK THE *Book. No method here returns a mutable handle, but a consumer that exports its *Book — or returns it from a function reachable by another realm — hands out every mutator on it, and borrow rule #2 commits those writes under YOUR realm's authority. Keep it in an unexported package-level variable.

  3. Has ANSWERS ABOUT AN ADDRESS, NOT ABOUT YOUR CALLER. It performs no authentication. Derive the address from your own crossing entrypoint's cur.Previous().Address() and pass it in.

  4. CHOOSE LIMITS DELIBERATELY. They are fixed for the life of a Book. A consumer that needs different bounds later must allocate a second Book; nothing here migrates state between them.

Note on the groups base set

A groups.Group carries a base address set alongside its named roles. This package never writes to it, so it is provably empty for any Book, and "in the base set" can never mean "holds a permission".

Constants 2

const MaxPermissionsCeiling, MaxHoldersCeiling, MaxNameLenCeiling

1const (
2	MaxPermissionsCeiling = 256
3	MaxHoldersCeiling     = 10000
4	MaxNameLenCeiling     = 64
5)
source

Ceilings on what Limits may be configured to. They bound worst-case gas and storage for any Book, however the consumer configures it.

MaxPermissionsCeiling is the one that bounds GAS rather than merely storage. Three operations walk every permission in the book at O(P log H) — Permissions, HasAny, and RevokeAll — and this ceiling is what bounds them. Everything else is logarithmic or paginated; see each method's own doc for its cost, which is authoritative.

In particular HasAny is NOT a cheap variant of Has. Has is two tree descents; HasAny is a full walk of the registry. Do not put HasAny on a hot authorization path or inside a per-item render loop.

These ceilings are not the objection this package raises against a shared registry. A shared registry's caps are rivalrous and unraisable because one immutable realm holds every tenant's state; here a consumer that needs more simply allocates another Book in its own realm, at no cost to anyone else.

Variables 1

var ErrUnauthorized, ErrNotLiveRealm, ErrInvalidAddress, ErrInvalidName, ErrInvalidLimits, ErrAlreadyGranted, ErrNotGranted, ErrPermissionLimit, ErrHolderLimit, ErrNoPendingAdmin, ErrNotPendingAdmin, ErrSameAdmin, ErrUnknownPermission

 1var (
 2	ErrUnauthorized      = errors.New("permbook: caller is not the admin")
 3	ErrNotLiveRealm      = errors.New("permbook: rlm is not the caller's live cur")
 4	ErrInvalidAddress    = errors.New("permbook: invalid address")
 5	ErrInvalidName       = errors.New("permbook: permission name must be 1..MaxNameLen chars, lowercase alphanumeric and underscore only")
 6	ErrInvalidLimits     = errors.New("permbook: limits must be positive and within the package ceilings")
 7	ErrAlreadyGranted    = errors.New("permbook: address already holds this permission")
 8	ErrNotGranted        = errors.New("permbook: address does not hold this permission")
 9	ErrPermissionLimit   = errors.New("permbook: permission limit reached for this book")
10	ErrHolderLimit       = errors.New("permbook: holder limit reached for this permission")
11	ErrNoPendingAdmin    = errors.New("permbook: no pending admin nomination")
12	ErrNotPendingAdmin   = errors.New("permbook: caller is not the pending admin")
13	ErrSameAdmin         = errors.New("permbook: nominee is already the admin")
14	ErrUnknownPermission = errors.New("permbook: no such permission")
15)
source

Functions 3

func New

1func New(admin address, lim Limits) (*Book, error)
source

New constructs an empty Book owned by admin, with explicit limits.

func NewDefault

1func NewDefault(admin address) (*Book, error)
source

NewDefault constructs an empty Book owned by admin, with DefaultLimits.

func DefaultLimits

1func DefaultLimits() Limits
source

DefaultLimits returns the limits used by NewDefault.

Types 2

type Book

struct
1type Book struct {
2	g            *groups.Group
3	admin        address
4	pendingAdmin address
5	lim          Limits
6}
source

Book is a bounded set of named permissions with an admin. The zero value is not usable; construct with New or NewDefault.

SECURITY: keep a *Book in an unexported variable. It is the capability. Every mutator on it is gated on the admin, but a realm that receives the pointer itself can invoke those mutators, and borrow rule #2 commits the writes under the ALLOCATING realm's authority.

Methods on Book

func AcceptAdmin

method on Book
1func (b *Book) AcceptAdmin(_ int, rlm realm) error
source

AcceptAdmin completes a pending handoff. Only the nominee may call it.

func Admin

method on Book
1func (b *Book) Admin() address
source

Admin returns the book's current admin.

func CancelNomination

method on Book
1func (b *Book) CancelNomination(_ int, rlm realm) error
source

CancelNomination withdraws a pending nomination. Admin only.

func DropPermission

method on Book
1func (b *Book) DropPermission(_ int, rlm realm, perm string) error
source

DropPermission removes a permission and every grant of it. Admin only.

Cost is O(log P): groups discards the whole member set with the role and does not walk it, so this is safe for a permission with many holders.

func Grant

method on Book
1func (b *Book) Grant(_ int, rlm realm, perm string, addr address) error
source

Grant gives addr the named permission. Admin only.

The permission is created if it does not exist. Granting a permission the address already holds returns ErrAlreadyGranted rather than silently succeeding, so a consumer cannot mistake a no-op for a state change.

func Has

method on Book
1func (b *Book) Has(perm string, addr address) bool
source

Has reports whether addr currently holds the named permission.

Two B+-tree descents, O(log P + log H). Independent of how many other permissions addr holds. Never panics; unknown permissions report false.

func HasAny

method on Book
1func (b *Book) HasAny(addr address) bool
source

HasAny reports whether addr holds any permission at all. O(P log H).

func HolderCount

method on Book
1func (b *Book) HolderCount(perm string) int
source

HolderCount returns how many addresses hold the named permission, or 0 if it does not exist.

func Holders

method on Book
1func (b *Book) Holders(perm string, offset, count int) []address
source

Holders returns up to count holders of the named permission, in sorted order, starting at offset. Paginated for the same reason as PermissionNames. Returns nil for an unknown permission.

func IsAdmin

method on Book
1func (b *Book) IsAdmin(addr address) bool
source

IsAdmin reports whether addr is the book's admin.

func Limits

method on Book
1func (b *Book) Limits() Limits
source

Limits returns the book's fixed limits.

func NominateAdmin

method on Book
1func (b *Book) NominateAdmin(_ int, rlm realm, nominee address) error
source

NominateAdmin records a nominee for the admin role. Admin only. The handoff does NOT take effect until the nominee calls AcceptAdmin, and a nomination may be withdrawn with CancelNomination until then.

func PendingAdmin

method on Book
1func (b *Book) PendingAdmin() address
source

PendingAdmin returns the nominated-but-not-yet-accepted admin, or the empty address if there is no pending nomination.

func PermissionCount

method on Book
1func (b *Book) PermissionCount() int
source

PermissionCount returns how many distinct permissions currently exist.

func PermissionNames

method on Book
1func (b *Book) PermissionNames(offset, count int) []string
source

PermissionNames returns up to count permission names in lexicographic order, starting at offset. Paginated so the caller, not the book, chooses how much work a single call does.

func Permissions

method on Book
1func (b *Book) Permissions(addr address) []string
source

Permissions returns the names addr holds, in lexicographic order, or nil. O(P log H) — bounded by MaxPermissions.

func Revoke

method on Book
1func (b *Book) Revoke(_ int, rlm realm, perm string, addr address) error
source

Revoke removes the named permission from addr. Admin only.

Revoking the last holder removes the permission itself, freeing its slot against MaxPermissions. Has reports false either way, so this is invisible to a caller and keeps the book from accumulating empty buckets.

func RevokeAll

method on Book
1func (b *Book) RevokeAll(_ int, rlm realm, addr address) (int, error)
source

RevokeAll removes addr from every permission in the book and reports how many were removed. Admin only.

Cost is O(P log H) in the book's permission count — bounded by MaxPermissions, which is why that limit has a ceiling.

Implementation note: the permission names are collected FIRST, into a value slice, and the registry is mutated only after that walk returns. groups documents that mutating the role registry mid-iteration can panic and abort the transaction.

type Limits

struct
 1type Limits struct {
 2	// MaxPermissions bounds how many distinct permission names may exist at
 3	// once. Because Permissions and RevokeAll walk all of them, this bounds
 4	// gas, not just storage.
 5	MaxPermissions int
 6
 7	// MaxHoldersPerPermission bounds how many addresses may hold any one
 8	// permission. No operation iterates holders unpaginated, so this bounds
 9	// storage rather than gas.
10	MaxHoldersPerPermission int
11
12	// MaxNameLen bounds permission-name length.
13	MaxNameLen int
14}
source

Limits are fixed at construction and never change for the life of a Book.

Imports 2

Source Files 2