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)
if !ok || !tok.Reaches(repo) {
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) }}
}
// 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 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))
}