Record mutations (create, update, delete) no longer require admin role. Authorization rules: - admin: full access (unchanged) - system mcp-agent: create/delete any record - system account α: create/delete records named α only - human users: read-only (unchanged) Zone mutations remain admin-only. Both REST and gRPC paths enforce the same rules. Update checks authorization against both old and new names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
120 lines
3.0 KiB
Go
120 lines
3.0 KiB
Go
package server
|
||
|
||
import (
|
||
"context"
|
||
"log/slog"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
mcdslauth "git.wntrmute.dev/mc/mcdsl/auth"
|
||
)
|
||
|
||
type contextKey string
|
||
|
||
const tokenInfoKey contextKey = "tokenInfo"
|
||
|
||
// requireAuth returns middleware that validates Bearer tokens via MCIAS.
|
||
func requireAuth(auth *mcdslauth.Authenticator) func(http.Handler) http.Handler {
|
||
return func(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
token := extractBearerToken(r)
|
||
if token == "" {
|
||
writeError(w, http.StatusUnauthorized, "authentication required")
|
||
return
|
||
}
|
||
|
||
info, err := auth.ValidateToken(token)
|
||
if err != nil {
|
||
writeError(w, http.StatusUnauthorized, "invalid or expired token")
|
||
return
|
||
}
|
||
|
||
ctx := context.WithValue(r.Context(), tokenInfoKey, info)
|
||
next.ServeHTTP(w, r.WithContext(ctx))
|
||
})
|
||
}
|
||
}
|
||
|
||
// requireAdmin is middleware that checks the caller has the admin role.
|
||
func requireAdmin(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
info := tokenInfoFromContext(r.Context())
|
||
if info == nil || !info.IsAdmin {
|
||
writeError(w, http.StatusForbidden, "admin role required")
|
||
return
|
||
}
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
|
||
// authorizeRecordMutation checks whether the caller may create, update,
|
||
// or delete a DNS record with the given name. The rules are:
|
||
//
|
||
// - admin role: always allowed
|
||
// - system account "mcp-agent": allowed for any record name
|
||
// - system account α: allowed only when recordName == α
|
||
// - all others: denied
|
||
func authorizeRecordMutation(info *mcdslauth.TokenInfo, recordName string) bool {
|
||
if info == nil {
|
||
return false
|
||
}
|
||
if info.IsAdmin {
|
||
return true
|
||
}
|
||
if info.AccountType != "system" {
|
||
return false
|
||
}
|
||
if info.Username == "mcp-agent" {
|
||
return true
|
||
}
|
||
return recordName == info.Username
|
||
}
|
||
|
||
// tokenInfoFromContext extracts the TokenInfo from the request context.
|
||
func tokenInfoFromContext(ctx context.Context) *mcdslauth.TokenInfo {
|
||
info, _ := ctx.Value(tokenInfoKey).(*mcdslauth.TokenInfo)
|
||
return info
|
||
}
|
||
|
||
// extractBearerToken extracts a bearer token from the Authorization header.
|
||
func extractBearerToken(r *http.Request) string {
|
||
h := r.Header.Get("Authorization")
|
||
if h == "" {
|
||
return ""
|
||
}
|
||
const prefix = "Bearer "
|
||
if !strings.HasPrefix(h, prefix) {
|
||
return ""
|
||
}
|
||
return strings.TrimSpace(h[len(prefix):])
|
||
}
|
||
|
||
// loggingMiddleware logs HTTP requests.
|
||
func loggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
|
||
return func(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
start := time.Now()
|
||
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
|
||
next.ServeHTTP(sw, r)
|
||
logger.Info("http",
|
||
"method", r.Method,
|
||
"path", r.URL.Path,
|
||
"status", sw.status,
|
||
"duration", time.Since(start),
|
||
"remote", r.RemoteAddr,
|
||
)
|
||
})
|
||
}
|
||
}
|
||
|
||
type statusWriter struct {
|
||
http.ResponseWriter
|
||
status int
|
||
}
|
||
|
||
func (w *statusWriter) WriteHeader(code int) {
|
||
w.status = code
|
||
w.ResponseWriter.WriteHeader(code)
|
||
}
|