Files
sift/internal/auth/auth.go
T
kyle 4372673184 p2.7: prometheus /metrics
Default GET /metrics is Prometheus text; JSON stays at /metrics.json
and ?format=json. :8443 needs the metrics caller bearer; loopback
stays open. Restricted callers have no per-caller series and no
tavily:restricted gauge.
2026-09-23 21:49:38 -07:00

220 lines
4.2 KiB
Go

package auth
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"syscall"
"github.com/pelletier/go-toml/v2"
)
const NameMetrics = "metrics"
type Caller struct {
Name string `toml:"name"`
Workspace string `toml:"workspace"`
KeySHA256 string `toml:"key_sha256"`
}
// MetricsOnly is the scrape-token caller: empty workspace, no fetch/search.
func (c *Caller) MetricsOnly() bool {
return c != nil && c.Name == NameMetrics
}
type file struct {
Caller []Caller `toml:"caller"`
}
// Store holds hashed keys. Reload on SIGHUP via Load.
type Store struct {
mu sync.RWMutex
path string
callers []Caller
}
func Load(path string) (*Store, error) {
s := &Store{path: path}
if err := s.reload(); err != nil {
return nil, err
}
return s, nil
}
func (s *Store) Reload() error { return s.reload() }
// Callers returns a copy of the loaded caller list (names and workspaces only).
func (s *Store) Callers() []Caller {
if s == nil {
return nil
}
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]Caller, len(s.callers))
copy(out, s.callers)
return out
}
func (s *Store) reload() error {
b, err := os.ReadFile(s.path)
if err != nil {
return err
}
var f file
if err := toml.Unmarshal(b, &f); err != nil {
return err
}
s.mu.Lock()
s.callers = f.Caller
s.mu.Unlock()
return nil
}
// LookupBearer returns the caller for a raw bearer token, or nil.
func (s *Store) LookupBearer(token string) *Caller {
if token == "" || s == nil {
return nil
}
sum := sha256.Sum256([]byte(token))
want := hex.EncodeToString(sum[:])
s.mu.RLock()
defer s.mu.RUnlock()
var found *Caller
for i := range s.callers {
got := strings.ToLower(strings.TrimSpace(s.callers[i].KeySHA256))
if len(got) != len(want) {
continue
}
if subtle.ConstantTimeCompare([]byte(got), []byte(want)) == 1 {
c := s.callers[i]
found = &c
}
}
return found
}
func HashKey(key string) string {
sum := sha256.Sum256([]byte(key))
return hex.EncodeToString(sum[:])
}
func GenerateKey() (string, error) {
var b [32]byte
if _, err := rand.Read(b[:]); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b[:]), nil
}
func Add(path, name, workspace, keyOut string) error {
if name == NameMetrics {
workspace = ""
}
if _, err := os.Stat(keyOut); err == nil {
return fmt.Errorf("refusing to overwrite %s", keyOut)
}
key, err := GenerateKey()
if err != nil {
return err
}
if err := os.WriteFile(keyOut, []byte(key+"\n"), 0o600); err != nil {
return err
}
var f file
if b, err := os.ReadFile(path); err == nil {
_ = toml.Unmarshal(b, &f)
}
f.Caller = append(f.Caller, Caller{Name: name, Workspace: workspace, KeySHA256: HashKey(key)})
out, err := toml.Marshal(f)
if err != nil {
return err
}
return writePreserving(path, out)
}
func List(path string) ([]Caller, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var f file
if err := toml.Unmarshal(b, &f); err != nil {
return nil, err
}
return f.Caller, nil
}
func Revoke(path, name string) error {
b, err := os.ReadFile(path)
if err != nil {
return err
}
var f file
if err := toml.Unmarshal(b, &f); err != nil {
return err
}
out := f.Caller[:0]
for _, c := range f.Caller {
if c.Name != name {
out = append(out, c)
}
}
f.Caller = out
raw, err := toml.Marshal(f)
if err != nil {
return err
}
return writePreserving(path, raw)
}
func writePreserving(path string, data []byte) error {
mode := os.FileMode(0o640)
uid, gid := -1, -1
if st, err := os.Stat(path); err == nil {
mode = st.Mode()
if sys, ok := st.Sys().(*syscall.Stat_t); ok {
uid, gid = int(sys.Uid), int(sys.Gid)
}
}
f, err := os.CreateTemp(filepath.Dir(path), ".sift-keys-*.tmp")
if err != nil {
return err
}
tmp := f.Name()
ok := false
defer func() {
if !ok {
_ = os.Remove(tmp)
}
}()
if _, err := f.Write(data); err != nil {
_ = f.Close()
return err
}
if err := f.Chmod(mode.Perm()); err != nil {
_ = f.Close()
return err
}
if uid >= 0 {
if err := f.Chown(uid, gid); err != nil {
_ = f.Close()
return err
}
}
if err := f.Close(); err != nil {
return err
}
if err := os.Rename(tmp, path); err != nil {
return err
}
ok = true
return nil
}