Files
run/protocol/job_validation_test.go

386 lines
18 KiB
Go

package protocol
import (
"strings"
"testing"
"time"
)
func TestValidateRunJobAssignmentScopedFilePayloads(t *testing.T) {
assignment := RunJobAssignment{
JobID: "job-config-write",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityConfigWrite,
TargetKey: "server.properties",
InputRef: "input://server-config/server-1/server.properties/v1",
IdempotencyKey: "idem-config",
}
if err := ValidateRunJobAssignment(assignment); err != nil {
t.Fatalf("expected valid config write assignment: %v", err)
}
assignment.TargetKey = "/Users/tasia/server.properties"
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "targetKey") {
t.Fatalf("expected raw host path rejection, got %v", err)
}
assignment.TargetKey = "server.properties"
assignment.InputRef = "sk-raw-secret"
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "inputRef") {
t.Fatalf("expected raw credential ref rejection, got %v", err)
}
assignment.InputRef = "input://server-config/server-1/server.properties/v1"
assignment.ExecutionInput.FileTargetKey = "SCUM/Saved/Config/WindowsServer/ServerSettings.ini"
if err := ValidateRunJobAssignment(assignment); err != nil {
t.Fatalf("expected valid declared relative file target: %v", err)
}
assignment.ExecutionInput.FileTargetKey = "../outside.ini"
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "fileTargetKey") {
t.Fatalf("expected declared file target escape rejection, got %v", err)
}
}
func TestValidateRunJobAssignmentScopedReadDoesNotRequireInputRef(t *testing.T) {
assignment := RunJobAssignment{
JobID: "job-files-read",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityFilesRead,
TargetKey: "logs/latest.log",
IdempotencyKey: "idem-read",
}
if err := ValidateRunJobAssignment(assignment); err != nil {
t.Fatalf("expected valid file read assignment: %v", err)
}
}
func TestValidateRunJobAssignmentRequiresBoundedSQLiteSchemaProbe(t *testing.T) {
assignment := RunJobAssignment{JobID: "job-probe", ServerInstanceID: "server-1", RunEndpointID: "run-1", Capability: RunCapabilityRemoteRunDBSQLiteProbe, TargetKey: "databases/current.db", IdempotencyKey: "idem-probe", MaxAttempts: 1, FencingToken: 1, ExecutionInput: RunJobExecutionInput{WorkspaceScope: "profile-1", SQLiteSchemaProbe: &SQLiteSchemaProbeRequest{RequestID: "probe-1", Binding: SQLiteSchemaProbeBinding{ServerInstanceID: "server-1", RunBindingID: "binding-1", RunEndpointID: "run-1", PluginID: "game.example", PluginVersion: "1.0.0", AdapterVersion: "adapter-1", DatabaseIdentity: "database-1"}, Limits: SQLiteSchemaProbeLimits{MaxObjects: 8, MaxColumnsPerObject: 8, MaxIndexesPerObject: 8, MaxForeignKeys: 8, MaxCardinalityReads: 8, MaxSampleRows: 2, TimeoutMS: 1000, MaxResultBytes: 1024}}}}
if err := ValidateRunJobAssignment(assignment); err != nil {
t.Fatalf("expected bounded SQLite schema probe to validate: %v", err)
}
assignment.ExecutionInput.SQLiteSchemaProbe.Limits.MaxSampleRows = 4
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "limits") {
t.Fatalf("expected oversized sample limit rejection, got %v", err)
}
assignment.ExecutionInput.SQLiteSchemaProbe.Limits.MaxSampleRows = 2
assignment.ExecutionInput.Content = "SELECT * FROM private"
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "must not carry") {
t.Fatalf("expected embedded query rejection, got %v", err)
}
}
func TestValidateRunJobAssignmentRejectsLegacyGameSpecificDeploymentPlan(t *testing.T) {
assignment := RunJobAssignment{
JobID: "job-legacy-game-plan",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityProcessInstall,
IdempotencyKey: "idem-legacy-game-plan",
ExecutionInput: RunJobExecutionInput{
Deployment: &ServerDeploymentExecution{SchemaVersion: "1", Mode: "guided-install", ServerRoot: "C:/game-server", Revision: 1},
ServerDeploymentPlan: &ServerDeploymentPlan{SchemaVersion: "1", PluginID: "game.example", TemplateKey: "legacy-game-template"},
},
}
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "legacy unsupported") {
t.Fatalf("expected legacy game-specific plan rejection, got %v", err)
}
}
func TestValidateRunJobAssignmentAllowsDeclaredProcessLogSourcesOnlyForStart(t *testing.T) {
assignment := RunJobAssignment{
JobID: "job-process-logs",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityProcessStart,
TargetKey: "actions/start.json",
IdempotencyKey: "idem-process-logs",
ExecutionInput: RunJobExecutionInput{LogSources: []RuntimeLogSourcePlan{
{Key: "console-stdout", Kind: "process.stdout", TargetKey: "process/server", StreamKey: "game.console.stdout", CursorKind: "sequence", RetentionDays: 30},
{Key: "console-stderr", Kind: "process.stderr", TargetKey: "process/server", StreamKey: "game.console.stderr", CursorKind: "sequence", RetentionDays: 30},
}},
}
if err := ValidateRunJobAssignment(assignment); err != nil {
t.Fatalf("expected process log sources to validate: %v", err)
}
assignment.ExecutionInput.LogSources[0].Kind = "file.tail"
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "process log source kind") {
t.Fatalf("expected file log source rejection on process.start, got %v", err)
}
assignment.ExecutionInput.LogSources[0].Kind = "process.stdout"
assignment.Capability = RunCapabilityProcessStop
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "process log sources") {
t.Fatalf("expected process log source rejection outside start, got %v", err)
}
}
func TestValidateRunJobAssignmentRemoteCapabilitiesAreBounded(t *testing.T) {
assignment := RunJobAssignment{
JobID: "job-remote-rcon",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityRemoteRunRCONCommand,
TargetKey: "rcon/command",
InputRef: "input://server-1/rcon/command/1",
IdempotencyKey: "idem-rcon",
}
if err := ValidateRunJobAssignment(assignment); err != nil {
t.Fatalf("expected valid remote rcon assignment: %v", err)
}
assignment.InputRef = "password=raw"
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "inputRef") {
t.Fatalf("expected unsafe inputRef rejection, got %v", err)
}
assignment.InputRef = "input://server-1/rcon/command/1"
assignment.TargetKey = "/Users/tasia/server.db"
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "targetKey") {
t.Fatalf("expected unsafe targetKey rejection, got %v", err)
}
}
func TestValidateRunJobAssignmentDistributionCapabilitiesAreBounded(t *testing.T) {
selfUpdate := RunJobAssignment{
JobID: "job-update",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityRunSelfUpdate,
TargetKey: "run/update",
InputRef: "artifact://artifact-run-latest",
IdempotencyKey: "idem-update",
}
if err := ValidateRunJobAssignment(selfUpdate); err != nil {
t.Fatalf("expected valid self-update assignment: %v", err)
}
selfUpdate.InputRef = "input://not-an-artifact"
if err := ValidateRunJobAssignment(selfUpdate); err == nil || !strings.Contains(err.Error(), "artifact") {
t.Fatalf("expected non-artifact self-update ref rejection, got %v", err)
}
check := RunJobAssignment{
JobID: "job-dependency-check",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityDependenciesCheck,
TargetKey: "dependencies/java-21",
IdempotencyKey: "idem-dep-check",
}
if err := ValidateRunJobAssignment(check); err != nil {
t.Fatalf("expected valid dependency check assignment: %v", err)
}
check.TargetKey = "dependencies/install/java;rm"
if err := ValidateRunJobAssignment(check); err == nil || !strings.Contains(err.Error(), "targetKey") {
t.Fatalf("expected shell-like dependency target rejection, got %v", err)
}
backfill := RunJobAssignment{
JobID: "job-log-backfill",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityLogsBackfill,
TargetKey: "logs/latest-log",
InputRef: "artifact://logs/checkpoint/1",
IdempotencyKey: "idem-log-backfill",
ExecutionInput: RunJobExecutionInput{LogSource: &RuntimeLogSourcePlan{Key: "latest-log", Kind: "file.tail", TargetKey: "logs/latest.log", StreamKey: "latest-log", CursorKind: "offset", RetentionDays: 30}},
}
if err := ValidateRunJobAssignment(backfill); err != nil {
t.Fatalf("expected valid log backfill assignment: %v", err)
}
backfill.ExecutionInput.LogSource.Kind = "sql.query"
if err := ValidateRunJobAssignment(backfill); err == nil || !strings.Contains(err.Error(), "unsupported") {
t.Fatalf("expected unsupported log source rejection, got %v", err)
}
backfill.ExecutionInput.LogSource.Kind = "file.tail"
backfill.InputRef = "password=raw"
if err := ValidateRunJobAssignment(backfill); err == nil || !strings.Contains(err.Error(), "inputRef") {
t.Fatalf("expected unsafe log checkpoint rejection, got %v", err)
}
}
func TestValidateRunJobAssignmentDLLExtensionsAreBounded(t *testing.T) {
assignment := RunJobAssignment{
JobID: "job-dll-extension",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityProcessStart,
TargetKey: "actions/start.json",
IdempotencyKey: "idem-dll-extension",
ExecutionInput: RunJobExecutionInput{
WorkspaceScope: "run-local",
DLLExtensions: []RuntimeDLLExtensionPlan{validRuntimeDLLExtensionPlan()},
},
}
if err := ValidateRunJobAssignment(assignment); err != nil {
t.Fatalf("expected valid DLL extension assignment: %v", err)
}
cases := []struct {
name string
mutate func(*RunJobAssignment)
want string
}{
{name: "not process start", mutate: func(value *RunJobAssignment) { value.Capability = RunCapabilityProcessStop }, want: "process.start"},
{name: "query URL", mutate: func(value *RunJobAssignment) { value.ExecutionInput.DLLExtensions[0].ReleaseURL += "?release=1" }, want: "release integrity"},
{name: "unsafe DLL path", mutate: func(value *RunJobAssignment) {
value.ExecutionInput.DLLExtensions[0].DLLRef = "ue4ss/Mods/scum_simple_rcon/dlls/other.dll"
}, want: "deployment path"},
{name: "bad checksum", mutate: func(value *RunJobAssignment) { value.ExecutionInput.DLLExtensions[0].Checksum = "sha256:bad" }, want: "release integrity"},
{name: "missing scope", mutate: func(value *RunJobAssignment) { value.ExecutionInput.WorkspaceScope = "" }, want: "process.start"},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
value := assignment
value.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), assignment.ExecutionInput.DLLExtensions...)
testCase.mutate(&value)
if err := ValidateRunJobAssignment(value); err == nil || !strings.Contains(err.Error(), testCase.want) {
t.Fatalf("expected %q validation error, got %v", testCase.want, err)
}
})
}
}
func TestValidateRunJobAssignmentSourceRCONPlanIsFrozenAndOneShot(t *testing.T) {
assignment := RunJobAssignment{
JobID: "job-source-rcon",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityRemoteRunRCONCommand,
TargetKey: "rcon.password",
InputRef: "input://source-rcon/job-source-rcon",
IdempotencyKey: "idem-source-rcon",
Attempt: 1,
MaxAttempts: 1,
ExecutionInput: RunJobExecutionInput{
WorkspaceScope: "run-local",
RemoteAdapterKey: "rcon",
RemoteAdapterKind: "rcon",
TimeoutSeconds: 30,
SourceRCON: &RuntimeSourceRCONPlan{Protocol: "source-rcon", ExtensionKey: "scum-simple-rcon", ModKey: "scum_simple_rcon", ConfigRef: "ue4ss/Mods/scum_simple_rcon/config.ini", DeploymentStateRef: "runtime/ue4ss-dll/ue4ss/scum-simple-rcon/release.json", Port: 27015},
},
}
if err := ValidateRunJobAssignment(assignment); err != nil {
t.Fatalf("expected valid frozen Source RCON plan: %v", err)
}
protected := assignment
protected.Capability = RunCapabilityRemoteRunProtectedRCON
protected.TargetKey = "scum-management"
protected.InputRef = "input://protected-request/job-protected-rcon"
protected.FencingToken = 7
protected.ExecutionInput.RemoteAdapterKey = "scum-management"
protected.ExecutionInput.RemoteAdapterKind = "protected-rcon"
protected.ExecutionInput.TimeoutSeconds = 120
plan := *assignment.ExecutionInput.SourceRCON
protected.ExecutionInput.SourceRCON = &plan
if err := ValidateRunJobAssignment(protected); err != nil {
t.Fatalf("expected valid protected Source RCON plan: %v", err)
}
cases := []struct {
name string
mutate func(*RunJobAssignment)
want string
}{
{name: "multiple attempts", mutate: func(value *RunJobAssignment) { value.MaxAttempts = 2 }, want: "exactly one attempt"},
{name: "wrong capability", mutate: func(value *RunJobAssignment) { value.Capability = RunCapabilityRemoteRunDBSQLiteQuery }, want: "remote.run.rcon.command or remote.run.protected.rcon"},
{name: "unsafe config reference", mutate: func(value *RunJobAssignment) {
value.ExecutionInput.SourceRCON.ConfigRef = "ue4ss/Mods/scum_simple_rcon/other.ini"
}, want: "config reference"},
{name: "unsafe deployment state reference", mutate: func(value *RunJobAssignment) {
value.ExecutionInput.SourceRCON.DeploymentStateRef = "runtime/ue4ss-dll/../../release.json"
}, want: "deployment state reference"},
{name: "unsafe port", mutate: func(value *RunJobAssignment) { value.ExecutionInput.SourceRCON.Port = 80 }, want: "port"},
{name: "ordinary input ref", mutate: func(value *RunJobAssignment) { value.InputRef = "input://server-1/rcon/command" }, want: "source-rcon input ref"},
{name: "missing plan", mutate: func(value *RunJobAssignment) { value.ExecutionInput.SourceRCON = nil }, want: "requires a Source RCON plan"},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
value := assignment
plan := *assignment.ExecutionInput.SourceRCON
value.ExecutionInput.SourceRCON = &plan
testCase.mutate(&value)
if err := ValidateRunJobAssignment(value); err == nil || !strings.Contains(err.Error(), testCase.want) {
t.Fatalf("expected %q validation error, got %v", testCase.want, err)
}
})
}
}
func TestValidateProtectedRequestAssignmentAndOneTimeInput(t *testing.T) {
assignment := RunJobAssignment{
JobID: "job-protected",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityRemoteRunProtectedSQL,
TargetKey: "scum-database",
InputRef: "input://protected-request/job-protected",
IdempotencyKey: "protected-1",
LeaseToken: "lease-protected",
FencingToken: 7,
Attempt: 1,
MaxAttempts: 1,
ExecutionInput: RunJobExecutionInput{
RemoteAdapterKey: "scum-database",
RemoteAdapterKind: "protected-sql",
TimeoutSeconds: 30,
},
}
if err := ValidateRunJobAssignment(assignment); err != nil {
t.Fatalf("expected valid protected assignment: %v", err)
}
input := ProtectedRequestExecutionInputResponse{JobID: assignment.JobID, ServerInstanceID: assignment.ServerInstanceID, RunEndpointID: assignment.RunEndpointID, FencingToken: assignment.FencingToken, Authorized: true, ApprovalState: "approved", QueueState: "claimed", ExpiresAt: time.Now().UTC().Add(time.Minute), Kind: "sql", TransportKey: "scum-database", TargetKey: assignment.TargetKey, RequestText: "SELECT player_id FROM players LIMIT 1"}
if !ValidProtectedRequestExecutionInput(input) {
t.Fatal("expected approved unexpired protected input")
}
for _, testCase := range []struct {
name string
mutate func(*RunJobAssignment)
want string
}{
{name: "missing fence", mutate: func(value *RunJobAssignment) { value.FencingToken = 0 }, want: "fenced"},
{name: "multiple attempts", mutate: func(value *RunJobAssignment) { value.MaxAttempts = 2 }, want: "single"},
{name: "wrong input ref", mutate: func(value *RunJobAssignment) { value.InputRef = "input://ordinary/request" }, want: "inputRef"},
{name: "wrong adapter", mutate: func(value *RunJobAssignment) { value.ExecutionInput.RemoteAdapterKind = "database" }, want: "adapter kind"},
{name: "inline text", mutate: func(value *RunJobAssignment) { value.ExecutionInput.Content = "SELECT secret" }, want: "must not"},
} {
t.Run(testCase.name, func(t *testing.T) {
value := assignment
testCase.mutate(&value)
if err := ValidateRunJobAssignment(value); err == nil || !strings.Contains(err.Error(), testCase.want) {
t.Fatalf("expected %q validation error, got %v", testCase.want, err)
}
})
}
input.ExpiresAt = time.Now().UTC().Add(-time.Second)
if ValidProtectedRequestExecutionInput(input) {
t.Fatal("expired protected input was accepted")
}
input.ExpiresAt = time.Now().UTC().Add(time.Minute)
input.QueueState = "pending"
if ValidProtectedRequestExecutionInput(input) {
t.Fatal("unclaimed protected input was accepted")
}
}
func validRuntimeDLLExtensionPlan() RuntimeDLLExtensionPlan {
return RuntimeDLLExtensionPlan{
Key: "scum-simple-rcon",
Version: "1.0.0",
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll",
Checksum: "sha256:" + strings.Repeat("a", 64),
SizeBytes: 1024,
TargetKey: "ue4ss/scum-simple-rcon",
ModKey: "scum_simple_rcon",
DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
TargetExecutableChecksum: "sha256:" + strings.Repeat("b", 64),
UE4SSABI: "ue4ss-3.0",
RCONPort: 27015,
}
}