129 lines
4.8 KiB
Go
129 lines
4.8 KiB
Go
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
|
|
}
|
|
}
|