Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149package spindle
import ( "bytes" "context" "encoding/json" "fmt" "log/slog" "net/url" "slices" "sync" "time"
"github.com/bluesky-social/indigo/atproto/syntax" "github.com/hashicorp/golang-lru/v2/expirable" gossh "golang.org/x/crypto/ssh"
"tangled.org/core/api/org_tangled" "tangled.org/core/rbac" "tangled.org/core/spindle/knotclient")
const ( debugKeyTTL = time.Minute // the owner plus this many collaborators at most, so one login can't fan out to every pds debugKeyHolderCap = 20 debugKeysPerHolder = 50 debugKeyCacheSize = 512)
// marshalled keys per did, only successful reads get cached so a pds blip doesn't lock people out for a minutetype debugKeyCache struct { once sync.Once byDid *expirable.LRU[syntax.DID, [][]byte]}
func (c *debugKeyCache) lru() *expirable.LRU[syntax.DID, [][]byte] { c.once.Do(func() { c.byDid = expirable.NewLRU[syntax.DID, [][]byte](debugKeyCacheSize, nil, debugKeyTTL) }) return c.byDid}
// fails closed, any error along the way refuses the keyfunc (s *Spindle) DebugKeyAllowed(ctx context.Context, repoDid syntax.DID, key gossh.PublicKey) bool { l := s.l.With("component", "debugssh", "repo", repoDid) repo, err := s.db.GetRepoByDid(repoDid) if err != nil { l.Warn("debug key refused, repo lookup failed", "err", err) return false } if ban, err := s.db.IsBanned(repoDid, repo.Owner); err != nil || ban != nil { l.Warn("debug key refused, repo is banned or the ban lookup failed", "err", err) return false } if gate, err := s.db.Gate(&s.cfg.Server, repoDid); err != nil || gate.Paused { l.Warn("debug key refused, owner is paused or the gate lookup failed", "err", err) return false }
want := key.Marshal() for _, holder := range s.debugKeyHolders(l, repoDid, repo.Owner) { keys, err := s.sshKeysOf(ctx, holder) if err != nil { l.Warn("debug key holder skipped, reading their keys failed", "holder", holder, "err", err) continue } if slices.ContainsFunc(keys, func(k []byte) bool { return bytes.Equal(k, want) }) { return true } } return false}
// owner first and a stable order, otherwise the cap could cut different collaborators each loginfunc (s *Spindle) debugKeyHolders(l *slog.Logger, repoDid, owner syntax.DID) []syntax.DID { candidates := []string{owner.String()} byRepo, err := s.e.GetCollaboratorsByRepo(rbac.ThisServer) if err != nil { l.Warn("debug key holders limited to the owner, collaborator lookup failed", "err", err) } else { candidates = append(candidates, byRepo[repoDid.String()]...) }
var holders []syntax.DID for _, candidate := range candidates { did, err := syntax.ParseDID(candidate) if err != nil || slices.Contains(holders, did) { continue } if ok, err := s.e.IsPushAllowed(candidate, rbac.ThisServer, repoDid.String()); err != nil || !ok { continue } if ban, err := s.db.GetBan(did); err != nil || ban != nil { continue } holders = append(holders, did) if len(holders) == debugKeyHolderCap { break } } return holders}
func (s *Spindle) sshKeysOf(ctx context.Context, did syntax.DID) ([][]byte, error) { cache := s.debugKeys.lru() if keys, ok := cache.Get(did); ok { return keys, nil } pds, err := s.pdsOf(ctx, did) if err != nil { return nil, err } records, err := s.knots.ListRecords(ctx, pds, did, org_tangled.KeySshKeyNSID, debugKeysPerHolder) if err != nil { return nil, fmt.Errorf("listing ssh keys of %s: %w", did, err) } keys := make([][]byte, 0, len(records)) for _, raw := range records { var record org_tangled.KeySshKey if err := json.Unmarshal(raw, &record); err != nil { continue } // usages default to auth and sign when omitted if len(record.Usages) > 0 && !slices.Contains(record.Usages, "auth") { continue } parsed, _, _, _, err := gossh.ParseAuthorizedKey([]byte(record.PublicKey)) if err != nil { continue } keys = append(keys, parsed.Marshal()) } cache.Add(did, keys) return keys, nil}
func (s *Spindle) pdsOf(ctx context.Context, did syntax.DID) (knotclient.Knot, error) { ident, err := s.res.ResolveIdent(ctx, did.String()) if err != nil { return knotclient.Knot{}, fmt.Errorf("resolving %s: %w", did, err) } endpoint, err := url.Parse(ident.PDSEndpoint()) if err != nil || endpoint.Host == "" { return knotclient.Knot{}, fmt.Errorf("%s has no usable pds endpoint", did) } return knotclient.Knot{Host: endpoint.Host, Scheme: endpoint.Scheme}, nil}