source: advance the checked projection
Source-Sha: 194a59adfd6c901635da7a9c18253bb2210e2426 Policy-Sha: b9b0be0ee74e55f561a803b8aef6be3c2134f5a83b46816a069a372f2eb04f4d Tree-Digest: 4697f2e281c1c6ecafff17cf6eeb391a0a1ac0fa6e5fc429f5b473f4579c252c
This commit is contained in:
parent
0401c76c79
commit
ffdccba10f
42 changed files with 2516 additions and 554 deletions
|
|
@ -7,10 +7,14 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/server/internal/api/middleware"
|
||||
"github.com/Fimeg/RedFlag/server/internal/config"
|
||||
"github.com/Fimeg/RedFlag/server/internal/database/queries"
|
||||
"github.com/Fimeg/RedFlag/server/internal/models"
|
||||
|
|
@ -20,22 +24,26 @@ import (
|
|||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
const fleetJoinRequestMetadataKey = "redflag_fleet_join_request_id"
|
||||
|
||||
// FleetJoinHandler handles standalone-to-fleet migration (SEC-025).
|
||||
type FleetJoinHandler struct {
|
||||
db *sqlx.DB
|
||||
tokenQueries *queries.RegistrationTokenQueries
|
||||
agentQueries *queries.AgentQueries
|
||||
signingPublicKey string
|
||||
signingPublicKey string
|
||||
signingKeyID string
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewFleetJoinHandler creates a FleetJoinHandler.
|
||||
func NewFleetJoinHandler(db *sqlx.DB, tokenQueries *queries.RegistrationTokenQueries, agentQueries *queries.AgentQueries, signingPublicKey string, cfg *config.Config) *FleetJoinHandler {
|
||||
func NewFleetJoinHandler(db *sqlx.DB, tokenQueries *queries.RegistrationTokenQueries, agentQueries *queries.AgentQueries, signingPublicKey, signingKeyID string, cfg *config.Config) *FleetJoinHandler {
|
||||
return &FleetJoinHandler{
|
||||
db: db,
|
||||
tokenQueries: tokenQueries,
|
||||
agentQueries: agentQueries,
|
||||
signingPublicKey: signingPublicKey,
|
||||
signingKeyID: signingKeyID,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
|
@ -44,27 +52,35 @@ func NewFleetJoinHandler(db *sqlx.DB, tokenQueries *queries.RegistrationTokenQue
|
|||
// seed never crosses this channel — the server holds it encrypted from token
|
||||
// creation and only the 6-digit code travels here.
|
||||
type FleetJoinRequest struct {
|
||||
RegistrationToken string `json:"registration_token" binding:"required"`
|
||||
TOTPCode string `json:"totp_code" binding:"required"`
|
||||
RequestID uuid.UUID `json:"request_id" binding:"required"`
|
||||
AgentID uuid.UUID `json:"agent_id" binding:"required"`
|
||||
RegistrationToken string `json:"registration_token" binding:"required"`
|
||||
TOTPCode string `json:"totp_code" binding:"required"`
|
||||
|
||||
// Standard agent registration fields (same as RegisterAgent).
|
||||
Hostname string `json:"hostname" binding:"required"`
|
||||
OSType string `json:"os_type" binding:"required"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
MachineID string `json:"machine_id"`
|
||||
PublicKeyFingerprint string `json:"public_key_fingerprint"`
|
||||
AvailableScanners []string `json:"available_scanners"`
|
||||
Hostname string `json:"hostname" binding:"required"`
|
||||
OSType string `json:"os_type" binding:"required"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
MachineID string `json:"machine_id"`
|
||||
PublicKeyFingerprint string `json:"public_key_fingerprint"`
|
||||
AvailableScanners []string `json:"available_scanners"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
DeviceType string `json:"device_type"`
|
||||
DeviceModel string `json:"device_model"`
|
||||
OSDistro string `json:"os_distro"`
|
||||
}
|
||||
|
||||
// FleetJoinResponse is the server's response on successful fleet join.
|
||||
type FleetJoinResponse struct {
|
||||
AgentID uuid.UUID `json:"agent_id"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
SigningPublicKey string `json:"signing_public_key"`
|
||||
ServerURL string `json:"server_url"`
|
||||
AgentID uuid.UUID `json:"agent_id"`
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
SigningPublicKey string `json:"signing_public_key"`
|
||||
SigningKeyID string `json:"signing_key_id"`
|
||||
ServerURL string `json:"server_url"`
|
||||
Config map[string]interface{} `json:"config"`
|
||||
}
|
||||
|
||||
// JoinFleet handles POST /api/v1/fleet-join. Validates the registration token
|
||||
|
|
@ -75,9 +91,13 @@ func (h *FleetJoinHandler) JoinFleet(c *gin.Context) {
|
|||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.RequestID == uuid.Nil || req.AgentID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "request_id and agent_id must be UUIDs"})
|
||||
return
|
||||
}
|
||||
|
||||
// Step 1: Validate registration token.
|
||||
tokenInfo, err := h.tokenQueries.ValidateRegistrationToken(req.RegistrationToken)
|
||||
tokenInfo, err := h.tokenQueries.GetFleetJoinToken(req.RegistrationToken)
|
||||
if err != nil || tokenInfo == nil {
|
||||
log.Printf("[SECURITY] [server] [fleet-join] invalid_token error=%v", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired registration token"})
|
||||
|
|
@ -90,6 +110,12 @@ func (h *FleetJoinHandler) JoinFleet(c *gin.Context) {
|
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "this token does not require 2FA — use the standard registration endpoint"})
|
||||
return
|
||||
}
|
||||
var tokenMetadata map[string]interface{}
|
||||
if err := json.Unmarshal(tokenInfo.Metadata, &tokenMetadata); err != nil || tokenMetadata["fleet_join"] != true {
|
||||
log.Printf("[SECURITY] [server] [fleet-join] wrong_token_class token_id=%s", tokenInfo.ID)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "this token is not a fleet join key"})
|
||||
return
|
||||
}
|
||||
|
||||
// Step 3: Validate the TOTP code against the server-held seed. The seed
|
||||
// was stored encrypted at token creation and never crosses this channel;
|
||||
|
|
@ -105,11 +131,49 @@ func (h *FleetJoinHandler) JoinFleet(c *gin.Context) {
|
|||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid TOTP code"})
|
||||
return
|
||||
}
|
||||
recovering, err := h.tokenQueries.FleetJoinTokenUsedBy(tokenInfo.ID, req.AgentID)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [server] [fleet-join] token_usage_lookup_failed request_id=%s error=%q", req.RequestID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "fleet join failed"})
|
||||
return
|
||||
}
|
||||
seatAvailable := tokenInfo.Status == "active" && tokenInfo.SeatsUsed < tokenInfo.MaxSeats
|
||||
if !seatAvailable && !recovering {
|
||||
log.Printf("[SECURITY] [server] [fleet-join] spent_token_wrong_agent request_id=%s agent_id=%s", req.RequestID, req.AgentID)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "fleet join token belongs to another Agent"})
|
||||
return
|
||||
}
|
||||
|
||||
existingByID, idErr := h.agentQueries.GetAgentByID(req.AgentID)
|
||||
if recovering {
|
||||
if idErr != nil || existingByID == nil {
|
||||
log.Printf("[ERROR] [server] [fleet-join] recovery_agent_missing request_id=%s agent_id=%s error=%v", req.RequestID, req.AgentID, idErr)
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "fleet join recovery cannot find the registered Agent"})
|
||||
return
|
||||
}
|
||||
if req.MachineID != "" && (existingByID.MachineID == nil || *existingByID.MachineID != req.MachineID) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "fleet join recovery machine ID mismatch"})
|
||||
return
|
||||
}
|
||||
storedRequestID, _ := existingByID.Metadata[fleetJoinRequestMetadataKey].(string)
|
||||
if storedRequestID == "" || storedRequestID != req.RequestID.String() {
|
||||
log.Printf("[SECURITY] [server] [fleet-join] recovery_request_mismatch request_id=%s agent_id=%s", req.RequestID, req.AgentID)
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "fleet join recovery request does not match the accepted transaction"})
|
||||
return
|
||||
}
|
||||
} else if idErr == nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "Agent ID is already registered"})
|
||||
return
|
||||
} else if !errors.Is(idErr, sql.ErrNoRows) {
|
||||
log.Printf("[ERROR] [server] [fleet-join] agent_lookup_failed request_id=%s error=%q", req.RequestID, idErr)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "fleet join failed"})
|
||||
return
|
||||
}
|
||||
|
||||
// Step 5: Check machine ID isn't already registered (same as RegisterAgent).
|
||||
if req.MachineID != "" {
|
||||
existing, err := h.agentQueries.GetAgentByMachineID(req.MachineID)
|
||||
if err == nil && existing != nil && existing.ID.String() != "" {
|
||||
if err == nil && existing != nil && existing.ID != req.AgentID {
|
||||
c.JSON(http.StatusConflict, gin.H{
|
||||
"error": "machine ID already registered to another agent",
|
||||
"existing_agent_id": existing.ID.String(),
|
||||
|
|
@ -139,9 +203,21 @@ func (h *FleetJoinHandler) JoinFleet(c *gin.Context) {
|
|||
if req.PublicKeyFingerprint != "" {
|
||||
pubKeyFP = &req.PublicKeyFingerprint
|
||||
}
|
||||
deviceType := req.DeviceType
|
||||
if !models.ValidDeviceType(deviceType) {
|
||||
deviceType = "server"
|
||||
}
|
||||
var deviceModel *string
|
||||
if req.DeviceModel != "" {
|
||||
deviceModel = &req.DeviceModel
|
||||
}
|
||||
var osDistro *string
|
||||
if req.OSDistro != "" {
|
||||
osDistro = &req.OSDistro
|
||||
}
|
||||
|
||||
agent := &models.Agent{
|
||||
ID: uuid.Must(uuid.NewV4()),
|
||||
ID: req.AgentID,
|
||||
Hostname: req.Hostname,
|
||||
OSType: req.OSType,
|
||||
OSVersion: req.OSVersion,
|
||||
|
|
@ -150,6 +226,9 @@ func (h *FleetJoinHandler) JoinFleet(c *gin.Context) {
|
|||
CurrentVersion: req.AgentVersion,
|
||||
MachineID: machineID,
|
||||
PublicKeyFingerprint: pubKeyFP,
|
||||
DeviceType: deviceType,
|
||||
DeviceModel: deviceModel,
|
||||
OSDistro: osDistro,
|
||||
LastSeen: time.Now().UTC(),
|
||||
Status: "online",
|
||||
Metadata: models.JSONB{},
|
||||
|
|
@ -160,30 +239,75 @@ func (h *FleetJoinHandler) JoinFleet(c *gin.Context) {
|
|||
agent.Metadata[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
createQuery := `
|
||||
INSERT INTO agents (
|
||||
id, hostname, os_type, os_version, os_architecture,
|
||||
agent_version, current_version, machine_id, public_key_fingerprint,
|
||||
last_seen, status, metadata
|
||||
) VALUES (
|
||||
:id, :hostname, :os_type, :os_version, :os_architecture,
|
||||
:agent_version, :current_version, :machine_id, :public_key_fingerprint,
|
||||
:last_seen, :status, :metadata
|
||||
)`
|
||||
if _, err := tx.NamedExec(createQuery, agent); err != nil {
|
||||
log.Printf("[ERROR] [server] [fleet-join] create_agent_failed error=%q", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to register agent"})
|
||||
return
|
||||
if recovering {
|
||||
for k, v := range existingByID.Metadata {
|
||||
if _, exists := agent.Metadata[k]; !exists {
|
||||
agent.Metadata[k] = v
|
||||
}
|
||||
}
|
||||
if agent.MachineID == nil {
|
||||
agent.MachineID = existingByID.MachineID
|
||||
}
|
||||
if agent.PublicKeyFingerprint == nil {
|
||||
agent.PublicKeyFingerprint = existingByID.PublicKeyFingerprint
|
||||
}
|
||||
if req.DeviceType == "" {
|
||||
agent.DeviceType = existingByID.DeviceType
|
||||
}
|
||||
if agent.DeviceModel == nil {
|
||||
agent.DeviceModel = existingByID.DeviceModel
|
||||
}
|
||||
if agent.OSDistro == nil {
|
||||
agent.OSDistro = existingByID.OSDistro
|
||||
}
|
||||
}
|
||||
agent.Metadata[fleetJoinRequestMetadataKey] = req.RequestID.String()
|
||||
|
||||
// Mark token as used.
|
||||
var tokenSuccess bool
|
||||
tokenHash := queries.HashRegistrationToken(req.RegistrationToken)
|
||||
if err := tx.QueryRow("SELECT mark_registration_token_used($1, $2)", tokenHash, agent.ID).Scan(&tokenSuccess); err != nil || !tokenSuccess {
|
||||
log.Printf("[ERROR] [server] [fleet-join] mark_token_failed error=%v success=%v", err, tokenSuccess)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "registration token could not be consumed"})
|
||||
return
|
||||
if recovering {
|
||||
updateQuery := `
|
||||
UPDATE agents SET
|
||||
hostname = :hostname, os_type = :os_type, os_version = :os_version,
|
||||
os_architecture = :os_architecture, agent_version = :agent_version,
|
||||
current_version = :current_version, machine_id = :machine_id,
|
||||
public_key_fingerprint = :public_key_fingerprint,
|
||||
device_type = :device_type, device_model = :device_model,
|
||||
os_distro = :os_distro, last_seen = :last_seen,
|
||||
status = :status, metadata = :metadata
|
||||
WHERE id = :id`
|
||||
if _, err := tx.NamedExec(updateQuery, agent); err != nil {
|
||||
log.Printf("[ERROR] [server] [fleet-join] recover_agent_failed request_id=%s error=%q", req.RequestID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to recover fleet join"})
|
||||
return
|
||||
}
|
||||
if _, err := tx.Exec("UPDATE refresh_tokens SET revoked = TRUE WHERE agent_id = $1 AND NOT revoked", agent.ID); err != nil {
|
||||
log.Printf("[ERROR] [server] [fleet-join] recover_revoke_tokens_failed request_id=%s error=%q", req.RequestID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to recover fleet join"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
createQuery := `
|
||||
INSERT INTO agents (
|
||||
id, hostname, os_type, os_version, os_architecture,
|
||||
agent_version, current_version, machine_id, public_key_fingerprint,
|
||||
device_type, device_model, os_distro, last_seen, status, metadata
|
||||
) VALUES (
|
||||
:id, :hostname, :os_type, :os_version, :os_architecture,
|
||||
:agent_version, :current_version, :machine_id, :public_key_fingerprint,
|
||||
:device_type, :device_model, :os_distro, :last_seen, :status, :metadata
|
||||
)`
|
||||
if _, err := tx.NamedExec(createQuery, agent); err != nil {
|
||||
log.Printf("[ERROR] [server] [fleet-join] create_agent_failed request_id=%s error=%q", req.RequestID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to register agent"})
|
||||
return
|
||||
}
|
||||
|
||||
var tokenSuccess bool
|
||||
tokenHash := queries.HashRegistrationToken(req.RegistrationToken)
|
||||
if err := tx.QueryRow("SELECT mark_registration_token_used($1, $2)", tokenHash, agent.ID).Scan(&tokenSuccess); err != nil || !tokenSuccess {
|
||||
log.Printf("[ERROR] [server] [fleet-join] mark_token_failed request_id=%s error=%v success=%v", req.RequestID, err, tokenSuccess)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "registration token could not be consumed"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate refresh token.
|
||||
|
|
@ -202,36 +326,53 @@ func (h *FleetJoinHandler) JoinFleet(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Create subsystems for available scanners.
|
||||
if len(req.AvailableScanners) > 0 {
|
||||
for _, scanner := range req.AvailableScanners {
|
||||
// Initial join creates the same observed scanner rows as registration.
|
||||
if !recovering {
|
||||
scanners := append([]string{}, req.AvailableScanners...)
|
||||
scanners = append(scanners, "storage", "system")
|
||||
for _, scanner := range scanners {
|
||||
interval := 60
|
||||
if scanner == "storage" || scanner == "system" {
|
||||
interval = 5
|
||||
}
|
||||
sub := models.AgentSubsystem{
|
||||
AgentID: agent.ID,
|
||||
Subsystem: scanner,
|
||||
Enabled: true,
|
||||
AutoRun: true,
|
||||
IntervalMinutes: 60,
|
||||
IntervalMinutes: interval,
|
||||
}
|
||||
subQuery := `INSERT INTO agent_subsystems (agent_id, subsystem, enabled, auto_run, interval_minutes) VALUES (:agent_id, :subsystem, :enabled, :auto_run, :interval_minutes)`
|
||||
subQuery := `INSERT INTO agent_subsystems (agent_id, subsystem, enabled, auto_run, interval_minutes) VALUES (:agent_id, :subsystem, :enabled, :auto_run, :interval_minutes) ON CONFLICT (agent_id, subsystem) DO NOTHING`
|
||||
if _, err := tx.NamedExec(subQuery, sub); err != nil {
|
||||
log.Printf("[WARNING] [server] [fleet-join] create_subsystem_failed scanner=%s error=%q", scanner, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
accessToken, err := middleware.GenerateAgentToken(agent.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate access token"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.Printf("[ERROR] [server] [fleet-join] commit_failed error=%q", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "fleet join failed"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[INFO] [server] [fleet-join] agent_registered agent_id=%s hostname=%s machine_id=%s",
|
||||
agent.ID, agent.Hostname, req.MachineID)
|
||||
log.Printf("[INFO] [server] [fleet-join] completed request_id=%s agent_id=%s hostname=%s recovered=%v",
|
||||
req.RequestID, agent.ID, agent.Hostname, recovering)
|
||||
|
||||
c.JSON(http.StatusCreated, FleetJoinResponse{
|
||||
AgentID: agent.ID,
|
||||
Token: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
SigningPublicKey: h.signingPublicKey,
|
||||
SigningKeyID: h.signingKeyID,
|
||||
ServerURL: resolveServerURL(c, h.config, "fleet-join"),
|
||||
Config: map[string]interface{}{
|
||||
"check_in_interval": h.config.CheckInInterval,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/server/internal/config"
|
||||
|
|
@ -44,10 +45,10 @@ func NewRegistrationTokenHandler(tokenQueries *queries.RegistrationTokenQueries,
|
|||
// GenerateRegistrationToken creates a new registration token
|
||||
func (h *RegistrationTokenHandler) GenerateRegistrationToken(c *gin.Context) {
|
||||
var request struct {
|
||||
Label string `json:"label" binding:"required"`
|
||||
ExpiresIn string `json:"expires_in"` // e.g., "24h", "7d", "168h"
|
||||
MaxSeats int `json:"max_seats"` // Number of agents that can use this token
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
Label string `json:"label" binding:"required"`
|
||||
ExpiresIn string `json:"expires_in"` // e.g., "24h", "7d", "168h"
|
||||
MaxSeats int `json:"max_seats"` // Number of agents that can use this token
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
// FleetJoin + TOTPSeed: SEC-025 fleet-join 2FA. When the operator
|
||||
// creates a fleet-join token, they enter the host's TOTP seed
|
||||
// (displayed on the standalone host). This is the one admin-
|
||||
|
|
@ -110,18 +111,23 @@ func (h *RegistrationTokenHandler) GenerateRegistrationToken(c *gin.Context) {
|
|||
}
|
||||
metadata["server_url"] = resolveServerURL(c, h.config, "registration-tokens")
|
||||
metadata["expires_in"] = expiresIn
|
||||
metadata["fleet_join"] = request.FleetJoin
|
||||
|
||||
// Default max_seats to 1 if not provided or invalid
|
||||
maxSeats := request.MaxSeats
|
||||
if maxSeats < 1 {
|
||||
maxSeats = 1
|
||||
}
|
||||
if request.FleetJoin {
|
||||
maxSeats = 1
|
||||
}
|
||||
|
||||
// Store token in database. For fleet-join tokens (SEC-025), the operator
|
||||
// provides the TOTP seed from the standalone host; it must be valid
|
||||
// base32 or neither an authenticator nor join validation can use it.
|
||||
var totpSeed string
|
||||
if request.FleetJoin {
|
||||
request.TOTPSeed = strings.ToUpper(strings.TrimSpace(request.TOTPSeed))
|
||||
if request.TOTPSeed == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "fleet-join tokens require a TOTP seed"})
|
||||
return
|
||||
|
|
@ -145,6 +151,9 @@ func (h *RegistrationTokenHandler) GenerateRegistrationToken(c *gin.Context) {
|
|||
// SEC-002: token travels in a header, never the URL — query strings land in
|
||||
// shell history, process lists, and access logs.
|
||||
installCommand := fmt.Sprintf("curl -sfL -H \"X-Registration-Token: %s\" \"%s/api/v1/install/linux\" | sudo bash", token, serverURL)
|
||||
if request.FleetJoin {
|
||||
installCommand = ""
|
||||
}
|
||||
|
||||
response := gin.H{
|
||||
"token": token,
|
||||
|
|
@ -152,6 +161,7 @@ func (h *RegistrationTokenHandler) GenerateRegistrationToken(c *gin.Context) {
|
|||
"expires_at": expiresAt,
|
||||
"install_command": installCommand,
|
||||
"metadata": metadata,
|
||||
"fleet_join": request.FleetJoin,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
|
|
|
|||
Loading…
Reference in a new issue