Add PUT /api/agents/{id} endpoint for updating agent WL/RL

HandleUpdateAgent allows updating:
- IP whitelist (ip_whitelist array)
- Rate limit per minute (rate_limit_minute)

Does NOT allow updating (security):
- Admin status (intentionally omitted)
- All_access flag
- Scopes

Requires admin token (PRF tap) since it's a sensitive operation.
This enables the Edit Agent GUI in agents.html to save changes.
This commit is contained in:
James 2026-04-02 02:14:00 -04:00
parent c4350a614e
commit d3e9f89bc0
2 changed files with 50 additions and 1 deletions

View File

@ -1094,7 +1094,8 @@ func (h *Handlers) HandleCreateAgent(w http.ResponseWriter, r *http.Request) {
}
func (h *Handlers) HandleListAgents(w http.ResponseWriter, r *http.Request) {
if h.requireAdmin(w, r) {
// Only web users (not agents) can list agents
if h.requireOwner(w, r) {
return
}
entries, err := lib.EntryList(h.db(r), h.vk(r), nil)
@ -1148,6 +1149,53 @@ func (h *Handlers) HandleDeleteAgent(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, http.StatusOK, map[string]string{"status": "deleted"})
}
func (h *Handlers) HandleUpdateAgent(w http.ResponseWriter, r *http.Request) {
if h.requireAdmin(w, r) {
return
}
entryID, err := lib.HexToID(chi.URLParam(r, "id"))
if err != nil {
ErrorResponse(w, http.StatusBadRequest, "invalid_id", "Invalid entry ID")
return
}
entry, _ := lib.EntryGet(h.db(r), h.vk(r), entryID)
if entry == nil || entry.Type != lib.TypeAgent {
ErrorResponse(w, http.StatusNotFound, "not_found", "Agent not found")
return
}
var req struct {
IPWhitelist []string `json:"ip_whitelist"`
RateLimitMinute int `json:"rate_limit_minute"`
RateLimitHour int `json:"rate_limit_hour"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
return
}
// Update allowed fields only (NOT admin, all_access, or scopes)
if entry.VaultData == nil {
ErrorResponse(w, http.StatusInternalServerError, "agent_data_missing", "Agent data not found")
return
}
if req.IPWhitelist != nil {
entry.VaultData.AllowedIPs = strings.Join(req.IPWhitelist, ",")
}
if req.RateLimitMinute > 0 {
entry.VaultData.RateLimit = req.RateLimitMinute
}
// Note: RateLimitHour is not stored separately in current model
// Re-encrypt and update entry
if err := lib.EntryUpdate(h.db(r), h.vk(r), entry); err != nil {
ErrorResponse(w, http.StatusInternalServerError, "update_failed", "Failed to update agent")
return
}
lib.AuditLog(h.db(r), &lib.AuditEvent{
Action: lib.ActionAgentUpdate, Actor: ActorFromContext(r.Context()),
IPAddr: realIP(r), Title: entry.Title,
})
JSONResponse(w, http.StatusOK, map[string]string{"status": "updated"})
}
func (h *Handlers) HandleUpdateEntryScopes(w http.ResponseWriter, r *http.Request) {
if h.requireAdmin(w, r) {
return

View File

@ -134,6 +134,7 @@ func mountAPIRoutes(r chi.Router, h *Handlers) {
// Agent management (admin-only — requires PRF tap + admin token)
r.Post("/agents", h.HandleCreateAgent)
r.Get("/agents", h.HandleListAgents)
r.Put("/agents/{id}", h.HandleUpdateAgent)
r.Delete("/agents/{id}", h.HandleDeleteAgent)
// WebAuthn credential management