mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
231 lines
6.8 KiB
Go
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
|
|
}
|