Digest, chunk-embed, and question-embed keys include fleet|restricted so a fleet caller cannot observe a restricted cache hit. Digest.Run takes the class from permit_restricted. A permit_restricted caller whose keys.toml workspace is not guardian-star is refused at Search (no Honcho request) and flagged at serve start and keys reload. docs: P3 restricted callers search like the fleet; cache/logs isolate.
210 lines
4.0 KiB
Go
210 lines
4.0 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"
|
|
)
|
|
|
|
type Caller struct {
|
|
Name string `toml:"name"`
|
|
Workspace string `toml:"workspace"`
|
|
KeySHA256 string `toml:"key_sha256"`
|
|
}
|
|
|
|
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 _, 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
|
|
}
|