mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Merge pull request #2209 from rcourtman/claude/msp-hosted-install-token
Mint agent install tokens in hosted client workspaces
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`.
|
||||
|
||||
### Org managers reach the agent install path in their own organization
|
||||
|
||||
An owner or admin of the selected organization now receives
|
||||
|
||||
@@ -4560,6 +4560,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.
|
||||
|
||||
### Org managers see org-bound settings, not recovery administration
|
||||
|
||||
`/api/security/status` now grants an owner or admin of the selected
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -25070,3 +25070,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