249 lines
7.5 KiB
Go
249 lines
7.5 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
PackageComponentRun = "run"
|
|
PackageComponentClientManager = "client-manager"
|
|
|
|
PackageConfigEnv = "RUN_PACKAGE_CONFIG"
|
|
)
|
|
|
|
type PackageConfig struct {
|
|
Kind string `json:"kind"`
|
|
ServerInstanceID string `json:"serverInstanceId"`
|
|
PluginID string `json:"pluginId"`
|
|
RunEndpointID string `json:"runEndpointId,omitempty"`
|
|
ProfileKey string `json:"profileKey,omitempty"`
|
|
TargetOS string `json:"targetOs"`
|
|
TargetArch string `json:"targetArch"`
|
|
SecretRef string `json:"secretRef"`
|
|
KeyGeneration int `json:"keyGeneration"`
|
|
AuthKey string `json:"authKey"`
|
|
}
|
|
|
|
type PackageIdentity struct {
|
|
Kind string `json:"kind"`
|
|
ServerInstanceID string `json:"serverInstanceId"`
|
|
PluginID string `json:"pluginId"`
|
|
RunEndpointID string `json:"runEndpointId,omitempty"`
|
|
ProfileKey string `json:"profileKey,omitempty"`
|
|
TargetOS string `json:"targetOs"`
|
|
TargetArch string `json:"targetArch"`
|
|
SecretRef string `json:"secretRef"`
|
|
KeyGeneration int `json:"keyGeneration"`
|
|
KeyFingerprint string `json:"keyFingerprint"`
|
|
}
|
|
|
|
type ComponentAuthResult struct {
|
|
ServerInstanceID string
|
|
Kind string
|
|
ProfileKey string
|
|
KeyGeneration int
|
|
Allowed bool
|
|
Reason string
|
|
}
|
|
|
|
func LoadPackageConfig(path string) (PackageConfig, error) {
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return PackageConfig{}, fmt.Errorf("read run package config: %w", err)
|
|
}
|
|
var cfg PackageConfig
|
|
if err := json.Unmarshal(body, &cfg); err != nil {
|
|
return PackageConfig{}, fmt.Errorf("decode run package config: %w", err)
|
|
}
|
|
if err := ValidatePackageConfig(cfg); err != nil {
|
|
return PackageConfig{}, err
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func LoadPackageConfigFromEnv() (PackageConfig, bool, error) {
|
|
path := strings.TrimSpace(os.Getenv(PackageConfigEnv))
|
|
if path == "" {
|
|
return PackageConfig{}, false, nil
|
|
}
|
|
cfg, err := LoadPackageConfig(path)
|
|
return cfg, true, err
|
|
}
|
|
|
|
func ValidatePackageConfig(cfg PackageConfig) error {
|
|
var violations []string
|
|
if cfg.Kind != PackageComponentRun && cfg.Kind != PackageComponentClientManager {
|
|
violations = append(violations, "kind is invalid")
|
|
}
|
|
if !safeIdentifier(cfg.ServerInstanceID) {
|
|
violations = append(violations, "serverInstanceId is invalid")
|
|
}
|
|
if !safePluginID(cfg.PluginID) {
|
|
violations = append(violations, "pluginId is invalid")
|
|
}
|
|
if cfg.RunEndpointID != "" && !safeIdentifier(cfg.RunEndpointID) {
|
|
violations = append(violations, "runEndpointId is invalid")
|
|
}
|
|
if cfg.ProfileKey != "" && !safeLogicalKey(cfg.ProfileKey) {
|
|
violations = append(violations, "profileKey is invalid")
|
|
}
|
|
if cfg.Kind == PackageComponentClientManager && cfg.ProfileKey == "" {
|
|
violations = append(violations, "profileKey is required for client-manager packages")
|
|
}
|
|
if !safeRuntimeTarget(cfg.TargetOS, cfg.TargetArch) {
|
|
violations = append(violations, "target platform is invalid")
|
|
}
|
|
if !strings.HasPrefix(cfg.SecretRef, "secret://runtime-keys/") || containsUnsafeDiagnosticText(cfg.SecretRef) {
|
|
violations = append(violations, "secretRef is invalid")
|
|
}
|
|
if cfg.KeyGeneration <= 0 {
|
|
violations = append(violations, "keyGeneration must be positive")
|
|
}
|
|
if strings.TrimSpace(cfg.AuthKey) == "" {
|
|
violations = append(violations, "authKey is required")
|
|
}
|
|
if containsUnsafeDiagnosticText(cfg.AuthKey) {
|
|
violations = append(violations, "authKey contains unsafe content")
|
|
}
|
|
if len(violations) > 0 {
|
|
return fmt.Errorf("invalid run package config: %s", strings.Join(violations, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ApplyPackageConfig(base Config, pkg PackageConfig) Config {
|
|
base.RegistrationToken = pkg.AuthKey
|
|
base.ServerInstanceID = pkg.ServerInstanceID
|
|
base.PluginID = pkg.PluginID
|
|
base.ComponentKind = pkg.Kind
|
|
base.ComponentKey = pkg.ProfileKey
|
|
base.KeyGeneration = pkg.KeyGeneration
|
|
base.SecretRef = pkg.SecretRef
|
|
if pkg.RunEndpointID != "" {
|
|
base.RunEndpointID = pkg.RunEndpointID
|
|
}
|
|
if base.DisplayName == "" || base.DisplayName == DefaultDisplayName {
|
|
base.DisplayName = "Run " + pkg.ServerInstanceID
|
|
}
|
|
return base
|
|
}
|
|
|
|
func (cfg PackageConfig) Identity() PackageIdentity {
|
|
return PackageIdentity{
|
|
Kind: cfg.Kind,
|
|
ServerInstanceID: cfg.ServerInstanceID,
|
|
PluginID: cfg.PluginID,
|
|
RunEndpointID: cfg.RunEndpointID,
|
|
ProfileKey: cfg.ProfileKey,
|
|
TargetOS: cfg.TargetOS,
|
|
TargetArch: cfg.TargetArch,
|
|
SecretRef: cfg.SecretRef,
|
|
KeyGeneration: cfg.KeyGeneration,
|
|
KeyFingerprint: fingerprint(cfg.AuthKey),
|
|
}
|
|
}
|
|
|
|
func (cfg PackageConfig) RedactedDiagnostics() map[string]string {
|
|
identity := cfg.Identity()
|
|
return map[string]string{
|
|
"kind": identity.Kind,
|
|
"serverInstanceId": identity.ServerInstanceID,
|
|
"pluginId": identity.PluginID,
|
|
"runEndpointId": identity.RunEndpointID,
|
|
"profileKey": identity.ProfileKey,
|
|
"target": identity.TargetOS + "/" + identity.TargetArch,
|
|
"secretRef": identity.SecretRef,
|
|
"keyGeneration": fmt.Sprintf("%d", identity.KeyGeneration),
|
|
"keyFingerprint": identity.KeyFingerprint,
|
|
}
|
|
}
|
|
|
|
func AuthenticatePackageGeneration(pkg PackageConfig, auth ComponentAuthResult) error {
|
|
if err := ValidatePackageConfig(pkg); err != nil {
|
|
return err
|
|
}
|
|
if auth.ServerInstanceID != pkg.ServerInstanceID || auth.Kind != pkg.Kind || auth.ProfileKey != pkg.ProfileKey {
|
|
return fmt.Errorf("component authentication scope does not match package")
|
|
}
|
|
if !auth.Allowed {
|
|
return fmt.Errorf("component authentication rejected: %s", redactedReason(auth.Reason))
|
|
}
|
|
if auth.KeyGeneration != pkg.KeyGeneration {
|
|
return fmt.Errorf("component key generation is no longer current")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fingerprint(value string) string {
|
|
sum := sha256.Sum256([]byte(value))
|
|
return hex.EncodeToString(sum[:])[:12]
|
|
}
|
|
|
|
func safeIdentifier(value string) bool {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || len(value) > 120 || containsUnsafeDiagnosticText(value) {
|
|
return false
|
|
}
|
|
for _, char := range value {
|
|
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func safePluginID(value string) bool {
|
|
return strings.HasPrefix(value, "game.") && safeIdentifier(value)
|
|
}
|
|
|
|
func safeLogicalKey(value string) bool {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || len(value) > 120 || strings.HasPrefix(value, "/") || strings.Contains(value, "..") || strings.Contains(value, `\`) || containsUnsafeDiagnosticText(value) {
|
|
return false
|
|
}
|
|
for _, char := range value {
|
|
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' || char == '/' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func safeRuntimeTarget(osName string, arch string) bool {
|
|
switch osName {
|
|
case "windows", "linux", "darwin":
|
|
default:
|
|
return false
|
|
}
|
|
switch arch {
|
|
case "amd64", "arm64":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func containsUnsafeDiagnosticText(value string) bool {
|
|
normalized := strings.ToLower(value)
|
|
for _, marker := range []string{"/users/", "/.ssh/", "password=", "apikey", "api_key", "bearer ", "sk-", "unix://", "tcp://", "mysql://", "sqlite://"} {
|
|
if strings.Contains(normalized, marker) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func redactedReason(value string) string {
|
|
if containsUnsafeDiagnosticText(value) {
|
|
return "[redacted]"
|
|
}
|
|
return value
|
|
}
|