Resolve declared file targets in Run
This commit is contained in:
@@ -44,6 +44,7 @@ type RunJobProgressReport struct {
|
|||||||
type RunJobExecutionInput struct {
|
type RunJobExecutionInput struct {
|
||||||
WorkspaceScope string `json:"workspaceScope,omitempty"`
|
WorkspaceScope string `json:"workspaceScope,omitempty"`
|
||||||
Content string `json:"content,omitempty"`
|
Content string `json:"content,omitempty"`
|
||||||
|
FileTargetKey string `json:"fileTargetKey,omitempty"`
|
||||||
ExpectedVersion int `json:"expectedVersion,omitempty"`
|
ExpectedVersion int `json:"expectedVersion,omitempty"`
|
||||||
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
|
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
|
||||||
MaxReadBytes int `json:"maxReadBytes,omitempty"`
|
MaxReadBytes int `json:"maxReadBytes,omitempty"`
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ func ValidateRunJobAssignment(assignment RunJobAssignment) error {
|
|||||||
if assignment.ExecutionInput.MaxReadBytes < 0 || assignment.ExecutionInput.MaxReadBytes > maxRunExecutionContentBytes {
|
if assignment.ExecutionInput.MaxReadBytes < 0 || assignment.ExecutionInput.MaxReadBytes > maxRunExecutionContentBytes {
|
||||||
return ValidationError("execution input maxReadBytes is out of bounds")
|
return ValidationError("execution input maxReadBytes is out of bounds")
|
||||||
}
|
}
|
||||||
|
if assignment.ExecutionInput.FileTargetKey != "" && !ValidLogicalFileKey(assignment.ExecutionInput.FileTargetKey) {
|
||||||
|
return ValidationError("execution input fileTargetKey is not allowed")
|
||||||
|
}
|
||||||
if assignment.ExecutionInput.WorkspaceScope != "" && !ValidLogicalFileKey(assignment.ExecutionInput.WorkspaceScope) {
|
if assignment.ExecutionInput.WorkspaceScope != "" && !ValidLogicalFileKey(assignment.ExecutionInput.WorkspaceScope) {
|
||||||
return ValidationError("execution input workspaceScope is not allowed")
|
return ValidationError("execution input workspaceScope is not allowed")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ func TestValidateRunJobAssignmentScopedFilePayloads(t *testing.T) {
|
|||||||
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "inputRef") {
|
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "inputRef") {
|
||||||
t.Fatalf("expected raw credential ref rejection, got %v", err)
|
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) {
|
func TestValidateRunJobAssignmentScopedReadDoesNotRequireInputRef(t *testing.T) {
|
||||||
|
|||||||
@@ -704,6 +704,47 @@ func TestScopedFileExecutorBoundsReads(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeclaredFileTargetKeyReadsAndWritesRelativeToDeploymentRoot(t *testing.T) {
|
||||||
|
workspaceRoot := t.TempDir()
|
||||||
|
serverRoot := t.TempDir()
|
||||||
|
filePath := filepath.Join(serverRoot, "SCUM", "Saved", "Config", "WindowsServer", "ServerSettings.ini")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(filePath), 0o700); err != nil {
|
||||||
|
t.Fatalf("mkdir declared file fixture: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filePath, []byte("[General]\nscum.MaxPlayers=63\n"), 0o600); err != nil {
|
||||||
|
t.Fatalf("write declared file fixture: %v", err)
|
||||||
|
}
|
||||||
|
executor, err := NewFileExecutor(workspaceRoot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new file executor: %v", err)
|
||||||
|
}
|
||||||
|
assignment := executionAssignment(protocol.RunCapabilityFilesRead)
|
||||||
|
assignment.TargetKey = "scum-server-settings"
|
||||||
|
assignment.ExecutionInput.FileTargetKey = "SCUM/Saved/Config/WindowsServer/ServerSettings.ini"
|
||||||
|
assignment.ExecutionInput.Deployment = &protocol.ServerDeploymentExecution{SchemaVersion: "1", Mode: "guided-install", ServerRoot: serverRoot, Revision: 1}
|
||||||
|
read := executor.Execute(context.Background(), assignment)
|
||||||
|
if read.State != lifecycleResultStateSucceeded || read.ExecutionResult.Kind != "file.read" || read.ExecutionResult.Content != "[General]\nscum.MaxPlayers=63\n" {
|
||||||
|
t.Fatalf("expected declared target read, got %+v", read)
|
||||||
|
}
|
||||||
|
assignment.Capability = protocol.RunCapabilityFilesWrite
|
||||||
|
assignment.InputRef = "input://server-execution/settings-write"
|
||||||
|
assignment.ExecutionInput.Content = "[General]\nscum.MaxPlayers=80\n"
|
||||||
|
assignment.ExecutionInput.ExpectedVersion = read.ExecutionResult.Version
|
||||||
|
assignment.ExecutionInput.ExpectedChecksum = read.ExecutionResult.Checksum
|
||||||
|
write := executor.Execute(context.Background(), assignment)
|
||||||
|
if write.State != lifecycleResultStateSucceeded || write.ExecutionResult.Kind != "file.write" {
|
||||||
|
t.Fatalf("expected declared target write, got %+v", write)
|
||||||
|
}
|
||||||
|
body, err := os.ReadFile(filePath)
|
||||||
|
if err != nil || string(body) != assignment.ExecutionInput.Content {
|
||||||
|
t.Fatalf("declared target write used the wrong path, body=%q err=%v", body, err)
|
||||||
|
}
|
||||||
|
assignment.ExecutionInput.FileTargetKey = "../outside.ini"
|
||||||
|
if unsafe := executor.Execute(context.Background(), assignment); unsafe.State != lifecycleResultStateFailed {
|
||||||
|
t.Fatalf("expected unsafe declared target rejection, got %+v", unsafe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestScopedFileExecutorListsDirectories(t *testing.T) {
|
func TestScopedFileExecutorListsDirectories(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
executor, err := NewFileExecutor(root)
|
executor, err := NewFileExecutor(root)
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ func (executor *FileExecutor) existingReadTargetForAssignment(assignment protoco
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", false, err
|
return "", "", false, err
|
||||||
}
|
}
|
||||||
targetKey := assignment.TargetKey
|
targetKey := fileTargetKeyForAssignment(assignment)
|
||||||
if deploymentRoot {
|
if deploymentRoot && assignment.ExecutionInput.FileTargetKey == "" {
|
||||||
targetKey = deploymentTargetKey(targetKey)
|
targetKey = deploymentTargetKey(targetKey)
|
||||||
filePath, err := existingDeploymentTarget(scope, targetKey)
|
filePath, err := existingDeploymentTarget(scope, targetKey)
|
||||||
return scope, filePath, deploymentRoot, err
|
return scope, filePath, deploymentRoot, err
|
||||||
@@ -245,8 +245,8 @@ func (executor *FileExecutor) read(ctx context.Context, scope string, deployment
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return lifecycleExecutionFailure("file_cancelled", "file read cancelled", false)
|
return lifecycleExecutionFailure("file_cancelled", "file read cancelled", false)
|
||||||
}
|
}
|
||||||
targetKey := assignment.TargetKey
|
targetKey := fileTargetKeyForAssignment(assignment)
|
||||||
if deploymentRoot {
|
if deploymentRoot && assignment.ExecutionInput.FileTargetKey == "" {
|
||||||
targetKey = deploymentTargetKey(targetKey)
|
targetKey = deploymentTargetKey(targetKey)
|
||||||
}
|
}
|
||||||
var filePath string
|
var filePath string
|
||||||
@@ -292,8 +292,8 @@ func (executor *FileExecutor) write(ctx context.Context, scope string, deploymen
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return lifecycleExecutionFailure("file_cancelled", "file write cancelled", false)
|
return lifecycleExecutionFailure("file_cancelled", "file write cancelled", false)
|
||||||
}
|
}
|
||||||
targetKey := assignment.TargetKey
|
targetKey := fileTargetKeyForAssignment(assignment)
|
||||||
if deploymentRoot {
|
if deploymentRoot && assignment.ExecutionInput.FileTargetKey == "" {
|
||||||
targetKey = deploymentTargetKey(targetKey)
|
targetKey = deploymentTargetKey(targetKey)
|
||||||
}
|
}
|
||||||
var filePath string
|
var filePath string
|
||||||
@@ -365,6 +365,13 @@ func (executor *FileExecutor) write(ctx context.Context, scope string, deploymen
|
|||||||
return LifecycleExecutionResult{State: lifecycleResultStateSucceeded, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "file write completed"}, Message: "file write completed", ExecutionResult: protocol.RunJobExecutionResult{Kind: "file.write", Version: next.Version, Checksum: checksum, SizeBytes: int64(len(content)), Summary: "atomic compare-and-swap file write"}}
|
return LifecycleExecutionResult{State: lifecycleResultStateSucceeded, Progress: protocol.RunJobProgressReport{Percent: 100, Message: "file write completed"}, Message: "file write completed", ExecutionResult: protocol.RunJobExecutionResult{Kind: "file.write", Version: next.Version, Checksum: checksum, SizeBytes: int64(len(content)), Summary: "atomic compare-and-swap file write"}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fileTargetKeyForAssignment(assignment protocol.RunJobAssignment) string {
|
||||||
|
if targetKey := strings.TrimSpace(assignment.ExecutionInput.FileTargetKey); targetKey != "" {
|
||||||
|
return targetKey
|
||||||
|
}
|
||||||
|
return assignment.TargetKey
|
||||||
|
}
|
||||||
|
|
||||||
func deploymentTargetKey(value string) string {
|
func deploymentTargetKey(value string) string {
|
||||||
cleaned := path.Clean(strings.TrimPrefix(strings.ReplaceAll(strings.TrimSpace(value), "\\", "/"), "/"))
|
cleaned := path.Clean(strings.TrimPrefix(strings.ReplaceAll(strings.TrimSpace(value), "\\", "/"), "/"))
|
||||||
if cleaned == "." || cleaned == "" {
|
if cleaned == "." || cleaned == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user