Files
ssh-access-tui/internal/model/types.go
T
2026-07-22 15:14:44 +03:00

140 lines
4.6 KiB
Go

// Package model defines the data types persisted as the tool's state:
// hosts (each offering one or more TCP services) grouped into access
// groups, and users assigned to those groups.
package model
import (
"fmt"
"regexp"
"time"
)
// nameRe restricts identifiers that end up as ssh_config tokens or
// authorized_keys comments: no spaces, no shell/config metacharacters.
var nameRe = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*$`)
// ValidName reports whether s is safe to use as a group/host/service name:
// letters, digits, dot, underscore, hyphen, no spaces. Names end up as
// ssh_config comments/tokens and authorized_keys metadata, where a stray
// space or "#" would corrupt the generated file.
func ValidName(s string) bool {
return nameRe.MatchString(s)
}
// Service is one TCP port reachable on a Host — e.g. the host's sshd on
// port 22, or an unrelated application on port 7888. A single Host commonly
// exposes several of these.
type Service struct {
Name string `yaml:"name"` // short identifier, e.g. "ssh" or "web-api"
Port int `yaml:"port"` // remote TCP port on the host
Description string `yaml:"description,omitempty"`
}
// Host is one jump-target reachable via port forwarding through the
// bastion, identified by address and offering one or more Services.
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
Services []Service `yaml:"services"`
}
// Group bundles hosts that share the same access policy (e.g. "team-billing").
// Users are assigned to one or more groups; their effective allowed
// forwards are the union of all services on 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
}
// Forward is one flattened (host, service) pair a user is allowed to reach.
// This is the actual unit both server-side enforcement (authorized_keys
// "permitopen=") and the client config (one "LocalForward" line per
// Forward) are built from.
type Forward struct {
HostName string
Address string
Port int
ServiceName string
Description string
}
// AllowedForwards returns the de-duplicated union of (host, service) pairs
// across all groups the user belongs to, in stable encounter order (group
// order, then host order, then service order, as defined in state.yaml).
// Two services that resolve to the same address:port (even from different
// groups) collapse to a single Forward — forwarding the same destination
// twice would be meaningless.
func (s *State) AllowedForwards(u *User) []Forward {
seen := map[string]bool{}
var out []Forward
for _, gname := range u.Groups {
g := s.FindGroup(gname)
if g == nil {
continue
}
for _, h := range g.Hosts {
for _, svc := range h.Services {
key := fmt.Sprintf("%s:%d", h.Address, svc.Port)
if seen[key] {
continue
}
seen[key] = true
out = append(out, Forward{
HostName: h.Name,
Address: h.Address,
Port: svc.Port,
ServiceName: svc.Name,
Description: svc.Description,
})
}
}
}
return out
}