Files
pulse/internal/api/hosted_agent_install_command_test.go
T
pulse-triage[bot] 083c73103c Align routed and hosted Proxmox private-input contracts
Keep fresh token issuance, scope, persistence, normalized product and origin checks while requiring the minted credential outside copied shell text. Dynamic executable coverage already pins decoded quoted arguments. No runtime or frontend content changes.

Change-source: pulse-maintainer
2026-10-01 05:51:09 +01:00

287 lines
9.8 KiB
Go

package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/api/agenttokens"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
"github.com/stretchr/testify/require"
)
func TestHostedTenantAgentInstallCommand_GeneratesOrgBoundTokenAndCommand(t *testing.T) {
setMockModeForTest(t, true)
dataDir := t.TempDir()
cfg := &config.Config{
DataPath: dataDir,
PublicURL: "https://cloud.example.com",
}
router := &Router{
config: cfg,
persistence: config.NewConfigPersistence(dataDir),
multiTenant: config.NewMultiTenantPersistence(dataDir),
hostedMode: true,
}
orgID := "acme"
// Ensure org directory exists so OrgExists() passes.
_, err := router.multiTenant.GetPersistence(orgID)
require.NoError(t, err)
// Initialize tenant monitor to verify we update already-loaded tenant configs.
router.mtMonitor = monitoring.NewMultiTenantMonitor(cfg, router.multiTenant, nil)
t.Cleanup(router.mtMonitor.Stop)
tenantMonitor, err := router.mtMonitor.GetMonitor(orgID)
require.NoError(t, err)
require.NotNil(t, tenantMonitor)
mux := http.NewServeMux()
mux.HandleFunc("POST /api/admin/orgs/{id}/agent-install-command", router.handleHostedTenantAgentInstallCommand)
req := httptest.NewRequest(http.MethodPost, "/api/admin/orgs/"+orgID+"/agent-install-command", strings.NewReader(`{"type":"pbs"}`))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
var resp hostedTenantAgentInstallCommandResponse
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
require.Equal(t, orgID, resp.OrgID)
require.Contains(t, resp.Command, "https://cloud.example.com/install.sh")
require.NotContains(t, resp.Command, resp.Token)
require.Contains(t, resp.Command, `--token-file "$token_file"`)
require.Contains(t, resp.Command, "--proxmox-type "+strings.ReplaceAll(posixShellQuote("pbs"), "'", `'"'"'`))
require.Regexp(t, regexp.MustCompile(`^[a-f0-9]{64}$`), resp.Token)
// Token is persisted to global token store with org binding.
tokens, err := router.persistence.LoadAPITokens()
require.NoError(t, err)
require.NotEmpty(t, tokens)
found := false
for _, tok := range tokens {
if tok.OrgID == orgID && tok.Name != "" && tok.Hash != "" {
// Validate raw token matches this record.
if _, ok := (&config.Config{APITokens: []config.APITokenRecord{tok}}).ValidateAPIToken(resp.Token); ok {
require.Equal(t, "pbs", tok.Metadata["install_type"])
require.Equal(t, "hosted_agent_install_command", tok.Metadata["issued_via"])
found = true
break
}
}
}
require.True(t, found, "expected persisted org-bound token record matching response token")
// Existing tenant monitor config sees the token immediately.
require.NotNil(t, tenantMonitor.GetConfig())
_, ok := tenantMonitor.GetConfig().ValidateAPIToken(resp.Token)
require.True(t, ok, "expected tenant monitor config to validate newly issued token")
}
func TestGenerateHostedTenantAgentInstallCommandEnforcesHostedOrgBoundary(t *testing.T) {
setMockModeForTest(t, true)
dataDir := t.TempDir()
cfg := &config.Config{
DataPath: dataDir,
PublicURL: "https://cloud.example.com",
}
persistence := config.NewConfigPersistence(dataDir)
multiTenant := config.NewMultiTenantPersistence(dataDir)
_, err := GenerateHostedTenantAgentInstallCommand(HostedTenantAgentInstallCommandOptions{
Config: cfg,
Persistence: persistence,
MultiTenant: multiTenant,
HostedMode: false,
OrgID: "acme",
InstallType: "pve",
BaseURL: "https://acme.cloud.example.com",
})
require.ErrorIs(t, err, ErrHostedTenantInstallRequiresHostedMode)
_, err = GenerateHostedTenantAgentInstallCommand(HostedTenantAgentInstallCommandOptions{
Config: cfg,
Persistence: persistence,
MultiTenant: multiTenant,
HostedMode: true,
OrgID: "missing",
InstallType: "pve",
BaseURL: "https://missing.cloud.example.com",
})
require.ErrorIs(t, err, ErrHostedTenantInstallInvalidOrg)
orgID := "acme"
_, err = multiTenant.GetPersistence(orgID)
require.NoError(t, err)
result, err := GenerateHostedTenantAgentInstallCommand(HostedTenantAgentInstallCommandOptions{
Config: cfg,
Persistence: persistence,
MultiTenant: multiTenant,
HostedMode: true,
OrgID: orgID,
InstallType: "pbs",
OwnerUserID: "owner-1",
BaseURL: "https://acme.cloud.example.com/",
})
require.NoError(t, err)
require.Equal(t, orgID, result.OrgID)
require.Equal(t, "pbs", result.InstallType)
require.NotEmpty(t, result.Command)
require.NotEmpty(t, result.Token)
require.NotEmpty(t, result.TokenID)
require.Contains(t, result.Command, "https://acme.cloud.example.com/install.sh")
require.Contains(t, result.Command, "--proxmox-type "+strings.ReplaceAll(posixShellQuote("pbs"), "'", `'"'"'`))
tokens, err := persistence.LoadAPITokens()
require.NoError(t, err)
require.Len(t, tokens, 1)
require.Equal(t, orgID, tokens[0].OrgID)
require.Equal(t, "owner-1", tokens[0].Metadata[apiTokenMetadataOwnerUserID])
require.Equal(t, "pbs", tokens[0].Metadata["install_type"])
require.Equal(t, "hosted_agent_install_command", tokens[0].Metadata["issued_via"])
_, ok := (&config.Config{APITokens: tokens}).ValidateAPIToken(result.Token)
require.True(t, ok)
}
func TestHostedTenantAgentInstallCommandCarriesCommandPolicyIntent(t *testing.T) {
setMockModeForTest(t, true)
dataDir := t.TempDir()
cfg := &config.Config{
DataPath: dataDir,
PublicURL: "https://cloud.example.com",
}
persistence := config.NewConfigPersistence(dataDir)
multiTenant := config.NewMultiTenantPersistence(dataDir)
orgID := "acme"
_, err := multiTenant.GetPersistence(orgID)
require.NoError(t, err)
for _, tc := range []struct {
installType string
wantIntent string
}{
{installType: "pve", wantIntent: agenttokens.CommandPolicyIntentDisabled},
{installType: "pbs", wantIntent: agenttokens.CommandPolicyIntentDisabled},
} {
result, err := GenerateHostedTenantAgentInstallCommand(HostedTenantAgentInstallCommandOptions{
Config: cfg,
Persistence: persistence,
MultiTenant: multiTenant,
HostedMode: true,
OrgID: orgID,
InstallType: tc.installType,
BaseURL: "https://acme.cloud.example.com",
})
require.NoError(t, err)
var record *config.APITokenRecord
for index := range cfg.APITokens {
if cfg.APITokens[index].ID == result.TokenID {
record = &cfg.APITokens[index]
break
}
}
require.NotNil(t, record, "minted token record not found for %s", tc.installType)
require.Equal(t, tc.wantIntent, record.Metadata[agenttokens.CommandPolicyIntentMetadataKey], "install type %s", tc.installType)
require.False(t, record.HasScope(config.ScopeAgentExec), "hosted monitoring token must not include exec for %s", tc.installType)
require.NotContains(t, result.Command, "--enable-commands", "hosted monitoring command must not enable exec for %s", tc.installType)
}
}
func TestHostedTenantAgentInstallTokenCannotReportToOtherTenant(t *testing.T) {
defer SetMultiTenantEnabled(false)
SetMultiTenantEnabled(true)
t.Setenv("PULSE_DEV", "true")
// Deliberately NOT mock mode. Mock mode drops real agent reports on the
// floor, which would leave both tenants empty and make the isolation
// assertion below pass without ever exercising the boundary. The tenants
// here have no configured infrastructure, so nothing polls anyway.
setMockModeForTest(t, false)
dataDir := t.TempDir()
cfg := &config.Config{
DataPath: dataDir,
ConfigPath: dataDir,
PublicURL: "https://msp.example.com",
}
persistence := config.NewConfigPersistence(dataDir)
multiTenant := config.NewMultiTenantPersistence(dataDir)
for _, orgID := range []string{"client-a", "client-b"} {
_, err := multiTenant.GetPersistence(orgID)
require.NoError(t, err)
}
mtm := monitoring.NewMultiTenantMonitor(cfg, multiTenant, nil)
t.Cleanup(mtm.Stop)
_, err := mtm.GetMonitor("client-a")
require.NoError(t, err)
install, err := GenerateHostedTenantAgentInstallCommand(HostedTenantAgentInstallCommandOptions{
Config: cfg,
Persistence: persistence,
MultiTenant: multiTenant,
TenantMonitor: mtm,
HostedMode: true,
OrgID: "client-a",
InstallType: "pve",
OwnerUserID: "owner-1",
BaseURL: "https://client-a.msp.example.com",
})
require.NoError(t, err)
router := NewRouter(cfg, nil, mtm, nil, nil, "1.0.0")
report := agentshost.Report{
Agent: agentshost.AgentInfo{
ID: "agent-client-a",
Version: "1.0.0",
Type: "unified",
},
Host: agentshost.HostInfo{
ID: "machine-client-a",
MachineID: "machine-client-a",
Hostname: "pve1",
Platform: "linux",
},
Timestamp: time.Now().UTC(),
}
body, err := json.Marshal(report)
require.NoError(t, err)
allowedReq := httptest.NewRequest(http.MethodPost, "/api/agents/agent/report", bytes.NewReader(body))
allowedReq.Header.Set("X-API-Token", install.Token)
allowedRec := httptest.NewRecorder()
router.Handler().ServeHTTP(allowedRec, allowedReq)
require.Equal(t, http.StatusOK, allowedRec.Code, allowedRec.Body.String())
retargetReq := httptest.NewRequest(http.MethodPost, "/api/agents/agent/report", bytes.NewReader(body))
retargetReq.Header.Set("X-API-Token", install.Token)
retargetReq.Header.Set("X-Pulse-Org-ID", "client-b")
retargetRec := httptest.NewRecorder()
router.Handler().ServeHTTP(retargetRec, retargetReq)
require.Equal(t, http.StatusForbidden, retargetRec.Code, retargetRec.Body.String())
monitorA, ok := mtm.PeekMonitor("client-a")
require.True(t, ok)
require.Len(t, monitorA.GetLiveHostsSnapshot(), 1)
_, ok = mtm.PeekMonitor("client-b")
require.False(t, ok, "retargeted client-a token must be denied before client-b runtime initialization")
}