Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/dokku_http_auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# dokku_http_auth

Manages HTTP authentication for a given dokku application

## Enable HTTP authentication for an app

```yaml
dokku_http_auth:
app: hello-world
username: admin
password: secret
```

## Disable HTTP authentication for an app

```yaml
dokku_http_auth:
app: hello-world
state: absent
```
204 changes: 204 additions & 0 deletions tasks/http_auth_task.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package tasks

import (
"fmt"
"omakase/subprocess"
"strings"

yaml "gopkg.in/yaml.v3"
)

// HttpAuthTask manages HTTP authentication for a dokku application
type HttpAuthTask struct {
// App is the name of the app
App string `required:"true" yaml:"app"`

// Username is the HTTP auth username
Username string `required:"false" yaml:"username,omitempty"`

// Password is the HTTP auth password
Password string `required:"false" yaml:"password,omitempty"`

// State is the state of the HTTP auth
State State `required:"false" yaml:"state,omitempty" default:"present" options:"present,absent"`
}

// HttpAuthTaskExample contains an example of an HttpAuthTask
type HttpAuthTaskExample struct {
// Name is the task name holding the HttpAuthTask description
Name string `yaml:"-"`

// DokkuHttpAuth is the HttpAuthTask configuration
DokkuHttpAuth HttpAuthTask `yaml:"dokku_http_auth"`
}

// DesiredState returns the desired state of the HTTP auth
func (t HttpAuthTask) DesiredState() State {
return t.State
}

// Doc returns the docblock for the HTTP auth task
func (t HttpAuthTask) Doc() string {
return "Manages HTTP authentication for a given dokku application"
}

// Examples returns a list of HttpAuthTaskExamples as yaml
func (t HttpAuthTask) Examples() ([]Doc, error) {
examples := []HttpAuthTaskExample{
{
Name: "Enable HTTP authentication for an app",
DokkuHttpAuth: HttpAuthTask{
App: "hello-world",
Username: "admin",
Password: "secret",
},
},
{
Name: "Disable HTTP authentication for an app",
DokkuHttpAuth: HttpAuthTask{
App: "hello-world",
State: "absent",
},
},
}

var output []Doc
for _, example := range examples {
b, err := yaml.Marshal(example)
if err != nil {
return nil, err
}

output = append(output, Doc{
Name: example.Name,
Codeblock: string(b),
})
}

return output, nil
}

// Execute enables or disables HTTP authentication for an app
func (t HttpAuthTask) Execute() TaskOutputState {
if t.State == StatePresent && t.Username == "" {
return TaskOutputState{
Error: fmt.Errorf("username is required when state is present"),
}
}
if t.State == StatePresent && t.Password == "" {
return TaskOutputState{
Error: fmt.Errorf("password is required when state is present"),
}
}

funcMap := map[State]func() TaskOutputState{
"present": func() TaskOutputState {
return enableHttpAuth(t.App, t.Username, t.Password)
},
"absent": func() TaskOutputState {
return disableHttpAuth(t.App)
},
}

fn, ok := funcMap[t.State]
if !ok {
return TaskOutputState{
Error: fmt.Errorf("invalid state: %s", t.State),
}
}
return fn()
}

// httpAuthEnabled checks if HTTP authentication is enabled for an app
func httpAuthEnabled(appName string) bool {
result, err := subprocess.CallExecCommand(subprocess.ExecCommandInput{
Command: "dokku",
Args: []string{
"--quiet",
"http-auth:report",
appName,
},
})
if err != nil {
return false
}

lines := strings.SplitN(result.StdoutContents(), "\n", 2)
if len(lines) == 0 {
return false
}

parts := strings.SplitN(lines[0], ":", 2)
if len(parts) < 2 {
return false
}

return strings.TrimSpace(parts[1]) == "true"
}

// enableHttpAuth enables HTTP authentication for an app
func enableHttpAuth(app, username, password string) TaskOutputState {
state := TaskOutputState{
Changed: false,
State: "absent",
}
if httpAuthEnabled(app) {
state.State = "present"
return state
}

result, err := subprocess.CallExecCommand(subprocess.ExecCommandInput{
Command: "dokku",
Args: []string{
"--quiet",
"http-auth:on",
app,
username,
password,
},
})
if err != nil {
state.Error = err
state.Message = result.StderrContents()
return state
}

state.Changed = true
state.State = "present"
return state
}

