// Package model defines the data types persisted as the tool's state: // hosts grouped into access groups, and users assigned to those groups. package model import "time" // Host is one jump-target reachable via port forwarding through the bastion. type Host struct { Name string `yaml:"name"` // short identifier, e.g. "db-prod-1" Address string `yaml:"address"` // hostname or IP as seen from the jump host Port int `yaml:"port"` // TCP port allowed for forwarding Description string `yaml:"description,omitempty"` } // Group bundles hosts that share the same access policy (e.g. "team-billing"). // Users are assigned to one or more groups; their effective allowed hosts are // the union of all hosts in their groups. type Group struct { Name string `yaml:"name"` Hosts []Host `yaml:"hosts"` } // UserStatus is the lifecycle state of a managed account. type UserStatus string const ( StatusActive UserStatus = "active" StatusLocked UserStatus = "locked" ) // User is one managed Linux/SSH account on the jump host. type User struct { Username string `yaml:"username"` Groups []string `yaml:"groups"` Status UserStatus `yaml:"status"` Fingerprint string `yaml:"fingerprint"` // SHA256 fingerprint of the current authorized public key PublicKey string `yaml:"public_key,omitempty"` // full "ssh-ed25519 AAAA... comment" line; safe to store/back up (public), lets us rewrite authorized_keys (e.g. after a group's host list changes) without re-issuing the keypair Comment string `yaml:"comment,omitempty"` CreatedAt time.Time `yaml:"created_at"` LockedAt *time.Time `yaml:"locked_at,omitempty"` } // State is the full persisted configuration of the tool. type State struct { Groups []Group `yaml:"groups"` Users []User `yaml:"users"` } // FindGroup returns a pointer to the group with the given name, or nil. func (s *State) FindGroup(name string) *Group { for i := range s.Groups { if s.Groups[i].Name == name { return &s.Groups[i] } } return nil } // FindUser returns a pointer to the user with the given username, or nil. func (s *State) FindUser(username string) *User { for i := range s.Users { if s.Users[i].Username == username { return &s.Users[i] } } return nil } // AllowedHosts returns the de-duplicated union of hosts across all groups // the user belongs to, sorted by "name" for stable authorized_keys output. func (s *State) AllowedHosts(u *User) []Host { seen := map[string]Host{} order := []string{} for _, gname := range u.Groups { g := s.FindGroup(gname) if g == nil { continue } for _, h := range g.Hosts { key := h.Address + ":" + itoa(h.Port) if _, ok := seen[key]; !ok { order = append(order, key) } seen[key] = h } } out := make([]Host, 0, len(order)) for _, k := range order { out = append(out, seen[k]) } return out } func itoa(n int) string { if n == 0 { return "0" } neg := n < 0 if neg { n = -n } var buf [20]byte i := len(buf) for n > 0 { i-- buf[i] = byte('0' + n%10) n /= 10 } if neg { i-- buf[i] = '-' } return string(buf[i:]) }