Files
pulse/internal/api/agent_install_command_shared_test.go
T
2026-08-29 22:12:41 +01:00

443 lines
18 KiB
Go

package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/api/agenttokens"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/stretchr/testify/require"
)
func TestNormalizeProxmoxInstallType(t *testing.T) {
installType, err := normalizeProxmoxInstallType(" PBS ")
require.NoError(t, err)
require.Equal(t, "pbs", installType)
_, err = normalizeProxmoxInstallType("vmware")
require.Error(t, err)
require.Equal(t, "Type must be 'pve' or 'pbs'", err.Error())
}
func TestAgentInstallCommandCarriesCommandPolicyIntent(t *testing.T) {
cfg := &config.Config{DataPath: t.TempDir(), AuthUser: "admin", AuthPass: "hashed-password"}
handler := newTestConfigHandlers(t, cfg)
req := httptest.NewRequest(
http.MethodPost,
"/api/agent-install-command",
bytes.NewBufferString(`{"type":"pve","enableCommands":true}`),
)
req.Host = "pulse.example:7655"
rec := httptest.NewRecorder()
handler.HandleAgentInstallCommand(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("install command status = %d: %s", rec.Code, rec.Body.String())
}
var response AgentInstallCommandResponse
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
t.Fatalf("decode install command response: %v", err)
}
if len(cfg.APITokens) != 1 || response.Token == "" {
t.Fatalf("install command did not mint one token: response=%+v tokens=%d", response, len(cfg.APITokens))
}
if got := cfg.APITokens[0].Metadata[agenttokens.CommandPolicyIntentMetadataKey]; got != agenttokens.CommandPolicyIntentEnabled {
t.Fatalf("command policy intent = %q, want enabled", got)
}
if !cfg.APITokens[0].HasScope(config.ScopeAgentExec) {
t.Fatalf("explicit command install token is missing %s: %v", config.ScopeAgentExec, cfg.APITokens[0].Scopes)
}
if !strings.Contains(response.Command, "--enable-commands") {
t.Fatalf("commands-enabled install omitted flag: %s", response.Command)
}
}
func TestAgentInstallCommandDefaultsToMonitoringOnlyAuthority(t *testing.T) {
cfg := &config.Config{DataPath: t.TempDir(), AuthUser: "admin", AuthPass: "hashed-password"}
handler := newTestConfigHandlers(t, cfg)
req := httptest.NewRequest(
http.MethodPost,
"/api/agent-install-command",
bytes.NewBufferString(`{"type":"pve"}`),
)
req.Host = "pulse.example:7655"
rec := httptest.NewRecorder()
handler.HandleAgentInstallCommand(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("install command status = %d: %s", rec.Code, rec.Body.String())
}
if len(cfg.APITokens) != 1 {
t.Fatalf("install command minted %d tokens, want 1", len(cfg.APITokens))
}
if cfg.APITokens[0].HasScope(config.ScopeAgentExec) {
t.Fatalf("monitoring-only install token includes %s: %v", config.ScopeAgentExec, cfg.APITokens[0].Scopes)
}
if got := cfg.APITokens[0].Metadata[agenttokens.CommandPolicyIntentMetadataKey]; got != agenttokens.CommandPolicyIntentDisabled {
t.Fatalf("command policy intent = %q, want disabled", got)
}
}
func TestBuildProxmoxAgentInstallCommand(t *testing.T) {
command := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
BaseURL: "https://pulse.example.com/",
Token: "token-123",
InstallType: "pbs",
IncludeInstallType: true,
})
require.Contains(t, command, posixShellQuote("https://pulse.example.com/install.sh"))
require.Contains(t, command, "printf %s "+posixShellQuote("token-123")+` > "$token_file"`)
require.Contains(t, command, `--token-file "$token_file"`)
require.Contains(t, command, `rm -f "$token_file"`)
require.Contains(t, command, "--proxmox-type "+posixShellQuote("pbs"))
require.NotContains(t, command, "--enable-commands")
}
func TestBuildProxmoxAgentInstallCommand_IncludesInsecureForPlainHTTP(t *testing.T) {
command := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
BaseURL: "http://pulse.example.com:7655/",
Token: "token-123",
InstallType: "pve",
IncludeInstallType: true,
})
require.Contains(t, command, posixShellQuote("http://pulse.example.com:7655/install.sh"))
require.Contains(t, command, "--url "+posixShellQuote("http://pulse.example.com:7655"))
require.Contains(t, command, "--insecure")
}
func TestBuildProxmoxAgentInstallCommand_IncludesExplicitInsecureForSelfSignedHTTPS(t *testing.T) {
command := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
BaseURL: "https://pulse.example.com:7655/",
Token: "token-123",
InstallType: "pve",
IncludeInstallType: true,
Insecure: true,
})
require.Contains(t, command, "curl -kfsSL "+posixShellQuote("https://pulse.example.com:7655/install.sh"))
require.Contains(t, command, "--url "+posixShellQuote("https://pulse.example.com:7655"))
require.Contains(t, command, "--insecure")
}
func TestBuildProxmoxAgentInstallCommand_UsesPrivilegeEscalationWrapper(t *testing.T) {
command := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
BaseURL: "https://pulse.example.com/",
Token: "token-123",
InstallType: "pve",
IncludeInstallType: true,
})
require.Contains(t, command, `| { if [ "$(id -u)" -eq 0 ]; then bash -s --`)
require.Contains(t, command, `elif command -v sudo >/dev/null 2>&1; then sudo bash -s --`)
require.Contains(t, command, `else echo "Root privileges required. Run as root (su -) and retry." >&2; exit 1; fi; }`)
require.NotContains(t, command, "| bash -s -- --url")
}
func TestBuildProxmoxAgentInstallCommand_OmitsTokenWhenNotProvided(t *testing.T) {
command := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
BaseURL: "https://pulse.example.com/",
Token: "",
InstallType: "pbs",
IncludeInstallType: true,
})
require.Contains(t, command, "--url "+posixShellQuote("https://pulse.example.com"))
require.Contains(t, command, "--proxmox-type "+posixShellQuote("pbs"))
require.NotContains(t, command, "--token")
}
func TestBuildProxmoxAgentInstallCommand_ShellEscapesArguments(t *testing.T) {
baseURL := "https://pulse.example.com' && touch /tmp/pwned #"
token := "tok'en"
command := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
BaseURL: baseURL,
Token: token,
InstallType: "pve",
IncludeInstallType: true,
})
require.Contains(t, command, posixShellQuote(baseURL+"/install.sh"))
require.Contains(t, command, "--url "+posixShellQuote(baseURL))
require.Contains(t, command, "printf %s "+posixShellQuote(token)+` > "$token_file"`)
require.Contains(t, command, `--token-file "$token_file"`)
require.Contains(t, command, "--proxmox-type "+posixShellQuote("pve"))
}
func TestBuildProxmoxAgentInstallCommand_NormalizesTrailingSlashes(t *testing.T) {
command := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
BaseURL: "https://pulse.example.com/base///",
Token: "token-123",
InstallType: "pve",
IncludeInstallType: true,
})
require.Contains(t, command, posixShellQuote("https://pulse.example.com/base/install.sh"))
require.Contains(t, command, "--url "+posixShellQuote("https://pulse.example.com/base"))
require.Contains(t, command, `--token-file "$token_file"`)
require.NotContains(t, command, "//install.sh")
require.NotContains(t, command, "--url "+posixShellQuote("https://pulse.example.com/base/"))
}
func TestBuildProxmoxAgentInstallCommand_IncludesCommandsWhenRequested(t *testing.T) {
command := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
BaseURL: "https://pulse.example.com/",
Token: "token-123",
InstallType: "pve",
IncludeInstallType: true,
EnableCommands: true,
})
require.Contains(t, command, "--enable-proxmox")
require.Contains(t, command, "--proxmox-type "+posixShellQuote("pve"))
require.Contains(t, command, "--enable-commands")
require.Contains(t, command, `| { if [ "$(id -u)" -eq 0 ]; then bash -s --`)
require.Contains(t, command, `rm -f "$token_file"`)
}
func TestBuildContainerRuntimeAgentInstallCommand_UsesLifecycleTransport(t *testing.T) {
command := buildContainerRuntimeAgentInstallCommand("https://pulse.example.com/base", "token-123", true)
require.Contains(t, command, posixShellQuote("https://pulse.example.com/base/install.sh"))
require.Contains(t, command, "--url "+posixShellQuote("https://pulse.example.com/base"))
require.Contains(t, command, "--token "+posixShellQuote("token-123"))
require.Contains(t, command, "--enable-docker")
require.Contains(t, command, "--enable-host")
require.NotContains(t, command, "--enable-host=false")
require.Contains(t, command, "--interval 30s")
require.Contains(t, command, `| { if [ "$(id -u)" -eq 0 ]; then bash -s --`)
require.Contains(t, command, `elif command -v sudo >/dev/null 2>&1; then sudo bash -s --`)
require.NotContains(t, command, "| sudo bash -s -- --url")
}
func TestBuildContainerRuntimeAgentInstallCommand_OmitsTokenAndAddsInsecureForHTTP(t *testing.T) {
command := buildContainerRuntimeAgentInstallCommand("http://pulse.example.com:7655/", "", true)
require.Contains(t, command, posixShellQuote("http://pulse.example.com:7655/install.sh"))
require.Contains(t, command, "--url "+posixShellQuote("http://pulse.example.com:7655"))
require.NotContains(t, command, "--token")
require.Contains(t, command, "--insecure")
}
func TestBuildContainerRuntimeAgentInstallCommand_PreservesWorkloadOnlyMode(t *testing.T) {
command := buildContainerRuntimeAgentInstallCommand("https://pulse.example.com", "token-123", false)
require.Contains(t, command, "--enable-docker")
require.Contains(t, command, "--enable-host=false")
}
func TestContainerRuntimeAgentScopesFollowHostMode(t *testing.T) {
dualMode := containerRuntimeAgentScopes(true)
require.ElementsMatch(t, []string{
config.ScopeDockerReport,
config.ScopeAgentReport,
config.ScopeAgentConfigRead,
config.ScopeAgentManage,
}, dualMode)
require.Equal(t, []string{config.ScopeDockerReport}, containerRuntimeAgentScopes(false))
}
func TestBuildSetupScriptCommand_UsesFailFastQuotedTransport(t *testing.T) {
command := buildSetupScriptCommand("https://pulse.example.com/api/setup-script?type=pve&host=pve1.local", "token-123")
require.Contains(t, command, "curl -fsSL "+posixShellQuote("https://pulse.example.com/api/setup-script?type=pve&host=pve1.local")+" | ")
require.Contains(t, command, `if [ "$(id -u)" -eq 0 ]; then PULSE_SETUP_TOKEN=`+posixShellQuote("token-123")+` bash`)
require.Contains(t, command, `elif command -v sudo >/dev/null 2>&1; then sudo env PULSE_SETUP_TOKEN=`+posixShellQuote("token-123")+` bash`)
require.Contains(t, command, `else echo "Root privileges required. Run as root (su -) and retry." >&2; exit 1; fi; }`)
}
func TestBuildSetupScriptCommand_OmitsTokenWhenNotProvided(t *testing.T) {
command := buildSetupScriptCommand("https://pulse.example.com/api/setup-script?type=pbs", "")
require.Contains(t, command, "curl -fsSL "+posixShellQuote("https://pulse.example.com/api/setup-script?type=pbs")+" | ")
require.Contains(t, command, `if [ "$(id -u)" -eq 0 ]; then bash`)
require.Contains(t, command, `elif command -v sudo >/dev/null 2>&1; then sudo bash`)
require.NotContains(t, command, "PULSE_SETUP_TOKEN=")
}
func TestBuildSetupScriptURL_PreservesEncodedHostAndBackupPerms(t *testing.T) {
scriptURL := buildSetupScriptURL(
"https://pulse.example.com:7656/",
"pve",
"https://[2001:db8::1]:8006",
"https://pulse.example.com:7656",
true,
)
require.Equal(
t,
"https://pulse.example.com:7656/api/setup-script?backup_perms=true&host=https%3A%2F%2F%5B2001%3Adb8%3A%3A1%5D%3A8006&pulse_url=https%3A%2F%2Fpulse.example.com%3A7656&type=pve",
scriptURL,
)
}
func TestBuildSetupScriptInstallArtifact_UsesSharedBackendShape(t *testing.T) {
expiresAt := time.Unix(1900000000, 0).UTC().Unix()
artifact := buildSetupScriptInstallArtifact(
"https://pulse.example",
"pve",
"https://pve1.local:8006",
"https://pulse.example",
true,
"setup-token-123",
expiresAt,
)
require.Equal(t, "pve", artifact.Type)
require.Equal(t, "https://pve1.local:8006", artifact.Host)
require.Contains(t, artifact.URL, "/api/setup-script?")
require.Contains(t, artifact.DownloadURL, "setup_token=setup-token-123")
require.Equal(t, "pulse-setup-pve.sh", artifact.ScriptFileName)
require.Equal(t, artifact.Command, artifact.CommandWithEnv)
require.Contains(t, artifact.CommandWithEnv, "PULSE_SETUP_TOKEN='setup-token-123'")
require.NotContains(t, artifact.CommandWithoutEnv, "PULSE_SETUP_TOKEN=")
require.Equal(t, expiresAt, artifact.Expires)
require.Equal(t, "setup-token-123", artifact.SetupToken)
require.Equal(t, "set…123", artifact.TokenHint)
}
func TestRenderSetupScript_UsesSharedInstallArtifactShape(t *testing.T) {
artifact := buildSetupScriptInstallArtifact(
"https://pulse.example",
"pve",
"https://pve1.local:8006",
"https://pulse.example",
true,
"setup-token-123",
0,
)
script := renderSetupScript("pve", setupScriptRenderContext{
ServerName: deriveSetupScriptServerName("https://pve1.local:8006"),
PulseURL: "https://pulse.example",
ServerHost: "https://pve1.local:8006",
SetupToken: "setup-token-123",
TokenName: buildPulseMonitorTokenName("https://pulse.example"),
TokenMatchPrefix: buildPulseMonitorTokenName("https://pulse.example"),
StoragePerms: `
pveum aclmod /storage -user pulse-monitor@pve -role PVEDatastoreAdmin
if [ "$TOKEN_CREATED" = true ]; then
pveum aclmod /storage -token "$PULSE_TOKEN_ID" -role PVEDatastoreAdmin
fi`,
SensorsPublicKey: "ssh-ed25519 AAAATEST pulse@test",
Artifact: artifact,
})
require.Contains(t, script, `SETUP_SCRIPT_URL="https://pulse.example/api/setup-script?backup_perms=true&host=https%3A%2F%2Fpve1.local%3A8006&pulse_url=https%3A%2F%2Fpulse.example&type=pve"`)
require.Contains(t, script, `PULSE_BOOTSTRAP_COMMAND_WITH_ENV='curl -fsSL '"'"'https://pulse.example/api/setup-script?backup_perms=true&host=https%3A%2F%2Fpve1.local%3A8006&pulse_url=https%3A%2F%2Fpulse.example&type=pve'"'"' | `)
require.Contains(t, script, `PULSE_SETUP_TOKEN="${PULSE_SETUP_TOKEN:-setup-token-123}"`)
require.Contains(t, script, `pveum aclmod /storage -user pulse-monitor@pve -role PVEDatastoreAdmin`)
require.Contains(t, script, `pveum aclmod /storage -token "$PULSE_TOKEN_ID" -role PVEDatastoreAdmin`)
}
func TestInstallBaseURLRequiresInsecure(t *testing.T) {
require.True(t, installBaseURLRequiresInsecure("http://pulse.example.com:7655"))
require.True(t, installBaseURLRequiresInsecure(" HTTP://pulse.example.com:7655/ "))
require.False(t, installBaseURLRequiresInsecure("https://pulse.example.com"))
require.False(t, installBaseURLRequiresInsecure(""))
}
func TestResolveConfigAgentInstallBaseURL(t *testing.T) {
cfg := &config.Config{
AgentConnectURL: "https://agents.example.com/",
PublicURL: "https://public.example.com",
FrontendPort: 7655,
}
req := httptest.NewRequest("POST", "/api/agent-install-command", nil)
req.Host = "127.0.0.1:7655"
req.Header.Set("X-Forwarded-Proto", "https")
require.Equal(t, "https://agents.example.com", resolveConfigAgentInstallBaseURL(req, cfg, false))
cfg.AgentConnectURL = ""
require.Equal(t, "https://public.example.com", resolveConfigAgentInstallBaseURL(req, cfg, false))
}
func TestResolveConfigAgentInstallBaseURL_PreservesConfiguredPublicSchemeOnLoopback(t *testing.T) {
cfg := &config.Config{
PublicURL: "https://public.example.com/base/",
FrontendPort: 7655,
}
req := httptest.NewRequest("POST", "/api/agent-install-command", nil)
req.Host = "127.0.0.1:7655"
require.Equal(t, "https://public.example.com/base", resolveConfigAgentInstallBaseURL(req, cfg, false))
}
func TestResolveConfigAgentInstallBaseURL_UsesConfiguredPublicURLForIPv6Loopback(t *testing.T) {
cfg := &config.Config{
PublicURL: "https://public.example.com",
FrontendPort: 7655,
}
req := httptest.NewRequest("POST", "/api/agent-install-command", nil)
req.Host = "[::1]:7655"
require.Equal(t, "https://public.example.com", resolveConfigAgentInstallBaseURL(req, cfg, false))
}
func TestIssueAndPersistAgentInstallToken(t *testing.T) {
dataDir := t.TempDir()
cfg := &config.Config{DataPath: dataDir}
persistence := config.NewConfigPersistence(dataDir)
rawToken, record, err := issueAndPersistAgentInstallToken(cfg, persistence, issueAgentInstallTokenOptions{
TokenName: "test-install-token",
OrgID: "acme",
OwnerUserID: "alice",
Metadata: map[string]string{
"install_type": "pbs",
"issued_via": "test",
},
})
require.NoError(t, err)
require.NotEmpty(t, rawToken)
require.NotNil(t, record)
require.Equal(t, "acme", record.OrgID)
require.Equal(t, "alice", record.Metadata[apiTokenMetadataOwnerUserID])
require.Equal(t, "pbs", record.Metadata["install_type"])
require.Equal(t, "test", record.Metadata["issued_via"])
require.Len(t, cfg.APITokens, 1)
savedRecord, ok := cfg.ValidateAPIToken(rawToken)
require.True(t, ok)
require.Equal(t, "acme", savedRecord.OrgID)
require.Equal(t, "alice", savedRecord.Metadata[apiTokenMetadataOwnerUserID])
tokens, err := persistence.LoadAPITokens()
require.NoError(t, err)
require.Len(t, tokens, 1)
require.True(t, strings.HasPrefix(tokens[0].Name, "test-install-token"))
require.Equal(t, "alice", tokens[0].Metadata[apiTokenMetadataOwnerUserID])
}
func TestIssueAndPersistAgentInstallTokenRejectsOwnerMetadataOverride(t *testing.T) {
dataDir := t.TempDir()
cfg := &config.Config{DataPath: dataDir}
persistence := config.NewConfigPersistence(dataDir)
rawToken, record, err := issueAndPersistAgentInstallToken(cfg, persistence, issueAgentInstallTokenOptions{
TokenName: "bad-install-token",
OrgID: "acme",
OwnerUserID: "alice",
Metadata: map[string]string{
apiTokenMetadataOwnerUserID: "mallory",
"install_type": "pbs",
},
})
require.Error(t, err)
require.ErrorIs(t, err, errAgentInstallTokenRecord)
require.Contains(t, err.Error(), "reserved token metadata key")
require.Empty(t, rawToken)
require.Nil(t, record)
require.Empty(t, cfg.APITokens)
tokens, loadErr := persistence.LoadAPITokens()
require.NoError(t, loadErr)
require.Empty(t, tokens)
}