Files
pulse/internal/api/configapi/dependencies.go
T
2026-08-29 22:12:41 +01:00

231 lines
6.8 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 proxmoxAgentInstallScopes(enableCommands bool) []string {
return agenttokens.ProxmoxScopes(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
}