Refactor RDP proxy handling and update related tests
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
package webingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const AdminRuntimeResponseSchema = "rap.web_ingress.admin_runtime_response.v1"
|
||||
const ControlAPIProjectionRequestSchema = "rap.web_ingress.control_api_projection_request.v1"
|
||||
const ControlAPIProjectionResponseSchema = "rap.web_ingress.control_api_projection_response.v1"
|
||||
|
||||
type AdminRuntimeDispatcher struct {
|
||||
ProjectionClient ControlAPIProjectionClient
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
type ControlAPIProjectionClient interface {
|
||||
Project(ctx context.Context, request ControlAPIProjectionRequest) (ControlAPIProjectionResponse, error)
|
||||
}
|
||||
|
||||
type ControlAPIProjectionRequest struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
Query string `json:"query,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Scope string `json:"scope"`
|
||||
ServiceClass string `json:"service_class"`
|
||||
ObservedAt string `json:"observed_at"`
|
||||
}
|
||||
|
||||
type ControlAPIProjectionResponse struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
StatusCode int `json:"status_code"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
Body json.RawMessage `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
type AdminRuntimeJSONResponse struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
ServiceClass string `json:"service_class,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Manifest map[string]any `json:"manifest,omitempty"`
|
||||
ObservedAt string `json:"observed_at"`
|
||||
}
|
||||
|
||||
func (d AdminRuntimeDispatcher) HandleFabricRequest(ctx context.Context, request FabricRequest) (FabricResponse, error) {
|
||||
method := strings.ToUpper(strings.TrimSpace(request.Method))
|
||||
path := normalizeRuntimePath(request.Path)
|
||||
if method == "" {
|
||||
method = http.MethodGet
|
||||
}
|
||||
if !allowedAdminRuntimeScope(strings.TrimSpace(request.Scope), strings.TrimSpace(request.ServiceClass)) {
|
||||
return d.json(http.StatusForbidden, request, "blocked", "admin_runtime_scope_rejected", nil), nil
|
||||
}
|
||||
switch {
|
||||
case method == http.MethodGet && (path == "/healthz" || path == "/readyz"):
|
||||
return d.json(http.StatusOK, request, "ready", "admin_runtime_ready", nil), nil
|
||||
case d.ProjectionClient != nil && (method == http.MethodGet || method == http.MethodHead):
|
||||
return d.project(ctx, request)
|
||||
case method == http.MethodGet && (path == "/ui-manifest" || strings.HasSuffix(path, "/ui-manifest")):
|
||||
return d.json(http.StatusOK, request, "ready", "ui_manifest_ready", d.manifest(request)), nil
|
||||
case method != http.MethodGet && method != http.MethodHead:
|
||||
return d.json(http.StatusForbidden, request, "blocked", "control_api_mutation_binding_not_implemented", nil), nil
|
||||
default:
|
||||
return d.json(http.StatusNotImplemented, request, "blocked", "control_api_projection_binding_not_implemented", nil), nil
|
||||
}
|
||||
}
|
||||
|
||||
func allowedAdminRuntimeScope(scope string, serviceClass string) bool {
|
||||
switch serviceClass {
|
||||
case "platform_admin":
|
||||
return scope == "platform"
|
||||
case "cluster_admin":
|
||||
return scope == "cluster"
|
||||
case "organization_portal":
|
||||
return scope == "organization"
|
||||
case "user_portal":
|
||||
return scope == "user" || scope == "organization"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (d AdminRuntimeDispatcher) project(ctx context.Context, request FabricRequest) (FabricResponse, error) {
|
||||
response, err := d.ProjectionClient.Project(ctx, ControlAPIProjectionRequest{
|
||||
SchemaVersion: ControlAPIProjectionRequestSchema,
|
||||
Method: strings.ToUpper(strings.TrimSpace(request.Method)),
|
||||
Path: normalizeRuntimePath(request.Path),
|
||||
Query: request.Query,
|
||||
Host: request.Host,
|
||||
Scope: request.Scope,
|
||||
ServiceClass: request.ServiceClass,
|
||||
ObservedAt: d.observedAt(),
|
||||
})
|
||||
if err != nil {
|
||||
return d.json(http.StatusBadGateway, request, "blocked", "control_api_projection_failed", nil), nil
|
||||
}
|
||||
if response.SchemaVersion != ControlAPIProjectionResponseSchema {
|
||||
return d.json(http.StatusBadGateway, request, "blocked", "control_api_projection_invalid_response", nil), nil
|
||||
}
|
||||
headers := http.Header{"Content-Type": []string{"application/json"}}
|
||||
for key, value := range response.Headers {
|
||||
if safeResponseHeader(key) && strings.TrimSpace(value) != "" {
|
||||
headers.Set(key, value)
|
||||
}
|
||||
}
|
||||
statusCode := response.StatusCode
|
||||
if statusCode < 100 || statusCode > 599 {
|
||||
statusCode = http.StatusOK
|
||||
}
|
||||
return FabricResponse{StatusCode: statusCode, Headers: headers, Body: append([]byte(nil), response.Body...)}, nil
|
||||
}
|
||||
|
||||
func (d AdminRuntimeDispatcher) json(statusCode int, request FabricRequest, status string, reason string, manifest map[string]any) FabricResponse {
|
||||
payload, _ := json.Marshal(AdminRuntimeJSONResponse{
|
||||
SchemaVersion: AdminRuntimeResponseSchema,
|
||||
Status: status,
|
||||
Reason: reason,
|
||||
Scope: request.Scope,
|
||||
ServiceClass: request.ServiceClass,
|
||||
Path: request.Path,
|
||||
Manifest: manifest,
|
||||
ObservedAt: d.observedAt(),
|
||||
})
|
||||
return FabricResponse{
|
||||
StatusCode: statusCode,
|
||||
Headers: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: payload,
|
||||
}
|
||||
}
|
||||
|
||||
func (d AdminRuntimeDispatcher) manifest(request FabricRequest) map[string]any {
|
||||
serviceClass := strings.TrimSpace(request.ServiceClass)
|
||||
sections := []string{}
|
||||
actions := []string{}
|
||||
switch serviceClass {
|
||||
case "platform_admin":
|
||||
sections = []string{"clusters", "nodes", "roles", "fabric", "workloads", "audit"}
|
||||
actions = []string{"read_platform_summary", "read_cluster_summaries", "read_node_status"}
|
||||
case "cluster_admin":
|
||||
sections = []string{"cluster", "nodes", "fabric", "workloads", "audit"}
|
||||
actions = []string{"read_cluster_summary", "read_node_status"}
|
||||
case "organization_portal":
|
||||
sections = []string{"organization", "sessions", "resources", "audit"}
|
||||
actions = []string{"read_organization_summary", "read_sessions"}
|
||||
case "user_portal":
|
||||
sections = []string{"profile", "sessions", "resources"}
|
||||
actions = []string{"read_profile", "read_sessions"}
|
||||
default:
|
||||
sections = []string{"status"}
|
||||
actions = []string{"read_status"}
|
||||
}
|
||||
return map[string]any{
|
||||
"schema_version": "rap.web_ingress.ui_manifest.v1",
|
||||
"scope": request.Scope,
|
||||
"service_class": serviceClass,
|
||||
"sections": sections,
|
||||
"allowed_actions": actions,
|
||||
"mutation_enabled": false,
|
||||
"projection_binding": "control_api_not_bound",
|
||||
}
|
||||
}
|
||||
|
||||
func (d AdminRuntimeDispatcher) observedAt() string {
|
||||
now := time.Now().UTC()
|
||||
if d.Now != nil {
|
||||
now = d.Now().UTC()
|
||||
}
|
||||
return now.Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
func normalizeRuntimePath(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return "/"
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
Reference in New Issue
Block a user