package protocol import "strings" const maxRunLogicalFileKeyLength = 160 func ValidateRunJobAssignment(assignment RunJobAssignment) error { if assignment.JobID == "" || assignment.RunEndpointID == "" || assignment.Capability == "" { return ValidationError("jobId, runEndpointId, and capability are required") } switch assignment.Capability { case RunCapabilityConfigWrite, RunCapabilityFilesRead, RunCapabilityFilesWrite: if assignment.ServerInstanceID == "" { return ValidationError("serverInstanceId is required for scoped file jobs") } if !ValidLogicalFileKey(assignment.TargetKey) { return ValidationError("targetKey is not allowed") } } switch assignment.Capability { case RunCapabilityConfigWrite, RunCapabilityFilesWrite: if !ValidScopedInputRef(assignment.InputRef) { return ValidationError("inputRef is not allowed") } } if IsRemoteCapability(assignment.Capability) { if assignment.ServerInstanceID == "" { return ValidationError("serverInstanceId is required for remote jobs") } if RemoteCapabilityRequiresTargetKey(assignment.Capability) && !ValidLogicalFileKey(assignment.TargetKey) { return ValidationError("targetKey is not allowed") } if RemoteCapabilityRequiresInputRef(assignment.Capability) && !ValidScopedInputRef(assignment.InputRef) { return ValidationError("inputRef is not allowed") } } switch assignment.Capability { case RunCapabilityRunSelfUpdate: if assignment.ServerInstanceID == "" { return ValidationError("serverInstanceId is required for self-update jobs") } if assignment.TargetKey != "run/update" { return ValidationError("targetKey must be run/update") } if !ValidScopedInputRef(assignment.InputRef) || !strings.HasPrefix(assignment.InputRef, "artifact://") { return ValidationError("inputRef must be an artifact ref for self-update") } case RunCapabilityDependenciesCheck, RunCapabilityDependenciesInstall: if assignment.ServerInstanceID == "" { return ValidationError("serverInstanceId is required for dependency jobs") } if !ValidLogicalFileKey(assignment.TargetKey) || !strings.HasPrefix(assignment.TargetKey, "dependencies/") { return ValidationError("targetKey is not allowed for dependency jobs") } if assignment.InputRef != "" { return ValidationError("dependency jobs must not carry arbitrary input refs") } case RunCapabilityLogsBackfill: if assignment.ServerInstanceID == "" { return ValidationError("serverInstanceId is required for log backfill jobs") } if !ValidLogicalFileKey(assignment.TargetKey) || !strings.HasPrefix(assignment.TargetKey, "logs/") { return ValidationError("targetKey is not allowed for log backfill jobs") } if assignment.InputRef != "" && !ValidScopedInputRef(assignment.InputRef) { return ValidationError("inputRef is not allowed for log backfill jobs") } } return nil } type ValidationError string func (err ValidationError) Error() string { return string(err) } func ValidLogicalFileKey(key string) bool { trimmed := strings.TrimSpace(key) if trimmed == "" || trimmed != key || len([]rune(key)) > maxRunLogicalFileKeyLength { return false } lower := strings.ToLower(key) if strings.HasPrefix(key, "/") || strings.Contains(key, "..") || strings.Contains(key, `\`) || strings.Contains(key, "://") || strings.Contains(lower, "/users/") || strings.Contains(lower, "password=") || strings.Contains(lower, "secret=") || strings.Contains(lower, "sk-") || strings.Contains(lower, "bearer ") { return false } for _, char := range key { if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '_' || char == '-' || char == '.' || char == '/' { continue } return false } return true } func ValidScopedInputRef(ref string) bool { trimmed := strings.TrimSpace(ref) lower := strings.ToLower(ref) if trimmed == "" || trimmed != ref || strings.Contains(lower, "/users/") || strings.Contains(lower, "password=") || strings.Contains(lower, "secret=") || strings.Contains(lower, "sk-") || strings.Contains(lower, "bearer ") { return false } return strings.HasPrefix(ref, "input://") || strings.HasPrefix(ref, "artifact://") } func IsRemoteCapability(capability string) bool { return strings.HasPrefix(capability, "remote.") } func RemoteCapabilityRequiresTargetKey(capability string) bool { switch capability { case RunCapabilityRemoteRunProcessStart, RunCapabilityRemoteRunProcessStop: return false default: return IsRemoteCapability(capability) } } func RemoteCapabilityRequiresInputRef(capability string) bool { switch capability { case RunCapabilityRemoteFTPWrite, RunCapabilityRemoteRsyncWrite, RunCapabilityRemoteRunFilesWrite, RunCapabilityRemoteRunDBMySQLQuery, RunCapabilityRemoteRunDBSQLiteQuery, RunCapabilityRemoteRunRCONCommand: return true default: return false } }