This commit is contained in:
redlineo committed 2026-07-22 15:14:44 +03:00
1 parent 23c7133fa9
commit 8c1b93c14e
16 files changed
+1187 -295

No files matched your search

+69 -44
View File
@@ -1,20 +1,46 @@
// Package model defines the data types persisted as the tool's state:
// hosts grouped into access groups, and users assigned to those groups.
// hosts (each offering one or more TCP services) grouped into access
// groups, and users assigned to those groups.
package model
import "time"
import (
"fmt"
"regexp"
"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
// 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 hosts are
// the union of all hosts in their groups.
// 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"`
@@ -66,49 +92,48 @@ func (s *State) FindUser(username string) *User {
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{}
// 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 {
key := h.Address + ":" + itoa(h.Port)
if _, ok := seen[key]; !ok {
order = append(order, key)
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,
})
}
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:])
}