Refactor RDP proxy handling and update related tests
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -33,6 +34,13 @@ type Module struct {
|
||||
vpnClientDiagnosticHub *vpnClientDiagnosticHub
|
||||
}
|
||||
|
||||
const (
|
||||
adminRuntimeProjectionRequestSchema = "rap.web_ingress.control_api_projection_request.v1"
|
||||
adminRuntimeProjectionResponseSchema = "rap.web_ingress.control_api_projection_response.v1"
|
||||
adminRuntimeProjectionBodySchema = "rap.control_api.admin_runtime_projection.v1"
|
||||
adminRuntimeManifestSchema = "rap.web_ingress.ui_manifest.v1"
|
||||
)
|
||||
|
||||
func NewModule(deps module.Dependencies, verifiers ...*authority.Verifier) *Module {
|
||||
store := NewPostgresStore(deps.Infra.DB, verifiers...)
|
||||
if deps.Config.Secret.EncryptionKeyBase64 != "" {
|
||||
@@ -52,6 +60,7 @@ func (m *Module) Name() string {
|
||||
}
|
||||
|
||||
func (m *Module) RegisterRoutes(router chi.Router) {
|
||||
router.Get("/downloads/{fileName}", m.downloadReleaseFile)
|
||||
router.Route("/clusters", func(r chi.Router) {
|
||||
r.Get("/", m.listClusters)
|
||||
r.Post("/", m.createCluster)
|
||||
@@ -90,6 +99,7 @@ func (m *Module) RegisterRoutes(router chi.Router) {
|
||||
r.Put("/{clusterID}/nodes/{nodeID}/workloads/{serviceType}/desired", m.setDesiredWorkload)
|
||||
r.Post("/{clusterID}/nodes/{nodeID}/workloads/{serviceType}/status", m.reportWorkloadStatus)
|
||||
r.Get("/{clusterID}/nodes/{nodeID}/workloads/status", m.listWorkloadStatuses)
|
||||
r.Post("/{clusterID}/nodes/{nodeID}/admin-runtime/projection", m.projectAdminRuntime)
|
||||
r.Get("/{clusterID}/mesh/links", m.listMeshLinks)
|
||||
r.Post("/{clusterID}/mesh/links", m.reportMeshLink)
|
||||
r.Get("/{clusterID}/mesh/route-intents", m.listRouteIntents)
|
||||
@@ -97,14 +107,6 @@ func (m *Module) RegisterRoutes(router chi.Router) {
|
||||
r.Post("/{clusterID}/mesh/route-intents/{routeIntentID}/expire", m.expireRouteIntent)
|
||||
r.Post("/{clusterID}/mesh/route-intents/{routeIntentID}/disable", m.disableRouteIntent)
|
||||
r.Get("/{clusterID}/mesh/qos-policies", m.listQoSPolicies)
|
||||
r.Get("/{clusterID}/fabric/entry-points", m.listFabricEntryPoints)
|
||||
r.Post("/{clusterID}/fabric/entry-points", m.createFabricEntryPoint)
|
||||
r.Get("/{clusterID}/fabric/entry-points/{entryPointID}/nodes", m.listFabricEntryPointNodes)
|
||||
r.Put("/{clusterID}/fabric/entry-points/{entryPointID}/nodes/{nodeID}", m.setFabricEntryPointNode)
|
||||
r.Get("/{clusterID}/fabric/egress-pools", m.listFabricEgressPools)
|
||||
r.Post("/{clusterID}/fabric/egress-pools", m.createFabricEgressPool)
|
||||
r.Get("/{clusterID}/fabric/egress-pools/{egressPoolID}/nodes", m.listFabricEgressPoolNodes)
|
||||
r.Put("/{clusterID}/fabric/egress-pools/{egressPoolID}/nodes/{nodeID}", m.setFabricEgressPoolNode)
|
||||
r.Get("/{clusterID}/fabric/service-channels/route-feedback", m.listFabricServiceChannelRouteFeedback)
|
||||
r.Post("/{clusterID}/fabric/service-channels/route-feedback/expire", m.expireFabricServiceChannelRouteFeedback)
|
||||
r.Get("/{clusterID}/fabric/service-channels/rebuild-attempts", m.listFabricServiceChannelRouteRebuildAttempts)
|
||||
@@ -172,6 +174,24 @@ func (m *Module) RegisterRoutes(router chi.Router) {
|
||||
router.Put("/fabric/testing-flags", m.upsertFabricTestingFlag)
|
||||
}
|
||||
|
||||
func (m *Module) downloadReleaseFile(w http.ResponseWriter, r *http.Request) {
|
||||
fileName := filepath.Base(strings.TrimSpace(chi.URLParam(r, "fileName")))
|
||||
if fileName == "" || fileName == "." || fileName != strings.TrimSpace(chi.URLParam(r, "fileName")) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
releaseDir := strings.TrimSpace(os.Getenv("RAP_RELEASE_DIR"))
|
||||
if releaseDir == "" {
|
||||
releaseDir = "/tmp/rap-release"
|
||||
}
|
||||
path := filepath.Join(releaseDir, fileName)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, path)
|
||||
}
|
||||
|
||||
func (m *Module) listClusters(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := m.service.ListClusters(r.Context(), r.URL.Query().Get("actor_user_id"))
|
||||
if writeServiceError(w, err) {
|
||||
@@ -239,6 +259,126 @@ func (m *Module) updateCluster(w http.ResponseWriter, r *http.Request) {
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"cluster": item})
|
||||
}
|
||||
|
||||
func (m *Module) projectAdminRuntime(w http.ResponseWriter, r *http.Request) {
|
||||
clusterID := chi.URLParam(r, "clusterID")
|
||||
nodeID := chi.URLParam(r, "nodeID")
|
||||
var payload struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
Query string `json:"query"`
|
||||
Host string `json:"host"`
|
||||
Scope string `json:"scope"`
|
||||
ServiceClass string `json:"service_class"`
|
||||
ObservedAt string `json:"observed_at"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid admin runtime projection payload")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(payload.SchemaVersion) != adminRuntimeProjectionRequestSchema {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid admin runtime projection schema")
|
||||
return
|
||||
}
|
||||
method := strings.ToUpper(strings.TrimSpace(payload.Method))
|
||||
path := strings.TrimSpace(payload.Path)
|
||||
if method != http.MethodGet && method != http.MethodHead {
|
||||
httpx.WriteJSON(w, http.StatusOK, adminRuntimeProjectionResponse(http.StatusForbidden, "blocked", "control_api_mutation_rejected", nil))
|
||||
return
|
||||
}
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
scope := strings.TrimSpace(payload.Scope)
|
||||
serviceClass := normalizeFabricServiceClass(payload.ServiceClass)
|
||||
if !isAllowedAdminRuntimeProjectionScope(scope, serviceClass) {
|
||||
httpx.WriteJSON(w, http.StatusOK, adminRuntimeProjectionResponse(http.StatusForbidden, "blocked", "control_api_projection_scope_rejected", nil))
|
||||
return
|
||||
}
|
||||
body := map[string]any{
|
||||
"schema_version": adminRuntimeProjectionBodySchema,
|
||||
"cluster_id": clusterID,
|
||||
"node_id": nodeID,
|
||||
"scope": scope,
|
||||
"service_class": serviceClass,
|
||||
"path": path,
|
||||
"query": payload.Query,
|
||||
"host": payload.Host,
|
||||
"projection": "read_only",
|
||||
"audit_required": true,
|
||||
}
|
||||
if path == "/ui-manifest" || strings.HasSuffix(path, "/ui-manifest") {
|
||||
body["manifest"] = adminRuntimeManifest(scope, serviceClass)
|
||||
httpx.WriteJSON(w, http.StatusOK, adminRuntimeProjectionResponse(http.StatusOK, "ready", "ui_manifest_ready", body))
|
||||
return
|
||||
}
|
||||
if path == "/healthz" || path == "/readyz" {
|
||||
httpx.WriteJSON(w, http.StatusOK, adminRuntimeProjectionResponse(http.StatusOK, "ready", "admin_runtime_projection_ready", body))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, adminRuntimeProjectionResponse(http.StatusNotImplemented, "blocked", "control_api_projection_not_implemented", body))
|
||||
}
|
||||
|
||||
func adminRuntimeProjectionResponse(statusCode int, status string, reason string, body map[string]any) map[string]any {
|
||||
raw, _ := json.Marshal(body)
|
||||
return map[string]any{
|
||||
"schema_version": adminRuntimeProjectionResponseSchema,
|
||||
"status": status,
|
||||
"reason": reason,
|
||||
"status_code": statusCode,
|
||||
"headers": map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
"body": json.RawMessage(raw),
|
||||
}
|
||||
}
|
||||
|
||||
func isAllowedAdminRuntimeProjectionScope(scope string, serviceClass string) bool {
|
||||
switch serviceClass {
|
||||
case FabricServiceClassPlatformAdmin:
|
||||
return scope == "platform"
|
||||
case FabricServiceClassClusterAdmin:
|
||||
return scope == "cluster"
|
||||
case FabricServiceClassOrganization:
|
||||
return scope == "organization"
|
||||
case FabricServiceClassUserPortal:
|
||||
return scope == "user" || scope == "organization"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func adminRuntimeManifest(scope string, serviceClass string) map[string]any {
|
||||
sections := []string{"status"}
|
||||
actions := []string{"read_status"}
|
||||
switch strings.TrimSpace(serviceClass) {
|
||||
case FabricServiceClassPlatformAdmin:
|
||||
sections = []string{"clusters", "nodes", "roles", "fabric", "workloads", "audit"}
|
||||
actions = []string{"read_platform_summary", "read_cluster_summaries", "read_node_status"}
|
||||
case FabricServiceClassClusterAdmin:
|
||||
sections = []string{"cluster", "nodes", "fabric", "workloads", "audit"}
|
||||
actions = []string{"read_cluster_summary", "read_node_status"}
|
||||
case FabricServiceClassOrganization:
|
||||
sections = []string{"organization", "sessions", "resources", "audit"}
|
||||
actions = []string{"read_organization_summary", "read_sessions"}
|
||||
case FabricServiceClassUserPortal:
|
||||
sections = []string{"profile", "sessions", "resources"}
|
||||
actions = []string{"read_profile", "read_sessions"}
|
||||
}
|
||||
return map[string]any{
|
||||
"schema_version": adminRuntimeManifestSchema,
|
||||
"scope": scope,
|
||||
"service_class": serviceClass,
|
||||
"sections": sections,
|
||||
"allowed_actions": actions,
|
||||
"mutation_enabled": false,
|
||||
"projection_binding": "control_api_read_only",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) listClusterNodes(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := m.service.ListClusterNodes(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
||||
if writeServiceError(w, err) {
|
||||
@@ -1073,160 +1213,6 @@ func (m *Module) listQoSPolicies(w http.ResponseWriter, r *http.Request) {
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"qos_policies": items})
|
||||
}
|
||||
|
||||
func (m *Module) listFabricEntryPoints(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := m.service.ListFabricEntryPoints(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
||||
if writeServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"entry_points": items})
|
||||
}
|
||||
|
||||
func (m *Module) createFabricEntryPoint(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ActorUserID string `json:"actor_user_id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
EndpointType string `json:"endpoint_type"`
|
||||
PublicEndpoint *string `json:"public_endpoint"`
|
||||
Policy json.RawMessage `json:"policy"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid fabric entry point payload")
|
||||
return
|
||||
}
|
||||
item, err := m.service.CreateFabricEntryPoint(r.Context(), CreateFabricEntryPointInput{
|
||||
ActorUserID: payload.ActorUserID,
|
||||
ClusterID: chi.URLParam(r, "clusterID"),
|
||||
Name: payload.Name,
|
||||
Status: payload.Status,
|
||||
EndpointType: payload.EndpointType,
|
||||
PublicEndpoint: payload.PublicEndpoint,
|
||||
Policy: payload.Policy,
|
||||
Metadata: payload.Metadata,
|
||||
})
|
||||
if writeServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"entry_point": item})
|
||||
}
|
||||
|
||||
func (m *Module) setFabricEntryPointNode(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ActorUserID string `json:"actor_user_id"`
|
||||
Status string `json:"status"`
|
||||
Priority int `json:"priority"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid fabric entry point node payload")
|
||||
return
|
||||
}
|
||||
item, err := m.service.SetFabricEntryPointNode(r.Context(), SetFabricEntryPointNodeInput{
|
||||
ActorUserID: payload.ActorUserID,
|
||||
ClusterID: chi.URLParam(r, "clusterID"),
|
||||
EntryPointID: chi.URLParam(r, "entryPointID"),
|
||||
NodeID: chi.URLParam(r, "nodeID"),
|
||||
Status: payload.Status,
|
||||
Priority: payload.Priority,
|
||||
Metadata: payload.Metadata,
|
||||
})
|
||||
if writeServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"entry_point_node": item})
|
||||
}
|
||||
|
||||
func (m *Module) listFabricEntryPointNodes(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := m.service.ListFabricEntryPointNodes(
|
||||
r.Context(),
|
||||
r.URL.Query().Get("actor_user_id"),
|
||||
chi.URLParam(r, "clusterID"),
|
||||
chi.URLParam(r, "entryPointID"),
|
||||
)
|
||||
if writeServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"entry_point_nodes": items})
|
||||
}
|
||||
|
||||
func (m *Module) listFabricEgressPools(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := m.service.ListFabricEgressPools(r.Context(), r.URL.Query().Get("actor_user_id"), chi.URLParam(r, "clusterID"))
|
||||
if writeServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"egress_pools": items})
|
||||
}
|
||||
|
||||
func (m *Module) createFabricEgressPool(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ActorUserID string `json:"actor_user_id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Description *string `json:"description"`
|
||||
RouteScope json.RawMessage `json:"route_scope"`
|
||||
Policy json.RawMessage `json:"policy"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid fabric egress pool payload")
|
||||
return
|
||||
}
|
||||
item, err := m.service.CreateFabricEgressPool(r.Context(), CreateFabricEgressPoolInput{
|
||||
ActorUserID: payload.ActorUserID,
|
||||
ClusterID: chi.URLParam(r, "clusterID"),
|
||||
Name: payload.Name,
|
||||
Status: payload.Status,
|
||||
Description: payload.Description,
|
||||
RouteScope: payload.RouteScope,
|
||||
Policy: payload.Policy,
|
||||
Metadata: payload.Metadata,
|
||||
})
|
||||
if writeServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"egress_pool": item})
|
||||
}
|
||||
|
||||
func (m *Module) setFabricEgressPoolNode(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ActorUserID string `json:"actor_user_id"`
|
||||
Status string `json:"status"`
|
||||
Priority int `json:"priority"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid fabric egress pool node payload")
|
||||
return
|
||||
}
|
||||
item, err := m.service.SetFabricEgressPoolNode(r.Context(), SetFabricEgressPoolNodeInput{
|
||||
ActorUserID: payload.ActorUserID,
|
||||
ClusterID: chi.URLParam(r, "clusterID"),
|
||||
EgressPoolID: chi.URLParam(r, "egressPoolID"),
|
||||
NodeID: chi.URLParam(r, "nodeID"),
|
||||
Status: payload.Status,
|
||||
Priority: payload.Priority,
|
||||
Metadata: payload.Metadata,
|
||||
})
|
||||
if writeServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"egress_pool_node": item})
|
||||
}
|
||||
|
||||
func (m *Module) listFabricEgressPoolNodes(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := m.service.ListFabricEgressPoolNodes(
|
||||
r.Context(),
|
||||
r.URL.Query().Get("actor_user_id"),
|
||||
chi.URLParam(r, "clusterID"),
|
||||
chi.URLParam(r, "egressPoolID"),
|
||||
)
|
||||
if writeServiceError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"egress_pool_nodes": items})
|
||||
}
|
||||
|
||||
func (m *Module) issueFabricServiceChannelLease(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ActorUserID string `json:"actor_user_id"`
|
||||
|
||||
Reference in New Issue
Block a user