// disableHttpAuth disables HTTP authentication for an app
func disableHttpAuth(app string) TaskOutputState {
state := TaskOutputState{
Changed: false,
State: "present",
}
if !httpAuthEnabled(app) {
state.State = "absent"
return state
}

result, err := subprocess.CallExecCommand(subprocess.ExecCommandInput{
Command: "dokku",
Args: []string{
"--quiet",
"http-auth:off",
app,
},
})
if err != nil {
state.Error = err
state.Message = result.StderrContents()
return state
}

state.Changed = true
state.State = "absent"
return state
}

// init registers the HttpAuthTask with the task registry
func init() {
RegisterTask(&HttpAuthTask{})
}
79 changes: 79 additions & 0 deletions tasks/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1717,3 +1717,82 @@ func TestIntegrationServiceLinkAndUnlink(t *testing.T) {
t.Errorf("expected state 'absent', got '%s'", result.State)
}
}

func TestIntegrationHttpAuth(t *testing.T) {
skipIfNoDokkuT(t)
skipIfPluginMissingT(t, "http-auth")

appName := "omakase-test-http-auth"

destroyApp(appName)
createApp(appName)
defer destroyApp(appName)

// enable http auth
enableTask := HttpAuthTask{
App: appName,
Username: "testuser",
Password: "testpass",
State: StatePresent,
}
result := enableTask.Execute()
if result.Error != nil {
t.Fatalf("failed to enable http auth: %v", result.Error)
}
if result.State != StatePresent {
t.Errorf("expected state 'present', got '%s'", result.State)
}
if !result.Changed {
t.Error("expected changed=true for enabling http auth")
}

// verify auth is enabled via http-auth:report
if !httpAuthEnabled(appName) {
t.Error("expected http auth to be enabled after enable")
}

// enabling again should be idempotent
result = enableTask.Execute()
if result.Error != nil {
t.Fatalf("idempotent enable failed: %v", result.Error)
}
if result.Changed {
t.Error("expected changed=false for already-enabled http auth")
}
if result.State != StatePresent {
t.Errorf("expected state 'present', got '%s'", result.State)
}

// disable http auth
disableTask := HttpAuthTask{
App: appName,
State: StateAbsent,
}
result = disableTask.Execute()
if result.Error != nil {
t.Fatalf("failed to disable http auth: %v", result.Error)
}
if result.State != StateAbsent {
t.Errorf("expected state 'absent', got '%s'", result.State)
}
if !result.Changed {
t.Error("expected changed=true for disabling http auth")
}

// verify auth is disabled via http-auth:report
if httpAuthEnabled(appName) {
t.Error("expected http auth to be disabled after disable")
}

// disabling again should be idempotent
result = disableTask.Execute()
if result.Error != nil {
t.Fatalf("idempotent disable failed: %v", result.Error)
}
if result.Changed {
t.Error("expected changed=false for already-disabled http auth")
}
if result.State != StateAbsent {
t.Errorf("expected state 'absent', got '%s'", result.State)
}
}
45 changes: 45 additions & 0 deletions tasks/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func TestRegisteredTasksExist(t *testing.T) {
"dokku_domains_toggle",
"dokku_git_from_image",
"dokku_git_sync",
"dokku_http_auth",
"dokku_network",
"dokku_network_property",
"dokku_ports",
Expand Down Expand Up @@ -979,3 +980,47 @@ func TestGetTasksDomainsTaskGlobalParsedCorrectly(t *testing.T) {
t.Errorf("expected state 'set', got %q", dTask.DesiredState())
}
}

func TestGetTasksHttpAuthTaskParsedCorrectly(t *testing.T) {
data := []byte(`---
- tasks:
- name: enable http auth
dokku_http_auth:
app: test-app
username: admin
password: secret
`)
context := map[string]interface{}{}

tasks, err := GetTasks(data, context)
if err != nil {
t.Fatalf("GetTasks failed: %v", err)
}

task := tasks.Get("enable http auth")
if task == nil {
t.Fatal("task 'enable http auth' not found")
}

haTask, ok := task.(*HttpAuthTask)
if !ok {
ht, ok2 := task.(HttpAuthTask)
if !ok2 {
t.Fatalf("task is not an HttpAuthTask (type is %T)", task)
}
haTask = &ht
}

if haTask.App != "test-app" {
t.Errorf("App = %q, want %q", haTask.App, "test-app")
}
if haTask.Username != "admin" {
t.Errorf("Username = %q, want %q", haTask.Username, "admin")
}
if haTask.Password != "secret" {
t.Errorf("Password = %q, want %q", haTask.Password, "secret")
}
if haTask.DesiredState() != StatePresent {
t.Errorf("expected default state 'present', got %q", haTask.DesiredState())
}
}
Loading
Loading