Make nmon easier to install and run without editing YAML by hand: - Settings: point-and-click forms over config.yaml (GET/PUT /api/v1/settings/form); the YAML editor moves to /settings/yaml. - First install: with no sign-in configured nmon starts in install mode, serves only the setup wizard (sign-in, basics, devices, alerting) and closes it for good once sign-in is saved. A self-signed certificate is generated when no TLS source is given. poller.interval becomes a config setting. - Reverse proxies: opt-in --http-listen plain-HTTP listener beside HTTPS; sign-in stays required. - Webhooks: Generic, Teams Adaptive Card, Slack and Discord presets, template preview and test send. - Help: the operations, coverage and profile docs served at /help. - Polling: per-device "Poll now", a dry-run test poll returning an SNMP trace, and time-limited in-memory SNMP debug recording, in the UI and as `nmon device poll`. - Ansible: deploys into install mode when no sign-in is set, and no longer overwrites the config.yaml the wizard wrote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017TuKgyKve2XLFtrrXWMi2w
78 lines
3.3 KiB
Go
78 lines
3.3 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"sort"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
|
|
"github.com/v10networks/nmon/internal/auth"
|
|
)
|
|
|
|
// Route is one operation of the API as the Go 1.22 ServeMux and auth.Policy write it.
|
|
type Route struct {
|
|
Method string
|
|
Pattern string // "PUT /api/v1/devices/{slug}"
|
|
Path string // "/api/v1/devices/{slug}"
|
|
}
|
|
|
|
// Routes lists every operation huma registers, taken from the same registration that serves them. The
|
|
// viewer-cannot-mutate test and the auth policy both start from it, so a new route cannot be missed.
|
|
func Routes() []Route {
|
|
s := &server{Deps: Deps{}.withDefaults()}
|
|
api := s.newHuma(http.NewServeMux())
|
|
s.register(api)
|
|
var out []Route
|
|
for path, item := range api.OpenAPI().Paths {
|
|
for method, op := range map[string]*huma.Operation{
|
|
http.MethodGet: item.Get, http.MethodPut: item.Put, http.MethodPost: item.Post,
|
|
http.MethodDelete: item.Delete, http.MethodPatch: item.Patch,
|
|
} {
|
|
if op == nil {
|
|
continue
|
|
}
|
|
out = append(out, Route{Method: method, Pattern: method + " " + path, Path: path})
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Pattern < out[j].Pattern })
|
|
return out
|
|
}
|
|
|
|
// adminReads are GET routes only an admin may call: they reveal who can reach nmon or how it is configured.
|
|
var adminReads = []string{"GET /api/v1/tokens", "GET /api/v1/settings", "GET /api/v1/settings/form", "GET /api/v1/audit",
|
|
"GET /api/v1/devices/{slug}/snmp-debug"}
|
|
|
|
// viewerPosts are non-GET routes that only read, so a viewer may call them (a query endpoint taking a body).
|
|
var viewerPosts = []string{"POST /api/v1/alert-policies/preview", "POST /api/v1/alert-rules/test"}
|
|
|
|
// installRoutes are the only API routes served in install mode (the first-install wizard, before any sign-in exists):
|
|
// the setup routes, the typed settings form, webhook presets/preview/test, adding and watching devices, alert
|
|
// policies, the profile list, secrets and meta. Everything else answers 503 "setup not finished".
|
|
var installRoutes = []string{
|
|
"GET /api/v1/setup", "POST /api/v1/setup/signin", "POST /api/v1/setup/recommended-policies", "POST /api/v1/setup/complete",
|
|
"GET /api/v1/meta", "GET /api/v1/settings/form", "PUT /api/v1/settings/form",
|
|
"GET /api/v1/notify/presets", "POST /api/v1/notify/preview", "POST /api/v1/notify/test",
|
|
"GET /api/v1/devices", "POST /api/v1/devices", "POST /api/v1/devices/import", "GET /api/v1/devices/{slug}",
|
|
"GET /api/v1/alert-policies", "POST /api/v1/alert-policies", "GET /api/v1/profiles", "PUT /api/v1/secrets/{name}",
|
|
}
|
|
|
|
// InstallRoutes lists the routes install mode serves, for the tests that keep it in step with the API.
|
|
func InstallRoutes() []string { return append([]string(nil), installRoutes...) }
|
|
|
|
// DeclarePolicy states the minimum role of each route that departs from the default (GET and HEAD need viewer,
|
|
// everything else admin).
|
|
func DeclarePolicy(p *auth.Policy) {
|
|
for _, pat := range adminReads {
|
|
p.Require(pat, auth.RoleAdmin)
|
|
}
|
|
for _, pat := range viewerPosts {
|
|
p.Require(pat, auth.RoleViewer)
|
|
}
|
|
// The setup state is public: the UI asks it before anyone can sign in, to learn whether to show the wizard.
|
|
p.Public("GET /api/v1/setup")
|
|
p.AllowInstall(installRoutes...)
|
|
}
|
|
|
|
// ReadOnlyPosts are the non-GET patterns declared viewer-level, for the viewer-cannot-mutate test to skip.
|
|
func ReadOnlyPosts() []string { return append([]string(nil), viewerPosts...) }
|