Add custom image handling and access policy management

- Implement tests for custom KVM and LXC image creation, ensuring invalid sources and architecture mismatches are rejected.
- Introduce access policy management in CLI, allowing configuration of allowed sources and trusted proxies.
- Add NAT network configuration with validation for RFC1918 compliance and subnet parsing.
- Create panel access policy management, including normalization and evaluation of access decisions based on client IPs and forwarded headers.
- Develop middleware for enforcing access policies in the server, returning appropriate responses for allowed and denied requests.
- Enhance custom image downloading and validation, ensuring integrity and security of downloaded root filesystem archives.
- Include comprehensive tests for all new functionalities to ensure reliability and correctness.
This commit is contained in:
MengMengCode
2026-07-26 04:04:45 +08:00
parent 8283b88ded
commit 38debab1aa
49 changed files with 4504 additions and 246 deletions
+39
View File
@@ -0,0 +1,39 @@
package server
import (
"encoding/json"
"net/http"
"strings"
"clicd/internal/config"
)
func panelAccessMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
decision := config.EvaluatePanelAccess(
config.AppConfig.PanelAccessPolicy,
r.RemoteAddr,
config.ForwardedClientHeaders{
ForwardedFor: r.Header.Get("X-Forwarded-For"),
RealIP: r.Header.Get("X-Real-IP"),
CFConnectingIP: r.Header.Get("CF-Connecting-IP"),
},
)
if decision.Allowed {
next.ServeHTTP(w, r)
return
}
w.Header().Set("Cache-Control", "no-store")
if strings.HasPrefix(r.URL.Path, "/api/") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
_ = json.NewEncoder(w).Encode(map[string]any{
"success": false,
"message": "Access denied by panel source policy",
})
return
}
http.Error(w, "Access denied by panel source policy", http.StatusForbidden)
})
}
@@ -0,0 +1,46 @@
package server
import (
"net/http"
"net/http/httptest"
"testing"
"clicd/internal/config"
)
func TestPanelAccessMiddleware(t *testing.T) {
previous := config.AppConfig
config.AppConfig = &config.ClicdConfig{
PanelAccessPolicy: config.PanelAccessPolicy{
Enabled: true,
AllowedSources: []string{"192.0.2.0/24"},
TrustedProxies: []string{"10.0.0.1"},
},
}
t.Cleanup(func() {
config.AppConfig = previous
})
handler := panelAccessMiddleware(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
allowed := httptest.NewRequest(http.MethodGet, "/api/version", nil)
allowed.RemoteAddr = "192.0.2.8:50000"
allowedRecorder := httptest.NewRecorder()
handler.ServeHTTP(allowedRecorder, allowed)
if allowedRecorder.Code != http.StatusNoContent {
t.Fatalf("allowed status = %d", allowedRecorder.Code)
}
denied := httptest.NewRequest(http.MethodGet, "/api/version", nil)
denied.RemoteAddr = "198.51.100.8:50000"
deniedRecorder := httptest.NewRecorder()
handler.ServeHTTP(deniedRecorder, denied)
if deniedRecorder.Code != http.StatusForbidden {
t.Fatalf("denied status = %d", deniedRecorder.Code)
}
if got := deniedRecorder.Header().Get("Content-Type"); got != "application/json" {
t.Fatalf("denied content type = %q", got)
}
}
+5 -1
View File
@@ -49,11 +49,13 @@ func setupRoutes(mux *http.ServeMux) {
mux.HandleFunc("/api/login-logs", corsMiddleware(api.AdminMiddleware(api.HandleLoginLogs)))
mux.HandleFunc("/api/ssl", corsMiddleware(api.AdminMiddleware(api.HandleSSLSettings)))
mux.HandleFunc("/api/webssh-origins", corsMiddleware(api.AdminMiddleware(api.HandleWebSSHOriginSettings)))
mux.HandleFunc("/api/access-policy", corsMiddleware(api.AdminMiddleware(api.HandlePanelAccessPolicy)))
mux.HandleFunc("/api/containers", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleContainers))))
mux.HandleFunc("/api/containers/list", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleContainerListAlias))))
mux.HandleFunc("/api/containers/", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleSingleContainer))))
mux.HandleFunc("/api/templates", corsMiddleware(api.AuthMiddleware(api.HandleTemplates)))
mux.HandleFunc("/api/images", corsMiddleware(api.AdminMiddleware(api.HandleImages)))
mux.HandleFunc("/api/images/custom", corsMiddleware(api.AdminMiddleware(api.HandleCustomKVMImages)))
mux.HandleFunc("/api/images/download", corsMiddleware(api.AdminMiddleware(api.HandleImageDownload)))
mux.HandleFunc("/api/images/cancel", corsMiddleware(api.AdminMiddleware(api.HandleImageCancel)))
mux.HandleFunc("/api/images/delete", corsMiddleware(api.AdminMiddleware(api.HandleImageDelete)))
@@ -101,6 +103,7 @@ func setupRoutes(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/containers/", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleSingleContainer))))
mux.HandleFunc("/api/v1/templates", corsMiddleware(api.AuthMiddleware(api.HandleTemplates)))
mux.HandleFunc("/api/v1/images", corsMiddleware(api.AuthMiddleware(api.HandleImages)))
mux.HandleFunc("/api/v1/images/custom", corsMiddleware(api.AuthMiddleware(api.HandleCustomKVMImages)))
mux.HandleFunc("/api/v1/images/download", corsMiddleware(api.AuthMiddleware(api.HandleImageDownload)))
mux.HandleFunc("/api/v1/images/cancel", corsMiddleware(api.AuthMiddleware(api.HandleImageCancel)))
mux.HandleFunc("/api/v1/images/delete", corsMiddleware(api.AuthMiddleware(api.HandleImageDelete)))
@@ -126,6 +129,7 @@ func setupRoutes(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/login-logs", corsMiddleware(api.AuthMiddleware(api.HandleLoginLogs)))
mux.HandleFunc("/api/v1/ssl", corsMiddleware(api.AdminMiddleware(api.HandleSSLSettings)))
mux.HandleFunc("/api/v1/webssh-origins", corsMiddleware(api.AdminMiddleware(api.HandleWebSSHOriginSettings)))
mux.HandleFunc("/api/v1/access-policy", corsMiddleware(api.AdminMiddleware(api.HandlePanelAccessPolicy)))
mux.HandleFunc("/api/v1/security/alerts", corsMiddleware(api.AuthMiddleware(api.ScopeMiddleware("security:read", api.HandleSecurityAlerts))))
mux.HandleFunc("/api/v1/security/check", corsMiddleware(api.AuthMiddleware(api.ScopeMiddleware("security:check", api.HandleSecurityCheck))))
mux.HandleFunc("/api/v1/security/logs", corsMiddleware(api.AuthMiddleware(api.ScopeMiddleware("security:read", api.HandleSecurityLogs))))
@@ -192,7 +196,7 @@ func Run() error {
server := &http.Server{
Addr: addr,
Handler: mux,
Handler: panelAccessMiddleware(mux),
}
if sslEnabled() {