220 lines
7.5 KiB
Go
220 lines
7.5 KiB
Go
package sessionbroker
|
|
|
|
import (
|
|
"crypto/rsa"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/example/remote-access-platform/backend/internal/platform/secrets"
|
|
sessioncontracts "github.com/example/remote-access-platform/backend/pkg/contracts/session"
|
|
)
|
|
|
|
const (
|
|
directWorkerTLSTrustModeSmokeInsecure = "smoke_insecure"
|
|
directWorkerTLSTrustModePublicCA = "public_ca"
|
|
directWorkerTLSTrustModePlatformCA = "platform_ca"
|
|
)
|
|
|
|
type DataPlaneTokenClaims struct {
|
|
SessionID string `json:"session_id"`
|
|
AttachmentID string `json:"attachment_id"`
|
|
UserID string `json:"user_id"`
|
|
OrganizationID string `json:"organization_id"`
|
|
ClusterID string `json:"cluster_id,omitempty"`
|
|
WorkerID string `json:"worker_id"`
|
|
ResourceID string `json:"resource_id"`
|
|
AllowedChannels []string `json:"allowed_channels"`
|
|
ExpiresAtValue time.Time `json:"expires_at"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
func (s *Service) buildDataPlaneOffer(session RemoteSession, attachment SessionAttachment) (*sessioncontracts.DataPlaneOffer, error) {
|
|
if s.cfg.DataPlane.TokenTTL <= 0 || s.cfg.DataPlane.TokenPrivateKeyPEM == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
now := s.now().UTC()
|
|
expiresAt := now.Add(s.cfg.DataPlane.TokenTTL)
|
|
allowedChannels := dataPlaneAllowedChannelsFromSession(session)
|
|
jti := uuid.NewString()
|
|
claims := DataPlaneTokenClaims{
|
|
SessionID: session.ID,
|
|
AttachmentID: attachment.ID,
|
|
UserID: attachment.UserID,
|
|
OrganizationID: session.OrganizationID,
|
|
WorkerID: session.WorkerID,
|
|
ResourceID: session.ResourceID,
|
|
AllowedChannels: allowedChannels,
|
|
ExpiresAtValue: expiresAt,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ID: jti,
|
|
Issuer: s.cfg.Auth.Issuer,
|
|
Subject: attachment.UserID,
|
|
Audience: jwt.ClaimStrings{"rap-data-plane", "worker:" + session.WorkerID},
|
|
IssuedAt: jwt.NewNumericDate(now),
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
|
},
|
|
}
|
|
token, err := signDataPlaneToken(claims, s.cfg.DataPlane.TokenPrivateKeyPEM)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
candidates := s.buildDataPlaneCandidates(session)
|
|
preferred := sessioncontracts.DataPlaneCandidateBackendGateway
|
|
if len(candidates) > 0 {
|
|
preferred = candidates[0].Type
|
|
}
|
|
|
|
return &sessioncontracts.DataPlaneOffer{
|
|
Preferred: preferred,
|
|
Token: token,
|
|
ExpiresAt: expiresAt,
|
|
Candidates: candidates,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) buildDataPlaneCandidates(session RemoteSession) []sessioncontracts.DataPlaneCandidate {
|
|
var candidates []sessioncontracts.DataPlaneCandidate
|
|
if directURL := s.directWorkerWSSURL(session.WorkerID); directURL != "" && s.canAdvertiseDirectWorkerWSS() {
|
|
metadata := map[string]any(nil)
|
|
if s.cfg.DataPlane.DirectWorkerJSONRuntime {
|
|
metadata = map[string]any{
|
|
"runtime_transport": "json_v1",
|
|
"traffic_ready": true,
|
|
}
|
|
s.addDirectWorkerTLSTrustMetadata(metadata)
|
|
if s.cfg.DataPlane.DirectWorkerBinaryRender {
|
|
metadata["render_transport"] = "binary_v1"
|
|
metadata["binary_render"] = true
|
|
metadata["supported_color_modes"] = []string{"full_color", "grayscale"}
|
|
metadata["default_color_mode"] = "full_color"
|
|
}
|
|
}
|
|
candidates = append(candidates, sessioncontracts.DataPlaneCandidate{
|
|
Type: sessioncontracts.DataPlaneCandidateDirectWorkerWSS,
|
|
URL: directURL,
|
|
WorkerID: session.WorkerID,
|
|
Priority: 10,
|
|
Metadata: metadata,
|
|
})
|
|
}
|
|
if s.cfg.DataPlane.BackendGatewayURL != "" {
|
|
candidates = append(candidates, sessioncontracts.DataPlaneCandidate{
|
|
Type: sessioncontracts.DataPlaneCandidateBackendGateway,
|
|
URL: s.cfg.DataPlane.BackendGatewayURL,
|
|
Priority: 100,
|
|
})
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
func (s *Service) canAdvertiseDirectWorkerWSS() bool {
|
|
trustMode := normalizeDirectWorkerTLSTrustMode(s.cfg.DataPlane.DirectWorkerTLSTrustMode)
|
|
return !secrets.IsProductionEnv(s.cfg.App.Env) || directWorkerTLSTrustModeIsProductionTrusted(trustMode)
|
|
}
|
|
|
|
func (s *Service) addDirectWorkerTLSTrustMetadata(metadata map[string]any) {
|
|
trustMode := normalizeDirectWorkerTLSTrustMode(s.cfg.DataPlane.DirectWorkerTLSTrustMode)
|
|
metadata["tls_trust_mode"] = trustMode
|
|
metadata["production_trusted"] = directWorkerTLSTrustModeIsProductionTrusted(trustMode)
|
|
metadata["smoke_only"] = trustMode == directWorkerTLSTrustModeSmokeInsecure
|
|
if s.cfg.DataPlane.DirectWorkerTLSCARef != "" {
|
|
metadata["tls_ca_ref"] = s.cfg.DataPlane.DirectWorkerTLSCARef
|
|
}
|
|
}
|
|
|
|
func normalizeDirectWorkerTLSTrustMode(mode string) string {
|
|
switch strings.ToLower(strings.TrimSpace(mode)) {
|
|
case directWorkerTLSTrustModePublicCA:
|
|
return directWorkerTLSTrustModePublicCA
|
|
case directWorkerTLSTrustModePlatformCA:
|
|
return directWorkerTLSTrustModePlatformCA
|
|
default:
|
|
return directWorkerTLSTrustModeSmokeInsecure
|
|
}
|
|
}
|
|
|
|
func directWorkerTLSTrustModeIsProductionTrusted(mode string) bool {
|
|
return mode == directWorkerTLSTrustModePublicCA || mode == directWorkerTLSTrustModePlatformCA
|
|
}
|
|
|
|
func (s *Service) directWorkerWSSURL(workerID string) string {
|
|
template := strings.TrimSpace(s.cfg.DataPlane.DirectWorkerWSSURLTemplate)
|
|
if template == "" || workerID == "" {
|
|
return ""
|
|
}
|
|
return strings.ReplaceAll(template, "{worker_id}", workerID)
|
|
}
|
|
|
|
func signDataPlaneToken(claims DataPlaneTokenClaims, privateKeyPEM string) (string, error) {
|
|
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKeyPEM))
|
|
if err != nil {
|
|
return "", fmt.Errorf("parse data-plane private key: %w", err)
|
|
}
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
|
signed, err := token.SignedString(privateKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("sign data-plane token: %w", err)
|
|
}
|
|
return signed, nil
|
|
}
|
|
|
|
func parseDataPlaneToken(tokenValue string, publicKey *rsa.PublicKey) (*DataPlaneTokenClaims, error) {
|
|
claims := &DataPlaneTokenClaims{}
|
|
token, err := jwt.ParseWithClaims(tokenValue, claims, func(token *jwt.Token) (any, error) {
|
|
if token.Method != jwt.SigningMethodRS256 {
|
|
return nil, fmt.Errorf("unexpected data-plane signing method: %s", token.Header["alg"])
|
|
}
|
|
return publicKey, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !token.Valid {
|
|
return nil, fmt.Errorf("data-plane token invalid")
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
func dataPlaneAllowedChannelsFromSession(session RemoteSession) []string {
|
|
channels := []string{
|
|
sessioncontracts.DataPlaneChannelControl,
|
|
sessioncontracts.DataPlaneChannelInput,
|
|
sessioncontracts.DataPlaneChannelRender,
|
|
sessioncontracts.DataPlaneChannelTelemetry,
|
|
}
|
|
metadata := decodeJSONMap(session.Metadata)
|
|
policy, _ := metadata["policy"].(map[string]any)
|
|
if policy != nil {
|
|
if mode, _ := policy["clipboard_mode"].(string); mode != "" && mode != string(ResourceClipboardModeDisabled) {
|
|
channels = append(channels, sessioncontracts.DataPlaneChannelClipboard)
|
|
}
|
|
if mode, _ := policy["file_transfer_mode"].(string); fileTransferAllowsClientToServer(ResourceFileTransferMode(mode)) {
|
|
channels = append(channels, sessioncontracts.DataPlaneChannelFileUpload)
|
|
}
|
|
if mode, _ := policy["file_transfer_mode"].(string); fileTransferAllowsServerToClient(ResourceFileTransferMode(mode)) {
|
|
channels = append(channels, sessioncontracts.DataPlaneChannelFileDownload)
|
|
}
|
|
}
|
|
return channels
|
|
}
|
|
|
|
func (s *Service) attachDataPlaneOffer(result *SessionControlResult) error {
|
|
if result == nil || result.Attachment == nil {
|
|
return nil
|
|
}
|
|
result.GatewayURL = s.cfg.DataPlane.BackendGatewayURL
|
|
offer, err := s.buildDataPlaneOffer(result.Session, *result.Attachment)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result.DataPlane = offer
|
|
return nil
|
|
}
|