Raw
  1package config
  2
  3import (
  4	"crypto/sha256"
  5	"slices"
  6	"strconv"
  7	"strings"
  8	"time"
  9)
 10
 11type Changes struct {
 12	Hot     []string
 13	Restart []string
 14}
 15
 16type leafPut func(parent, key, value string)
 17
 18type leafValue struct {
 19	value, pattern, parent string
 20}
 21
 22const present = "present"
 23
 24func duration(d time.Duration) string {
 25	return d.String()
 26}
 27
 28func set(values []string) string {
 29	sorted := slices.Clone(values)
 30	slices.Sort(sorted)
 31	return strings.Join(sorted, "\n")
 32}
 33
 34func appKey(a App) string {
 35	return "app[" + a.Domain + "]"
 36}
 37
 38func clientKey(c Client) string {
 39	return "client[" + c.ID + "]"
 40}
 41
 42var diffLeaves = map[string]func(c *Config, put leafPut){
 43	"server.socket":                  func(c *Config, put leafPut) { put("", "server.socket", c.Server.Socket) },
 44	"server.listen":                  func(c *Config, put leafPut) { put("", "server.listen", c.Server.Listen) },
 45	"server.metrics_listen":          func(c *Config, put leafPut) { put("", "server.metrics_listen", c.Server.MetricsListen) },
 46	"server.domain_idp":              func(c *Config, put leafPut) { put("", "server.domain_idp", c.Server.DomainIDP) },
 47	"server.domain_account":          func(c *Config, put leafPut) { put("", "server.domain_account", c.Server.DomainAccount) },
 48	"server.domain_admin":            func(c *Config, put leafPut) { put("", "server.domain_admin", c.Server.DomainAdmin) },
 49	"server.admin_group":             func(c *Config, put leafPut) { put("", "server.admin_group", c.Server.AdminGroup) },
 50	"server.site_name":               func(c *Config, put leafPut) { put("", "server.site_name", c.Server.SiteName) },
 51	"config.source":                  func(c *Config, put leafPut) { put("", "config.source", c.Source) },
 52	"invite.ttl":                     func(c *Config, put leafPut) { put("", "invite.ttl", duration(c.Invite.TTL)) },
 53	"invite.default_groups":          func(c *Config, put leafPut) { put("", "invite.default_groups", set(c.Invite.DefaultGroups)) },
 54	"session.ttl":                    func(c *Config, put leafPut) { put("", "session.ttl", duration(c.Session.TTL)) },
 55	"audit.retention":                func(c *Config, put leafPut) { put("", "audit.retention", duration(c.Audit.Retention)) },
 56	"ldap.primary_group":             func(c *Config, put leafPut) { put("ldap", "ldap.primary_group", c.PrimaryGroup) },
 57	"portdistrict.listen":            portdistrictLeaf("listen", func(p *Portdistrict) string { return p.Listen }),
 58	"portdistrict.peer_table":        portdistrictLeaf("peer_table", func(p *Portdistrict) string { return p.PeerTable }),
 59	"portdistrict.table_max_age":     portdistrictLeaf("table_max_age", func(p *Portdistrict) string { return duration(p.TableMaxAge) }),
 60	"config.git.remote":              configGitLeaf("remote", func(g *ConfigGit) string { return g.Remote }),
 61	"config.git.branch":              configGitLeaf("branch", func(g *ConfigGit) string { return g.Branch }),
 62	"config.git.allowed_signers":     configGitLeaf("allowed_signers", func(g *ConfigGit) string { return g.AllowedSigners }),
 63	"config.git.known_hosts":         configGitLeaf("known_hosts", func(g *ConfigGit) string { return g.KnownHosts }),
 64	"userbase.repo":                  userbaseLeaf("repo", func(u *Userbase) string { return u.Repo }),
 65	"userbase.mirror":                userbaseLeaf("mirror", func(u *Userbase) string { return u.Mirror }),
 66	"userbase.known_hosts":           userbaseLeaf("known_hosts", func(u *Userbase) string { return u.KnownHosts }),
 67	"userbase.break_glass_recipient": userbaseLeaf("break_glass_recipient", func(u *Userbase) string { return u.BreakGlassRecipient }),
 68	"userbase.export_signing_keys":   userbaseLeaf("export_signing_keys", func(u *Userbase) string { return strconv.FormatBool(u.ExportSigningKeys) }),
 69	"userbase.restore_on_first_boot": userbaseLeaf("restore_on_first_boot", func(u *Userbase) string { return strconv.FormatBool(u.RestoreOnFirstBoot) }),
 70	"google.client_id":               googleLeaf("client_id", func(g *Google) string { return g.ClientID }),
 71	"google.allowed_hd":              googleLeaf("allowed_hd", func(g *Google) string { return g.AllowedHD }),
 72	"ldap.listen":                    ldapLeaf("listen", func(l *LDAP) string { return l.Listen }),
 73	"ldap.base_dn":                   ldapLeaf("base_dn", func(l *LDAP) string { return l.BaseDN }),
 74	"ldap.tls_cert":                  ldapLeaf("tls_cert", func(l *LDAP) string { return l.TLSCert }),
 75	"ldap.tls_key":                   ldapLeaf("tls_key", func(l *LDAP) string { return l.TLSKey }),
 76	"client[].id": func(c *Config, put leafPut) {
 77		for _, cl := range c.Clients {
 78			put("", clientKey(cl), present)
 79		}
 80	},
 81	"client[].redirect_uris": func(c *Config, put leafPut) {
 82		for _, cl := range c.Clients {
 83			put(clientKey(cl), clientKey(cl)+".redirect_uris", set(cl.RedirectURIs))
 84		}
 85	},
 86	"client[].public": func(c *Config, put leafPut) {
 87		for _, cl := range c.Clients {
 88			put(clientKey(cl), clientKey(cl)+".public", strconv.FormatBool(cl.Public))
 89		}
 90	},
 91	"app[].domain": func(c *Config, put leafPut) {
 92		for _, a := range c.Apps {
 93			put("", appKey(a), present)
 94		}
 95	},
 96	"app[].session_ttl": func(c *Config, put leafPut) {
 97		for _, a := range c.Apps {
 98			put(appKey(a), appKey(a)+".session_ttl", duration(a.SessionTTL))
 99		}
100	},
101	"app[].groups": func(c *Config, put leafPut) {
102		for _, a := range c.Apps {
103			put(appKey(a), appKey(a)+".groups", set(a.Groups))
104		}
105	},
106}
107
108var hotLeaves = map[string]bool{
109	"session.ttl":           true,
110	"app[].session_ttl":     true,
111	"google.allowed_hd":     true,
112	"invite.ttl":            true,
113	"invite.default_groups": true,
114}
115
116func portdistrictLeaf(name string, get func(*Portdistrict) string) func(*Config, leafPut) {
117	return func(c *Config, put leafPut) {
118		if c.Portdistrict != nil {
119			put("portdistrict", "portdistrict."+name, get(c.Portdistrict))
120		}
121	}
122}
123
124func configGitLeaf(name string, get func(*ConfigGit) string) func(*Config, leafPut) {
125	return func(c *Config, put leafPut) {
126		if c.ConfigGit != nil {
127			put("config.git", "config.git."+name, get(c.ConfigGit))
128		}
129	}
130}
131
132func userbaseLeaf(name string, get func(*Userbase) string) func(*Config, leafPut) {
133	return func(c *Config, put leafPut) {
134		if c.Userbase != nil {
135			put("userbase", "userbase."+name, get(c.Userbase))
136		}
137	}
138}
139
140func googleLeaf(name string, get func(*Google) string) func(*Config, leafPut) {
141	return func(c *Config, put leafPut) {
142		if c.Google != nil {
143			put("google", "google."+name, get(c.Google))
144		}
145	}
146}
147
148func ldapLeaf(name string, get func(*LDAP) string) func(*Config, leafPut) {
149	return func(c *Config, put leafPut) {
150		if c.LDAP != nil {
151			put("ldap", "ldap."+name, get(c.LDAP))
152		}
153	}
154}
155
156func secretValue(s Secret) string {
157	sum := sha256.Sum256(s.Bytes())
158	return string(sum[:])
159}
160
161func flatten(c *Config) map[string]leafValue {
162	out := map[string]leafValue{}
163	if c == nil {
164		return out
165	}
166	for pattern, emit := range diffLeaves {
167		emit(c, func(parent, key, value string) {
168			out[key] = leafValue{value: value, pattern: pattern, parent: parent}
169		})
170	}
171	tables := map[string]bool{
172		"portdistrict": c.Portdistrict != nil,
173		"config.git":   c.ConfigGit != nil,
174		"userbase":     c.Userbase != nil,
175		"google":       c.Google != nil,
176		"ldap":         c.LDAP != nil,
177	}
178	for name, ok := range tables {
179		if ok {
180			out[name] = leafValue{value: present, pattern: name}
181		}
182	}
183	secrets := map[string]Secret{
184		nameSessionKey:    c.Secrets.SessionMasterKey,
185		nameGoogleSecret:  c.Secrets.GoogleClientSecret,
186		nameAgeIdentity:   c.Secrets.AgeIdentity,
187		nameUserbaseSign:  c.Secrets.UserbaseSigningKey,
188		nameUserbaseKey:   c.Secrets.UserbaseDeployKey,
189		nameConfigRepoKey: c.Secrets.ConfigDeployKey,
190	}
191	for name, s := range secrets {
192		if !s.isZero() {
193			out["secrets."+name] = leafValue{value: secretValue(s), pattern: "secrets"}
194		}
195	}
196	for id, s := range c.Secrets.ClientSecrets {
197		if !s.isZero() {
198			out["secrets."+clientSecretName(id)] = leafValue{value: secretValue(s), pattern: "secrets", parent: "client[" + id + "]"}
199		}
200	}
201	return out
202}
203
204func Diff(old, new *Config) Changes {
205	a, b := flatten(old), flatten(new)
206	changed := map[string]leafValue{}
207	for k, v := range a {
208		if w, ok := b[k]; !ok || w.value != v.value {
209			changed[k] = v
210		}
211	}
212	for k, v := range b {
213		if _, ok := a[k]; !ok {
214			changed[k] = v
215		}
216	}
217	var ch Changes
218	for k, v := range changed {
219		if _, parentChanged := changed[v.parent]; v.parent != "" && parentChanged {
220			continue
221		}
222		if hotLeaves[v.pattern] {
223			ch.Hot = append(ch.Hot, k)
224		} else {
225			ch.Restart = append(ch.Restart, k)
226		}
227	}
228	slices.Sort(ch.Hot)
229	slices.Sort(ch.Restart)
230	return ch
231}
232
233func (c Changes) Empty() bool {
234	return len(c.Hot) == 0 && len(c.Restart) == 0
235}