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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

policies.gno

6.42 Kb · 233 lines
  1// Package policies is the Remix Lab permission block. A Policy maps named
  2// operations to an eligibility rule evaluated against a membership reader.
  3//
  4// Each operation is defined once with a floor: the most permissive rule it
  5// may ever be set to. Administrative operations get a floor of RuleReviewer
  6// or RuleOwner, so reconfiguring a policy can never hand admin powers to
  7// ordinary members or the public.
  8//
  9// Operations with an owner floor (typically policy changes and owner
 10// appointment) can never be disabled: setting them to RuleNobody would lock
 11// the owners out permanently.
 12//
 13// Policy changes affect future operations only. Ballot eligibility follows
 14// the electorate frozen when the ballot opened, not the current policy.
 15package policies
 16
 17import (
 18	"gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/core/v1"
 19	"gno.land/p/nt/avl/v0"
 20)
 21
 22// Rule is an eligibility requirement. Rules are ordered from most to least
 23// permissive.
 24type Rule int
 25
 26const (
 27	RulePublic Rule = iota
 28	RuleMember
 29	RuleReviewer
 30	RuleOwner
 31	RuleNobody // operation disabled
 32)
 33
 34func (r Rule) String() string {
 35	switch r {
 36	case RulePublic:
 37		return "public"
 38	case RuleMember:
 39		return "member"
 40	case RuleReviewer:
 41		return "reviewer"
 42	case RuleOwner:
 43		return "owner"
 44	case RuleNobody:
 45		return "nobody"
 46	}
 47	return "unknown"
 48}
 49
 50// ParseRule converts the canonical rule name to a Rule.
 51func ParseRule(s string) (Rule, error) {
 52	for r := RulePublic; r <= RuleNobody; r++ {
 53		if r.String() == s {
 54			return r, nil
 55		}
 56	}
 57	return RuleNobody, core.Errorf(core.ErrInvalidInput, "unknown rule")
 58}
 59
 60// MaxOperations bounds the number of operations one policy defines.
 61const MaxOperations = 64
 62
 63type entry struct {
 64	rule  Rule
 65	floor Rule
 66}
 67
 68// RuleView is an inert description of one operation's rule.
 69type RuleView struct {
 70	Op    string
 71	Rule  Rule
 72	Floor Rule
 73}
 74
 75// Policy evaluates named operations. Hold it in an unexported host-realm
 76// variable; its Set method changes authorization.
 77type Policy struct {
 78	members core.MembershipReader
 79	ops     *avl.Tree // op -> *entry
 80}
 81
 82// New creates a policy reading roles from members. members may be the
 83// host's own membership set or an adapter to a reviewed shared realm.
 84func New(members core.MembershipReader) *Policy {
 85	if members == nil {
 86		panic(core.Errorf(core.ErrInvalidInput, "nil membership reader").Error())
 87	}
 88	return &Policy{members: members, ops: avl.NewTree()}
 89}
 90
 91// Define registers op with an initial rule and a floor. It panics on invalid
 92// input because host realms call it from init with constant arguments.
 93func (p *Policy) Define(op string, rule, floor Rule) {
 94	if err := checkOp(op); err != nil {
 95		panic(err.Error())
 96	}
 97	if !floor.valid() || !rule.valid() {
 98		panic(core.Errorf(core.ErrInvalidInput, "invalid rule").Error())
 99	}
100	if rule < floor {
101		panic(core.Errorf(core.ErrInvalidInput, op+": rule below floor").Error())
102	}
103	if floor >= RuleOwner && rule == RuleNobody {
104		panic(core.Errorf(core.ErrInvalidInput, op+": cannot be disabled").Error())
105	}
106	if p.ops.Has(op) {
107		panic(core.Errorf(core.ErrConflict, op+": already defined").Error())
108	}
109	if p.ops.Size() >= MaxOperations {
110		panic(core.Errorf(core.ErrLimitExceeded, "too many operations").Error())
111	}
112	p.ops.Set(op, &entry{rule: rule, floor: floor})
113}
114
115// Set changes the rule for a defined op. The host must have authorized the
116// actor (typically through a "policy.set" operation with an owner floor).
117func (p *Policy) Set(op string, rule Rule) error {
118	e := p.get(op)
119	if e == nil {
120		return core.Errorf(core.ErrNotFound, "unknown operation")
121	}
122	if !rule.valid() {
123		return core.Errorf(core.ErrInvalidInput, "invalid rule")
124	}
125	if rule < e.floor {
126		return core.Errorf(core.ErrInvalidInput, op+": rule below floor "+e.floor.String())
127	}
128	if e.floor >= RuleOwner && rule == RuleNobody {
129		return core.Errorf(core.ErrInvalidInput, op+": cannot be disabled")
130	}
131	e.rule = rule
132	return nil
133}
134
135// Rule returns the current rule for op.
136func (p *Policy) Rule(op string) (Rule, bool) {
137	e := p.get(op)
138	if e == nil {
139		return RuleNobody, false
140	}
141	return e.rule, true
142}
143
144// Authorize implements core.AuthorizationPolicy. Unknown operations are
145// denied.
146func (p *Policy) Authorize(op string, actor address) error {
147	e := p.get(op)
148	if e == nil {
149		return core.Errorf(core.ErrUnauthorized, "unknown operation "+op)
150	}
151	if allowed(e.rule, p.members.RoleOf(actor)) {
152		return nil
153	}
154	if e.rule == RuleNobody {
155		return core.Errorf(core.ErrUnauthorized, op+" is disabled")
156	}
157	return core.Errorf(core.ErrUnauthorized, op+" requires "+e.rule.String())
158}
159
160// Allowed reports whether actor may perform op, without building an error.
161func (p *Policy) Allowed(op string, actor address) bool {
162	e := p.get(op)
163	return e != nil && allowed(e.rule, p.members.RoleOf(actor))
164}
165
166// List returns every operation's rule, ordered by operation name.
167func (p *Policy) List() []RuleView {
168	var out []RuleView
169	p.ops.Iterate("", "", func(k string, v any) bool {
170		e := v.(*entry)
171		out = append(out, RuleView{Op: k, Rule: e.rule, Floor: e.floor})
172		return false
173	})
174	return out
175}
176
177func (p *Policy) get(op string) *entry {
178	v := p.ops.Get(op)
179	if v == nil {
180		return nil
181	}
182	return v.(*entry)
183}
184
185// allowed treats a role outside the defined range as no role, so a faulty
186// MembershipReader can never grant more than owner.
187func allowed(rule Rule, role core.Role) bool {
188	if !role.Valid() {
189		return rule == RulePublic
190	}
191	switch rule {
192	case RulePublic:
193		return true
194	case RuleMember:
195		return role.AtLeast(core.RoleMember)
196	case RuleReviewer:
197		return role.AtLeast(core.RoleReviewer)
198	case RuleOwner:
199		return role.AtLeast(core.RoleOwner)
200	}
201	return false
202}
203
204func (r Rule) valid() bool { return r >= RulePublic && r <= RuleNobody }
205
206// checkOp accepts lowercase dotted identifiers such as "poll.open": each
207// dot- or underscore-separated part starts with a letter and continues with
208// letters or digits.
209func checkOp(op string) error {
210	if len(op) == 0 || len(op) > 48 {
211		return core.Errorf(core.ErrInvalidInput, "operation name length")
212	}
213	partStart := true
214	for _, c := range op {
215		lower := c >= 'a' && c <= 'z'
216		digit := c >= '0' && c <= '9'
217		switch {
218		case c == '.' || c == '_':
219			if partStart {
220				return core.Errorf(core.ErrInvalidInput, "invalid operation name")
221			}
222			partStart = true
223		case lower || (digit && !partStart):
224			partStart = false
225		default:
226			return core.Errorf(core.ErrInvalidInput, "invalid operation name")
227		}
228	}
229	if partStart {
230		return core.Errorf(core.ErrInvalidInput, "invalid operation name")
231	}
232	return nil
233}