package state import ( "crypto/rand" "encoding/base64" "encoding/json" "errors" "os" "path/filepath" "time" ) const FileName = "identity.json" type Identity struct { NodeID string `json:"node_id"` ClusterID string `json:"cluster_id"` NodeName string `json:"node_name"` NodeFingerprint string `json:"node_fingerprint"` PublicKey string `json:"public_key"` IdentityStatus string `json:"identity_status"` PendingJoinRequestID string `json:"pending_join_request_id,omitempty"` ClusterAuthorityPublicKey string `json:"cluster_authority_public_key,omitempty"` ClusterAuthorityFingerprint string `json:"cluster_authority_fingerprint,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func LoadOrCreate(dir, clusterID, nodeName string) (Identity, error) { path := filepath.Join(dir, FileName) existing, err := Load(path) if err == nil { return existing, nil } if !errors.Is(err, os.ErrNotExist) { return Identity{}, err } now := time.Now().UTC() fingerprint, err := randomToken("rap-node-fp") if err != nil { return Identity{}, err } publicKey, err := randomToken("rap-node-pub") if err != nil { return Identity{}, err } identity := Identity{ ClusterID: clusterID, NodeName: nodeName, NodeFingerprint: fingerprint, PublicKey: publicKey, IdentityStatus: "new", CreatedAt: now, UpdatedAt: now, } if err := Save(path, identity); err != nil { return Identity{}, err } return identity, nil } func Load(path string) (Identity, error) { payload, err := os.ReadFile(path) if err != nil { return Identity{}, err } var identity Identity if err := json.Unmarshal(payload, &identity); err != nil { return Identity{}, err } return identity, nil } func Save(path string, identity Identity) error { if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return err } identity.UpdatedAt = time.Now().UTC() payload, err := json.MarshalIndent(identity, "", " ") if err != nil { return err } return os.WriteFile(path, payload, 0o600) } func MarkEnrollmentSubmitted(dir, clusterID, joinRequestID string) (Identity, error) { path := filepath.Join(dir, FileName) identity, err := Load(path) if err != nil { return Identity{}, err } identity.ClusterID = clusterID identity.PendingJoinRequestID = joinRequestID identity.IdentityStatus = "pending_approval" if err := Save(path, identity); err != nil { return Identity{}, err } return identity, nil } func MarkApproved(dir string, nodeID, clusterID, status string) (Identity, error) { return MarkApprovedWithAuthority(dir, nodeID, clusterID, status, "", "") } func MarkApprovedWithAuthority(dir string, nodeID, clusterID, status, authorityPublicKey, authorityFingerprint string) (Identity, error) { path := filepath.Join(dir, FileName) identity, err := Load(path) if err != nil { return Identity{}, err } identity.NodeID = nodeID identity.ClusterID = clusterID identity.IdentityStatus = status identity.PendingJoinRequestID = "" identity.ClusterAuthorityPublicKey = authorityPublicKey identity.ClusterAuthorityFingerprint = authorityFingerprint if err := Save(path, identity); err != nil { return Identity{}, err } return identity, nil } func randomToken(prefix string) (string, error) { var random [32]byte if _, err := rand.Read(random[:]); err != nil { return "", err } return prefix + "_" + base64.RawURLEncoding.EncodeToString(random[:]), nil }