From e61715462a8a19c3dc4a3e6a9d37fcffa3cb55b6 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 21 Aug 2026 14:30:13 +0100 Subject: [PATCH] Canonicalize install security helpers --- internal/api/agent_install_command_shared.go | 135 ++---------------- internal/api/contract_test.go | 7 +- .../api/identity_invariant_contract_test.go | 8 +- 3 files changed, 21 insertions(+), 129 deletions(-) diff --git a/internal/api/agent_install_command_shared.go b/internal/api/agent_install_command_shared.go index 5448583ea..2bcc54b37 100644 --- a/internal/api/agent_install_command_shared.go +++ b/internal/api/agent_install_command_shared.go @@ -1,16 +1,14 @@ package api import ( - "errors" "fmt" "net/http" "net/url" "strings" - "time" + "github.com/rcourtman/pulse-go-rewrite/internal/api/agenttokens" "github.com/rcourtman/pulse-go-rewrite/internal/api/configapi" "github.com/rcourtman/pulse-go-rewrite/internal/config" - internalauth "github.com/rcourtman/pulse-go-rewrite/pkg/auth" ) const ( @@ -23,9 +21,9 @@ const ( ) var ( - errAgentInstallTokenGeneration = errors.New("agent install token generation failed") - errAgentInstallTokenRecord = errors.New("agent install token record failed") - errAgentInstallTokenPersist = errors.New("agent install token persistence failed") + errAgentInstallTokenGeneration = agenttokens.ErrGeneration + errAgentInstallTokenRecord = agenttokens.ErrRecord + errAgentInstallTokenPersist = agenttokens.ErrPersist ) func normalizeProxmoxInstallType(raw string) (string, error) { @@ -37,12 +35,7 @@ func normalizeProxmoxInstallType(raw string) (string, error) { } func proxmoxAgentInstallScopes() []string { - return []string{ - config.ScopeAgentReport, - config.ScopeAgentConfigRead, - config.ScopeAgentManage, - config.ScopeAgentExec, - } + return agenttokens.ProxmoxScopes() } // hostAgentInstallScopes returns the scopes for a generic unified host agent @@ -50,87 +43,16 @@ func proxmoxAgentInstallScopes() []string { // command execution, because the token is minted before the agent enrols and // scopes cannot be upgraded on an existing token. func hostAgentInstallScopes(enableCommands bool) []string { - scopes := []string{ - config.ScopeAgentReport, - config.ScopeAgentConfigRead, - config.ScopeAgentManage, - config.ScopeDockerReport, - config.ScopeKubernetesReport, - } - if enableCommands { - scopes = append(scopes, config.ScopeAgentExec) - } - return scopes + return agenttokens.HostScopes(enableCommands) } -type issueAgentInstallTokenOptions struct { - TokenName string - OrgID string - OwnerUserID string - Metadata map[string]string - // Scopes overrides the default Proxmox install scope set when non-empty - // (used by the generic host agent flow, which honours the operator's - // command-execution choice instead of always granting exec). - Scopes []string -} +type issueAgentInstallTokenOptions = agenttokens.IssueOptions func issueAndPersistAgentInstallToken(cfg *config.Config, persistence *config.ConfigPersistence, opts issueAgentInstallTokenOptions) (string, *config.APITokenRecord, error) { - if cfg == nil { - return "", nil, fmt.Errorf("config is required") - } - - rawToken, err := internalauth.GenerateAPIToken() - if err != nil { - return "", nil, fmt.Errorf("%w: %w", errAgentInstallTokenGeneration, err) - } - - scopes := opts.Scopes - if len(scopes) == 0 { - scopes = proxmoxAgentInstallScopes() - } - record, err := config.NewAPITokenRecord(rawToken, opts.TokenName, scopes) - if err != nil { - return "", nil, fmt.Errorf("%w: %w", errAgentInstallTokenRecord, err) - } - - record.OrgID = strings.TrimSpace(opts.OrgID) - setAPITokenOwnerUserID(record, opts.OwnerUserID) - if err := mergeAPITokenMetadata(record, opts.Metadata); err != nil { - return "", nil, fmt.Errorf("%w: %w", errAgentInstallTokenRecord, err) - } - - // Install tokens are minted without an expiry because the agent reports - // with them for the life of the install. Stamp the mint time so the - // one-shot Proxmox bootstrap grant they carry can expire on its own clock - // (proxmoxInstallBootstrapGrantTTL) instead of staying live forever. - if record.Metadata == nil { - record.Metadata = make(map[string]string) - } - record.Metadata[agentInstallTokenIssuedAtKey] = record.CreatedAt.UTC().Format(time.RFC3339) - - config.Mu.Lock() - defer config.Mu.Unlock() - - cfg.APITokens = append(cfg.APITokens, *record) - cfg.SortAPITokens() - if persistence != nil { - if err := persistence.SaveAPITokens(cfg.APITokens); err != nil { - cfg.APITokens = cfg.APITokens[:len(cfg.APITokens)-1] - return "", nil, fmt.Errorf("%w: %w", errAgentInstallTokenPersist, err) - } - } - - return rawToken, record, nil + return agenttokens.IssueAndPersist(cfg, persistence, opts) } -type agentInstallCommandOptions struct { - BaseURL string - Token string - InstallType string - IncludeInstallType bool - EnableCommands bool - Insecure bool -} +type agentInstallCommandOptions = configapi.AgentInstallCommandOptions type setupScriptInstallArtifact = configapi.SetupScriptInstallArtifact @@ -175,44 +97,7 @@ func withPrivilegeEscalation(command string) string { } func buildProxmoxAgentInstallCommand(opts agentInstallCommandOptions) string { - baseURL := normalizeAgentInstallBaseURL(opts.BaseURL) - installScriptURL := baseURL + "/install.sh" - curlFlags := "-fsSL" - if opts.Insecure { - curlFlags = "-kfsSL" - } - token := strings.TrimSpace(opts.Token) - tokenSetup := "" - tokenArg := "" - tokenCleanup := "" - if token != "" { - tokenSetup = fmt.Sprintf(`token_file=$(mktemp) && chmod 600 "$token_file" && printf %%s %s > "$token_file" && `, posixShellQuote(token)) - tokenArg = ` \ - --token-file "$token_file"` - tokenCleanup = `; rc=$?; rm -f "$token_file"; exit $rc` - } - command := fmt.Sprintf(`%scurl %s %s | bash -s -- \ - --url %s \ - --enable-proxmox`, - tokenSetup, curlFlags, posixShellQuote(installScriptURL), posixShellQuote(baseURL)) - command += tokenArg - - if opts.Insecure || installBaseURLRequiresInsecure(baseURL) { - command += ` \ - --insecure` - } - - if opts.IncludeInstallType { - command += fmt.Sprintf(` \ - --proxmox-type %s`, posixShellQuote(opts.InstallType)) - } - - if opts.EnableCommands { - command += ` \ - --enable-commands` - } - - return withPrivilegeEscalation(command) + tokenCleanup + return configapi.BuildProxmoxAgentInstallCommand(opts) } func containerRuntimeAgentScopes(enableHost bool) []string { diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go index ee17aa458..5d7162b46 100644 --- a/internal/api/contract_test.go +++ b/internal/api/contract_test.go @@ -1700,9 +1700,12 @@ func TestContract_APITokenOwnerBindingCoversSharedMintingPaths(t *testing.T) { "reserved token metadata key", }, "agent_install_command_shared.go": { + "return agenttokens.IssueAndPersist(cfg, persistence, opts)", + }, + "agenttokens/install.go": { "OwnerUserID string", - "setAPITokenOwnerUserID(record, opts.OwnerUserID)", - "mergeAPITokenMetadata(record, opts.Metadata)", + "setOwnerUserID(record, opts.OwnerUserID)", + "mergeMetadata(record, opts.Metadata)", }, "deploy_handlers.go": { "setAPITokenOwnerUserID(record, ownerUserID)", diff --git a/internal/api/identity_invariant_contract_test.go b/internal/api/identity_invariant_contract_test.go index e865d4eb4..a64eaef7a 100644 --- a/internal/api/identity_invariant_contract_test.go +++ b/internal/api/identity_invariant_contract_test.go @@ -30,6 +30,7 @@ func TestContract_HostedIdentityUsesStablePrincipals(t *testing.T) { "security_tokens.go": readIdentityContractFile(t, "security_tokens.go"), "org_handlers.go": readIdentityContractFile(t, "org_handlers.go"), "agent_install_command_shared.go": readIdentityContractFile(t, "agent_install_command_shared.go"), + "agenttokens/install.go": readIdentityContractFile(t, "agenttokens/install.go"), "deploy_handlers.go": readIdentityContractFile(t, "deploy_handlers.go"), "router.go": readIdentityContractFile(t, "router.go"), "security_setup_fix.go": readIdentityContractFile(t, "security_setup_fix.go"), @@ -92,9 +93,12 @@ func TestContract_HostedIdentityUsesStablePrincipals(t *testing.T) { "apiTokenOwnerUserIDForRequest", }, "agent_install_command_shared.go": { + "return agenttokens.IssueAndPersist(cfg, persistence, opts)", + }, + "agenttokens/install.go": { "OwnerUserID string", - "setAPITokenOwnerUserID(record, opts.OwnerUserID)", - "mergeAPITokenMetadata(record, opts.Metadata)", + "setOwnerUserID(record, opts.OwnerUserID)", + "mergeMetadata(record, opts.Metadata)", }, "deploy_handlers.go": { "setAPITokenOwnerUserID(record, ownerUserID)",