89 lines
2.7 KiB
Go
89 lines
2.7 KiB
Go
// HTTP-transport (PAT) access checks.
|
|
//
|
|
// A PAT is an opaque credential. Only its sha256 hash is stored; the plaintext
|
|
// is returned once at creation. AccessByPAT takes the PAT string directly (the
|
|
// cmd layer parses it out of the Authorization header) and checks it against the
|
|
// repo's (or its namespace's) ACL -- the state-layer half of the IState.AccessByPAT
|
|
// contract.
|
|
//
|
|
// PATs are created via ACLUpsertPATOnNS/OnRepo (IStateMut); there is no separate
|
|
// CreatePAT and intentionally no list/management read surface.
|
|
|
|
package state
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"simplegit/common"
|
|
)
|
|
|
|
const patPrefix = "sgp_" // plaintext token prefix
|
|
|
|
// AccessByPAT authorizes pat for perm on owner/name.
|
|
//
|
|
// - public read is open to everyone, including anonymous (pat ignored);
|
|
// - an empty pat is anonymous: allowed only by the public-read shortcut,
|
|
// otherwise ErrDenied (the caller decides 401 vs 403 from pat presence);
|
|
// - a present-but-unknown or expired pat yields ErrInvalidToken;
|
|
// - an authenticated but insufficient grant yields ErrDenied;
|
|
// - an unknown repo yields ErrDenied (existence never leaked).
|
|
//
|
|
// skip-auth allows all.
|
|
func (s *LocalState) AccessByPAT(ctx context.Context, pat, owner, name string, perm common.Perm) (bool, error) {
|
|
r, err := s.findRepo(owner, name)
|
|
if err != nil {
|
|
if errors.Is(err, ErrRepoNotFound) {
|
|
return false, ErrDenied
|
|
}
|
|
return false, err
|
|
}
|
|
// public read is open to everyone, including anonymous
|
|
if perm == common.PermRead && !r.IsPrivate {
|
|
return true, nil
|
|
}
|
|
if pat == "" {
|
|
// anonymous, but this needs a grant (private read, or any write)
|
|
return false, ErrDenied
|
|
}
|
|
p, err := s.lookupPAT(pat)
|
|
if err != nil {
|
|
return false, err // ErrInvalidToken
|
|
}
|
|
ok, err := s.hasAccess(CredTypePAT, p.ID, r.ID, r.NamespaceID, perm)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !ok {
|
|
return false, ErrDenied
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// lookupPAT hashes the presented token and returns the matching PAT row, or
|
|
// ErrInvalidToken if unknown or expired. Best-effort last-used stamp.
|
|
func (s *LocalState) lookupPAT(token string) (*PAT, error) {
|
|
sum := sha256.Sum256([]byte(token))
|
|
var pat PAT
|
|
has, err := s.engine.Where("token_hash = ?", hex.EncodeToString(sum[:])).Get(&pat)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("lookup pat: %w", err)
|
|
}
|
|
if !has {
|
|
return nil, ErrInvalidToken
|
|
}
|
|
if pat.ExpiresAt != nil && time.Now().After(*pat.ExpiresAt) {
|
|
return nil, ErrInvalidToken
|
|
}
|
|
// best-effort last-used stamp; a failure here must not block access
|
|
now := time.Now()
|
|
if _, err := s.engine.ID(pat.ID).Cols("last_used_at").Update(&PAT{LastUsedAt: &now}); err == nil {
|
|
pat.LastUsedAt = &now
|
|
}
|
|
return &pat, nil
|
|
}
|