Add authentication: admin/agent bearer tokens for the API, login for the dashboard
control-api: every route now carries a mandatory access level (admin / agent / open) in a route table. All /api/v1/admin/* require the admin token; the write calls of validator-agent and prober (self-check, events, results, complete) require a separate static agent token; register, heartbeat and fetching the assignment stay open. Tokens come from env vars, are compared in constant time and never logged. An empty token leaves that level open with a startup warning (backward compatible). validator-agent / prober: apiclient sends the agent token only to control-api. admin-dashboard: login/password (from env) with a stateless HMAC session cookie, Origin-based CSRF check, per-IP brute-force throttle, HX-Redirect for htmx polls, logout in the sidebar; the dashboard calls control-api with the admin token. Login page layout fixed after review. Also: env plumbing in docker-compose/rxprod-compose/systemd/config examples, e2e script with token assertions, tests, docs (API, SETUP, USAGE, DASHBOARD, README), plan and review under docs/changes/, bin/ rebuilt with new SHA256SUMS. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
972ad47d0c
commit
debf2afed2
67 files changed
+2050
-105
No files matched your search
@@ -0,0 +1,108 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// accessLevel says which credential a route requires.
|
||||
type accessLevel int
|
||||
|
||||
const (
|
||||
// accessOpen: no credential required (registration, heartbeat, fetching
|
||||
// the assignment, /healthz).
|
||||
accessOpen accessLevel = iota + 1
|
||||
// accessAgent: requires the static agent token (validator-agents and
|
||||
// probers writing results/events).
|
||||
accessAgent
|
||||
// accessAdmin: requires the administrator token (/api/v1/admin/*).
|
||||
accessAdmin
|
||||
)
|
||||
|
||||
func (a accessLevel) String() string {
|
||||
switch a {
|
||||
case accessOpen:
|
||||
return "open"
|
||||
case accessAgent:
|
||||
return "agent"
|
||||
case accessAdmin:
|
||||
return "admin"
|
||||
}
|
||||
return "invalid"
|
||||
}
|
||||
|
||||
// Authenticator holds the static bearer tokens. An empty token disables the
|
||||
// check for that level (backward compatibility: the API starts open with a
|
||||
// warning, see cmd/control-api).
|
||||
type Authenticator struct {
|
||||
AdminToken string
|
||||
AgentToken string
|
||||
Log *slog.Logger
|
||||
}
|
||||
|
||||
// WithAuth sets the admin and agent tokens. Call it before Handler().
|
||||
func (s *Server) WithAuth(admin, agent string) *Server {
|
||||
s.Auth = Authenticator{AdminToken: admin, AgentToken: agent}
|
||||
return s
|
||||
}
|
||||
|
||||
func (a Authenticator) tokenFor(level accessLevel) string {
|
||||
switch level {
|
||||
case accessAdmin:
|
||||
return a.AdminToken
|
||||
case accessAgent:
|
||||
return a.AgentToken
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// require wraps h so that it is only reached with the credential demanded by
|
||||
// level. Open routes, and levels with an empty configured token, pass through.
|
||||
func (a Authenticator) require(level accessLevel, h http.HandlerFunc) http.HandlerFunc {
|
||||
if level == accessOpen {
|
||||
return h
|
||||
}
|
||||
if level != accessAdmin && level != accessAgent {
|
||||
// An unclassified route must never be served.
|
||||
panic("httpapi: route registered without a valid access level")
|
||||
}
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
want := a.tokenFor(level)
|
||||
if want == "" {
|
||||
h(w, r)
|
||||
return
|
||||
}
|
||||
got, ok := bearerToken(r)
|
||||
if !ok || !tokensEqual(got, want) {
|
||||
if a.Log != nil {
|
||||
a.Log.Warn("unauthorized request", "level", level.String(),
|
||||
"method", r.Method, "path", r.URL.Path, "remote", r.RemoteAddr)
|
||||
}
|
||||
w.Header().Set("WWW-Authenticate", "Bearer")
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
h(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// bearerToken extracts the token from "Authorization: Bearer <token>".
|
||||
func bearerToken(r *http.Request) (string, bool) {
|
||||
h := r.Header.Get("Authorization")
|
||||
const prefix = "bearer "
|
||||
if len(h) <= len(prefix) || !strings.EqualFold(h[:len(prefix)], prefix) {
|
||||
return "", false
|
||||
}
|
||||
tok := strings.TrimSpace(h[len(prefix):])
|
||||
return tok, tok != ""
|
||||
}
|
||||
|
||||
// tokensEqual compares in constant time; hashing first hides the length.
|
||||
func tokensEqual(got, want string) bool {
|
||||
g := sha256.Sum256([]byte(got))
|
||||
w := sha256.Sum256([]byte(want))
|
||||
return subtle.ConstantTimeCompare(g[:], w[:]) == 1
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cloudipvalidator/internal/config"
|
||||
"cloudipvalidator/internal/db"
|
||||
"cloudipvalidator/internal/openstack"
|
||||
"cloudipvalidator/internal/orchestrator"
|
||||
)
|
||||
|
||||
const (
|
||||
testAdminToken = "admin-token-for-tests"
|
||||
testAgentToken = "agent-token-for-tests"
|
||||
)
|
||||
|
||||
func newAuthTestServer(t *testing.T, admin, agent string) (*Server, *httptest.Server) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
d, err := db.Open(ctx, filepath.Join(t.TempDir(), "test.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { d.Close() })
|
||||
cfg := &config.ControlAPI{
|
||||
Orchestrator: config.OrchestratorConfig{
|
||||
PollIntervalSeconds: 1, SelfCheckTimeoutSeconds: 10, MaxSelfCheckRetries: 3,
|
||||
CheckingWindowSeconds: 120, MaxRetries: 3, LeaseTTLSeconds: 180, HeartbeatTimeoutSeconds: 30,
|
||||
},
|
||||
Inbound: config.InboundConfig{Ports: []int{22}, ICMP: true},
|
||||
}
|
||||
if err := d.BootstrapFromConfig(ctx, cfg); err != nil {
|
||||
t.Fatalf("bootstrap: %v", err)
|
||||
}
|
||||
log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
||||
srv := New(d, orchestrator.New(d, openstack.NewMockClient(), cfg, log), log).WithAuth(admin, agent)
|
||||
ts := httptest.NewServer(srv.Handler())
|
||||
t.Cleanup(ts.Close)
|
||||
return srv, ts
|
||||
}
|
||||
|
||||
// concretePath fills the {wildcards} of a ServeMux pattern with dummy values.
|
||||
func concretePath(pattern string) (method, path string) {
|
||||
method, path, _ = strings.Cut(pattern, " ")
|
||||
var b strings.Builder
|
||||
for {
|
||||
i := strings.IndexByte(path, '{')
|
||||
if i < 0 {
|
||||
b.WriteString(path)
|
||||
break
|
||||
}
|
||||
j := strings.IndexByte(path, '}')
|
||||
b.WriteString(path[:i])
|
||||
b.WriteString("1")
|
||||
path = path[j+1:]
|
||||
}
|
||||
return method, b.String()
|
||||
}
|
||||
|
||||
func callWithToken(t *testing.T, ts *httptest.Server, pattern, token string) *http.Response {
|
||||
t.Helper()
|
||||
method, path := concretePath(pattern)
|
||||
req, err := http.NewRequest(method, ts.URL+path, strings.NewReader("{}"))
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
resp, err := ts.Client().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", pattern, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
return resp
|
||||
}
|
||||
|
||||
func TestRouteTableIsClassified(t *testing.T) {
|
||||
srv, _ := newAuthTestServer(t, "", "")
|
||||
counts := map[string]int{}
|
||||
for _, rt := range srv.Routes() {
|
||||
counts[rt.Access]++
|
||||
if rt.Access == "invalid" {
|
||||
t.Fatalf("route %q has no valid access level", rt.Pattern)
|
||||
}
|
||||
if strings.Contains(rt.Pattern, "/api/v1/admin/") && rt.Access != "admin" {
|
||||
t.Fatalf("admin route %q is %s, want admin", rt.Pattern, rt.Access)
|
||||
}
|
||||
}
|
||||
if counts["admin"] != 33 || counts["agent"] != 5 || counts["open"] != 7 {
|
||||
t.Fatalf("access counts = %v, want admin=33 agent=5 open=7", counts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMatrixOverRouteTable(t *testing.T) {
|
||||
srv, ts := newAuthTestServer(t, testAdminToken, testAgentToken)
|
||||
for _, rt := range srv.Routes() {
|
||||
rt := rt
|
||||
t.Run(rt.Pattern, func(t *testing.T) {
|
||||
tokens := []struct {
|
||||
name, token string
|
||||
allowed bool
|
||||
}{
|
||||
{"none", "", rt.Access == "open"},
|
||||
{"wrong", "definitely-wrong", rt.Access == "open"},
|
||||
{"admin token", testAdminToken, rt.Access == "open" || rt.Access == "admin"},
|
||||
{"agent token", testAgentToken, rt.Access == "open" || rt.Access == "agent"},
|
||||
}
|
||||
for _, tc := range tokens {
|
||||
resp := callWithToken(t, ts, rt.Pattern, tc.token)
|
||||
if tc.allowed && resp.StatusCode == http.StatusUnauthorized {
|
||||
t.Fatalf("%s: got 401, want request to pass auth", tc.name)
|
||||
}
|
||||
if !tc.allowed {
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("%s: status=%d, want 401", tc.name, resp.StatusCode)
|
||||
}
|
||||
if resp.Header.Get("WWW-Authenticate") != "Bearer" {
|
||||
t.Fatalf("%s: missing WWW-Authenticate: Bearer", tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyTokensLeaveEverythingOpen(t *testing.T) {
|
||||
srv, ts := newAuthTestServer(t, "", "")
|
||||
for _, rt := range srv.Routes() {
|
||||
if resp := callWithToken(t, ts, rt.Pattern, ""); resp.StatusCode == http.StatusUnauthorized {
|
||||
t.Fatalf("%s: got 401 with no tokens configured", rt.Pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnlyConfiguredLevelIsEnforced(t *testing.T) {
|
||||
_, ts := newAuthTestServer(t, testAdminToken, "")
|
||||
if resp := callWithToken(t, ts, "GET /api/v1/admin/status", ""); resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("admin without token: status=%d, want 401", resp.StatusCode)
|
||||
}
|
||||
if resp := callWithToken(t, ts, "POST /api/v1/agents/{id}/results", ""); resp.StatusCode == http.StatusUnauthorized {
|
||||
t.Fatalf("agent route must stay open while agent token is unset")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthzAlwaysOpenAndBearerParsing(t *testing.T) {
|
||||
_, ts := newAuthTestServer(t, testAdminToken, testAgentToken)
|
||||
if resp := callWithToken(t, ts, "GET /healthz", ""); resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("healthz: status=%d, want 200", resp.StatusCode)
|
||||
}
|
||||
// Raw token without the Bearer scheme must not authenticate.
|
||||
req, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/v1/admin/status", nil)
|
||||
req.Header.Set("Authorization", testAdminToken)
|
||||
resp, err := ts.Client().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("scheme-less token: status=%d, want 401", resp.StatusCode)
|
||||
}
|
||||
// Lowercase scheme is accepted (RFC 7235: case-insensitive).
|
||||
req, _ = http.NewRequest(http.MethodGet, ts.URL+"/api/v1/admin/status", nil)
|
||||
req.Header.Set("Authorization", "bearer "+testAdminToken)
|
||||
resp, err = ts.Client().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("lowercase bearer: status=%d, want 200", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
+75
-53
@@ -2,61 +2,83 @@ package httpapi
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (s *Server) routes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /healthz", s.handleHealthz)
|
||||
// route is one entry of the Control API route table. The access level is
|
||||
// mandatory: every route must state who may call it, so a new endpoint cannot
|
||||
// be exposed by accident. The same table is used by the tests.
|
||||
type route struct {
|
||||
Pattern string
|
||||
Handler http.HandlerFunc
|
||||
Access accessLevel
|
||||
}
|
||||
|
||||
mux.HandleFunc("POST /api/v1/agents/register", s.handleAgentRegister)
|
||||
mux.HandleFunc("POST /api/v1/agents/{id}/heartbeat", s.handleAgentHeartbeat)
|
||||
mux.HandleFunc("GET /api/v1/agents/{id}/assignment", s.handleAgentAssignment)
|
||||
mux.HandleFunc("POST /api/v1/agents/{id}/self-check", s.handleAgentSelfCheck)
|
||||
mux.HandleFunc("POST /api/v1/agents/{id}/events", s.handleAgentEvent)
|
||||
mux.HandleFunc("POST /api/v1/agents/{id}/results", s.handleAgentResults)
|
||||
mux.HandleFunc("POST /api/v1/agents/{id}/complete", s.handleAgentComplete)
|
||||
// RouteInfo is the exported, handler-free view of a route table entry.
|
||||
type RouteInfo struct {
|
||||
Pattern string
|
||||
Access string // "open", "agent" or "admin"
|
||||
}
|
||||
|
||||
mux.HandleFunc("POST /api/v1/probers/register", s.handleProberRegister)
|
||||
mux.HandleFunc("POST /api/v1/probers/{site_id}/heartbeat", s.handleProberHeartbeat)
|
||||
mux.HandleFunc("GET /api/v1/probers/{site_id}/assignments", s.handleProberAssignments)
|
||||
mux.HandleFunc("POST /api/v1/probers/{site_id}/results", s.handleProberResults)
|
||||
// Routes returns the access classification of every registered route.
|
||||
func (s *Server) Routes() []RouteInfo {
|
||||
table := s.routeTable()
|
||||
out := make([]RouteInfo, 0, len(table))
|
||||
for _, rt := range table {
|
||||
out = append(out, RouteInfo{Pattern: rt.Pattern, Access: rt.Access.String()})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
mux.HandleFunc("GET /api/v1/admin/status", s.handleAdminStatus)
|
||||
mux.HandleFunc("GET /api/v1/admin/ips", s.handleAdminIPs)
|
||||
mux.HandleFunc("POST /api/v1/admin/ips", s.handleAdminSubmitIPs)
|
||||
mux.HandleFunc("POST /api/v1/admin/ips/scan", s.handleAdminScanFloatingIPs)
|
||||
mux.HandleFunc("GET /api/v1/admin/ips/{ip}", s.handleAdminIPDetail)
|
||||
mux.HandleFunc("POST /api/v1/admin/ips/{ip}/cancel", s.handleAdminCancelIP)
|
||||
mux.HandleFunc("DELETE /api/v1/admin/ips/{ip}", s.handleAdminDeleteIP)
|
||||
mux.HandleFunc("POST /api/v1/admin/ips/delete", s.handleAdminDeleteIPs)
|
||||
mux.HandleFunc("POST /api/v1/admin/ips/clear", s.handleAdminClearQueue)
|
||||
mux.HandleFunc("GET /api/v1/admin/validators", s.handleAdminValidators)
|
||||
|
||||
mux.HandleFunc("GET /api/v1/admin/auto-cycle", s.handleAdminGetAutoCycle)
|
||||
mux.HandleFunc("PUT /api/v1/admin/auto-cycle", s.handleAdminPutAutoCycle)
|
||||
mux.HandleFunc("POST /api/v1/admin/auto-cycle/start", s.handleAdminStartAutoCycle)
|
||||
mux.HandleFunc("POST /api/v1/admin/auto-cycle/stop", s.handleAdminStopAutoCycle)
|
||||
|
||||
mux.HandleFunc("GET /api/v1/admin/registry", s.handleAdminRegistry)
|
||||
mux.HandleFunc("GET /api/v1/admin/registry/{ip}", s.handleAdminRegistryHistory)
|
||||
|
||||
mux.HandleFunc("GET /api/v1/admin/config/validators", s.handleConfigListValidators)
|
||||
mux.HandleFunc("POST /api/v1/admin/config/validators", s.handleConfigCreateValidator)
|
||||
mux.HandleFunc("PUT /api/v1/admin/config/validators/{id}", s.handleConfigUpdateValidator)
|
||||
mux.HandleFunc("DELETE /api/v1/admin/config/validators/{id}", s.handleConfigDeleteValidator)
|
||||
|
||||
mux.HandleFunc("GET /api/v1/admin/config/sites", s.handleConfigListSites)
|
||||
mux.HandleFunc("PUT /api/v1/admin/config/sites/{index}", s.handleConfigPutSite)
|
||||
mux.HandleFunc("DELETE /api/v1/admin/config/sites/{index}", s.handleConfigDeleteSite)
|
||||
|
||||
mux.HandleFunc("GET /api/v1/admin/config/targets", s.handleConfigListTargets)
|
||||
mux.HandleFunc("PUT /api/v1/admin/config/targets/{group}", s.handleConfigPutTargetGroup)
|
||||
mux.HandleFunc("DELETE /api/v1/admin/config/targets/{group}", s.handleConfigDeleteTargetGroup)
|
||||
|
||||
mux.HandleFunc("GET /api/v1/admin/config/check-types", s.handleConfigListCheckTypes)
|
||||
mux.HandleFunc("PUT /api/v1/admin/config/check-types/{name}", s.handleConfigPutCheckType)
|
||||
mux.HandleFunc("DELETE /api/v1/admin/config/check-types/{name}", s.handleConfigDeleteCheckType)
|
||||
|
||||
mux.HandleFunc("GET /api/v1/admin/config/orchestrator", s.handleConfigGetOrchestratorSettings)
|
||||
mux.HandleFunc("PUT /api/v1/admin/config/orchestrator", s.handleConfigPutOrchestratorSettings)
|
||||
func (s *Server) routes(mux *http.ServeMux) {
|
||||
for _, rt := range s.routeTable() {
|
||||
mux.HandleFunc(rt.Pattern, s.Auth.require(rt.Access, rt.Handler))
|
||||
}
|
||||
}
|
||||
|
||||
mux.HandleFunc("GET /api/v1/admin/config/inbound-checks", s.handleConfigGetInboundChecks)
|
||||
mux.HandleFunc("PUT /api/v1/admin/config/inbound-checks", s.handleConfigPutInboundChecks)
|
||||
func (s *Server) routeTable() []route {
|
||||
return []route{
|
||||
{"GET /healthz", s.handleHealthz, accessOpen},
|
||||
{"POST /api/v1/agents/register", s.handleAgentRegister, accessOpen},
|
||||
{"POST /api/v1/agents/{id}/heartbeat", s.handleAgentHeartbeat, accessOpen},
|
||||
{"GET /api/v1/agents/{id}/assignment", s.handleAgentAssignment, accessOpen},
|
||||
{"POST /api/v1/agents/{id}/self-check", s.handleAgentSelfCheck, accessAgent},
|
||||
{"POST /api/v1/agents/{id}/events", s.handleAgentEvent, accessAgent},
|
||||
{"POST /api/v1/agents/{id}/results", s.handleAgentResults, accessAgent},
|
||||
{"POST /api/v1/agents/{id}/complete", s.handleAgentComplete, accessAgent},
|
||||
{"POST /api/v1/probers/register", s.handleProberRegister, accessOpen},
|
||||
{"POST /api/v1/probers/{site_id}/heartbeat", s.handleProberHeartbeat, accessOpen},
|
||||
{"GET /api/v1/probers/{site_id}/assignments", s.handleProberAssignments, accessOpen},
|
||||
{"POST /api/v1/probers/{site_id}/results", s.handleProberResults, accessAgent},
|
||||
{"GET /api/v1/admin/status", s.handleAdminStatus, accessAdmin},
|
||||
{"GET /api/v1/admin/ips", s.handleAdminIPs, accessAdmin},
|
||||
{"POST /api/v1/admin/ips", s.handleAdminSubmitIPs, accessAdmin},
|
||||
{"POST /api/v1/admin/ips/scan", s.handleAdminScanFloatingIPs, accessAdmin},
|
||||
{"GET /api/v1/admin/ips/{ip}", s.handleAdminIPDetail, accessAdmin},
|
||||
{"POST /api/v1/admin/ips/{ip}/cancel", s.handleAdminCancelIP, accessAdmin},
|
||||
{"DELETE /api/v1/admin/ips/{ip}", s.handleAdminDeleteIP, accessAdmin},
|
||||
{"POST /api/v1/admin/ips/delete", s.handleAdminDeleteIPs, accessAdmin},
|
||||
{"POST /api/v1/admin/ips/clear", s.handleAdminClearQueue, accessAdmin},
|
||||
{"GET /api/v1/admin/validators", s.handleAdminValidators, accessAdmin},
|
||||
{"GET /api/v1/admin/auto-cycle", s.handleAdminGetAutoCycle, accessAdmin},
|
||||
{"PUT /api/v1/admin/auto-cycle", s.handleAdminPutAutoCycle, accessAdmin},
|
||||
{"POST /api/v1/admin/auto-cycle/start", s.handleAdminStartAutoCycle, accessAdmin},
|
||||
{"POST /api/v1/admin/auto-cycle/stop", s.handleAdminStopAutoCycle, accessAdmin},
|
||||
{"GET /api/v1/admin/registry", s.handleAdminRegistry, accessAdmin},
|
||||
{"GET /api/v1/admin/registry/{ip}", s.handleAdminRegistryHistory, accessAdmin},
|
||||
{"GET /api/v1/admin/config/validators", s.handleConfigListValidators, accessAdmin},
|
||||
{"POST /api/v1/admin/config/validators", s.handleConfigCreateValidator, accessAdmin},
|
||||
{"PUT /api/v1/admin/config/validators/{id}", s.handleConfigUpdateValidator, accessAdmin},
|
||||
{"DELETE /api/v1/admin/config/validators/{id}", s.handleConfigDeleteValidator, accessAdmin},
|
||||
{"GET /api/v1/admin/config/sites", s.handleConfigListSites, accessAdmin},
|
||||
{"PUT /api/v1/admin/config/sites/{index}", s.handleConfigPutSite, accessAdmin},
|
||||
{"DELETE /api/v1/admin/config/sites/{index}", s.handleConfigDeleteSite, accessAdmin},
|
||||
{"GET /api/v1/admin/config/targets", s.handleConfigListTargets, accessAdmin},
|
||||
{"PUT /api/v1/admin/config/targets/{group}", s.handleConfigPutTargetGroup, accessAdmin},
|
||||
{"DELETE /api/v1/admin/config/targets/{group}", s.handleConfigDeleteTargetGroup, accessAdmin},
|
||||
{"GET /api/v1/admin/config/check-types", s.handleConfigListCheckTypes, accessAdmin},
|
||||
{"PUT /api/v1/admin/config/check-types/{name}", s.handleConfigPutCheckType, accessAdmin},
|
||||
{"DELETE /api/v1/admin/config/check-types/{name}", s.handleConfigDeleteCheckType, accessAdmin},
|
||||
{"GET /api/v1/admin/config/orchestrator", s.handleConfigGetOrchestratorSettings, accessAdmin},
|
||||
{"PUT /api/v1/admin/config/orchestrator", s.handleConfigPutOrchestratorSettings, accessAdmin},
|
||||
{"GET /api/v1/admin/config/inbound-checks", s.handleConfigGetInboundChecks, accessAdmin},
|
||||
{"PUT /api/v1/admin/config/inbound-checks", s.handleConfigPutInboundChecks, accessAdmin},
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ type Server struct {
|
||||
DB *db.DB
|
||||
Orch *orchestrator.Orchestrator
|
||||
Log *slog.Logger
|
||||
Auth Authenticator
|
||||
}
|
||||
|
||||
func New(d *db.DB, o *orchestrator.Orchestrator, log *slog.Logger) *Server {
|
||||
@@ -26,6 +27,7 @@ func New(d *db.DB, o *orchestrator.Orchestrator, log *slog.Logger) *Server {
|
||||
}
|
||||
|
||||
func (s *Server) Handler() http.Handler {
|
||||
s.Auth.Log = s.Log
|
||||
mux := http.NewServeMux()
|
||||
s.routes(mux)
|
||||
return loggingMiddleware(s.Log, mux)
|
||||
|
||||
Reference in new issue
Block a user