Canonicalize install security helpers

This commit is contained in:
rcourtman
2026-08-21 14:30:13 +01:00
parent c68d5dd3d8
commit e61715462a
3 changed files with 21 additions and 129 deletions
+10 -125
View File
@@ -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 {
+5 -2
View File
@@ -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)",
@@ -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)",