Skip to content

Commit 7deb9d0

Browse files
feat: add default flows deployment for Authentik authentication
- Added new "default-flows" feature to deploy opinionated Authentik 2025.10 authentication flows - Implemented ImportFlow API method to upload flow blueprints via YAML - Added DeleteFlowBySlug helper to safely remove existing flows before updates - Extended CLI flags with --update-existing option to control replacement of existing resources - Updated help text and error messages to document the new default-flows feature The changes add support
1 parent e0338fb commit 7deb9d0

4 files changed

Lines changed: 1050 additions & 27 deletions

File tree

cmd/update/hecate.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ func init() {
211211
updateHecateCmd.Flags().String("add", "", "Add a new service to Hecate (service name)")
212212
updateHecateCmd.Flags().String("remove", "", "Remove a service from Hecate (service name)")
213213
updateHecateCmd.Flags().String("fix", "", "Fix drift/misconfigurations for a service (service name)")
214-
updateHecateCmd.Flags().String("enable", "", "Enable a feature for Hecate (feature name: self-enrollment, oauth2-signout)")
214+
updateHecateCmd.Flags().String("enable", "", "Enable a feature for Hecate (feature name: self-enrollment, oauth2-signout, default-flows)")
215215
updateHecateCmd.Flags().StringP("dns", "d", "", "Domain/subdomain for the service (required with --add)")
216216
updateHecateCmd.Flags().StringP("upstream", "u", "", "Backend address (ip:port or hostname:port, required with --add)")
217217

@@ -223,6 +223,7 @@ func init() {
223223
updateHecateCmd.Flags().Bool("enable-captcha", true, "Enable captcha for self-enrollment (default: true, uses test keys initially)")
224224
updateHecateCmd.Flags().Bool("disable-captcha", false, "Disable captcha protection (NOT RECOMMENDED for production)")
225225
updateHecateCmd.Flags().Bool("require-approval", false, "New users inactive until admin approves (default: active immediately)")
226+
updateHecateCmd.Flags().Bool("update-existing", true, "Replace existing Authentik resources when enabling default flows")
226227

227228
// Optional flags for --add
228229
updateHecateCmd.Flags().Bool("sso", false, "Enable SSO for this route (NOTE: BionicGPT always uses Authentik forward auth regardless of this flag)")
@@ -409,7 +410,9 @@ func runEnableFeature(rc *eos_io.RuntimeContext, cmd *cobra.Command, feature str
409410
return runEnableOAuth2Signout(rc, cmd)
410411
case "self-enrollment":
411412
return runEnableSelfEnrollment(rc, cmd)
413+
case "default-flows":
414+
return runEnableDefaultFlows(rc, cmd)
412415
default:
413-
return fmt.Errorf("unknown feature: %s\n\nAvailable features:\n - oauth2-signout\n - self-enrollment", feature)
416+
return fmt.Errorf("unknown feature: %s\n\nAvailable features:\n - oauth2-signout\n - self-enrollment\n - default-flows", feature)
414417
}
415418
}

cmd/update/hecate_enable.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ OLD SYNTAX (deprecated, still works):
3535
Available features:
3636
oauth2-signout - Add /oauth2/sign_out logout handlers to Authentik-protected routes
3737
self-enrollment - Enable user self-registration via Authentik enrollment flow
38+
default-flows - Deploy opinionated Authentik 2025.10 default flows for an app
3839
3940
The enable command modifies live configuration via APIs (zero-downtime):
4041
- Uses Caddy Admin API to inject route handlers
@@ -75,7 +76,7 @@ Examples (NEW SYNTAX - RECOMMENDED):
7576
case "self-enrollment":
7677
return runEnableSelfEnrollment(rc, cmd)
7778
default:
78-
return fmt.Errorf("unknown feature: %s\n\nAvailable features:\n - oauth2-signout\n - self-enrollment", feature)
79+
return fmt.Errorf("unknown feature: %s\n\nAvailable features:\n - oauth2-signout\n - self-enrollment\n - default-flows", feature)
7980
}
8081
}),
8182
}
@@ -191,3 +192,33 @@ func runEnableSelfEnrollment(rc *eos_io.RuntimeContext, cmd *cobra.Command) erro
191192

192193
return nil
193194
}
195+
196+
// runEnableDefaultFlows deploys the opinionated Authentik default flows.
197+
func runEnableDefaultFlows(rc *eos_io.RuntimeContext, cmd *cobra.Command) error {
198+
logger := otelzap.Ctx(rc.Ctx)
199+
200+
appName, _ := cmd.Flags().GetString("app")
201+
domain, _ := cmd.Flags().GetString("dns")
202+
dryRun, _ := cmd.Flags().GetBool("dry-run")
203+
updateExisting, _ := cmd.Flags().GetBool("update-existing")
204+
205+
if appName == "" {
206+
appName = hecate.BionicGPTApplicationSlug
207+
logger.Info("No --app provided, defaulting to", zap.String("app", appName))
208+
}
209+
210+
logger.Info("Enabling Authentik default flows",
211+
zap.String("app", appName),
212+
zap.String("domain", domain),
213+
zap.Bool("dry_run", dryRun),
214+
zap.Bool("replace_existing", updateExisting))
215+
216+
config := &hecate.DefaultFlowsConfig{
217+
App: appName,
218+
Domain: domain,
219+
DryRun: dryRun,
220+
UpdateExisting: updateExisting,
221+
}
222+
223+
return hecate.EnableDefaultFlows(rc, config)
224+
}

