Skip to content
quarry Artifact [ed1c72d1a8]
Login

Artifact [ed1c72d1a8]

Artifact ed1c72d1a8501b16511fe4a1a6c967e3c218b31ed5953d6d77722d19a8c6373a:


package main

import (
	"context"
	"net"
	"net/http"
	"strconv"
	"strings"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
	"craftdesign.group/quarry/internal/web"
)

// tokenDoors is RFC 0021's one resolver as the forge's doors see it: a
// token presented as an HTTP Basic password, checked for the door's
// scope, with every refused token counted as a miss against the
// account it names on the limiter the sign-in form already uses.
//
// A password without a token's shape never reaches it, so an account
// that holds no token meets every door exactly as before.
type tokenDoors struct {
	db    *oltp.DB
	limit *web.LoginLimiter // nil: misses are not counted
	// addr is the client's address by the trusted-proxy rule; set once
	// the proxy is known, the peer address until then.
	addr func(*http.Request) string
}

// presented is what a request's Basic credential turned out to be.
type presented int

const (
	notToken presented = iota // no Basic, or a password without a token's shape
	held                      // a token for an account the limiter is holding
	refused                   // a token that did not resolve for this scope
	resolved
)

func (d *tokenDoors) ip(req *http.Request) string {
	if d.addr != nil {
		return d.addr(req)
	}
	if host, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
		return host
	}
	return req.RemoteAddr
}

// resolve answers the token req's Basic password is, for scope.
func (d *tokenDoors) resolve(req *http.Request, scope string) (oltp.Token, presented) {
	user, pass, ok := req.BasicAuth()
	if d == nil || d.db == nil || !ok || user == "" {
		return oltp.Token{}, notToken
	}
	shaped := oltp.LooksLikeToken(pass)
	if !shaped && scope != oltp.ScopeSync {
		return oltp.Token{}, notToken
	}
	// The sync door also takes the push credentials minted before
	// tokens had a shape; those are not counted, being indistinguishable
	// from the wrong password the password check already refused.
	count := shaped && d.limit != nil
	if count {
		if ok, _ := d.limit.Allowed(user, d.ip(req)); !ok {
			return oltp.Token{}, held
		}
	}
	tok, ok := d.db.ResolveToken(user, pass, scope)
	switch {
	case ok && count:
		d.limit.Succeeded(user, d.ip(req))
	case !ok && count:
		d.limit.Failed(user, d.ip(req))
	}
	if !ok {
		if shaped {
			return oltp.Token{}, refused
		}
		return oltp.Token{}, notToken
	}
	return tok, resolved
}

// verify is the Verify behind a door of scope. A password shaped like a
// token is a token: found by its id, checked for this door's scope, its
// expiry, revocation and repositories, and judged on the letters it
// resolves to. Anything else is byPassword's, as it always was.
func (d *tokenDoors) verify(scope string, byPassword api.Verify) api.Verify {
	return func(repo, user, pass string, need ops.Auth) api.Decision {
		if d.db == nil || !d.db.HasAccounts() || !oltp.LooksLikeToken(pass) {
			return byPassword(repo, user, pass, need)
		}
		tok, ok := d.db.ResolveToken(user, pass, scope)
		// The API's forge scope is the router's to judge for a token
		// limited to repositories, since one op there (the caller's own
		// identity) stays open to it. Every other door takes Reaches
		// as it stands.
		if !ok || !(tok.Reaches(repo) || (repo == "" && scope == oltp.ScopeAPI)) {
			return api.Denied
		}
		if need != ops.Admin || d.db.TokenCaps(tok, repo).HasAny("a") {
			return api.Allowed
		}
		return api.Insufficient
	}
}

// auth is the token as ops carries it on the context.
func (d *tokenDoors) auth(tok oltp.Token) ops.TokenAuth {
	return ops.TokenAuth{ID: tok.ID, Caps: func(repo string) ops.Caps { return d.db.TokenCaps(tok, repo) },
		Limited: len(tok.Repos) > 0}
}

// wrap puts the token a page or API request came in on onto its
// context, where the router's and the pages' capability checks read
// it, and answers a held account at once. The verifier behind each door
// still makes the decision; this is what makes the decision bounded
// and narrowed.
func (d *tokenDoors) wrap(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		scope := oltp.ScopeWeb
		if strings.HasPrefix(req.URL.Path, "/api/") {
			scope = oltp.ScopeAPI
		}
		tok, p := d.resolve(req, scope)
		switch p {
		case held:
			user, _, _ := req.BasicAuth()
			_, wait := d.limit.Allowed(user, d.ip(req))
			w.Header().Set("Retry-After", strconv.Itoa(int(wait.Seconds())))
			http.Error(w, "too many failed sign-ins; try again in "+wait.String(), http.StatusTooManyRequests)
			return
		case refused:
			// A run token whose run is over is told so, not told it
			// failed to authenticate (RFC 0021 §runs).
			user, _, _ := req.BasicAuth()
			if why := d.db.RunTokenEnded(user); why != "" {
				http.Error(w, why, http.StatusUnauthorized)
				return
			}
		case resolved:
			req = req.WithContext(ops.WithToken(req.Context(), d.auth(tok)))
		}
		next.ServeHTTP(w, req)
	})
}

// Allowed, Failed and Succeeded are the limiter as the xfer front's
// login card sees it.
func (d *tokenDoors) Allowed(req *http.Request, user string) bool {
	if d.limit == nil {
		return true
	}
	ok, _ := d.limit.Allowed(user, d.ip(req))
	return ok
}

func (d *tokenDoors) Failed(req *http.Request, user string) {
	if d.limit != nil {
		d.limit.Failed(user, d.ip(req))
	}
}

func (d *tokenDoors) Succeeded(req *http.Request, user string) {
	if d.limit != nil {
		d.limit.Succeeded(user, d.ip(req))
	}
}

// tokenCtx is a context naming the token's account and carrying the
// token, for a door that resolved one outside wrap.
func (d *tokenDoors) tokenCtx(ctx context.Context, user string, tok oltp.Token) context.Context {
	return ops.WithToken(ops.WithUser(ctx, user), d.auth(tok))
}