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
6 changes: 4 additions & 2 deletions cmd/shared/configloader/configloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ func Load(path string) (*authulamodels.Config, bool, error) {
return nil, false, fmt.Errorf("read config file: %w", err)
}

var loaded authulamodels.Config
if err := toml.Unmarshal(data, &loaded); err != nil {
// Decode on top of the default config to ensure that all fields
// are populated with defaults if not specified in the file.
loaded := authulaconfig.NewDefaultConfig()
if err := toml.Unmarshal(data, loaded); err != nil {
return nil, true, fmt.Errorf("failed to unmarshal config: %w", err)
}

Expand Down
76 changes: 76 additions & 0 deletions cmd/shared/configloader/configloader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package configloader

import (
"os"
"path/filepath"
"testing"

"github.com/Authula/authula/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLoadKeepsDefaultsForOmittedKeys(t *testing.T) {
tests := []struct {
name string
file string
wantHttpOnly bool
wantMaxSessions int
wantAllowCredentials bool
wantAllowedOrigins []string
}{
{
name: "file without a session or security section",
file: util.Dedent(`
app_name = "demo"
`),
wantHttpOnly: true,
wantMaxSessions: 5,
wantAllowCredentials: true,
wantAllowedOrigins: []string{"*"},
},
{
name: "session section that sets only the cookie name",
file: util.Dedent(`
[session]
cookie_name = "sid"
`),
wantHttpOnly: true,
wantMaxSessions: 5,
wantAllowCredentials: true,
wantAllowedOrigins: []string{"*"},
},
{
name: "explicit values still win",
file: util.Dedent(`
[session]
http_only = false
max_sessions_per_user = 2

[security.cors]
allow_credentials = false
allowed_origins = ["https://example.com"]
`),
wantHttpOnly: false,
wantMaxSessions: 2,
wantAllowCredentials: false,
wantAllowedOrigins: []string{"https://example.com"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.toml")
require.NoError(t, os.WriteFile(path, []byte(tt.file), 0o600))

config, exists, err := Load(path)
require.NoError(t, err)
require.True(t, exists)

assert.Equal(t, tt.wantHttpOnly, config.Session.HttpOnly, "session.http_only")
assert.Equal(t, tt.wantMaxSessions, config.Session.MaxSessionsPerUser, "session.max_sessions_per_user")
assert.Equal(t, tt.wantAllowCredentials, config.Security.CORS.AllowCredentials, "security.cors.allow_credentials")
assert.Equal(t, tt.wantAllowedOrigins, config.Security.CORS.AllowedOrigins, "security.cors.allowed_origins")
})
}
}
16 changes: 11 additions & 5 deletions config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ const defaultSecret = "authula-secret-0123456789"

type ConfigOption func(*models.Config)

// NewConfig builds a Config using functional options with sensible defaults.
// Panics if event bus configuration is invalid or if required secrets are missing in production.
func NewConfig(options ...ConfigOption) *models.Config {
// Define sensible defaults first
config := &models.Config{
// NewDefaultConfig returns the default configuration, without applying
// options or validating. A config file is decoded on top of it, so a key the
// file leaves out keeps its default instead of becoming the zero value.
func NewDefaultConfig() *models.Config {
return &models.Config{
AppName: "Authula",
BaseURL: "http://localhost:8080",
BasePath: "/auth",
Expand Down Expand Up @@ -74,6 +74,12 @@ func NewConfig(options ...ConfigOption) *models.Config {
PreParsedConfigs: make(map[string]any),
CoreServiceHooks: nil,
}
}

// NewConfig builds a Config using functional options with sensible defaults.
// Panics if certain required fields are missing or invalid.
func NewConfig(options ...ConfigOption) *models.Config {
config := NewDefaultConfig()

// Apply the options - they override defaults only if non-zero/non-empty
for _, option := range options {
Expand Down
28 changes: 28 additions & 0 deletions util/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,31 @@ func NormalizeRoutePattern(pattern string) string {
}
return strings.ReplaceAll(trimmed, "//", "/")
}

// Dedent removes common leading whitespace from each line in a multi-line string.
func Dedent(s string) string {
lines := strings.Split(strings.Trim(s, "\n"), "\n")

min := -1
for _, line := range lines {
trimmed := strings.TrimLeft(line, " \t")
if trimmed == "" {
continue
}
indent := len(line) - len(trimmed)
if min < 0 || indent < min {
min = indent
}
}
if min > 0 {
for i, line := range lines {
if strings.TrimLeft(line, " \t") == "" {
lines[i] = ""
continue
}
lines[i] = line[min:]
}
}

return strings.Join(lines, "\n") + "\n"
}
Loading