Skip to content

Commit fd753bb

Browse files
feat: merge Authentik config export into health check command
- Consolidated --authentik and --authentik-config flags into a single --authentik flag that performs both health check and config export - Added comprehensive configuration export to debug command showing brands, flows, stages, groups and applications - Enhanced UpdateBrand API to return updated brand object for verification - Removed redundant export_authentik.go file and moved functionality into debug.go - Updated help text and examples to reflect simplifie
1 parent 1fecb07 commit fd753bb

6 files changed

Lines changed: 211 additions & 315 deletions

File tree

cmd/debug/hecate.go

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ import (
88
)
99

1010
var (
11-
hecateComponent string
12-
hecateAuthentikCheck bool
13-
hecateAuthentikConfig bool
14-
hecateBionicGPTCheck bool
15-
hecatePath string
16-
hecateVerbose bool
11+
hecateComponent string
12+
hecateAuthentikCheck bool
13+
hecateBionicGPTCheck bool
14+
hecatePath string
15+
hecateVerbose bool
1716
)
1817

1918
var hecateCmd = &cobra.Command{
@@ -57,14 +56,6 @@ Authentik-specific diagnostics (--authentik flag):
5756
• Memory usage analysis
5857
• Backup status
5958
60-
Authentik configuration export (--authentik-config flag):
61-
• Brands configuration
62-
• Flows (authentication, enrollment, recovery, etc.)
63-
• Stages and stage bindings
64-
• Groups and permissions
65-
• Applications and providers
66-
• Full observability of self-enrollment setup
67-
6859
BionicGPT integration diagnostics (--bionicgpt flag):
6960
• Authentik → Caddy → BionicGPT triangle validation
7061
• Caddy forward_auth configuration check
@@ -79,17 +70,15 @@ BionicGPT integration diagnostics (--bionicgpt flag):
7970
8071
Flags:
8172
--component <name> Only check specific component (caddy|authentik|postgresql|redis|nginx|coturn)
82-
--authentik Run comprehensive Authentik pre-upgrade health check
83-
--authentik-config Export Authentik configuration (brands, flows, stages, groups, applications)
73+
--authentik Run comprehensive Authentik health check + configuration export
8474
--bionicgpt Run BionicGPT integration diagnostics (Authentik-Caddy-BionicGPT)
8575
--path <path> Path to Hecate installation (default: /opt/hecate)
8676
--verbose Show detailed diagnostic output
8777
8878
Examples:
8979
eos debug hecate # Full diagnostics + file display
9080
eos debug hecate --component authentik # Only diagnose Authentik
91-
eos debug hecate --authentik # Full Authentik pre-upgrade check
92-
eos debug hecate --authentik-config # Export Authentik configuration
81+
eos debug hecate --authentik # Full Authentik health check + config export
9382
eos debug hecate --bionicgpt # BionicGPT integration diagnostics
9483
eos debug hecate --path /custom/path # Custom installation path
9584
@@ -99,8 +88,7 @@ Output is automatically saved to ~/.eos/debug/eos-debug-hecate-{timestamp}.txt`,
9988

10089
func init() {
10190
hecateCmd.Flags().StringVar(&hecateComponent, "component", "", "Specific component to check")
102-
hecateCmd.Flags().BoolVar(&hecateAuthentikCheck, "authentik", false, "Run comprehensive Authentik pre-upgrade check")
103-
hecateCmd.Flags().BoolVar(&hecateAuthentikConfig, "authentik-config", false, "Export Authentik configuration (brands, flows, stages, groups, applications)")
91+
hecateCmd.Flags().BoolVar(&hecateAuthentikCheck, "authentik", false, "Run comprehensive Authentik health check + configuration export")
10492
hecateCmd.Flags().BoolVar(&hecateBionicGPTCheck, "bionicgpt", false, "Run BionicGPT integration diagnostics (Authentik-Caddy-BionicGPT triangle)")
10593
hecateCmd.Flags().StringVar(&hecatePath, "path", "/opt/hecate", "Path to Hecate installation")
10694
hecateCmd.Flags().BoolVar(&hecateVerbose, "verbose", false, "Show detailed diagnostic output")

pkg/authentik/brand.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,34 +96,47 @@ func (c *APIClient) GetBrand(ctx context.Context, pk string) (*BrandResponse, er
9696
return &brand, nil
9797
}
9898

99-
// UpdateBrand updates a brand's configuration
99+
// UpdateBrand updates a brand's configuration and returns the updated brand
100100
// Only non-empty fields in updates will be modified
101-
func (c *APIClient) UpdateBrand(ctx context.Context, pk string, updates map[string]interface{}) error {
101+
// Returns the updated brand object from API response for verification
102+
func (c *APIClient) UpdateBrand(ctx context.Context, pk string, updates map[string]interface{}) (*BrandResponse, error) {
102103
jsonBody, err := json.Marshal(updates)
103104
if err != nil {
104-
return fmt.Errorf("failed to marshal update request: %w", err)
105+
return nil, fmt.Errorf("failed to marshal update request: %w", err)
105106
}
106107

107108
// P0 FIX: Correct API endpoint path
108109
url := fmt.Sprintf("%s/api/v3/core/brands/%s/", c.BaseURL, pk)
109110
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, url, bytes.NewReader(jsonBody))
110111
if err != nil {
111-
return fmt.Errorf("failed to create request: %w", err)
112+
return nil, fmt.Errorf("failed to create request: %w", err)
112113
}
113114

114115
req.Header.Set("Authorization", "Bearer "+c.Token)
115116
req.Header.Set("Content-Type", "application/json")
117+
req.Header.Set("Accept", "application/json") // Request JSON response
116118

117119
resp, err := c.HTTPClient.Do(req)
118120
if err != nil {
119-
return fmt.Errorf("brand update request failed: %w", err)
121+
return nil, fmt.Errorf("brand update request failed: %w", err)
120122
}
121123
defer func() { _ = resp.Body.Close() }()
122124

125+
// Read response body for both success and error cases
126+
body, err := io.ReadAll(io.LimitReader(resp.Body, 8192))
127+
if err != nil {
128+
return nil, fmt.Errorf("failed to read response body: %w", err)
129+
}
130+
123131
if resp.StatusCode != http.StatusOK {
124-
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
125-
return fmt.Errorf("brand update failed with status %d: %s", resp.StatusCode, string(body))
132+
return nil, fmt.Errorf("brand update failed with status %d: %s", resp.StatusCode, string(body))
133+
}
134+
135+
// Parse response body to get updated brand
136+
var updatedBrand BrandResponse
137+
if err := json.Unmarshal(body, &updatedBrand); err != nil {
138+
return nil, fmt.Errorf("failed to parse brand update response: %w\nResponse body: %s", err, string(body))
126139
}
127140

128-
return nil
141+
return &updatedBrand, nil
129142
}

pkg/authentik/debug.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,23 @@ func RunAuthentikDebug(rc *eos_io.RuntimeContext, config *DebugConfig) error {
115115
zap.Int("failed", countFailed(allResults)),
116116
zap.Int("warnings", countWarnings(allResults)))
117117

118+
// 14. Configuration Export - Show complete Authentik state
119+
fmt.Println()
120+
fmt.Println("=========================================")
121+
fmt.Println("Authentik Configuration Export")
122+
fmt.Println("=========================================")
123+
fmt.Println()
124+
125+
if err := displayAuthentikConfiguration(rc, config.HecatePath); err != nil {
126+
logger.Warn("Failed to export Authentik configuration",
127+
zap.Error(err))
128+
fmt.Printf("\n❌ Configuration export failed: %v\n", err)
129+
fmt.Println("\nYou can manually check configuration in Authentik UI:")
130+
fmt.Println(" • http://localhost:9000/if/admin/#/core/brands")
131+
fmt.Println(" • http://localhost:9000/if/admin/#/flow/flows")
132+
fmt.Println(" • http://localhost:9000/if/admin/#/identity/groups")
133+
}
134+
118135
return nil
119136
}
120137

@@ -1721,3 +1738,148 @@ func displayPreUpgradeSummary(results []AuthentikCheckResult) {
17211738
fmt.Println(" eos update hecate --authentik")
17221739
fmt.Println()
17231740
}
1741+
1742+
// displayAuthentikConfiguration exports and displays complete Authentik configuration
1743+
// Integrated into debug command to provide full observability
1744+
func displayAuthentikConfiguration(rc *eos_io.RuntimeContext, hecatePath string) error {
1745+
logger := otelzap.Ctx(rc.Ctx)
1746+
1747+
// Read .env file to get API token
1748+
envPath := filepath.Join(hecatePath, ".env")
1749+
logger.Debug("Reading Authentik API token from .env",
1750+
zap.String("env_path", envPath))
1751+
1752+
envFile, err := os.ReadFile(envPath)
1753+
if err != nil {
1754+
return fmt.Errorf("failed to read .env file: %w\nPath: %s", err, envPath)
1755+
}
1756+
1757+
// Parse AUTHENTIK_TOKEN from .env
1758+
var apiToken string
1759+
for _, line := range strings.Split(string(envFile), "\n") {
1760+
line = strings.TrimSpace(line)
1761+
if strings.HasPrefix(line, "AUTHENTIK_TOKEN=") {
1762+
apiToken = strings.TrimPrefix(line, "AUTHENTIK_TOKEN=")
1763+
apiToken = strings.Trim(apiToken, "\"'") // Remove quotes
1764+
break
1765+
}
1766+
}
1767+
1768+
if apiToken == "" {
1769+
return fmt.Errorf("AUTHENTIK_TOKEN not found in .env file\nPath: %s", envPath)
1770+
}
1771+
1772+
logger.Debug("✓ API token found in .env")
1773+
1774+
// Authentik URL (localhost:9000 for host access)
1775+
authentikURL := fmt.Sprintf("http://%s:%d", shared.GetInternalHostname(), shared.PortAuthentik)
1776+
1777+
// Create Authentik API client
1778+
authentikClient := NewClient(authentikURL, apiToken)
1779+
1780+
// Fetch and display brands
1781+
logger.Debug("Fetching brands configuration")
1782+
brands, err := authentikClient.ListBrands(rc.Ctx)
1783+
if err != nil {
1784+
return fmt.Errorf("failed to list brands: %w", err)
1785+
}
1786+
1787+
fmt.Printf("BRANDS (%d)\n", len(brands))
1788+
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
1789+
for _, brand := range brands {
1790+
fmt.Printf(" • %s\n", brand.BrandingTitle)
1791+
fmt.Printf(" Domain: %s\n", brand.Domain)
1792+
fmt.Printf(" Brand UUID: %s\n", brand.PK)
1793+
if brand.FlowEnrollment != "" {
1794+
fmt.Printf(" Enrollment Flow: %s ✓\n", brand.FlowEnrollment)
1795+
} else {
1796+
fmt.Printf(" Enrollment Flow: Not configured\n")
1797+
}
1798+
fmt.Println()
1799+
}
1800+
1801+
// Fetch and display flows
1802+
logger.Debug("Fetching flows configuration")
1803+
flows, err := authentikClient.ListFlows(rc.Ctx, "") // All designations
1804+
if err != nil {
1805+
return fmt.Errorf("failed to list flows: %w", err)
1806+
}
1807+
1808+
fmt.Printf("\nFLOWS (%d)\n", len(flows))
1809+
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
1810+
1811+
// Group flows by designation
1812+
flowsByDesignation := make(map[string][]FlowResponse)
1813+
for _, flow := range flows {
1814+
flowsByDesignation[flow.Designation] = append(flowsByDesignation[flow.Designation], flow)
1815+
}
1816+
1817+
for designation, flows := range flowsByDesignation {
1818+
fmt.Printf("\n %s FLOWS:\n", strings.ToUpper(designation))
1819+
for _, flow := range flows {
1820+
fmt.Printf(" • %s (%s)\n", flow.Title, flow.Slug)
1821+
fmt.Printf(" Flow PK: %s\n", flow.PK)
1822+
1823+
// Show stages for this flow
1824+
bindings, err := authentikClient.GetFlowStages(rc.Ctx, flow.PK)
1825+
if err != nil {
1826+
logger.Debug("Failed to fetch stages for flow",
1827+
zap.String("flow_slug", flow.Slug),
1828+
zap.Error(err))
1829+
} else {
1830+
fmt.Printf(" Stages: %d\n", len(bindings))
1831+
}
1832+
}
1833+
}
1834+
1835+
// Fetch and display groups
1836+
logger.Debug("Fetching groups configuration")
1837+
groups, err := authentikClient.ListGroups(rc.Ctx, "") // All groups
1838+
if err != nil {
1839+
return fmt.Errorf("failed to list groups: %w", err)
1840+
}
1841+
1842+
fmt.Printf("\n\nGROUPS (%d)\n", len(groups))
1843+
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
1844+
for _, group := range groups {
1845+
fmt.Printf(" • %s\n", group.Name)
1846+
if group.IsSuperuser {
1847+
fmt.Println(" Role: Superuser")
1848+
} else {
1849+
fmt.Println(" Role: Standard user")
1850+
}
1851+
if attrs, ok := group.Attributes["eos_managed"].(bool); ok && attrs {
1852+
fmt.Println(" Managed by: Eos ✓")
1853+
}
1854+
fmt.Println()
1855+
}
1856+
1857+
// Fetch and display applications
1858+
logger.Debug("Fetching applications configuration")
1859+
applications, err := authentikClient.ListApplications(rc.Ctx)
1860+
if err != nil {
1861+
return fmt.Errorf("failed to list applications: %w", err)
1862+
}
1863+
1864+
fmt.Printf("APPLICATIONS (%d)\n", len(applications))
1865+
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
1866+
for _, app := range applications {
1867+
fmt.Printf(" • %s (%s)\n", app.Name, app.Slug)
1868+
if app.Provider != 0 {
1869+
fmt.Printf(" Provider PK: %d", app.Provider)
1870+
if app.ProviderObj.Name != "" {
1871+
fmt.Printf(" (%s)", app.ProviderObj.Name)
1872+
}
1873+
fmt.Println()
1874+
}
1875+
if app.MetaLaunchURL != "" {
1876+
fmt.Printf(" Launch URL: %s\n", app.MetaLaunchURL)
1877+
}
1878+
fmt.Println()
1879+
}
1880+
1881+
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
1882+
1883+
logger.Debug("✓ Authentik configuration export complete")
1884+
return nil
1885+
}

pkg/hecate/debug.go

Lines changed: 1 addition & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ func RunHecateDebug(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string
5555
// Get flags
5656
component, _ := cmd.Flags().GetString("component")
5757
authentikCheck, _ := cmd.Flags().GetBool("authentik")
58-
authentikConfig, _ := cmd.Flags().GetBool("authentik-config")
5958
bionicgptCheck, _ := cmd.Flags().GetBool("bionicgpt")
6059
hecatePath, _ := cmd.Flags().GetString("path")
6160
verbose, _ := cmd.Flags().GetBool("verbose")
@@ -64,7 +63,6 @@ func RunHecateDebug(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string
6463
zap.String("component_filter", component),
6564
zap.String("path", hecatePath),
6665
zap.Bool("authentik_check", authentikCheck),
67-
zap.Bool("authentik_config", authentikConfig),
6866
zap.Bool("bionicgpt_check", bionicgptCheck))
6967

7068
// If --bionicgpt flag is set, run BionicGPT integration diagnostics
@@ -76,12 +74,8 @@ func RunHecateDebug(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string
7674
return RunBionicGPTIntegrationDebug(rc, config)
7775
}
7876

79-
// If --authentik-config flag is set, export Authentik configuration
80-
if authentikConfig {
81-
return runAuthentikConfigExport(rc, hecatePath)
82-
}
83-
8477
// If --authentik flag is set, run comprehensive Authentik check
78+
// Now includes configuration export integrated into the command
8579
// Delegate to pkg/authentik for business logic
8680
if authentikCheck {
8781
config := &authentik.DebugConfig{
@@ -842,60 +836,3 @@ func displayContainerStatus(rc *eos_io.RuntimeContext, hecatePath string) []Heca
842836

843837
// runAuthentikConfigExport exports Authentik configuration for observability
844838
// P0 OBSERVABILITY: Shows users what self-enrollment has configured
845-
func runAuthentikConfigExport(rc *eos_io.RuntimeContext, hecatePath string) error {
846-
logger := otelzap.Ctx(rc.Ctx)
847-
848-
logger.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
849-
logger.Info("Authentik Configuration Export")
850-
logger.Info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
851-
852-
// Read .env file to get API token
853-
envPath := filepath.Join(hecatePath, ".env")
854-
logger.Info("Reading Authentik API token from .env",
855-
zap.String("env_path", envPath))
856-
857-
envFile, err := os.ReadFile(envPath)
858-
if err != nil {
859-
return fmt.Errorf("failed to read .env file: %w\nPath: %s\nEnsure Hecate is installed: eos create hecate", err, envPath)
860-
}
861-
862-
// Parse AUTHENTIK_TOKEN from .env
863-
var apiToken string
864-
for _, line := range strings.Split(string(envFile), "\n") {
865-
line = strings.TrimSpace(line)
866-
if strings.HasPrefix(line, "AUTHENTIK_TOKEN=") {
867-
apiToken = strings.TrimPrefix(line, "AUTHENTIK_TOKEN=")
868-
apiToken = strings.Trim(apiToken, "\"'") // Remove quotes
869-
break
870-
}
871-
}
872-
873-
if apiToken == "" {
874-
return fmt.Errorf("AUTHENTIK_TOKEN not found in .env file\nPath: %s\nEnsure self-enrollment is enabled: eos update hecate --enable self-enrollment", envPath)
875-
}
876-
877-
logger.Info("✓ API token found in .env")
878-
879-
// Authentik URL (localhost:9000 for host access)
880-
authentikURL := fmt.Sprintf("http://%s:%d", AuthentikHost, AuthentikPort)
881-
882-
// Export configuration
883-
report, err := ExportAuthentikConfig(rc, authentikURL, apiToken)
884-
if err != nil {
885-
return fmt.Errorf("failed to export Authentik configuration: %w", err)
886-
}
887-
888-
// Format and display report
889-
formattedReport := FormatAuthentikReport(report)
890-
fmt.Print(formattedReport)
891-
892-
logger.Info("✓ Authentik configuration export complete")
893-
logger.Info("")
894-
logger.Info("NEXT STEPS:")
895-
logger.Info(" • Review brands configuration to verify enrollment flow is set")
896-
logger.Info(" • Check groups to see self-enrolled user permissions")
897-
logger.Info(" • Verify flows contain expected stages")
898-
logger.Info(" • Configure application policies for group access")
899-
900-
return nil
901-
}

0 commit comments

Comments
 (0)