pkg/authentik/flows.go

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/json"
99
"fmt"
1010
"io"
11+
"mime/multipart"
1112
"net/http"
1213
)
1314

@@ -163,6 +164,83 @@ func (c *APIClient) UpdateFlow(ctx context.Context, pk string, updates map[strin
163164
return nil
164165
}
165166

167+
// DeleteFlow removes a flow by PK.
168+
func (c *APIClient) DeleteFlow(ctx context.Context, pk string) error {
169+
url := fmt.Sprintf("%s/api/v3/flows/instances/%s/", c.BaseURL, pk)
170+
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
171+
if err != nil {
172+
return fmt.Errorf("failed to create request: %w", err)
173+
}
174+
175+
req.Header.Set("Authorization", "Bearer "+c.Token)
176+
177+
resp, err := c.HTTPClient.Do(req)
178+
if err != nil {
179+
return fmt.Errorf("flow deletion request failed: %w", err)
180+
}
181+
defer func() { _ = resp.Body.Close() }()
182+
183+
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound {
184+
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
185+
return fmt.Errorf("flow deletion failed with status %d: %s", resp.StatusCode, string(body))
186+
}
187+
188+
return nil
189+
}
190+
191+
// DeleteFlowBySlug removes a flow if it exists.
192+
func (c *APIClient) DeleteFlowBySlug(ctx context.Context, slug string) error {
193+
flow, err := c.GetFlow(ctx, slug)
194+
if err != nil {
195+
return err
196+
}
197+
if flow == nil {
198+
return nil
199+
}
200+
return c.DeleteFlow(ctx, flow.PK)
201+
}
202+
203+
// ImportFlow uploads a blueprint YAML definition for a flow.
204+
func (c *APIClient) ImportFlow(ctx context.Context, yaml []byte) error {
205+
url := fmt.Sprintf("%s/api/v3/flows/instances/import/", c.BaseURL)
206+
207+
body := &bytes.Buffer{}
208+
writer := multipart.NewWriter(body)
209+
part, err := writer.CreateFormFile("file", "flow.yaml")
210+
if err != nil {
211+
return fmt.Errorf("failed to create multipart form: %w", err)
212+
}
213+
214+
if _, err := part.Write(yaml); err != nil {
215+
return fmt.Errorf("failed to write flow blueprint: %w", err)
216+
}
217+
218+
if err := writer.Close(); err != nil {
219+
return fmt.Errorf("failed to finalize multipart form: %w", err)
220+
}
221+
222+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
223+
if err != nil {
224+
return fmt.Errorf("failed to create request: %w", err)
225+
}
226+
227+
req.Header.Set("Authorization", "Bearer "+c.Token)
228+
req.Header.Set("Content-Type", writer.FormDataContentType())
229+
230+
resp, err := c.HTTPClient.Do(req)
231+
if err != nil {
232+
return fmt.Errorf("flow import request failed: %w", err)
233+
}
234+
defer func() { _ = resp.Body.Close() }()
235+
236+
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
237+
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
238+
return fmt.Errorf("flow import failed with status %d: %s", resp.StatusCode, string(body))
239+
}
240+
241+
return nil
242+
}
243+
166244
// CreateEnrollmentFlow creates a new enrollment flow
167245
func (c *APIClient) CreateEnrollmentFlow(ctx context.Context, name, slug, title string) (*FlowResponse, error) {
168246
reqBody := map[string]interface{}{
@@ -211,30 +289,6 @@ func (c *APIClient) CreateEnrollmentFlow(ctx context.Context, name, slug, title
211289
return &flow, nil
212290
}
213291

214-
// DeleteFlow deletes a flow by PK
215-
func (c *APIClient) DeleteFlow(ctx context.Context, pk string) error {
216-
url := fmt.Sprintf("%s/api/v3/flows/instances/%s/", c.BaseURL, pk)
217-
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
218-
if err != nil {
219-
return fmt.Errorf("failed to create request: %w", err)
220-
}
221-
222-
req.Header.Set("Authorization", "Bearer "+c.Token)
223-
224-
resp, err := c.HTTPClient.Do(req)
225-
if err != nil {
226-
return fmt.Errorf("flow deletion request failed: %w", err)
227-
}
228-
defer func() { _ = resp.Body.Close() }()
229-
230-
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
231-
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
232-
return fmt.Errorf("flow deletion failed with status %d: %s", resp.StatusCode, string(body))
233-
}
234-
235-
return nil
236-
}
237-
238292
// GetFlowStages retrieves all stage bindings for a flow (ordered by binding order)
239293
// Returns stages in execution order for the given flow
240294
func (c *APIClient) GetFlowStages(ctx context.Context, flowPK string) ([]StageBindingResponse, error) {

0 commit comments

Comments
 (0)