f0e44d6810
Fixes #72 ## Why `compilePatterns` silently discards any pattern that fails to compile. A typo in a blocklist entry therefore turns a deny rule into a no-op — a fail-open with security impact. ## Changes - Add `Remote.ValidatePatterns`, which compiles every pattern list (patterns, blocklist, mutable/immutable patterns, ban_tags) and returns an error on the first invalid regex. - Reject invalid patterns with 400 at remote create and update time. - Unit test for valid and invalid patterns. ## Validation - `go test ./pkg/models/` and `make e2e` pass. Reviewed-on: #87 Co-authored-by: Ben Vincent <ben@unkin.net> Co-committed-by: Ben Vincent <ben@unkin.net>
116 lines
3.1 KiB
Go
116 lines
3.1 KiB
Go
package v2
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"git.unkin.net/unkin/artifactapi/internal/database"
|
|
"git.unkin.net/unkin/artifactapi/pkg/models"
|
|
)
|
|
|
|
type RemotesHandler struct {
|
|
db *database.DB
|
|
}
|
|
|
|
func NewRemotesHandler(db *database.DB) *RemotesHandler {
|
|
return &RemotesHandler{db: db}
|
|
}
|
|
|
|
func (h *RemotesHandler) Routes() chi.Router {
|
|
r := chi.NewRouter()
|
|
r.Get("/", h.list)
|
|
r.Post("/", h.create)
|
|
r.Get("/{name}", h.get)
|
|
r.Put("/{name}", h.update)
|
|
r.Delete("/{name}", h.del)
|
|
return r
|
|
}
|
|
|
|
func (h *RemotesHandler) list(w http.ResponseWriter, r *http.Request) {
|
|
remotes, err := h.db.ListRemotes(r.Context())
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, remotes)
|
|
}
|
|
|
|
func (h *RemotesHandler) get(w http.ResponseWriter, r *http.Request) {
|
|
name := chi.URLParam(r, "name")
|
|
remote, err := h.db.GetRemote(r.Context(), name)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("remote %q not found", name), http.StatusNotFound)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, remote)
|
|
}
|
|
|
|
func (h *RemotesHandler) create(w http.ResponseWriter, r *http.Request) {
|
|
var remote models.Remote
|
|
if err := json.NewDecoder(r.Body).Decode(&remote); err != nil {
|
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if !remote.PackageType.Valid() {
|
|
http.Error(w, fmt.Sprintf("invalid package type: %q", remote.PackageType), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if remote.RepoType == "" {
|
|
remote.RepoType = models.RepoTypeRemote
|
|
}
|
|
if !remote.RepoType.Valid() {
|
|
http.Error(w, fmt.Sprintf("invalid repo type: %q", remote.RepoType), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if remote.RepoType == models.RepoTypeRemote && remote.BaseURL == "" {
|
|
http.Error(w, "base_url is required for remote repositories", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := remote.ValidatePatterns(); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := h.db.CreateRemote(r.Context(), &remote); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, remote)
|
|
}
|
|
|
|
func (h *RemotesHandler) update(w http.ResponseWriter, r *http.Request) {
|
|
name := chi.URLParam(r, "name")
|
|
var remote models.Remote
|
|
if err := json.NewDecoder(r.Body).Decode(&remote); err != nil {
|
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
remote.Name = name
|
|
if err := remote.ValidatePatterns(); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := h.db.UpdateRemote(r.Context(), &remote); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, remote)
|
|
}
|
|
|
|
func (h *RemotesHandler) del(w http.ResponseWriter, r *http.Request) {
|
|
name := chi.URLParam(r, "name")
|
|
if err := h.db.DeleteRemote(r.Context(), name); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(v)
|
|
}
|