mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Mint agent install tokens in hosted client workspaces
Every provider MSP client workspace runs in hosted mode, where users sign in through the control plane handoff. The install endpoints decided whether to mint a token from the workspace's own config, which on a fresh client has no local credential, API token, proxy secret or SSO provider. So Settings > Infrastructure > Generate token answered 200 with an empty token and the Proxmox flow built a command without one: a provider could not install a single agent from the client workspace. Found on a fresh v6.4.1 provider bundle. The router's configTransferAuthenticationConfigured already counts hosted mode as enforced authentication; the agent-lifecycle copy of the rule never gained that term. Add it. Self-hosted runtimes with no auth keep the token-less command.
This commit is contained in:
@@ -3410,6 +3410,23 @@ Agent` secondary handoff against the live setup wizard instead of relying
|
||||
|
||||
## Current State
|
||||
|
||||
### Hosted runtimes always mint agent install tokens
|
||||
|
||||
`POST /api/agent-install-command` mints and embeds an agent install token
|
||||
whenever the runtime enforces authentication. A hosted runtime
|
||||
(`PULSE_HOSTED_MODE=true`, which every provider MSP client workspace runs)
|
||||
always does, so `ConfigHandlers.authConfiguredForAgentLifecycle` now treats
|
||||
hosted mode as configured authentication, the same term the router's
|
||||
`configTransferAuthenticationConfigured` already carried. Before, a fresh
|
||||
client workspace had no local credential, API token, proxy secret or SSO
|
||||
provider for the per-config check to find: the host flow answered `200` with
|
||||
an empty token and the Proxmox flow built a command with none, so an agent
|
||||
installed from the provider's client workspace could not report. Self-hosted
|
||||
runtimes with no authentication configured keep the token-less command.
|
||||
Regression coverage: `TestHostedRuntimeMintsAgentInstallTokensWithoutLocalAuth`
|
||||
and `TestSelfHostedRuntimeWithoutAuthStillOmitsInstallToken` in
|
||||
`internal/api/configapi/hosted_agent_install_token_test.go`.
|
||||
|
||||
### Auto-register preserves the operator's stored TLS choice
|
||||
|
||||
Re-registering an existing Proxmox node through the canonical auto-register
|
||||
|
||||
@@ -4541,6 +4541,19 @@ auto-register mutation boundary.
|
||||
|
||||
## Current State
|
||||
|
||||
### Hosted install command payloads carry a minted token
|
||||
|
||||
`POST /api/agent-install-command` returns a non-empty `token` (and, for
|
||||
Proxmox types, a `command` embedding it) on any hosted runtime, including a
|
||||
fresh provider MSP client workspace whose config holds no local credential,
|
||||
API token, proxy secret or SSO provider. Hosted mode is enforced
|
||||
authentication for this decision, as it already is for
|
||||
`configTransferAuthenticationConfigured`. The response shape is unchanged; a
|
||||
self-hosted runtime with no authentication configured still receives the
|
||||
token-less payload. `TestContract_HostedRuntimeAgentInstallCommandCarriesToken`
|
||||
in `internal/api/contract_test.go` pins the hosted payload through the
|
||||
router-built config handlers.
|
||||
|
||||
### Node auto-register TLS fields are operator-owned on update
|
||||
|
||||
The `/api/auto-register` completion request and response shapes are unchanged.
|
||||
|
||||
@@ -2618,6 +2618,15 @@ vdev layout is reported` in
|
||||
|
||||
## Current State
|
||||
|
||||
### Hosted install tokens persist like any other workspace token
|
||||
|
||||
Hosted client workspaces now mint agent install tokens where they previously
|
||||
returned none. Each token is issued through `issueAndPersistAgentInstallToken`
|
||||
into the workspace's own config persistence, the same store and write path as
|
||||
every other API token, so backup, restore and recovery of a client workspace
|
||||
carry these tokens without any new storage surface. A restored workspace keeps
|
||||
its agents' tokens; revoking one remains Settings → Infrastructure → Tokens.
|
||||
|
||||
### TLS verification preference survives node re-registration
|
||||
|
||||
A PVE or PBS node whose operator disabled certificate verification keeps that
|
||||
|
||||
@@ -129,8 +129,16 @@ func agentInstallCommandPolicyIntent(enableCommands bool) string {
|
||||
return agenttokens.CommandPolicyIntent(enableCommands)
|
||||
}
|
||||
|
||||
// authConfiguredForAgentLifecycle decides whether install commands carry a
|
||||
// minted agent token. A hosted runtime always enforces authentication, the
|
||||
// same as the router's configTransferAuthenticationConfigured says: its
|
||||
// sessions arrive through the control plane handoff, so a fresh client
|
||||
// workspace has no local credential, API token, proxy secret or SSO provider
|
||||
// for the per-config check to find. Without this term every install command
|
||||
// from a provider's client workspace came back without a token, and an agent
|
||||
// installed from it could not report.
|
||||
func (h *ConfigHandlers) authConfiguredForAgentLifecycle(cfg *config.Config) bool {
|
||||
return h.runtimeDependencies().AuthConfigured(cfg)
|
||||
return h.hostedMode || h.runtimeDependencies().AuthConfigured(cfg)
|
||||
}
|
||||
|
||||
func (h *ConfigHandlers) apiTokenOwnerUserIDForRequest(cfg *config.Config, r *http.Request) string {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package configapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
)
|
||||
|
||||
// A provider MSP client workspace runs in hosted mode with no local
|
||||
// credential, API token, proxy secret or SSO provider: its users sign in
|
||||
// through the control plane handoff. The install endpoints still have to mint
|
||||
// a token, because the runtime enforces authentication and an agent without
|
||||
// one cannot report. Before hosted mode counted as configured auth, the host
|
||||
// flow answered 200 with an empty token and the Proxmox flow built a command
|
||||
// with no token in it.
|
||||
func TestHostedRuntimeMintsAgentInstallTokensWithoutLocalAuth(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
body string
|
||||
}{
|
||||
{name: "host agent", body: `{"type":"host","name":"client-host"}`},
|
||||
{name: "proxmox agent", body: `{"type":"pve"}`},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
DataPath: t.TempDir(),
|
||||
PublicURL: "https://t-client.msp.example",
|
||||
}
|
||||
handler := newTestConfigHandlers(t, cfg)
|
||||
handler.hostedMode = true
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/agent-install-command", strings.NewReader(tc.body))
|
||||
req.Host = "t-client.msp.example"
|
||||
rec := httptest.NewRecorder()
|
||||
handler.HandleAgentInstallCommand(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp AgentInstallCommandResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if strings.TrimSpace(resp.Token) == "" {
|
||||
t.Fatal("hosted runtime returned an install response without a token")
|
||||
}
|
||||
if len(cfg.APITokens) != 1 {
|
||||
t.Fatalf("persisted API tokens = %d, want 1", len(cfg.APITokens))
|
||||
}
|
||||
if resp.Command != "" && !strings.Contains(resp.Command, resp.Token) {
|
||||
t.Fatalf("install command does not carry the minted token: %s", resp.Command)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Self-hosted runtimes keep the existing rule: with no authentication
|
||||
// configured at all there is nothing for a token to authenticate against.
|
||||
func TestSelfHostedRuntimeWithoutAuthStillOmitsInstallToken(t *testing.T) {
|
||||
cfg := &config.Config{DataPath: t.TempDir(), PublicURL: "http://pulse.local:7655"}
|
||||
handler := newTestConfigHandlers(t, cfg)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/agent-install-command", strings.NewReader(`{"type":"host","name":"open-host"}`))
|
||||
req.Host = "pulse.local:7655"
|
||||
rec := httptest.NewRecorder()
|
||||
handler.HandleAgentInstallCommand(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var resp AgentInstallCommandResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.Token != "" || len(cfg.APITokens) != 0 {
|
||||
t.Fatalf("unauthenticated self-hosted runtime minted a token: %+v", resp)
|
||||
}
|
||||
}
|
||||
@@ -24960,3 +24960,34 @@ func TestContract_QuickSetupSynchronizesConfiguredAdminCapabilities(t *testing.T
|
||||
t.Fatal("startup synchronisation did not restore capabilities")
|
||||
}
|
||||
}
|
||||
|
||||
// A provider MSP client workspace is a hosted runtime whose users sign in
|
||||
// through the control plane handoff, so its config carries no local
|
||||
// credential, API token, proxy secret or SSO provider. The install command
|
||||
// payload must still carry a minted token there, or an agent installed from
|
||||
// the client workspace cannot report.
|
||||
func TestContract_HostedRuntimeAgentInstallCommandCarriesToken(t *testing.T) {
|
||||
router := newConfigTransferTestRouter(t, true, nil)
|
||||
router.config.PublicURL = "https://t-client.msp.example"
|
||||
|
||||
for _, body := range []string{`{"type":"host","name":"client-host"}`, `{"type":"pve"}`} {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/agent-install-command", strings.NewReader(body))
|
||||
req.Host = "t-client.msp.example"
|
||||
rec := httptest.NewRecorder()
|
||||
router.configHandlers.HandleAgentInstallCommand(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("%s: status = %d, body=%s", body, rec.Code, rec.Body.String())
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("%s: decode: %v", body, err)
|
||||
}
|
||||
token, _ := payload["token"].(string)
|
||||
if strings.TrimSpace(token) == "" {
|
||||
t.Fatalf("%s: hosted install payload has no token: %s", body, rec.Body.String())
|
||||
}
|
||||
if command, _ := payload["command"].(string); command != "" && !strings.Contains(command, token) {
|
||||
t.Fatalf("%s: install command does not carry the minted token", body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user