- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation. - Changed default base URL for Playwright tests and updated security headers to reflect the new branding. - Enhanced README and API documentation to include new authentication endpoints and product access details. This refactor aligns the project branding and improves clarity in the API documentation.
146 lines
5.4 KiB
Go
146 lines
5.4 KiB
Go
package track4
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"net/http"
|
|
"net/http/httptest"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type stubRoleManager struct {
|
|
allowed bool
|
|
gotIP string
|
|
logs int
|
|
}
|
|
|
|
func (s *stubRoleManager) IsIPWhitelisted(_ context.Context, _ string, ipAddress string) (bool, error) {
|
|
s.gotIP = ipAddress
|
|
return s.allowed, nil
|
|
}
|
|
|
|
func (s *stubRoleManager) LogOperatorEvent(_ context.Context, _ string, _ *int, _ string, _ string, _ string, _ map[string]interface{}, _ string, _ string) error {
|
|
s.logs++
|
|
return nil
|
|
}
|
|
|
|
func TestHandleRunScriptUsesForwardedClientIPAndRunsAllowlistedScript(t *testing.T) {
|
|
root := t.TempDir()
|
|
scriptPath := filepath.Join(root, "echo.sh")
|
|
require.NoError(t, os.WriteFile(scriptPath, []byte("#!/usr/bin/env bash\necho hello \"$1\"\n"), 0o644))
|
|
|
|
t.Setenv("OPERATOR_SCRIPTS_ROOT", root)
|
|
t.Setenv("OPERATOR_SCRIPT_ALLOWLIST", "echo.sh")
|
|
t.Setenv("OPERATOR_SCRIPT_TIMEOUT_SEC", "30")
|
|
t.Setenv("TRUST_PROXY_CIDRS", "10.0.0.0/8")
|
|
|
|
roleMgr := &stubRoleManager{allowed: true}
|
|
s := &Server{roleMgr: roleMgr, chainID: 138}
|
|
|
|
reqBody := []byte(`{"script":"echo.sh","args":["world"]}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/track4/operator/run-script", bytes.NewReader(reqBody))
|
|
req = req.WithContext(context.WithValue(req.Context(), "user_address", "0x4A666F96fC8764181194447A7dFdb7d471b301C8"))
|
|
req.RemoteAddr = "10.0.0.10:8080"
|
|
req.Header.Set("X-Forwarded-For", "203.0.113.9, 10.0.0.10")
|
|
w := httptest.NewRecorder()
|
|
|
|
s.HandleRunScript(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
require.Equal(t, "203.0.113.9", roleMgr.gotIP)
|
|
require.Equal(t, 2, roleMgr.logs)
|
|
|
|
var out struct {
|
|
Data map[string]any `json:"data"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out))
|
|
require.Equal(t, "echo.sh", out.Data["script"])
|
|
require.Equal(t, float64(0), out.Data["exit_code"])
|
|
require.Equal(t, "hello world", out.Data["stdout"])
|
|
require.Equal(t, false, out.Data["timed_out"])
|
|
}
|
|
|
|
func TestHandleRunScriptRejectsNonAllowlistedScript(t *testing.T) {
|
|
root := t.TempDir()
|
|
require.NoError(t, os.WriteFile(filepath.Join(root, "allowed.sh"), []byte("#!/usr/bin/env bash\necho ok\n"), 0o644))
|
|
require.NoError(t, os.WriteFile(filepath.Join(root, "blocked.sh"), []byte("#!/usr/bin/env bash\necho blocked\n"), 0o644))
|
|
|
|
t.Setenv("OPERATOR_SCRIPTS_ROOT", root)
|
|
t.Setenv("OPERATOR_SCRIPT_ALLOWLIST", "allowed.sh")
|
|
|
|
s := &Server{roleMgr: &stubRoleManager{allowed: true}, chainID: 138}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/track4/operator/run-script", bytes.NewReader([]byte(`{"script":"blocked.sh"}`)))
|
|
req = req.WithContext(context.WithValue(req.Context(), "user_address", "0x4A666F96fC8764181194447A7dFdb7d471b301C8"))
|
|
req.RemoteAddr = "127.0.0.1:9999"
|
|
w := httptest.NewRecorder()
|
|
|
|
s.HandleRunScript(w, req)
|
|
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
|
require.Contains(t, w.Body.String(), "script not in OPERATOR_SCRIPT_ALLOWLIST")
|
|
}
|
|
|
|
func TestHandleRunScriptRejectsFilenameCollisionOutsideAllowlistedPath(t *testing.T) {
|
|
root := t.TempDir()
|
|
require.NoError(t, os.MkdirAll(filepath.Join(root, "safe"), 0o755))
|
|
require.NoError(t, os.MkdirAll(filepath.Join(root, "unsafe"), 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(root, "safe", "backup.sh"), []byte("#!/usr/bin/env bash\necho safe\n"), 0o644))
|
|
require.NoError(t, os.WriteFile(filepath.Join(root, "unsafe", "backup.sh"), []byte("#!/usr/bin/env bash\necho unsafe\n"), 0o644))
|
|
|
|
t.Setenv("OPERATOR_SCRIPTS_ROOT", root)
|
|
t.Setenv("OPERATOR_SCRIPT_ALLOWLIST", "safe/backup.sh")
|
|
|
|
s := &Server{roleMgr: &stubRoleManager{allowed: true}, chainID: 138}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/track4/operator/run-script", bytes.NewReader([]byte(`{"script":"unsafe/backup.sh"}`)))
|
|
req = req.WithContext(context.WithValue(req.Context(), "user_address", "0x4A666F96fC8764181194447A7dFdb7d471b301C8"))
|
|
req.RemoteAddr = "127.0.0.1:9999"
|
|
w := httptest.NewRecorder()
|
|
|
|
s.HandleRunScript(w, req)
|
|
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
|
require.Contains(t, w.Body.String(), "script not in OPERATOR_SCRIPT_ALLOWLIST")
|
|
}
|
|
|
|
func TestHandleRunScriptTruncatesLargeOutput(t *testing.T) {
|
|
root := t.TempDir()
|
|
scriptPath := filepath.Join(root, "large.sh")
|
|
require.NoError(t, os.WriteFile(scriptPath, []byte("#!/usr/bin/env bash\npython3 - <<'PY'\nprint('x' * 70000)\nPY\n"), 0o644))
|
|
|
|
t.Setenv("OPERATOR_SCRIPTS_ROOT", root)
|
|
t.Setenv("OPERATOR_SCRIPT_ALLOWLIST", "large.sh")
|
|
t.Setenv("OPERATOR_SCRIPT_TIMEOUT_SEC", "30")
|
|
|
|
s := &Server{roleMgr: &stubRoleManager{allowed: true}, chainID: 138}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/track4/operator/run-script", bytes.NewReader([]byte(`{"script":"large.sh"}`)))
|
|
req = req.WithContext(context.WithValue(req.Context(), "user_address", "0x4A666F96fC8764181194447A7dFdb7d471b301C8"))
|
|
req.RemoteAddr = "127.0.0.1:9999"
|
|
w := httptest.NewRecorder()
|
|
|
|
s.HandleRunScript(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var out struct {
|
|
Data struct {
|
|
ExitCode float64 `json:"exit_code"`
|
|
Stdout string `json:"stdout"`
|
|
StdoutTruncated bool `json:"stdout_truncated"`
|
|
} `json:"data"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out))
|
|
require.Equal(t, float64(0), out.Data.ExitCode)
|
|
require.True(t, out.Data.StdoutTruncated)
|
|
require.Contains(t, out.Data.Stdout, "[truncated after")
|
|
require.LessOrEqual(t, len(out.Data.Stdout), maxOperatorScriptOutputBytes+64)
|
|
}
|