mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-04 04:34:07 +09:00
types: reject non-boolean values for boolean settings
This commit is contained in:
1 parent
14baf55421
commit
f2ecf949a7
2 files changed
+208
No files matched your search
@@ -715,10 +715,55 @@ func validateServerConfig() error {
|
||||
return v.Err()
|
||||
}
|
||||
|
||||
var boolConfigKeys = []string{
|
||||
"auto_update.enabled",
|
||||
"cli.insecure",
|
||||
"database.debug",
|
||||
"database.gorm.parameterized_queries",
|
||||
"database.gorm.skip_err_record_not_found",
|
||||
"database.sqlite.write_ahead_log",
|
||||
"derp.auto_update_enabled",
|
||||
"derp.server.automatically_add_embedded_derp_region",
|
||||
"derp.server.enabled",
|
||||
"derp.server.verify_clients",
|
||||
"disable_check_updates",
|
||||
"dns.certificates.enabled",
|
||||
"dns.magic_dns",
|
||||
"dns.override_local_dns",
|
||||
"logtail.enabled",
|
||||
"oidc.email_verified_required",
|
||||
"oidc.only_start_if_oidc_is_available",
|
||||
"oidc.pkce.enabled",
|
||||
"oidc.use_expiry_from_token",
|
||||
"taildrop.enabled",
|
||||
}
|
||||
|
||||
// Validate raw values before GetBool can discard conversion errors.
|
||||
func validateBoolConfig(v *configValidator, keys ...string) {
|
||||
for _, key := range keys {
|
||||
value := viper.Get(key)
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := strconv.ParseBool(fmt.Sprint(value))
|
||||
if err != nil {
|
||||
v.Add(&ConfigError{
|
||||
Reason: key + " is not a boolean",
|
||||
Current: []KV{{key, value}},
|
||||
Hint: "use true or false",
|
||||
Cause: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateServerConfigInto runs every viper-level validation rule
|
||||
// against the provided validator. Caller owns the validator and is
|
||||
// responsible for inspecting v.Err() once all sub-builders have run.
|
||||
func validateServerConfigInto(v *configValidator) {
|
||||
validateBoolConfig(v, boolConfigKeys...)
|
||||
|
||||
depr := deprecator{seen: make(set.Set[string])}
|
||||
|
||||
// Register aliases for backward compatibility
|
||||
@@ -1468,6 +1513,14 @@ func trustedProxies() ([]netip.Prefix, error) {
|
||||
// LoadCLIConfig returns the needed configuration for the CLI client
|
||||
// of Headscale to connect to a Headscale server.
|
||||
func LoadCLIConfig() (*Config, error) {
|
||||
v := &configValidator{}
|
||||
validateBoolConfig(v, "cli.insecure", "disable_check_updates")
|
||||
|
||||
err := v.Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logConfig := logConfig()
|
||||
zerolog.SetGlobalLevel(logConfig.Level)
|
||||
|
||||
|
||||
@@ -4,11 +4,15 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/fs"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -1672,3 +1676,154 @@ func TestServesRoute(t *testing.T) {
|
||||
narrow := netip.MustParsePrefix("100.64.0.0/24")
|
||||
assert.True(t, (&Config{PrefixV4: &narrow}).ServesRoute(netip.MustParsePrefix("100.100.0.1/32")))
|
||||
}
|
||||
|
||||
func TestBoolConfigValidation(t *testing.T) {
|
||||
for _, source := range []string{"file", "env"} {
|
||||
for _, tt := range []struct {
|
||||
value string
|
||||
want bool
|
||||
invalid bool
|
||||
}{
|
||||
{"true", true, false},
|
||||
{"false", false, false},
|
||||
{"1", true, false},
|
||||
{"0", false, false},
|
||||
{"T", true, false},
|
||||
{"f", false, false},
|
||||
{"TRUE", true, false},
|
||||
{"False", false, false},
|
||||
{"True", true, false},
|
||||
{"FALSE", false, false},
|
||||
{"", true, false},
|
||||
{"null", true, false},
|
||||
{"yes", false, true},
|
||||
{"on", false, true},
|
||||
{"ture", false, true},
|
||||
{"tRuE", false, true},
|
||||
{"2", false, true},
|
||||
{"[true]", false, true},
|
||||
{`""`, false, true},
|
||||
} {
|
||||
if source == "env" && tt.value == "null" {
|
||||
continue
|
||||
}
|
||||
|
||||
t.Run(source+"/"+tt.value, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
t.Cleanup(viper.Reset)
|
||||
t.Setenv("HEADSCALE_OIDC_EMAIL_VERIFIED_REQUIRED", "")
|
||||
|
||||
config := `server_url: https://headscale.example.com
|
||||
noise:
|
||||
private_key_path: noise_private.key
|
||||
prefixes:
|
||||
v4: 100.64.0.0/10
|
||||
database:
|
||||
type: sqlite3
|
||||
dns:
|
||||
magic_dns: false
|
||||
override_local_dns: false
|
||||
`
|
||||
if source == "file" && tt.value != "" {
|
||||
config += "oidc:\n email_verified_required: " + tt.value + "\n"
|
||||
}
|
||||
|
||||
if source == "env" {
|
||||
t.Setenv("HEADSCALE_OIDC_EMAIL_VERIFIED_REQUIRED", tt.value)
|
||||
}
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(config), 0o600))
|
||||
require.NoError(t, LoadConfig(path, true))
|
||||
|
||||
loaded, err := LoadServerConfig()
|
||||
if tt.invalid {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "oidc.email_verified_required")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, loaded.OIDC.EmailVerifiedRequired)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoolConfigKeysComplete(t *testing.T) {
|
||||
file, err := parser.ParseFile(token.NewFileSet(), "config.go", nil, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys := make(map[string]bool)
|
||||
|
||||
ast.Inspect(file, func(node ast.Node) bool {
|
||||
call, ok := node.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
selector, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok || selector.Sel.Name != "GetBool" {
|
||||
return true
|
||||
}
|
||||
|
||||
object, ok := selector.X.(*ast.Ident)
|
||||
if !ok || object.Name != "viper" {
|
||||
return true
|
||||
}
|
||||
|
||||
require.Len(t, call.Args, 1)
|
||||
literal, ok := call.Args[0].(*ast.BasicLit)
|
||||
require.True(t, ok, "boolean config keys must be string literals")
|
||||
|
||||
key, err := strconv.Unquote(literal.Value)
|
||||
require.NoError(t, err)
|
||||
|
||||
keys[key] = true
|
||||
|
||||
return true
|
||||
})
|
||||
require.NotEmpty(t, keys)
|
||||
|
||||
for _, key := range boolConfigKeys {
|
||||
assert.Contains(t, keys, key, "unused or repeated boolean config key")
|
||||
delete(keys, key)
|
||||
}
|
||||
|
||||
assert.Empty(t, keys, "GetBool keys missing from boolean validation")
|
||||
}
|
||||
|
||||
func TestExampleConfigBooleanSettings(t *testing.T) {
|
||||
viper.Reset()
|
||||
t.Cleanup(viper.Reset)
|
||||
require.NoError(t, LoadConfig("../../config-example.yaml", true))
|
||||
|
||||
_, err := LoadServerConfig()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoadCLIConfigRejectsNonBoolean(t *testing.T) {
|
||||
for _, key := range []string{"cli.insecure", "disable_check_updates"} {
|
||||
t.Run(key, func(t *testing.T) {
|
||||
viper.Reset()
|
||||
t.Cleanup(viper.Reset)
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
require.NoError(t, os.WriteFile(path, nil, 0o600))
|
||||
require.NoError(t, LoadConfig(path, true))
|
||||
|
||||
env := "HEADSCALE_" + strings.ToUpper(strings.ReplaceAll(key, ".", "_"))
|
||||
t.Setenv(env, "true")
|
||||
|
||||
_, err := LoadCLIConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Setenv(env, "ture")
|
||||
|
||||
_, err = LoadCLIConfig()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), key)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user