API keys for server scope: edges, server alerts and orgs can be scripted

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-27 16:54:14 -06:00
1 parent 39659f0144
commit 1c2a5b71bd
6 files changed
+232 -9

No files matched your search

+79 -5
View File
@@ -29,7 +29,9 @@ func (s *Server) createOrg(w http.ResponseWriter, r *http.Request, p *principal)
}
s.mu.Lock()
org, adminRole := s.newOrgLocked(in.Name, in.AllowLocal)
s.st.Members = append(s.st.Members, &Member{UserID: p.User.ID, OrgID: org.ID, RoleID: adminRole.ID})
if p.User.ID != "" { // a server key belongs to no user
s.st.Members = append(s.st.Members, &Member{UserID: p.User.ID, OrgID: org.ID, RoleID: adminRole.ID})
}
s.save()
o := *org
s.mu.Unlock()
@@ -423,7 +425,8 @@ func (s *Server) listKeys(w http.ResponseWriter, r *http.Request, c *orgCtx) {
writeJSON(w, 200, out)
}
func (s *Server) createKey(w http.ResponseWriter, r *http.Request, c *orgCtx) {
// issueKey stores a new key for the org ("" = a server key) and returns its ID and full token.
func (s *Server) issueKey(w http.ResponseWriter, r *http.Request, org, actor string) {
var in struct{ Name string }
if !decode(w, r, &in) {
return
@@ -435,11 +438,82 @@ func (s *Server) createKey(w http.ResponseWriter, r *http.Request, c *orgCtx) {
}
id, secret := randHex(4), randHex(24)
s.mu.Lock()
s.st.APIKeys = append(s.st.APIKeys, &APIKey{ID: id, OrgID: c.Org.ID, Name: in.Name, Hash: sha(secret), Created: time.Now()})
s.st.APIKeys = append(s.st.APIKeys, &APIKey{ID: id, OrgID: org, Name: in.Name, Hash: sha(secret), Created: time.Now()})
s.save()
s.mu.Unlock()
s.audit(c.Org.ID, c.Actor, "apikey.create", in.Name)
writeJSON(w, 200, map[string]string{"key": "hhk_" + id + "_" + secret})
s.audit(org, actor, "apikey.create", in.Name)
writeJSON(w, 200, map[string]string{"id": id, "key": "hhk_" + id + "_" + secret})
}
func (s *Server) createKey(w http.ResponseWriter, r *http.Request, c *orgCtx) {
s.issueKey(w, r, c.Org.ID, c.Actor)
}
// Server keys manage edges, server-level alerts and organizations, so only a signed-in
// server administrator may mint, list or revoke them (a key can't make more keys).
func (s *Server) withServerAdminUser(fn func(http.ResponseWriter, *http.Request, *principal)) http.HandlerFunc {
return s.withUser(func(w http.ResponseWriter, r *http.Request, p *principal) {
if !p.User.ServerAdmin {
fail(w, 403, errors.New("server administrators only"))
return
}
fn(w, r, p)
})
}
func (s *Server) listServerKeys(w http.ResponseWriter, r *http.Request, p *principal) {
s.mu.Lock()
defer s.mu.Unlock()
out := []APIKey{}
for _, k := range s.st.APIKeys {
if k.OrgID == "" {
cp := *k
cp.Hash = ""
out = append(out, cp)
}
}
writeJSON(w, 200, out)
}
func (s *Server) createServerKey(w http.ResponseWriter, r *http.Request, p *principal) {
s.issueKey(w, r, "", p.User.Email)
}
func (s *Server) deleteServerKey(w http.ResponseWriter, r *http.Request, p *principal) {
id := r.PathValue("id")
s.mu.Lock()
defer s.mu.Unlock()
s.st.APIKeys = filter(s.st.APIKeys, func(k *APIKey) bool { return !(k.ID == id && k.OrgID == "") })
s.save()
s.audit("", p.User.Email, "apikey.delete", id)
ok(w)
}
// listOrgs shows the organizations the caller can reach: a server key sees all of them,
// an org key its own.
func (s *Server) listOrgs(w http.ResponseWriter, r *http.Request) {
p := s.principalFrom(r)
if p == nil {
fail(w, 401, errors.New("Sign in to continue."))
return
}
s.mu.Lock()
defer s.mu.Unlock()
out := []Org{}
for _, o := range s.st.Orgs {
switch {
case p.serverKey() || p.User.ServerAdmin:
out = append(out, *o)
case p.Key != nil:
if p.Key.OrgID == o.ID {
out = append(out, *o)
}
case s.member(p.User.ID, o.ID) != nil:
out = append(out, *o)
}
}
writeJSON(w, 200, out)
}
func (s *Server) deleteKey(w http.ResponseWriter, r *http.Request, c *orgCtx) {