sharedmembers.gno
5.13 Kb · 196 lines
1// Package sharedmembers is the Remix Lab reference shared membership
2// service: one membership set that several apps consult.
3//
4// Other realms get read-only access through RoleOf, Size and Snapshot,
5// which together implement core.MembershipReader (membership.read/v1).
6// Mutations happen only through this realm's own authorized entry points;
7// reading a member's role never grants the reader any power over that
8// member.
9package sharedmembers
10
11import (
12 "strconv"
13 "strings"
14
15 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/core/v1"
16 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/membership/v1"
17 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/policies/v1"
18 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/render/v1"
19)
20
21const (
22 pkgPath = "gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/sharedmembers"
23 pageSize = 25
24
25 // InterfaceVersion is the read interface this realm provides.
26 InterfaceVersion = "membership.read/v1"
27)
28
29const (
30 opInvite = "members.invite"
31 opRemove = "members.remove"
32 opSetRole = "members.set_role"
33)
34
35var (
36 members *membership.Set
37 policy *policies.Policy
38)
39
40func init(cur realm) {
41 deployer := cur.Previous()
42 if deployer.IsUser() && deployer.Address().IsValid() {
43 setup(deployer.Address())
44 }
45}
46
47func setup(owner address) {
48 members = membership.New(membership.Config{
49 JoinMode: membership.JoinInvitation,
50 MaxMembers: 100,
51 }, owner)
52 policy = policies.New(members)
53 policy.Define(opInvite, policies.RuleOwner, policies.RuleReviewer)
54 policy.Define(opRemove, policies.RuleOwner, policies.RuleOwner)
55 policy.Define(opSetRole, policies.RuleOwner, policies.RuleOwner)
56}
57
58// --- membership.read/v1 ---
59
60// RoleOf returns addr's role.
61func RoleOf(addr address) core.Role {
62 if members == nil {
63 return core.RoleNone
64 }
65 return members.RoleOf(addr)
66}
67
68// Size returns the number of members.
69func Size() int {
70 if members == nil {
71 return 0
72 }
73 return members.Size()
74}
75
76// Snapshot returns members with at least minRole, or ok=false when more
77// than max qualify or the service is not initialized. The returned slice is
78// a fresh copy.
79func Snapshot(minRole core.Role, max int) ([]address, bool) {
80 if members == nil {
81 return nil, false
82 }
83 return members.Snapshot(minRole, max)
84}
85
86// Members pages through members ordered by address.
87func Members(offset, limit int) []core.MemberView {
88 if members == nil {
89 return nil
90 }
91 if limit < 1 || limit > pageSize {
92 limit = pageSize
93 }
94 return members.List(offset, limit)
95}
96
97// --- Authorized mutations ---
98
99// Invite lets addr join with role. Inviting a reviewer or owner also
100// requires the set-role permission.
101func Invite(cur realm, addr address, role string) {
102 actor := authorize(cur, opInvite)
103 r, err := core.ParseRole(role)
104 must(err)
105 if r > core.RoleMember {
106 must(policy.Authorize(opSetRole, actor))
107 }
108 must(members.Invite(addr, r))
109}
110
111// RevokeInvite cancels a pending invitation. Revoking an invitation for a
112// reviewer or owner also requires the set-role permission.
113func RevokeInvite(cur realm, addr address) {
114 actor := authorize(cur, opInvite)
115 if r, ok := members.InviteRole(addr); ok && r > core.RoleMember {
116 must(policy.Authorize(opSetRole, actor))
117 }
118 must(members.RevokeInvite(addr))
119}
120
121// Invites pages through pending invitations ordered by address.
122func Invites(offset, limit int) []core.MemberView {
123 if members == nil {
124 return nil
125 }
126 if limit < 1 || limit > pageSize {
127 limit = pageSize
128 }
129 return members.Invites(offset, limit)
130}
131
132// Join accepts the caller's pending invitation.
133func Join(cur realm) {
134 ready()
135 must(members.Join(cur.Previous().Address()))
136}
137
138// Leave removes the caller.
139func Leave(cur realm) {
140 ready()
141 must(members.Leave(cur.Previous().Address()))
142}
143
144// RemoveMember removes addr.
145func RemoveMember(cur realm, addr address) {
146 authorize(cur, opRemove)
147 must(members.Remove(addr))
148}
149
150// SetRole changes addr's role.
151func SetRole(cur realm, addr address, role string) {
152 authorize(cur, opSetRole)
153 r, err := core.ParseRole(role)
154 must(err)
155 must(members.SetRole(addr, r))
156}
157
158// Render lists the members.
159func Render(path string) string {
160 if members == nil {
161 return render.Heading(1, "Shared membership") + "This service is not initialized.\n"
162 }
163 _, q := render.ParsePath(path)
164 pages := render.PageCount(members.Size(), pageSize)
165 page := render.Page(q, pages)
166 var rows [][]string
167 for _, m := range members.List((page-1)*pageSize, pageSize) {
168 rows = append(rows, render.Row(m.Address.String(), m.Role.String()))
169 }
170 var sb strings.Builder
171 sb.WriteString(render.Heading(1, "Shared membership"))
172 sb.WriteString("Apps read this list through " + render.Code(InterfaceVersion) + ". ")
173 sb.WriteString(strconv.Itoa(members.Size()) + " members.\n\n")
174 sb.WriteString(render.Table([]string{"Address", "Role"}, rows))
175 sb.WriteString(render.Pager(pkgPath, "", page, pages))
176 return sb.String()
177}
178
179func authorize(cur realm, op string) address {
180 ready()
181 actor := cur.Previous().Address()
182 must(policy.Authorize(op, actor))
183 return actor
184}
185
186func ready() {
187 if members == nil {
188 panic(core.Errorf(core.ErrInvalidState, "service is not initialized").Error())
189 }
190}
191
192func must(err error) {
193 if err != nil {
194 panic(err.Error())
195 }
196}