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}