git@cosmocr.at/gitweb.git @ main · summary refs log
main / internal / auth / authz.go /
internal/auth/authz.go · 3.9 KiB · raw
view: Source ☐ force
  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
// 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
}