Files
OwnCord/Server/admin/api.go
T
J3vbandClaude Fable 5 a3459e5f80 fix(admin): route admin-panel bans through ModerationService (W1-4)
requireBanAuthority (BAN_MEMBERS + role hierarchy) was wired only into
ModerationService.BanUser/UnbanUser — which had zero production callers.
The live path, handlePatchUser, ran a raw UPDATE with no hierarchy check,
so any admin-panel actor could ban an equal- or higher-ranked user,
including the owner. The ban/unban branch now calls the service (dead code
becomes THE code — ban path 1 of 3 consolidated), which also audits as
user_ban/user_unban, keeping the historical audit vocabulary.

Authorization now runs in permission → existence → hierarchy order: an
actor without ban authority sees Forbidden, never NotFound, so the ban
path cannot enumerate user ids. The role+ban transaction is gone — the
ban leg lives in the service, runs first, and a refusal returns before
the role change executes, so a rejected ban never half-applies a PATCH.
MemStore gains honest BanUser/UnbanUser so the matrix is testable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:41:12 +02:00

74 lines
3.4 KiB
Go

package admin
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/owncord/server/auth"
"github.com/owncord/server/db"
"github.com/owncord/server/service"
"github.com/owncord/server/updater"
)
// ─── NewAdminAPI ──────────────────────────────────────────────────────────────
// NewAdminAPI returns a chi router with all /admin/api/* routes. All routes
// are protected by adminAuthMiddleware which requires the ADMINISTRATOR bit,
// except for the setup endpoints which are unauthenticated.
func NewAdminAPI(database *db.DB, version string, hub HubBroadcaster, u *updater.Updater, logBuf *RingBuffer, allowedOrigins []string, permInvalidator PermissionInvalidator, mod *service.ModerationService) http.Handler {
r := chi.NewRouter()
// Setup endpoints — unauthenticated, only functional when no users exist.
setupLimiter := auth.NewRateLimiter()
r.Get("/setup/status", handleSetupStatus(database))
r.Post("/setup", handleSetup(database, setupLimiter, allowedOrigins))
// SSE log stream — auth is via a single-use ticket from POST /logs/ticket.
// EventSource cannot send Authorization headers, so the client first
// obtains a short-lived ticket via the authenticated ticket endpoint,
// then passes it as ?ticket= to the SSE stream.
if logBuf != nil {
r.Get("/logs/stream", handleLogStream(database, logBuf))
}
// All remaining routes require authentication and ADMINISTRATOR permission.
r.Group(func(r chi.Router) {
r.Use(adminAuthMiddleware(database))
// Log stream ticket — issues a single-use, 30s TTL ticket for SSE auth.
r.Post("/logs/ticket", handleLogTicket(database))
r.Get("/stats", handleGetStats(database, hub))
r.Get("/users", handleListUsers(database))
r.Patch("/users/{id}", handlePatchUser(database, hub, permInvalidator, mod))
r.Delete("/users/{id}/sessions", handleForceLogout(database))
r.Get("/channels", handleListChannels(database))
r.Post("/channels", handleCreateChannel(database, hub))
r.Patch("/channels/{id}", handlePatchChannel(database, hub))
r.Delete("/channels/{id}", handleDeleteChannel(database, hub))
r.Get("/audit-log", handleGetAuditLog(database))
r.Get("/settings", handleGetSettings(database))
r.Patch("/settings", handlePatchSettings(database))
r.Post("/backup", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ownerOnlyMiddleware(database, handleBackup(database)).ServeHTTP(w, req)
}))
r.Get("/backups", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ownerOnlyMiddleware(database, handleListBackups()).ServeHTTP(w, req)
}))
r.Delete("/backups/{name}", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ownerOnlyMiddleware(database, handleDeleteBackup(database)).ServeHTTP(w, req)
}))
r.Post("/backups/{name}/restore", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ownerOnlyMiddleware(database, handleRestoreBackup(database, hub)).ServeHTTP(w, req)
}))
r.Get("/updates", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ownerOnlyMiddleware(database, handleCheckUpdate(u)).ServeHTTP(w, req)
}))
r.Post("/updates/apply", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ownerOnlyMiddleware(database, handleApplyUpdate(u, hub, version)).ServeHTTP(w, req)
}))
})
return r
}