feat(volume-browser): absolute scope with allowlist security
- Add 'absolute' volume scope for direct host paths (NFS, external mounts) - Allowlist in settings: allowed_volume_paths (JSON array of prefixes) - Validation: absolute source must be under an allowed prefix - Empty allowlist = absolute scope disabled entirely - Settings API exposes/validates allowed_volume_paths - Frontend type updated with absolute scope
This commit is contained in:
@@ -5,11 +5,13 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alexei/docker-watcher/internal/crypto"
|
||||
"github.com/alexei/docker-watcher/internal/npm"
|
||||
"github.com/alexei/docker-watcher/internal/store"
|
||||
"github.com/alexei/docker-watcher/internal/volume"
|
||||
"github.com/alexei/docker-watcher/internal/webhook"
|
||||
)
|
||||
|
||||
@@ -24,8 +26,9 @@ type settingsRequest struct {
|
||||
NpmEmail string `json:"npm_email"`
|
||||
NpmPassword string `json:"npm_password"`
|
||||
PollingInterval string `json:"polling_interval"`
|
||||
SSLCertificateID *int `json:"ssl_certificate_id,omitempty"`
|
||||
StaleThresholdDays *int `json:"stale_threshold_days,omitempty"`
|
||||
SSLCertificateID *int `json:"ssl_certificate_id,omitempty"`
|
||||
StaleThresholdDays *int `json:"stale_threshold_days,omitempty"`
|
||||
AllowedVolumePaths *string `json:"allowed_volume_paths,omitempty"`
|
||||
}
|
||||
|
||||
// getSettings handles GET /api/settings.
|
||||
@@ -48,8 +51,9 @@ func (s *Server) getSettings(w http.ResponseWriter, r *http.Request) {
|
||||
"has_npm_password": settings.NpmPassword != "",
|
||||
"polling_interval": settings.PollingInterval,
|
||||
"ssl_certificate_id": settings.SSLCertificateID,
|
||||
"stale_threshold_days": settings.StaleThresholdDays,
|
||||
"updated_at": settings.UpdatedAt,
|
||||
"stale_threshold_days": settings.StaleThresholdDays,
|
||||
"allowed_volume_paths": settings.AllowedVolumePaths,
|
||||
"updated_at": settings.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -110,6 +114,23 @@ func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
updated.StaleThresholdDays = *req.StaleThresholdDays
|
||||
}
|
||||
if req.AllowedVolumePaths != nil {
|
||||
// Validate it's valid JSON array of strings.
|
||||
paths, err := volume.ParseAllowedPaths(*req.AllowedVolumePaths)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "allowed_volume_paths must be a JSON array of strings")
|
||||
return
|
||||
}
|
||||
// Validate each path is absolute.
|
||||
for _, p := range paths {
|
||||
if !filepath.IsAbs(p) {
|
||||
respondError(w, http.StatusBadRequest, "each allowed volume path must be absolute")
|
||||
return
|
||||
}
|
||||
}
|
||||
updated.AllowedVolumePaths = *req.AllowedVolumePaths
|
||||
_ = paths // validated
|
||||
}
|
||||
|
||||
if err := s.store.UpdateSettings(updated); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to update settings: "+err.Error())
|
||||
|
||||
Reference in New Issue
Block a user