@@ -31,13 +31,30 @@ type tenantEmailSettings struct {
3131}
3232
3333type tenantSummary struct {
34- PK string `json:"pk"`
35- Domain string `json:"domain"`
36- Default bool `json:"default"`
34+ PK string `json:"pk"`
35+ TenantUUID string `json:"tenant_uuid"`
36+ UUID string `json:"uuid"`
37+ ID string `json:"id"`
38+ Domain string `json:"domain"`
39+ Default bool `json:"default"`
3740}
3841
39- type tenantListResponse struct {
40- Results []tenantSummary `json:"results"`
42+ func (t * tenantSummary ) identifier () string {
43+ if t == nil {
44+ return ""
45+ }
46+ switch {
47+ case strings .TrimSpace (t .PK ) != "" :
48+ return t .PK
49+ case strings .TrimSpace (t .TenantUUID ) != "" :
50+ return t .TenantUUID
51+ case strings .TrimSpace (t .UUID ) != "" :
52+ return t .UUID
53+ case strings .TrimSpace (t .ID ) != "" :
54+ return t .ID
55+ default :
56+ return ""
57+ }
4158}
4259
4360// ConfigureAuthentikEmail updates tenant-level SMTP settings using values from /opt/hecate/.env.
@@ -142,17 +159,16 @@ func ConfigureAuthentikEmail(rc *eos_io.RuntimeContext, cfg *AuthentikEmailConfi
142159
143160 client := authentik .NewUnifiedClient (baseURL , token )
144161
145- tenant , err := resolveAuthentikTenant (rc , client )
162+ tenant , patchPath , err := resolveAuthentikTenant (rc , client )
146163 if err != nil {
147164 return fmt .Errorf ("failed to resolve Authentik tenant: %w" , err )
148165 }
149166
150167 logger .Info ("Updating Authentik tenant" ,
151- zap .String ("tenant_pk " , tenant .PK ),
168+ zap .String ("tenant_identifier " , tenant .identifier () ),
152169 zap .String ("tenant_domain" , tenant .Domain ),
153- zap .Bool ("tenant_default" , tenant .Default ))
154-
155- patchPath := fmt .Sprintf ("/core/tenants/%s/" , tenant .PK )
170+ zap .Bool ("tenant_default" , tenant .Default ),
171+ zap .String ("tenant_patch_endpoint" , patchPath ))
156172
157173 respBody , err := client .Patch (rc .Ctx , patchPath , payload )
158174 if err != nil {
@@ -177,34 +193,137 @@ func ConfigureAuthentikEmail(rc *eos_io.RuntimeContext, cfg *AuthentikEmailConfi
177193 return nil
178194}
179195
180- func resolveAuthentikTenant (rc * eos_io.RuntimeContext , client * authentik.UnifiedClient ) (* tenantSummary , error ) {
196+ type tenantEndpointCandidate struct {
197+ ListPath string
198+ BuildPatchPath func (string ) string
199+ Description string
200+ }
201+
202+ func resolveAuthentikTenant (rc * eos_io.RuntimeContext , client * authentik.UnifiedClient ) (* tenantSummary , string , error ) {
181203 logger := otelzap .Ctx (rc .Ctx )
182204
183- data , err := client .Get (rc .Ctx , "/core/tenants/" )
184- if err != nil {
185- return nil , fmt .Errorf ("failed to list Authentik tenants: %w" , err )
205+ candidates := []tenantEndpointCandidate {
206+ {
207+ ListPath : "/core/tenants/" ,
208+ BuildPatchPath : func (id string ) string {
209+ return fmt .Sprintf ("/core/tenants/%s/" , id )
210+ },
211+ Description : "Authentik ≤2024.12 core tenants endpoint" ,
212+ },
213+ {
214+ ListPath : "/tenants/tenants/" ,
215+ BuildPatchPath : func (id string ) string {
216+ return fmt .Sprintf ("/tenants/tenants/%s/" , id )
217+ },
218+ Description : "Authentik 2025 tenants endpoint" ,
219+ },
220+ {
221+ ListPath : "/tenants/" ,
222+ BuildPatchPath : func (id string ) string {
223+ return fmt .Sprintf ("/tenants/%s/" , id )
224+ },
225+ Description : "Authentik 2025 short tenants endpoint" ,
226+ },
186227 }
187228
188- var tenants tenantListResponse
189- if err := json .Unmarshal (data , & tenants ); err != nil {
190- return nil , fmt .Errorf ("failed to decode Authentik tenant list: %w" , err )
229+ var lastErr error
230+
231+ for _ , candidate := range candidates {
232+ logger .Debug ("Attempting Authentik tenant discovery" ,
233+ zap .String ("endpoint" , candidate .ListPath ),
234+ zap .String ("description" , candidate .Description ))
235+
236+ data , err := client .Get (rc .Ctx , candidate .ListPath )
237+ if err != nil {
238+ logger .Debug ("Tenant endpoint request failed" ,
239+ zap .String ("endpoint" , candidate .ListPath ),
240+ zap .Error (err ))
241+ lastErr = err
242+ continue
243+ }
244+
245+ tenants , err := parseTenantListResponse (data )
246+ if err != nil {
247+ logger .Debug ("Failed to parse tenant list response" ,
248+ zap .String ("endpoint" , candidate .ListPath ),
249+ zap .Error (err ))
250+ lastErr = err
251+ continue
252+ }
253+
254+ if len (tenants ) == 0 {
255+ logger .Debug ("Tenant endpoint returned no tenants" ,
256+ zap .String ("endpoint" , candidate .ListPath ))
257+ lastErr = fmt .Errorf ("no tenants returned from %s" , candidate .ListPath )
258+ continue
259+ }
260+
261+ selected := selectTenant (tenants )
262+ id := selected .identifier ()
263+ if id == "" {
264+ logger .Debug ("Tenant missing identifier fields" ,
265+ zap .Any ("tenant" , selected ))
266+ lastErr = fmt .Errorf ("tenant has no identifier in %s" , candidate .ListPath )
267+ continue
268+ }
269+
270+ patchPath := candidate .BuildPatchPath (id )
271+ logger .Debug ("Tenant resolved" ,
272+ zap .String ("endpoint" , candidate .ListPath ),
273+ zap .String ("tenant_identifier" , id ),
274+ zap .String ("patch_endpoint" , patchPath ))
275+
276+ return selected , patchPath , nil
277+ }
278+
279+ if lastErr != nil {
280+ return nil , "" , fmt .Errorf ("failed to list Authentik tenants: %w" , lastErr )
191281 }
192282
193- if len (tenants .Results ) == 0 {
194- return nil , fmt .Errorf ("no tenants returned by Authentik API" )
283+ return nil , "" , fmt .Errorf ("failed to list Authentik tenants: no endpoints succeeded" )
284+ }
285+
286+ func parseTenantListResponse (data []byte ) ([]* tenantSummary , error ) {
287+ type tenantEnvelope struct {
288+ Results []* tenantSummary `json:"results"`
289+ Tenants []* tenantSummary `json:"tenants"`
290+ }
291+
292+ var envelope tenantEnvelope
293+ if err := json .Unmarshal (data , & envelope ); err == nil {
294+ switch {
295+ case len (envelope .Results ) > 0 :
296+ return envelope .Results , nil
297+ case len (envelope .Tenants ) > 0 :
298+ return envelope .Tenants , nil
299+ }
300+ }
301+
302+ var arr []* tenantSummary
303+ if err := json .Unmarshal (data , & arr ); err == nil && len (arr ) > 0 {
304+ return arr , nil
305+ }
306+
307+ // If both attempts failed, return last error for context.
308+ if len (envelope .Results ) == 0 && len (envelope .Tenants ) == 0 && len (arr ) == 0 {
309+ return nil , fmt .Errorf ("unexpected tenant response format" )
310+ }
311+
312+ return arr , nil
313+ }
314+
315+ func selectTenant (tenants []* tenantSummary ) * tenantSummary {
316+ if len (tenants ) == 0 {
317+ return nil
195318 }
196319
197- // Prefer the default tenant if flagged as such.
198- for _ , tenant := range tenants .Results {
199- if tenant .Default {
200- return & tenant , nil
320+ for _ , tenant := range tenants {
321+ if tenant != nil && tenant .Default {
322+ return tenant
201323 }
202324 }
203325
204- // Fall back to the first tenant in the list.
205- logger .Warn ("No default tenant flagged; using first tenant from list" ,
206- zap .Int ("tenant_count" , len (tenants .Results )))
207- return & tenants .Results [0 ], nil
326+ return tenants [0 ]
208327}
209328
210329func maskSensitive (value string ) string {
0 commit comments