mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Adds a provider-hosted MSP proof command that exercises provider bootstrap, workspace creation, hosted tenant install-token generation, handoff exchange, setup-facts visibility, and cross-tenant token isolation.
175 lines
6.4 KiB
Go
175 lines
6.4 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type hostedTenantAgentInstallCommandResponse struct {
|
|
OrgID string `json:"orgId"`
|
|
Command string `json:"command"`
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
type HostedTenantAgentInstallCommandOptions struct {
|
|
Config *config.Config
|
|
Persistence *config.ConfigPersistence
|
|
MultiTenant *config.MultiTenantPersistence
|
|
TenantMonitor *monitoring.MultiTenantMonitor
|
|
HostedMode bool
|
|
OrgID string
|
|
InstallType string
|
|
OwnerUserID string
|
|
BaseURL string
|
|
}
|
|
|
|
type HostedTenantAgentInstallCommandResult struct {
|
|
OrgID string
|
|
InstallType string
|
|
Command string
|
|
Token string
|
|
TokenID string
|
|
}
|
|
|
|
var (
|
|
ErrHostedTenantInstallRequiresHostedMode = errors.New("hosted tenant agent install command requires hosted mode")
|
|
ErrHostedTenantInstallMissingOrgID = errors.New("hosted tenant agent install command requires org id")
|
|
ErrHostedTenantInstallInvalidOrg = errors.New("hosted tenant agent install command org does not exist")
|
|
ErrHostedTenantInstallMissingBaseURL = errors.New("hosted tenant agent install command requires base url")
|
|
)
|
|
|
|
func GenerateHostedTenantAgentInstallCommand(opts HostedTenantAgentInstallCommandOptions) (*HostedTenantAgentInstallCommandResult, error) {
|
|
if !opts.HostedMode {
|
|
return nil, ErrHostedTenantInstallRequiresHostedMode
|
|
}
|
|
orgID := strings.TrimSpace(opts.OrgID)
|
|
if orgID == "" {
|
|
return nil, ErrHostedTenantInstallMissingOrgID
|
|
}
|
|
if opts.MultiTenant == nil || !opts.MultiTenant.OrgExists(orgID) {
|
|
return nil, ErrHostedTenantInstallInvalidOrg
|
|
}
|
|
installType, err := normalizeProxmoxInstallType(opts.InstallType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
baseURL := strings.TrimRight(strings.TrimSpace(opts.BaseURL), "/")
|
|
if baseURL == "" {
|
|
return nil, ErrHostedTenantInstallMissingBaseURL
|
|
}
|
|
|
|
tokenName := fmt.Sprintf("cloud-tenant-agent-%s-%s-%d", orgID, installType, time.Now().UTC().Unix())
|
|
rawToken, record, err := issueAndPersistAgentInstallToken(opts.Config, opts.Persistence, issueAgentInstallTokenOptions{
|
|
TokenName: tokenName,
|
|
OrgID: orgID,
|
|
OwnerUserID: strings.TrimSpace(opts.OwnerUserID),
|
|
Metadata: map[string]string{
|
|
"install_type": installType,
|
|
"issued_via": "hosted_agent_install_command",
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the tenant monitor is already initialized, ensure it sees the new token immediately.
|
|
// If not initialized, future GetMonitor() calls will deep-copy the updated base config.
|
|
config.Mu.Lock()
|
|
if opts.TenantMonitor != nil {
|
|
if m, ok := opts.TenantMonitor.PeekMonitor(orgID); ok && m != nil && m.GetConfig() != nil {
|
|
m.GetConfig().APITokens = append(m.GetConfig().APITokens, *record)
|
|
m.GetConfig().SortAPITokens()
|
|
}
|
|
}
|
|
config.Mu.Unlock()
|
|
|
|
command := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{
|
|
BaseURL: baseURL,
|
|
Token: rawToken,
|
|
InstallType: installType,
|
|
IncludeInstallType: true,
|
|
})
|
|
|
|
tokenID := ""
|
|
if record != nil {
|
|
tokenID = record.ID
|
|
}
|
|
return &HostedTenantAgentInstallCommandResult{
|
|
OrgID: orgID,
|
|
InstallType: installType,
|
|
Command: command,
|
|
Token: rawToken,
|
|
TokenID: tokenID,
|
|
}, nil
|
|
}
|
|
|
|
// handleHostedTenantAgentInstallCommand is a hosted-mode-only control-plane endpoint that generates a
|
|
// tenant-scoped agent install command by minting an org-bound API token.
|
|
func (r *Router) handleHostedTenantAgentInstallCommand(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodPost {
|
|
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed", nil)
|
|
return
|
|
}
|
|
|
|
orgID := strings.TrimSpace(req.PathValue("id"))
|
|
var payload struct {
|
|
Type string `json:"type"` // "pve" or "pbs"
|
|
}
|
|
if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
|
|
writeErrorResponse(w, http.StatusBadRequest, "invalid_request", "Invalid request body", map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
result, err := GenerateHostedTenantAgentInstallCommand(HostedTenantAgentInstallCommandOptions{
|
|
Config: r.config,
|
|
Persistence: r.persistence,
|
|
MultiTenant: r.multiTenant,
|
|
TenantMonitor: r.mtMonitor,
|
|
HostedMode: r.hostedMode,
|
|
OrgID: orgID,
|
|
InstallType: payload.Type,
|
|
OwnerUserID: apiTokenOwnerUserIDForRequest(r.config, req),
|
|
BaseURL: r.resolvePublicURL(req),
|
|
})
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, ErrHostedTenantInstallRequiresHostedMode):
|
|
http.NotFound(w, req)
|
|
case errors.Is(err, ErrHostedTenantInstallMissingOrgID):
|
|
writeErrorResponse(w, http.StatusBadRequest, "missing_org_id", "Organization ID required", nil)
|
|
case errors.Is(err, ErrHostedTenantInstallInvalidOrg):
|
|
writeErrorResponse(w, http.StatusBadRequest, "invalid_org", "Invalid Organization ID", nil)
|
|
case errors.Is(err, ErrHostedTenantInstallMissingBaseURL):
|
|
writeErrorResponse(w, http.StatusInternalServerError, "missing_base_url", "Failed to resolve Pulse URL", nil)
|
|
case errors.Is(err, errAgentInstallTokenGeneration):
|
|
log.Error().Err(err).Msg("Failed to generate hosted tenant agent API token")
|
|
writeErrorResponse(w, http.StatusInternalServerError, "token_generation_failed", "Failed to generate API token", nil)
|
|
case errors.Is(err, errAgentInstallTokenRecord):
|
|
log.Error().Err(err).Str("org_id", orgID).Msg("Failed to construct hosted tenant agent token record")
|
|
writeErrorResponse(w, http.StatusInternalServerError, "token_generation_failed", "Failed to generate token", nil)
|
|
case errors.Is(err, errAgentInstallTokenPersist):
|
|
log.Error().Err(err).Msg("Failed to persist hosted tenant agent token")
|
|
writeErrorResponse(w, http.StatusInternalServerError, "token_persist_failed", "Failed to save token to disk", map[string]string{"error": err.Error()})
|
|
case strings.Contains(err.Error(), "Type must be"):
|
|
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
|
|
default:
|
|
log.Error().Err(err).Msg("Failed to create hosted tenant agent token")
|
|
writeErrorResponse(w, http.StatusInternalServerError, "token_generation_failed", "Failed to generate API token", nil)
|
|
}
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, hostedTenantAgentInstallCommandResponse{
|
|
OrgID: result.OrgID,
|
|
Command: result.Command,
|
|
Token: result.Token,
|
|
})
|
|
}
|