package config import ( "crypto/sha256" "slices" "strconv" "strings" "time" ) type Changes struct { Hot []string Restart []string } type leafPut func(parent, key, value string) type leafValue struct { value, pattern, parent string } const present = "present" func duration(d time.Duration) string { return d.String() } func set(values []string) string { sorted := slices.Clone(values) slices.Sort(sorted) return strings.Join(sorted, "\n") } func appKey(a App) string { return "app[" + a.Domain + "]" } func clientKey(c Client) string { return "client[" + c.ID + "]" } var diffLeaves = map[string]func(c *Config, put leafPut){ "server.socket": func(c *Config, put leafPut) { put("", "server.socket", c.Server.Socket) }, "server.listen": func(c *Config, put leafPut) { put("", "server.listen", c.Server.Listen) }, "server.metrics_listen": func(c *Config, put leafPut) { put("", "server.metrics_listen", c.Server.MetricsListen) }, "server.domain_idp": func(c *Config, put leafPut) { put("", "server.domain_idp", c.Server.DomainIDP) }, "server.domain_account": func(c *Config, put leafPut) { put("", "server.domain_account", c.Server.DomainAccount) }, "server.domain_admin": func(c *Config, put leafPut) { put("", "server.domain_admin", c.Server.DomainAdmin) }, "server.admin_group": func(c *Config, put leafPut) { put("", "server.admin_group", c.Server.AdminGroup) }, "server.site_name": func(c *Config, put leafPut) { put("", "server.site_name", c.Server.SiteName) }, "config.source": func(c *Config, put leafPut) { put("", "config.source", c.Source) }, "invite.ttl": func(c *Config, put leafPut) { put("", "invite.ttl", duration(c.Invite.TTL)) }, "invite.default_groups": func(c *Config, put leafPut) { put("", "invite.default_groups", set(c.Invite.DefaultGroups)) }, "session.ttl": func(c *Config, put leafPut) { put("", "session.ttl", duration(c.Session.TTL)) }, "audit.retention": func(c *Config, put leafPut) { put("", "audit.retention", duration(c.Audit.Retention)) }, "ldap.primary_group": func(c *Config, put leafPut) { put("ldap", "ldap.primary_group", c.PrimaryGroup) }, "portdistrict.listen": portdistrictLeaf("listen", func(p *Portdistrict) string { return p.Listen }), "portdistrict.peer_table": portdistrictLeaf("peer_table", func(p *Portdistrict) string { return p.PeerTable }), "portdistrict.table_max_age": portdistrictLeaf("table_max_age", func(p *Portdistrict) string { return duration(p.TableMaxAge) }), "config.git.remote": configGitLeaf("remote", func(g *ConfigGit) string { return g.Remote }), "config.git.branch": configGitLeaf("branch", func(g *ConfigGit) string { return g.Branch }), "config.git.allowed_signers": configGitLeaf("allowed_signers", func(g *ConfigGit) string { return g.AllowedSigners }), "config.git.known_hosts": configGitLeaf("known_hosts", func(g *ConfigGit) string { return g.KnownHosts }), "userbase.repo": userbaseLeaf("repo", func(u *Userbase) string { return u.Repo }), "userbase.mirror": userbaseLeaf("mirror", func(u *Userbase) string { return u.Mirror }), "userbase.known_hosts": userbaseLeaf("known_hosts", func(u *Userbase) string { return u.KnownHosts }), "userbase.break_glass_recipient": userbaseLeaf("break_glass_recipient", func(u *Userbase) string { return u.BreakGlassRecipient }), "userbase.export_signing_keys": userbaseLeaf("export_signing_keys", func(u *Userbase) string { return strconv.FormatBool(u.ExportSigningKeys) }), "userbase.restore_on_first_boot": userbaseLeaf("restore_on_first_boot", func(u *Userbase) string { return strconv.FormatBool(u.RestoreOnFirstBoot) }), "google.client_id": googleLeaf("client_id", func(g *Google) string { return g.ClientID }), "google.allowed_hd": googleLeaf("allowed_hd", func(g *Google) string { return g.AllowedHD }), "ldap.listen": ldapLeaf("listen", func(l *LDAP) string { return l.Listen }), "ldap.base_dn": ldapLeaf("base_dn", func(l *LDAP) string { return l.BaseDN }), "ldap.tls_cert": ldapLeaf("tls_cert", func(l *LDAP) string { return l.TLSCert }), "ldap.tls_key": ldapLeaf("tls_key", func(l *LDAP) string { return l.TLSKey }), "client[].id": func(c *Config, put leafPut) { for _, cl := range c.Clients { put("", clientKey(cl), present) } }, "client[].redirect_uris": func(c *Config, put leafPut) { for _, cl := range c.Clients { put(clientKey(cl), clientKey(cl)+".redirect_uris", set(cl.RedirectURIs)) } }, "client[].public": func(c *Config, put leafPut) { for _, cl := range c.Clients { put(clientKey(cl), clientKey(cl)+".public", strconv.FormatBool(cl.Public)) } }, "app[].domain": func(c *Config, put leafPut) { for _, a := range c.Apps { put("", appKey(a), present) } }, "app[].session_ttl": func(c *Config, put leafPut) { for _, a := range c.Apps { put(appKey(a), appKey(a)+".session_ttl", duration(a.SessionTTL)) } }, "app[].groups": func(c *Config, put leafPut) { for _, a := range c.Apps { put(appKey(a), appKey(a)+".groups", set(a.Groups)) } }, } var hotLeaves = map[string]bool{ "session.ttl": true, "app[].session_ttl": true, "google.allowed_hd": true, "invite.ttl": true, "invite.default_groups": true, } func portdistrictLeaf(name string, get func(*Portdistrict) string) func(*Config, leafPut) { return func(c *Config, put leafPut) { if c.Portdistrict != nil { put("portdistrict", "portdistrict."+name, get(c.Portdistrict)) } } } func configGitLeaf(name string, get func(*ConfigGit) string) func(*Config, leafPut) { return func(c *Config, put leafPut) { if c.ConfigGit != nil { put("config.git", "config.git."+name, get(c.ConfigGit)) } } } func userbaseLeaf(name string, get func(*Userbase) string) func(*Config, leafPut) { return func(c *Config, put leafPut) { if c.Userbase != nil { put("userbase", "userbase."+name, get(c.Userbase)) } } } func googleLeaf(name string, get func(*Google) string) func(*Config, leafPut) { return func(c *Config, put leafPut) { if c.Google != nil { put("google", "google."+name, get(c.Google)) } } } func ldapLeaf(name string, get func(*LDAP) string) func(*Config, leafPut) { return func(c *Config, put leafPut) { if c.LDAP != nil { put("ldap", "ldap."+name, get(c.LDAP)) } } } func secretValue(s Secret) string { sum := sha256.Sum256(s.Bytes()) return string(sum[:]) } func flatten(c *Config) map[string]leafValue { out := map[string]leafValue{} if c == nil { return out } for pattern, emit := range diffLeaves { emit(c, func(parent, key, value string) { out[key] = leafValue{value: value, pattern: pattern, parent: parent} }) } tables := map[string]bool{ "portdistrict": c.Portdistrict != nil, "config.git": c.ConfigGit != nil, "userbase": c.Userbase != nil, "google": c.Google != nil, "ldap": c.LDAP != nil, } for name, ok := range tables { if ok { out[name] = leafValue{value: present, pattern: name} } } secrets := map[string]Secret{ nameSessionKey: c.Secrets.SessionMasterKey, nameGoogleSecret: c.Secrets.GoogleClientSecret, nameAgeIdentity: c.Secrets.AgeIdentity, nameUserbaseSign: c.Secrets.UserbaseSigningKey, nameUserbaseKey: c.Secrets.UserbaseDeployKey, nameConfigRepoKey: c.Secrets.ConfigDeployKey, } for name, s := range secrets { if !s.isZero() { out["secrets."+name] = leafValue{value: secretValue(s), pattern: "secrets"} } } for id, s := range c.Secrets.ClientSecrets { if !s.isZero() { out["secrets."+clientSecretName(id)] = leafValue{value: secretValue(s), pattern: "secrets", parent: "client[" + id + "]"} } } return out } func Diff(old, new *Config) Changes { a, b := flatten(old), flatten(new) changed := map[string]leafValue{} for k, v := range a { if w, ok := b[k]; !ok || w.value != v.value { changed[k] = v } } for k, v := range b { if _, ok := a[k]; !ok { changed[k] = v } } var ch Changes for k, v := range changed { if _, parentChanged := changed[v.parent]; v.parent != "" && parentChanged { continue } if hotLeaves[v.pattern] { ch.Hot = append(ch.Hot, k) } else { ch.Restart = append(ch.Restart, k) } } slices.Sort(ch.Hot) slices.Sort(ch.Restart) return ch } func (c Changes) Empty() bool { return len(c.Hot) == 0 && len(c.Restart) == 0 }