forked from upspin/upspin
-
Notifications
You must be signed in to change notification settings - Fork 0
/
perm.go
349 lines (315 loc) · 10.1 KB
/
perm.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
// Copyright 2016 The Upspin Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package perm implements mutation permission checking for servers.
package perm
import (
"sync"
"time"
"github.com/palager/upspin/access"
"github.com/palager/upspin/bind"
"github.com/palager/upspin/client/clientutil"
"github.com/palager/upspin/errors"
"github.com/palager/upspin/log"
"github.com/palager/upspin/path"
"github.com/palager/upspin/upspin"
"github.com/palager/upspin/user"
)
// WritersGroupFile is the name of the Group file that specifies
// writers for a Perm instance.
const WritersGroupFile = "Writers"
// retryTimeout is the default interval between attempts when a failure occurs.
const retryTimeout = 30 * time.Second
// Perm tracks the set of users with write access to a server, as specified by
// the Writers Group file. These might be users who can write blocks to a
// StoreServer or create a root on a DirServer.
type Perm struct {
cfg upspin.Config
targetUser upspin.UserName
targetFile upspin.PathName
lookupFunc lookupFunc
watchFunc watchFunc
// onUpdate is a testing stub that is called after each user list update occurs.
onUpdate func()
// onRetry is called after an unsuccessful Watch or when the event
// channel is closed.
onRetry func()
// done signals the watch loop to exit.
done <-chan struct{}
// errors collects the errors for lookup and the first watch.
// They are only logged after a third error occurs.
errors []error
// writers is the set of users allowed to write. If it's nil, all users
// are allowed. An empty map means no one is allowed.
writers map[upspin.UserName]bool
mu sync.RWMutex // guards writers
}
// lookupFunc looks up name, as defined by upspin.DirServer.
type lookupFunc func(upspin.PathName) (*upspin.DirEntry, error)
// watchFunc watches name, as defined by upspin.DirServer.
type watchFunc func(upspin.PathName, int64, <-chan struct{}) (<-chan upspin.Event, error)
// New creates a new Perm monitoring the target user's Writers Group file,
// resolving the DirServer using the given config. The target user is
// typically the user name of a server, such as a StoreServer or a DirServer.
func New(cfg upspin.Config, ready <-chan struct{}, target upspin.UserName) *Perm {
const op errors.Op = "serverutil/perm.New"
return newPerm(op, cfg, ready, target, nil, nil, noop, retry, nil)
}
// NewWithDir creates a new Perm monitoring the target user's Writers Group
// file which must reside on the given DirServer. The target user is typically
// the user name of a server, such as a StoreServer or a DirServer.
func NewWithDir(cfg upspin.Config, ready <-chan struct{}, target upspin.UserName, dir upspin.DirServer) *Perm {
const op errors.Op = "serverutil/perm.NewFromDir"
return newPerm(op, cfg, ready, target, dir.Lookup, dir.Watch, noop, retry, nil)
}
func noop() {}
// retry is the default implementation of Perm.onRetry.
func retry() { time.Sleep(retryTimeout) }
// newPerm creates a new Perm monitoring the target user's Writers Group file,
// using the provided LookupFunc for lookups and the WatchFunc function to
// watch changes on the writers file. If lookup or watch are nil the DirServer
// is resolved using bind and the given config. The target user is typically
// the user name of a server, such as a StoreServer or a DirServer.
func newPerm(op errors.Op, cfg upspin.Config, ready <-chan struct{}, target upspin.UserName, lookup lookupFunc, watch watchFunc, onUpdate, onRetry func(), done <-chan struct{}) *Perm {
p := &Perm{
cfg: cfg,
targetUser: target,
targetFile: upspin.PathName(target) + "/Group/" + WritersGroupFile,
lookupFunc: lookup,
watchFunc: watch,
onUpdate: onUpdate,
onRetry: onRetry,
writers: nil, // Start open.
done: done,
}
go func() {
<-ready
err := p.Update()
if err != nil {
p.errors = append(p.errors, errors.E(op, err))
}
go p.updateLoop(op)
}()
return p
}
// updateLoop continuously watches for updates on WritersGroupFile.
// It must be run in a goroutine.
func (p *Perm) updateLoop(op errors.Op) {
var (
events <-chan upspin.Event
accessSeq int64 = -1
done = func() {}
)
for {
select {
case <-p.done:
done()
return
default:
}
var err error
if events == nil {
// Channel is not yet open. Open now.
doneCh := make(chan struct{})
done = func() {
if doneCh != nil {
close(doneCh)
doneCh = nil
}
}
// TODO(edpin,adg): start watching at most recently seen sequence number.
events, err = p.watch(upspin.PathName(p.targetUser)+"/", -1, doneCh)
if err != nil {
if err == upspin.ErrNotSupported {
log.Info.Println(p.targetUser, err)
return
}
err = errors.E(op, err)
// Only log the errors after three failures have occurred.
if n := len(p.errors); n > 0 {
p.errors = append(p.errors, err)
if n >= 2 {
for _, err := range p.errors {
log.Error.Print(err)
}
p.errors = nil
}
} else {
log.Error.Print(err)
}
p.onRetry()
continue
}
}
e, ok := <-events
if !ok {
log.Debug.Printf("%s: watch channel closed. Re-opening...", op)
events = nil
p.onRetry()
continue
}
if e.Error != nil {
log.Error.Printf("%s: watch event error: %s", op, e.Error)
done()
continue
}
// An Access file could have granted or revoked our permission
// to watch the Writers file. Therefore, we must start the Watch
// again, after the Access event.
if isRelevantAccess(e.Entry.Name) && e.Entry.Sequence > accessSeq {
accessSeq = e.Entry.Sequence
done()
continue
}
if accessSeq < 0 {
// If we haven't seen a sequence number before then we should
// remember the first one we see, so that we don't
// restart watching during the initial traversal.
// Do this after the check above, in case the first watch
// event we see is a new Access file, granting us access.
// We rely on the fact that the server won't send us an
// event for the Access file first if we do have access
// during the first traversal.
accessSeq = e.Entry.Sequence
}
// Process event.
if e.Entry.Name != p.targetFile {
continue
}
if e.Delete {
p.deleteUsers()
continue
}
err = p.updateUsers(e.Entry)
if err != nil {
log.Error.Printf("%s: updateUsers: %s", op, err)
}
}
}
// isRelevantAccess access reports whether name is an Access file in a Group
// directory or at the root.
func isRelevantAccess(name upspin.PathName) bool {
p, err := path.Parse(name)
if err != nil {
log.Error.Printf("serverutil/perm.isRelevantAccess: unexpected error: %s", err)
return false
}
file := p.FilePath()
return file == "Access" || file == "Group/Access"
}
// Update retrieves and parses the Group file that rules over the set of allowed
// writers. This is mostly only exported for testing, but servers may use it to
// force immediate updates.
func (p *Perm) Update() error {
entry, err := p.lookup(p.targetFile)
if err != nil {
// If the group file does not exist, reset writers map.
if errors.Is(errors.NotExist, err) {
p.deleteUsers() // Calls onUpdate.
return nil
}
p.onUpdate() // Even if we failed, unblock tests.
return err
}
return p.updateUsers(entry) // Calls onUpdate.
}
// updateUsers reads the writers Group file entry and updates the user set.
func (p *Perm) updateUsers(entry *upspin.DirEntry) error {
users, err := p.allowedWriters(entry)
if err != nil {
p.onUpdate() // Even if we failed, unblock tests.
return err
}
log.Info.Printf("serverutil/perm: Setting writers to: %v", users)
p.mu.Lock()
p.writers = make(map[upspin.UserName]bool, len(users))
for _, u := range users {
p.writers[u] = true
}
p.mu.Unlock()
p.onUpdate()
return nil
}
// deleteUsers resets the writers list to nil.
func (p *Perm) deleteUsers() {
p.mu.Lock()
p.writers = nil
p.mu.Unlock()
p.onUpdate()
}
// allowedWriters reads the contents of the entry, interprets it exactly as
// an access Group file, expanding recursively if needed, and returns the slice
// of users allowed to write to the store.
func (p *Perm) allowedWriters(entry *upspin.DirEntry) ([]upspin.UserName, error) {
// Pretend this is an Access file, so we can easily use it to retrieve a
// slice of all authorized users.
fakeAccess := "w,d:" + entry.Name
access.RemoveGroup(entry.Name)
acc, err := access.Parse(upspin.PathName(p.targetUser+"/"), []byte(fakeAccess))
if err != nil {
return nil, err
}
return acc.Users(access.Write, p.load)
}
// load loads the contents of a name.
func (p *Perm) load(name upspin.PathName) ([]byte, error) {
entry, err := p.lookup(name)
if err != nil {
return nil, err
}
return clientutil.ReadAll(p.cfg, entry)
}
// IsWriter reports whether the user has write privileges on this Perm.
func (p *Perm) IsWriter(u upspin.UserName) bool {
p.mu.RLock()
defer p.mu.RUnlock()
// Everyone is allowed if there is no Writers Group file.
if p.writers == nil {
return true
}
// If the special user "all@github.com/palager/upspin" is present, allow all.
if p.writers[access.AllUsers] {
return true
}
// Is this exact user allowed?
if p.writers[u] {
return true
}
// Maybe the domain is wildcarded. Check this case last as it's the most
// expensive.
_, _, domain, err := user.Parse(u)
if err != nil {
// Should never happen at this point.
log.Error.Printf("serverutil/perm: unexpected error: %s", err)
return false
}
return p.writers[upspin.UserName("*@"+domain)]
}
func (p *Perm) lookup(name upspin.PathName) (*upspin.DirEntry, error) {
if f := p.lookupFunc; f != nil {
return f(name)
}
parsed, err := path.Parse(name)
if err != nil {
return nil, err
}
dir, err := bind.DirServerFor(p.cfg, parsed.User())
if err != nil {
return nil, err
}
return dir.Lookup(name)
}
func (p *Perm) watch(name upspin.PathName, sequence int64, done <-chan struct{}) (<-chan upspin.Event, error) {
if f := p.watchFunc; f != nil {
return f(name, sequence, done)
}
parsed, err := path.Parse(name)
if err != nil {
return nil, err
}
dir, err := bind.DirServerFor(p.cfg, parsed.User())
if err != nil {
return nil, err
}
return dir.Watch(name, sequence, done)
}