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
|
|
@ -29,35 +29,35 @@ type RegistrationTokenQueries struct {
|
|||
}
|
||||
|
||||
type RegistrationToken struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TokenHash string `json:"-" db:"token_hash"`
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TokenHash string `json:"-" db:"token_hash"`
|
||||
// TokenEncrypted is the AES-256-GCM ciphertext (nonce||ct) of the plaintext
|
||||
// token. Never serialised; decrypted into Token for live tokens only.
|
||||
TokenEncrypted []byte `json:"-" db:"token_encrypted"`
|
||||
TokenEncrypted []byte `json:"-" db:"token_encrypted"`
|
||||
// Token is the decrypted plaintext, populated only for live tokens so the
|
||||
// operator UI can rebuild the install one-liner. Empty for spent/expired/
|
||||
// revoked tokens and for legacy one-way-hashed tokens.
|
||||
Token string `json:"token,omitempty" db:"-"`
|
||||
Label *string `json:"label" db:"label"`
|
||||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UsedAt *time.Time `json:"used_at" db:"used_at"`
|
||||
UsedByAgentID *uuid.UUID `json:"used_by_agent_id" db:"used_by_agent_id"`
|
||||
Revoked bool `json:"revoked" db:"revoked"`
|
||||
RevokedAt *time.Time `json:"revoked_at" db:"revoked_at"`
|
||||
RevokedReason *string `json:"revoked_reason" db:"revoked_reason"`
|
||||
Status string `json:"status" db:"status"`
|
||||
CreatedBy string `json:"created_by" db:"created_by"`
|
||||
Metadata json.RawMessage `json:"metadata" db:"metadata"`
|
||||
MaxSeats int `json:"max_seats" db:"max_seats"`
|
||||
SeatsUsed int `json:"seats_used" db:"seats_used"`
|
||||
Token string `json:"token,omitempty" db:"-"`
|
||||
Label *string `json:"label" db:"label"`
|
||||
ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UsedAt *time.Time `json:"used_at" db:"used_at"`
|
||||
UsedByAgentID *uuid.UUID `json:"used_by_agent_id" db:"used_by_agent_id"`
|
||||
Revoked bool `json:"revoked" db:"revoked"`
|
||||
RevokedAt *time.Time `json:"revoked_at" db:"revoked_at"`
|
||||
RevokedReason *string `json:"revoked_reason" db:"revoked_reason"`
|
||||
Status string `json:"status" db:"status"`
|
||||
CreatedBy string `json:"created_by" db:"created_by"`
|
||||
Metadata json.RawMessage `json:"metadata" db:"metadata"`
|
||||
MaxSeats int `json:"max_seats" db:"max_seats"`
|
||||
SeatsUsed int `json:"seats_used" db:"seats_used"`
|
||||
// TOTPSeedEncrypted is the TOTP seed for fleet-join 2FA (SEC-025), stored
|
||||
// as AES-256-GCM ciphertext (nonce||ct) of the base32 seed. Nil for
|
||||
// standard registration tokens; set only when the operator creates a
|
||||
// fleet-join token that requires the standalone host to prove possession
|
||||
// of the seed via a TOTP code. Never serialised; decrypted only at join
|
||||
// validation. The join request carries the code, never the seed.
|
||||
TOTPSeedEncrypted []byte `json:"-" db:"totp_seed_encrypted"`
|
||||
TOTPSeedEncrypted []byte `json:"-" db:"totp_seed_encrypted"`
|
||||
}
|
||||
|
||||
// isLive reports whether a token is still usable for enrollment — the only state
|
||||
|
|
@ -81,17 +81,17 @@ func (q *RegistrationTokenQueries) revealPlaintext(t *RegistrationToken) {
|
|||
}
|
||||
|
||||
type TokenRequest struct {
|
||||
Label string `json:"label"`
|
||||
ExpiresIn string `json:"expires_in"` // e.g., "24h", "7d"
|
||||
MaxSeats int `json:"max_seats"` // Number of agents that can use this token (default: 1)
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
Label string `json:"label"`
|
||||
ExpiresIn string `json:"expires_in"` // e.g., "24h", "7d"
|
||||
MaxSeats int `json:"max_seats"` // Number of agents that can use this token (default: 1)
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
Label string `json:"label"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
InstallCommand string `json:"install_command"`
|
||||
Token string `json:"token"`
|
||||
Label string `json:"label"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
InstallCommand string `json:"install_command"`
|
||||
}
|
||||
|
||||
// NewRegistrationTokenQueries builds the query handle. encKeyB64 is the base64
|
||||
|
|
@ -205,6 +205,45 @@ func (q *RegistrationTokenQueries) ValidateRegistrationToken(token string) (*Reg
|
|||
return ®Token, nil
|
||||
}
|
||||
|
||||
// GetFleetJoinToken resolves the raw join secret even after its one seat was
|
||||
// consumed. The fleet-join handler uses that spent state only to recover the
|
||||
// same Agent ID after a response was lost; standard enrollment never calls it.
|
||||
func (q *RegistrationTokenQueries) GetFleetJoinToken(token string) (*RegistrationToken, error) {
|
||||
var regToken RegistrationToken
|
||||
query := `
|
||||
SELECT id, token_hash, label, expires_at, created_at, used_at, used_by_agent_id,
|
||||
revoked, revoked_at, revoked_reason, status, created_by, metadata,
|
||||
max_seats, seats_used, totp_seed_encrypted
|
||||
FROM registration_tokens
|
||||
WHERE token_hash = $1
|
||||
AND status IN ('active', 'used')
|
||||
AND NOT revoked
|
||||
AND expires_at > NOW()
|
||||
AND totp_seed_encrypted IS NOT NULL
|
||||
`
|
||||
if err := q.db.Get(®Token, query, HashRegistrationToken(token)); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("invalid, expired, or revoked fleet join token")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to resolve fleet join token: %w", err)
|
||||
}
|
||||
return ®Token, nil
|
||||
}
|
||||
|
||||
func (q *RegistrationTokenQueries) FleetJoinTokenUsedBy(tokenID, agentID uuid.UUID) (bool, error) {
|
||||
var used bool
|
||||
err := q.db.Get(&used, `
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM registration_token_usage
|
||||
WHERE token_id = $1 AND agent_id = $2
|
||||
)
|
||||
`, tokenID, agentID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to resolve fleet join token use: %w", err)
|
||||
}
|
||||
return used, nil
|
||||
}
|
||||
|
||||
// MarkTokenUsed marks a token as used by an agent.
|
||||
// The caller passes the plaintext token; this function hashes it before calling
|
||||
// the stored procedure (which now matches on token_hash).
|
||||
|
|
@ -448,4 +487,4 @@ func (q *RegistrationTokenQueries) GetTokenUsageStats() (map[string]int, error)
|
|||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue