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>
199 lines
5.1 KiB
Go
199 lines
5.1 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"git.wntrmute.dev/mc/mcns/internal/db"
|
|
)
|
|
|
|
type createRecordRequest struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Value string `json:"value"`
|
|
TTL int `json:"ttl"`
|
|
}
|
|
|
|
func listRecordsHandler(database *db.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
zoneName := chi.URLParam(r, "zone")
|
|
nameFilter := r.URL.Query().Get("name")
|
|
typeFilter := r.URL.Query().Get("type")
|
|
|
|
records, err := database.ListRecords(zoneName, nameFilter, typeFilter)
|
|
if errors.Is(err, db.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "zone not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to list records")
|
|
return
|
|
}
|
|
if records == nil {
|
|
records = []db.Record{}
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"records": records})
|
|
}
|
|
}
|
|
|
|
func getRecordHandler(database *db.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
idStr := chi.URLParam(r, "id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid record ID")
|
|
return
|
|
}
|
|
|
|
record, err := database.GetRecord(id)
|
|
if errors.Is(err, db.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "record not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to get record")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, record)
|
|
}
|
|
}
|
|
|
|
func createRecordHandler(database *db.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
zoneName := chi.URLParam(r, "zone")
|
|
|
|
var req createRecordRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if req.Name == "" {
|
|
writeError(w, http.StatusBadRequest, "name is required")
|
|
return
|
|
}
|
|
if req.Type == "" {
|
|
writeError(w, http.StatusBadRequest, "type is required")
|
|
return
|
|
}
|
|
if req.Value == "" {
|
|
writeError(w, http.StatusBadRequest, "value is required")
|
|
return
|
|
}
|
|
|
|
if !authorizeRecordMutation(tokenInfoFromContext(r.Context()), req.Name) {
|
|
writeError(w, http.StatusForbidden, "not authorized for record name")
|
|
return
|
|
}
|
|
|
|
record, err := database.CreateRecord(zoneName, req.Name, req.Type, req.Value, req.TTL)
|
|
if errors.Is(err, db.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "zone not found")
|
|
return
|
|
}
|
|
if errors.Is(err, db.ErrConflict) {
|
|
writeError(w, http.StatusConflict, err.Error())
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, record)
|
|
}
|
|
}
|
|
|
|
func updateRecordHandler(database *db.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
idStr := chi.URLParam(r, "id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid record ID")
|
|
return
|
|
}
|
|
|
|
var req createRecordRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if req.Name == "" {
|
|
writeError(w, http.StatusBadRequest, "name is required")
|
|
return
|
|
}
|
|
if req.Type == "" {
|
|
writeError(w, http.StatusBadRequest, "type is required")
|
|
return
|
|
}
|
|
if req.Value == "" {
|
|
writeError(w, http.StatusBadRequest, "value is required")
|
|
return
|
|
}
|
|
|
|
// Authorize against both old and new record names.
|
|
info := tokenInfoFromContext(r.Context())
|
|
existing, lookupErr := database.GetRecord(id)
|
|
if lookupErr == nil && !authorizeRecordMutation(info, existing.Name) {
|
|
writeError(w, http.StatusForbidden, "not authorized for record name")
|
|
return
|
|
}
|
|
if !authorizeRecordMutation(info, req.Name) {
|
|
writeError(w, http.StatusForbidden, "not authorized for record name")
|
|
return
|
|
}
|
|
|
|
record, err := database.UpdateRecord(id, req.Name, req.Type, req.Value, req.TTL)
|
|
if errors.Is(err, db.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "record not found")
|
|
return
|
|
}
|
|
if errors.Is(err, db.ErrConflict) {
|
|
writeError(w, http.StatusConflict, err.Error())
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, record)
|
|
}
|
|
}
|
|
|
|
func deleteRecordHandler(database *db.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
idStr := chi.URLParam(r, "id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid record ID")
|
|
return
|
|
}
|
|
|
|
// Look up the record to authorize by name.
|
|
existing, lookupErr := database.GetRecord(id)
|
|
if lookupErr == nil && !authorizeRecordMutation(tokenInfoFromContext(r.Context()), existing.Name) {
|
|
writeError(w, http.StatusForbidden, "not authorized for record name")
|
|
return
|
|
}
|
|
|
|
err = database.DeleteRecord(id)
|
|
if errors.Is(err, db.ErrNotFound) {
|
|
writeError(w, http.StatusNotFound, "record not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to delete record")
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|