92 lines
2.2 KiB
Go
92 lines
2.2 KiB
Go
package fleet
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// PolicySnapshot is a shareable frozen policy bundle.
|
|
type PolicySnapshot struct {
|
|
Token string `json:"token"`
|
|
PolicyJSON json.RawMessage `json:"policy_json"`
|
|
ExpiresAt string `json:"expires_at,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
// CreatePolicySnapshot stores a policy JSON blob and returns an opaque token.
|
|
func (s *Store) CreatePolicySnapshot(policy map[string]any, ttl time.Duration) (*PolicySnapshot, error) {
|
|
raw, err := json.Marshal(policy)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal policy: %w", err)
|
|
}
|
|
|
|
token, err := randomToken(16)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
expires := ""
|
|
if ttl > 0 {
|
|
expires = now.Add(ttl).Format(time.RFC3339)
|
|
}
|
|
|
|
_, err = s.db.Exec(`
|
|
INSERT INTO policy_snapshots (token, policy_json, expires_at, created_at)
|
|
VALUES (?, ?, ?, ?)
|
|
`, token, string(raw), nullIfEmpty(expires), now.Format(time.RFC3339))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert policy snapshot: %w", err)
|
|
}
|
|
|
|
return &PolicySnapshot{
|
|
Token: token,
|
|
PolicyJSON: raw,
|
|
ExpiresAt: expires,
|
|
CreatedAt: now.Format(time.RFC3339),
|
|
}, nil
|
|
}
|
|
|
|
// GetPolicySnapshot loads a snapshot by token (public summon link).
|
|
func (s *Store) GetPolicySnapshot(token string) (*PolicySnapshot, error) {
|
|
row := s.db.QueryRow(`
|
|
SELECT token, policy_json, expires_at, created_at
|
|
FROM policy_snapshots WHERE token = ?
|
|
`, token)
|
|
|
|
var snap PolicySnapshot
|
|
var expires sql.NullString
|
|
var created string
|
|
if err := row.Scan(&snap.Token, &snap.PolicyJSON, &expires, &created); err != nil {
|
|
return nil, err
|
|
}
|
|
if expires.Valid {
|
|
snap.ExpiresAt = expires.String
|
|
t, err := time.Parse(time.RFC3339, expires.String)
|
|
if err == nil && time.Now().After(t) {
|
|
return nil, sql.ErrNoRows
|
|
}
|
|
}
|
|
snap.CreatedAt = created
|
|
return &snap, nil
|
|
}
|
|
|
|
func randomToken(n int) (string, error) {
|
|
b := make([]byte, n)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(b), nil
|
|
}
|
|
|
|
func nullIfEmpty(s string) sql.NullString {
|
|
if s == "" {
|
|
return sql.NullString{}
|
|
}
|
|
return sql.NullString{String: s, Valid: true}
|
|
}
|