Pulse/internal/api/hosted_agent_install_command_test.go
rcourtman d729461cad Harden provider MSP tenant isolation
- isolate provider MSP tenant runtimes onto per-client Docker networks
- add adversarial proofs for workspace cap races, handoff retargeting, and org-bound agent report retargeting
- pin provider deploy proof to start Traefik before tenant network creation
2026-06-02 17:34:10 +01:00

236 lines
7.9 KiB
Go

package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"time"
"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.Contains(t, resp.Command, "printf %s "+posixShellQuote(resp.Token)+` > "$token_file"`)
require.Contains(t, resp.Command, `--token-file "$token_file"`)
require.Contains(t, resp.Command, "--proxmox-type "+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 "+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 TestHostedTenantAgentInstallTokenCannotReportToOtherTenant(t *testing.T) {
defer SetMultiTenantEnabled(false)
SetMultiTenantEnabled(true)
t.Setenv("PULSE_DEV", "true")
setMockModeForTest(t, true)
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)
monitorB, ok := mtm.PeekMonitor("client-b")
require.True(t, ok)
require.Empty(t, monitorB.GetLiveHostsSnapshot(), "retargeted client-a token must not write into client-b")
}