mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
87ee3cd449
Project freshly minted installer command intent onto the stable host before returning remote config, and consume that intent once so later admin policy changes remain authoritative.
227 lines
6.7 KiB
Go
227 lines
6.7 KiB
Go
package configapi
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/api/agenttokens"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/api/apicontext"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
internalauth "github.com/rcourtman/pulse-go-rewrite/pkg/auth"
|
|
)
|
|
|
|
const (
|
|
OrgIDContextKey = apicontext.OrgIDContextKey
|
|
agentInstallIssuedViaConfig = "config_agent_install_command"
|
|
agentInstallIssuedViaHosted = "hosted_agent_install_command"
|
|
agentInstallTypeHost = "host"
|
|
agentInstallCommandPolicyIntentKey = agenttokens.CommandPolicyIntentMetadataKey
|
|
)
|
|
|
|
var (
|
|
errAgentInstallTokenGeneration = agenttokens.ErrGeneration
|
|
errAgentInstallTokenRecord = agenttokens.ErrRecord
|
|
errAgentInstallTokenPersist = agenttokens.ErrPersist
|
|
)
|
|
|
|
type GuestMetadataReloader interface {
|
|
Reload(context.Context) error
|
|
}
|
|
|
|
type RuntimeDependencies struct {
|
|
AuditEvent func(orgID, event, user, ip, path string, success bool, details string)
|
|
ClientIP func(*http.Request) string
|
|
AuthUsername func(*config.Config, *http.Request) string
|
|
TokenOwnerUserID func(*config.Config, *http.Request) string
|
|
AuthConfigured func(*config.Config) bool
|
|
ResolvePublicURL func(*http.Request, *config.Config, bool) string
|
|
}
|
|
|
|
func defaultRuntimeDependencies() RuntimeDependencies {
|
|
return RuntimeDependencies{
|
|
AuditEvent: func(string, string, string, string, string, bool, string) {},
|
|
ClientIP: func(r *http.Request) string {
|
|
if r == nil {
|
|
return ""
|
|
}
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err == nil {
|
|
return host
|
|
}
|
|
return strings.Trim(r.RemoteAddr, "[]")
|
|
},
|
|
AuthUsername: func(_ *config.Config, r *http.Request) string {
|
|
if r == nil {
|
|
return ""
|
|
}
|
|
return internalauth.GetUser(r.Context())
|
|
},
|
|
TokenOwnerUserID: func(_ *config.Config, r *http.Request) string {
|
|
if r == nil {
|
|
return ""
|
|
}
|
|
return internalauth.GetUser(r.Context())
|
|
},
|
|
AuthConfigured: func(cfg *config.Config) bool {
|
|
return cfg != nil && ((strings.TrimSpace(cfg.AuthUser) != "" && strings.TrimSpace(cfg.AuthPass) != "") ||
|
|
cfg.HasAPITokens() || strings.TrimSpace(cfg.ProxyAuthSecret) != "")
|
|
},
|
|
ResolvePublicURL: defaultPublicURL,
|
|
}
|
|
}
|
|
|
|
func (h *ConfigHandlers) SetRuntimeDependencies(deps RuntimeDependencies) {
|
|
defaults := defaultRuntimeDependencies()
|
|
if deps.AuditEvent == nil {
|
|
deps.AuditEvent = defaults.AuditEvent
|
|
}
|
|
if deps.ClientIP == nil {
|
|
deps.ClientIP = defaults.ClientIP
|
|
}
|
|
if deps.AuthUsername == nil {
|
|
deps.AuthUsername = defaults.AuthUsername
|
|
}
|
|
if deps.TokenOwnerUserID == nil {
|
|
deps.TokenOwnerUserID = defaults.TokenOwnerUserID
|
|
}
|
|
if deps.AuthConfigured == nil {
|
|
deps.AuthConfigured = defaults.AuthConfigured
|
|
}
|
|
if deps.ResolvePublicURL == nil {
|
|
deps.ResolvePublicURL = defaults.ResolvePublicURL
|
|
}
|
|
h.stateMu.Lock()
|
|
h.runtime = deps
|
|
h.stateMu.Unlock()
|
|
}
|
|
|
|
type apiTokenDTO struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Prefix string `json:"prefix"`
|
|
Suffix string `json:"suffix"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
|
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
|
Scopes []string `json:"scopes"`
|
|
OwnerUserID string `json:"ownerUserId,omitempty"`
|
|
}
|
|
|
|
type issueAgentInstallTokenOptions = agenttokens.IssueOptions
|
|
|
|
func issueAndPersistAgentInstallToken(cfg *config.Config, persistence *config.ConfigPersistence, opts issueAgentInstallTokenOptions) (string, *config.APITokenRecord, error) {
|
|
return agenttokens.IssueAndPersist(cfg, persistence, opts)
|
|
}
|
|
|
|
func hostAgentInstallScopes(enableCommands bool) []string {
|
|
return agenttokens.HostScopes(enableCommands)
|
|
}
|
|
|
|
func agentInstallCommandPolicyIntent(enableCommands bool) string {
|
|
return agenttokens.CommandPolicyIntent(enableCommands)
|
|
}
|
|
|
|
func (h *ConfigHandlers) authConfiguredForAgentLifecycle(cfg *config.Config) bool {
|
|
return h.runtimeDependencies().AuthConfigured(cfg)
|
|
}
|
|
|
|
func (h *ConfigHandlers) apiTokenOwnerUserIDForRequest(cfg *config.Config, r *http.Request) string {
|
|
return h.runtimeDependencies().TokenOwnerUserID(cfg, r)
|
|
}
|
|
|
|
func toAPITokenDTO(record config.APITokenRecord) apiTokenDTO {
|
|
return apiTokenDTO{
|
|
ID: record.ID,
|
|
Name: record.Name,
|
|
Prefix: record.Prefix,
|
|
Suffix: record.Suffix,
|
|
CreatedAt: record.CreatedAt,
|
|
LastUsedAt: record.LastUsedAt,
|
|
ExpiresAt: record.ExpiresAt,
|
|
Scopes: append([]string{}, record.Scopes...),
|
|
OwnerUserID: agenttokens.OwnerUserID(record),
|
|
}
|
|
}
|
|
|
|
func (h *ConfigHandlers) resolveConfigAgentInstallBaseURL(req *http.Request, cfg *config.Config) string {
|
|
return h.runtimeDependencies().ResolvePublicURL(req, cfg, h.hostedMode)
|
|
}
|
|
|
|
func writeConfigAgentInstallBaseURLUnavailable(w http.ResponseWriter) {
|
|
http.Error(w, "A valid external Pulse URL is required", http.StatusServiceUnavailable)
|
|
}
|
|
|
|
func defaultPublicURL(req *http.Request, cfg *config.Config, hostedMode bool) string {
|
|
if cfg == nil {
|
|
return ""
|
|
}
|
|
if agentURL := strings.TrimSpace(cfg.AgentConnectURL); agentURL != "" {
|
|
return strings.TrimRight(agentURL, "/")
|
|
}
|
|
publicURL := strings.TrimSpace(cfg.PublicURL)
|
|
if publicURL != "" && (!cfg.PublicURLAutoDetected || hostedMode) {
|
|
return strings.TrimRight(publicURL, "/")
|
|
}
|
|
if hostedMode {
|
|
return ""
|
|
}
|
|
if req != nil && strings.TrimSpace(req.Host) != "" {
|
|
scheme := "http"
|
|
if req.TLS != nil {
|
|
scheme = "https"
|
|
}
|
|
return scheme + "://" + strings.TrimSpace(req.Host)
|
|
}
|
|
if publicURL != "" {
|
|
return strings.TrimRight(publicURL, "/")
|
|
}
|
|
if cfg.FrontendPort > 0 {
|
|
return "http://localhost:" + fmt.Sprint(cfg.FrontendPort)
|
|
}
|
|
return "http://localhost:7655"
|
|
}
|
|
|
|
func (h *ConfigHandlers) runtimeDependencies() RuntimeDependencies {
|
|
h.stateMu.RLock()
|
|
deps := h.runtime
|
|
h.stateMu.RUnlock()
|
|
return deps
|
|
}
|
|
|
|
func safePrefixForLog(value string, n int) string {
|
|
if n <= 0 || len(value) <= n {
|
|
return value
|
|
}
|
|
return value[:n]
|
|
}
|
|
|
|
func explicitAPITokenFromRequest(r *http.Request) (string, bool) {
|
|
if r == nil {
|
|
return "", false
|
|
}
|
|
if values := r.Header.Values("X-API-Token"); len(values) > 0 {
|
|
return strings.TrimSpace(values[0]), true
|
|
}
|
|
if header := r.Header.Get("Authorization"); strings.HasPrefix(strings.ToLower(header), "bearer ") {
|
|
return strings.TrimSpace(header[7:]), true
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(r.Header.Get("Upgrade")), "websocket") {
|
|
if values, ok := r.URL.Query()["token"]; ok && len(values) > 0 {
|
|
return strings.TrimSpace(values[0]), true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func normalizeProxmoxInstallType(raw string) (string, error) {
|
|
installType := strings.ToLower(strings.TrimSpace(raw))
|
|
if installType != "pve" && installType != "pbs" {
|
|
return "", fmt.Errorf("Type must be 'pve' or 'pbs'")
|
|
}
|
|
return installType, nil
|
|
}
|