// Package auth decides who may read and write repositories, and verifies the // credentials that identify a requesting upspin user. // // Authorization is derived entirely from upspin Access files, evaluated through // the store.Store abstraction so it behaves identically in production (upspin) // and in dev/tests (LocalStore). A repo is "public" when its governing Access // file grants read to all; otherwise it is private and requires an // authenticated user who is granted the relevant right. package auth
import ( "strings" "sync" "time"
"gitweb/internal/store"
"upspin.io/access"
"upspin.io/upspin"
)
// Authorizer answers read/write questions about upspin paths by locating and // evaluating the governing Access file. Parsed Access files are cached briefly // so a burst of git requests to one repo does not re-read the tree each time. type Authorizer struct { st store.Store ttl time.Duration
mu sync.Mutex
cache map[string]govEntry // resource path -> governing decision inputs
}
type govEntry struct { acc *access.Access // nil means no Access file governs the path expires time.Time }
// NewAuthorizer returns an Authorizer reading Access files through st. func NewAuthorizer(st store.Store) *Authorizer { return &Authorizer{st: st, ttl: 30 * time.Second, cache: map[string]govEntry{}} }
// CanRead reports whether user may read p. An empty user means an anonymous // request, which is allowed only when the governing Access file grants read to // all (a "public" path). func (a *Authorizer) CanRead(user, p string) (bool, error) { return a.can(user, access.Read, p) }
// CanWrite reports whether user may write (push to) p. Anonymous requests are // never allowed to write. func (a *Authorizer) CanWrite(user, p string) (bool, error) { if user == "" { return false, nil } return a.can(user, access.Write, p) }
func (a *Authorizer) can(user string, right access.Right, p string) (bool, error) { owner := ownerOf(p) if owner == "" { return false, nil } acc, err := a.governing(p) if err != nil { return false, err } if acc == nil { // No Access file anywhere: upspin's default is owner-only. return user != "" && user == owner, nil } if user == "" { // Anonymous: only a read of a world-readable path is permitted. return right == access.Read && acc.IsReadableByAll(), nil } // The owner always retains full rights over their own tree. if user == owner { return true, nil } load := func(n upspin.PathName) ([]byte, error) { return a.st.Get(string(n)) } return acc.Can(upspin.UserName(user), right, upspin.PathName(p), load) }
// governing returns the Access file that governs p — the one in the nearest // ancestor directory (p itself included) — or nil if none exists. The result is // cached per resource path for a.ttl. func (a *Authorizer) governing(p string) (*access.Access, error) { now := time.Now() a.mu.Lock() if e, ok := a.cache[p]; ok && now.Before(e.expires) { a.mu.Unlock() return e.acc, nil } a.mu.Unlock()
acc, err := a.lookupGoverning(p)
if err != nil {
return nil, err
}
a.mu.Lock()
a.cache[p] = govEntry{acc: acc, expires: now.Add(a.ttl)}
a.mu.Unlock()
return acc, nil
}
func (a *Authorizer) lookupGoverning(p string) (*access.Access, error) { elems := strings.Split(strings.Trim(p, "/"), "/") // Walk from the deepest candidate directory (p itself) up to the user root; // the nearest Access file wins (upspin semantics). for i := len(elems); i >= 1; i-- { dir := strings.Join(elems[:i], "/") accessPath := dir + "/Access" data, err := a.st.Get(accessPath) if err == store.ErrNotFound { continue } if err != nil { return nil, err } return access.Parse(upspin.PathName(accessPath), data) } return nil, nil }
// ownerOf returns the first path element (the upspin user that owns the tree), // or "" if p has none. func ownerOf(p string) string { p = strings.TrimLeft(p, "/") if i := strings.IndexByte(p, '/'); i >= 0 { return p[:i] } return p }