blob: 12e6b8ec2580cada555b2f91e1c653920bdd38e8 [file] [log] [blame]
// 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 server // import "upspin.io/dir/server"
// This file deals with loading Access files and checking access permissions.
import (
"time"
"upspin.io/access"
"upspin.io/bind"
"upspin.io/client/clientutil"
"upspin.io/errors"
"upspin.io/log"
"upspin.io/path"
"upspin.io/upspin"
)
// accessEntry holds parsed Access files and a sequence number for their entries.
// It is the unit stored in the access cache.
type accessEntry struct {
sequence int64 // sequence number of the DirEntry parsed.
acc *access.Access // parsed contents of the Access file.
}
// remoteGroupDuration is how long a remote Group can be cached.
// Modified by tests.
var remoteGroupDuration = 2 * time.Minute
// whichAccess implements DirServer.WhichAccess.
func (s *server) whichAccess(p path.Parsed, opts ...options) (*upspin.DirEntry, error) {
o, ss := subspan("whichAccess", opts)
defer ss.End()
if isSnapshotUser(p.User()) {
return nil, nil
}
tree, err := s.loadTreeFor(p.User(), o)
if err != nil {
return nil, errors.E(err)
}
// Do tree lookups until we find an Access file. Lookups start at the
// root and go forward till the named path. If no Access file is there,
// pop up one level. This is so we can find the closest Acesss file,
// while being aware of links in the way.
for {
accPath, err := path.Parse(path.Join(p.Path(), "Access"))
if err != nil {
return nil, err
}
entry, _, err := tree.Lookup(accPath)
if err == upspin.ErrFollowLink {
// WhichAccess(link) always returns the link
// and ErrFollowLink.
return entry, err
}
if errors.Is(errors.NotExist, err) {
if p.IsRoot() {
// Already at the root, nothing found.
return nil, nil
}
p = p.Drop(1)
continue
}
if err != nil {
return nil, err
}
// Found the Access file.
return entry, nil
}
}
// loadAccess loads and processes an Access file from its DirEntry.
func (s *server) loadAccess(entry *upspin.DirEntry, opts ...options) (*access.Access, error) {
defer span(opts).StartSpan("loadAccess").End()
buf, err := clientutil.ReadAll(s.serverConfig, entry)
if err != nil {
return nil, err
}
return access.Parse(entry.Name, buf)
}
// loadPath loads a name from the Store, if its entry can be resolved by this
// DirServer. Intended for use with access.Can only.
func (s *server) loadPath(name upspin.PathName) ([]byte, error) {
p, err := path.Parse(name)
if err != nil {
return nil, err
}
var entry *upspin.DirEntry
if s.userName == p.User() {
entry, err = s.lookup(p, entryMustBeClean)
} else {
entry, err = s.remoteLookup(p)
if err == nil {
// Remember this Group file so we can then forget it
// when it gets stale. This is guaranteed to be a Group
// file because Access files are local only. If this
// ever changes, we must first check whether
// access.IsGroupFile(p.path()).
s.remoteGroups.Add(p.Path(), lastLoad(s.now()))
}
}
if err != nil {
return nil, err
}
// entry contains a valid value now. Read it.
return clientutil.ReadAll(s.serverConfig, entry)
}
// remoteLookup performs a lookup on the canonical DirServer for the path,
// which might be remote.
func (s *server) remoteLookup(p path.Parsed) (*upspin.DirEntry, error) {
key, err := bind.KeyServer(s.serverConfig, s.serverConfig.KeyEndpoint())
if err != nil {
return nil, err
}
u, err := key.Lookup(p.User())
if err != nil {
return nil, err
}
var firstErr error
check := func(err error) error {
if firstErr == nil {
firstErr = err
}
return err
}
for _, e := range u.Dirs {
if e == s.serverConfig.DirEndpoint() {
// It's okay to load the tree for this user, because they
// live in this dir server, according to the KeyServer.
return s.lookup(p, entryMustBeClean)
}
dir, err := bind.DirServer(s.serverConfig, e)
if check(err) != nil {
// Skip bad bind.
continue
}
return dir.Lookup(p.Path())
}
if firstErr != nil {
return nil, firstErr
}
return nil, errors.E(errors.NotExist, p.Path(), "no remote entry for path")
}
// hasRight reports whether the current user has the given right on the path. If
// ErrFollowLink is returned, the DirEntry will be that of the link.
func (s *server) hasRight(right access.Right, p path.Parsed, opts ...options) (bool, *upspin.DirEntry, error) {
o, ss := subspan("hasRight", opts)
defer ss.End()
// The owner of a snapshot has r,l rights over it and can create the
// root, but nothing else. No one else has any rights.
if isSnapshotUser(p.User()) {
if s.isSnapshotOwner(p.User()) {
switch right {
case access.Read, access.List, access.AnyRight:
return true, nil, nil
case access.Create:
return p.IsRoot(), nil, nil
}
}
return false, nil, nil
}
entry, err := s.whichAccess(p, o)
if err == upspin.ErrFollowLink {
// If we need to follow a link to get to p
// return that link.
if entry.Name != p.Path() {
return false, entry, err
}
// If p is the link then find the Access file for p.
entry, err = s.whichAccess(p.Drop(1), o)
}
if err != nil {
return false, nil, err
}
var acc *access.Access
if entry != nil {
// We have the Access file entry. Get the contents.
acc, err = s.getAccess(entry, o)
if err != nil {
// There was an error acquiring or parsing this Access file.
// That means an Access file is recorded here but is invalid,
// at least temporarily. Instead of refusing all rights by
// returning an error, we log the error and restore default
// (owner-only) rights.
log.Error.Printf("dir/server: bad Access file %q: %v; using default rights", entry.Name, err)
acc, err = s.getDefaultAccess(p.User())
}
} else {
// No Access file exists anywhere. Use an implicit one.
// Get the implicit one from the defaultAccess cache.
acc, err = s.getDefaultAccess(p.User())
}
if err != nil {
return false, nil, err
}
// Finally, check whether the user has the requested right.
can, err := acc.Can(s.userName, right, p.Path(), s.loadPath)
if err != nil {
return false, nil, err
}
return can, nil, nil
}
// getAccess returns the parsed contents of the Access file described by entry.
func (s *server) getAccess(entry *upspin.DirEntry, opts ...options) (*access.Access, error) {
o, ss := subspan("getAccess", opts)
defer ss.End()
// Sanity check: is this really an Access file?
if !access.IsAccessFile(entry.Name) {
return nil, errors.E(errors.Internal, entry.Name, "not an Access file")
}
// Is it in the cache?
var accEntry *accessEntry
a, found := s.access.Get(entry.Name)
if found {
var ok bool
accEntry, ok = a.(*accessEntry)
if !ok {
return nil, errors.E(errors.Internal, "invalid accessEntry")
}
if entry.Sequence == accEntry.sequence {
return accEntry.acc, nil
}
// A race happened and we have a cached version that is
// different than the requested one. Fall through and load the
// requested one.
}
// Not in cache; load data from the Store.
acc, err := s.loadAccess(entry, o)
if err != nil {
return nil, err
}
// Add or update cache.
s.access.Add(entry.Name, &accessEntry{
sequence: entry.Sequence,
acc: acc,
})
return acc, nil
}
// getDefaultAccess returns the implicit Access file for a user.
func (s *server) getDefaultAccess(userName upspin.UserName) (acc *access.Access, err error) {
cacheEntry, found := s.defaultAccess.Get(userName)
if !found {
// Create one now and add to the cache.
acc, err = access.New(upspin.PathName(userName + "/"))
if err != nil {
return
}
s.defaultAccess.Add(userName, acc)
} else {
var ok bool
acc, ok = cacheEntry.(*access.Access)
if !ok {
return nil, errors.E(errors.Internal, "not an Access file")
}
}
return
}
// loadGroup loads a group file from its entry and parses it, but does not
// pass it to access.AddGroup
func (s *server) loadGroup(p path.Parsed, entry *upspin.DirEntry) error {
data, err := clientutil.ReadAll(s.serverConfig, entry)
if err != nil {
return err
}
_, err = access.ParseGroup(p, data)
return err
}
// groupRefreshLoop periodically removes potentially stale Group files from the
// access group cache. It runs continuously and must be in a goroutine.
func (s *server) groupRefreshLoop() {
for {
time.Sleep(remoteGroupDuration)
for {
k, v := s.remoteGroups.PeekOldest()
if k == nil || v == nil {
// Nothing to do.
break
}
lastLoaded, ok := v.(lastLoad)
if !ok {
log.Error.Printf("dir/server.groupRefreshLoop: value is not of type lastLoad")
return
}
expiration := upspin.Time(lastLoaded) + upspin.Time(remoteGroupDuration.Seconds())
if expiration < s.now() {
// Remote the oldest (LRU) and calls OnEviction.
key, _ := s.remoteGroups.RemoveOldest()
lastLoaded.OnEviction(key)
continue // look for the next one to expire.
}
break // Oldest entry is not old enough.
}
}
}
// lastLoad represents the time a remote Group file was loaded by the DirServer.
// It is the value stored in the remoteGroup cache.
type lastLoad upspin.Time
// OnEviction implements cache.EvictionNotifier. It is called when the remote
// group cache is full or an item is forcefully evicted (by calling RemoveOldest
// on the cache). In effect, this "forgets" the Group file if it was loaded.
func (l lastLoad) OnEviction(key interface{}) {
name, ok := key.(upspin.PathName)
if !ok {
log.Error.Printf("dir/server: key in remote group cache is not a pathname: %v", key)
return
}
access.RemoveGroup(name) // ignore return, it may not have been loaded.
}