// 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 }