primeiro commit
This commit is contained in:
153
internal/config/config.go
Normal file
153
internal/config/config.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Addr string `yaml:"addr" env:"ADDR" env-default:":8080"`
|
||||
PDNSAPIURL string `yaml:"pdns_api_url" env:"PDNS_API_URL" env-default:"http://localhost:8081"`
|
||||
PDNSAPIKey string `yaml:"pdns_api_key" env:"PDNS_API_KEY"`
|
||||
PDNSServerID string `yaml:"pdns_server_id" env:"PDNS_SERVER_ID" env-default:"localhost"`
|
||||
Auth AuthConfig `yaml:"auth"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
Disabled bool `yaml:"disabled" env:"AUTH_DISABLED"`
|
||||
LDAP LDAPConfig `yaml:"ldap"`
|
||||
}
|
||||
|
||||
type LDAPConfig struct {
|
||||
URL string `yaml:"url" env:"AUTH_LDAP_URL"`
|
||||
StartTLS bool `yaml:"start_tls" env:"AUTH_LDAP_START_TLS"`
|
||||
InsecureSkipVerify bool `yaml:"insecure_skip_verify" env:"AUTH_LDAP_INSECURE_SKIP_VERIFY"`
|
||||
BindDN string `yaml:"bind_dn" env:"AUTH_LDAP_BIND_DN"`
|
||||
BindPassword string `yaml:"bind_password" env:"AUTH_LDAP_BIND_PASSWORD"`
|
||||
UserBaseDN string `yaml:"user_base_dn" env:"AUTH_LDAP_USER_BASE_DN"`
|
||||
UsernameAttribute string `yaml:"username_attribute" env:"AUTH_LDAP_USERNAME_ATTRIBUTE" env-default:"uid"`
|
||||
UserFilter string `yaml:"user_filter" env:"AUTH_LDAP_USER_FILTER" env-default:"({username_attribute}={username})"`
|
||||
GroupBaseDN string `yaml:"group_base_dn" env:"AUTH_LDAP_GROUP_BASE_DN"`
|
||||
GroupFilter string `yaml:"group_filter" env:"AUTH_LDAP_GROUP_FILTER"`
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
path := strings.TrimSpace(os.Getenv("CONFIG_FILE"))
|
||||
if path == "" {
|
||||
path = defaultConfigPath()
|
||||
}
|
||||
|
||||
return LoadFile(path)
|
||||
}
|
||||
|
||||
func LoadFile(path string) (Config, error) {
|
||||
var cfg Config
|
||||
|
||||
var err error
|
||||
if strings.TrimSpace(path) == "" {
|
||||
err = cleanenv.ReadEnv(&cfg)
|
||||
} else {
|
||||
err = cleanenv.ReadConfig(path, &cfg)
|
||||
}
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
normalize(&cfg)
|
||||
if cfg.PDNSAPIKey == "" {
|
||||
return Config{}, errors.New("PDNS_API_KEY is required")
|
||||
}
|
||||
if err := validateAuth(cfg.Auth); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func defaultConfigPath() string {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
homeDir = ""
|
||||
}
|
||||
|
||||
return firstExistingConfigPath(configSearchPaths(homeDir, runtime.GOOS))
|
||||
}
|
||||
|
||||
func configSearchPaths(homeDir, goos string) []string {
|
||||
paths := make([]string, 0, 3)
|
||||
if isUnixLike(goos) {
|
||||
paths = append(paths, "/etc/pdns_admin/config.yaml")
|
||||
}
|
||||
if homeDir != "" {
|
||||
paths = append(paths, filepath.Join(homeDir, "config.yaml"))
|
||||
}
|
||||
return append(paths, "config.yaml")
|
||||
}
|
||||
|
||||
func isUnixLike(goos string) bool {
|
||||
switch goos {
|
||||
case "aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos", "ios", "linux", "netbsd", "openbsd", "solaris":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func firstExistingConfigPath(paths []string) string {
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalize(cfg *Config) {
|
||||
cfg.Addr = strings.TrimSpace(cfg.Addr)
|
||||
cfg.PDNSAPIURL = strings.TrimSpace(cfg.PDNSAPIURL)
|
||||
cfg.PDNSAPIKey = strings.TrimSpace(cfg.PDNSAPIKey)
|
||||
cfg.PDNSServerID = strings.TrimSpace(cfg.PDNSServerID)
|
||||
cfg.Auth.LDAP.URL = strings.TrimSpace(cfg.Auth.LDAP.URL)
|
||||
cfg.Auth.LDAP.BindDN = strings.TrimSpace(cfg.Auth.LDAP.BindDN)
|
||||
cfg.Auth.LDAP.BindPassword = strings.TrimSpace(cfg.Auth.LDAP.BindPassword)
|
||||
cfg.Auth.LDAP.UserBaseDN = strings.TrimSpace(cfg.Auth.LDAP.UserBaseDN)
|
||||
cfg.Auth.LDAP.UsernameAttribute = strings.TrimSpace(cfg.Auth.LDAP.UsernameAttribute)
|
||||
cfg.Auth.LDAP.UserFilter = strings.TrimSpace(cfg.Auth.LDAP.UserFilter)
|
||||
cfg.Auth.LDAP.GroupBaseDN = strings.TrimSpace(cfg.Auth.LDAP.GroupBaseDN)
|
||||
cfg.Auth.LDAP.GroupFilter = strings.TrimSpace(cfg.Auth.LDAP.GroupFilter)
|
||||
}
|
||||
|
||||
func validateAuth(auth AuthConfig) error {
|
||||
if auth.Disabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
missing := make([]string, 0)
|
||||
for field, value := range map[string]string{
|
||||
"AUTH_LDAP_URL": auth.LDAP.URL,
|
||||
"AUTH_LDAP_BIND_DN": auth.LDAP.BindDN,
|
||||
"AUTH_LDAP_BIND_PASSWORD": auth.LDAP.BindPassword,
|
||||
"AUTH_LDAP_USER_BASE_DN": auth.LDAP.UserBaseDN,
|
||||
"AUTH_LDAP_USERNAME_ATTRIBUTE": auth.LDAP.UsernameAttribute,
|
||||
"AUTH_LDAP_USER_FILTER": auth.LDAP.UserFilter,
|
||||
} {
|
||||
if value == "" {
|
||||
missing = append(missing, field)
|
||||
}
|
||||
}
|
||||
if auth.LDAP.GroupFilter != "" && auth.LDAP.GroupBaseDN == "" {
|
||||
missing = append(missing, "AUTH_LDAP_GROUP_BASE_DN")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
sort.Strings(missing)
|
||||
return errors.New("LDAP auth is enabled; missing required configuration: " + strings.Join(missing, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
285
internal/config/config_test.go
Normal file
285
internal/config/config_test.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadFileReadsYAML(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
path := writeConfig(t, `
|
||||
addr: ":9000"
|
||||
pdns_api_url: "http://pdns.example.test:8081"
|
||||
pdns_api_key: "from-file"
|
||||
pdns_server_id: "authoritative"
|
||||
auth:
|
||||
disabled: true
|
||||
`)
|
||||
|
||||
cfg, err := LoadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFile returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Addr != ":9000" {
|
||||
t.Fatalf("unexpected addr: %q", cfg.Addr)
|
||||
}
|
||||
if cfg.PDNSAPIURL != "http://pdns.example.test:8081" {
|
||||
t.Fatalf("unexpected api url: %q", cfg.PDNSAPIURL)
|
||||
}
|
||||
if cfg.PDNSAPIKey != "from-file" {
|
||||
t.Fatalf("unexpected api key: %q", cfg.PDNSAPIKey)
|
||||
}
|
||||
if cfg.PDNSServerID != "authoritative" {
|
||||
t.Fatalf("unexpected server id: %q", cfg.PDNSServerID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileEnvironmentOverridesYAML(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
t.Setenv("PDNS_API_KEY", "from-env")
|
||||
t.Setenv("PDNS_API_URL", "http://env.example.test:8081")
|
||||
path := writeConfig(t, `
|
||||
pdns_api_url: "http://file.example.test:8081"
|
||||
pdns_api_key: "from-file"
|
||||
pdns_server_id: "from-file"
|
||||
auth:
|
||||
disabled: true
|
||||
`)
|
||||
|
||||
cfg, err := LoadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFile returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.PDNSAPIKey != "from-env" {
|
||||
t.Fatalf("expected env api key to win, got %q", cfg.PDNSAPIKey)
|
||||
}
|
||||
if cfg.PDNSAPIURL != "http://env.example.test:8081" {
|
||||
t.Fatalf("expected env api url to win, got %q", cfg.PDNSAPIURL)
|
||||
}
|
||||
if cfg.PDNSServerID != "from-file" {
|
||||
t.Fatalf("expected yaml server id, got %q", cfg.PDNSServerID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileUsesDefaultsWithoutYAML(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
t.Setenv("PDNS_API_KEY", "secret")
|
||||
t.Setenv("AUTH_DISABLED", "true")
|
||||
|
||||
cfg, err := LoadFile("")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFile returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Addr != ":8080" {
|
||||
t.Fatalf("unexpected default addr: %q", cfg.Addr)
|
||||
}
|
||||
if cfg.PDNSAPIURL != "http://localhost:8081" {
|
||||
t.Fatalf("unexpected default api url: %q", cfg.PDNSAPIURL)
|
||||
}
|
||||
if cfg.PDNSServerID != "localhost" {
|
||||
t.Fatalf("unexpected default server id: %q", cfg.PDNSServerID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileRequiresAPIKey(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
|
||||
_, err := LoadFile("")
|
||||
if err == nil {
|
||||
t.Fatal("expected missing api key error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileReadsLDAPConfig(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
path := writeConfig(t, `
|
||||
pdns_api_key: "secret"
|
||||
auth:
|
||||
ldap:
|
||||
url: ldap://ldap.example.com:389
|
||||
start_tls: true
|
||||
insecure_skip_verify: true
|
||||
bind_dn: cn=dashboard-reader,ou=service,dc=example,dc=com
|
||||
bind_password: change-me
|
||||
user_base_dn: ou=users,dc=example,dc=com
|
||||
username_attribute: uid
|
||||
user_filter: "({username_attribute}={username})"
|
||||
group_base_dn: ou=groups,dc=example,dc=com
|
||||
group_filter: "(&(objectClass=groupOfNames)(cn=media-admins)(member={user_dn}))"
|
||||
`)
|
||||
|
||||
cfg, err := LoadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFile returned error: %v", err)
|
||||
}
|
||||
if cfg.Auth.Disabled {
|
||||
t.Fatal("auth should be enabled")
|
||||
}
|
||||
if cfg.Auth.LDAP.URL != "ldap://ldap.example.com:389" {
|
||||
t.Fatalf("unexpected ldap url: %q", cfg.Auth.LDAP.URL)
|
||||
}
|
||||
if !cfg.Auth.LDAP.StartTLS {
|
||||
t.Fatal("expected start_tls to be true")
|
||||
}
|
||||
if !cfg.Auth.LDAP.InsecureSkipVerify {
|
||||
t.Fatal("expected insecure_skip_verify to be true")
|
||||
}
|
||||
if cfg.Auth.LDAP.GroupFilter == "" {
|
||||
t.Fatal("expected group filter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileLDAPEnvironmentOverridesYAML(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
t.Setenv("AUTH_LDAP_URL", "ldap://env.example.com:389")
|
||||
t.Setenv("AUTH_LDAP_BIND_PASSWORD", "from-env")
|
||||
path := writeConfig(t, `
|
||||
pdns_api_key: "secret"
|
||||
auth:
|
||||
ldap:
|
||||
url: ldap://file.example.com:389
|
||||
bind_dn: cn=dashboard-reader,ou=service,dc=example,dc=com
|
||||
bind_password: from-file
|
||||
user_base_dn: ou=users,dc=example,dc=com
|
||||
`)
|
||||
|
||||
cfg, err := LoadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFile returned error: %v", err)
|
||||
}
|
||||
if cfg.Auth.LDAP.URL != "ldap://env.example.com:389" {
|
||||
t.Fatalf("expected env ldap url, got %q", cfg.Auth.LDAP.URL)
|
||||
}
|
||||
if cfg.Auth.LDAP.BindPassword != "from-env" {
|
||||
t.Fatalf("expected env bind password, got %q", cfg.Auth.LDAP.BindPassword)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileRequiresLDAPUnlessAuthDisabled(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
t.Setenv("PDNS_API_KEY", "secret")
|
||||
|
||||
_, err := LoadFile("")
|
||||
if err == nil {
|
||||
t.Fatal("expected missing ldap configuration error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSearchPathsOrderOnUnix(t *testing.T) {
|
||||
paths := configSearchPaths("/home/tester", "linux")
|
||||
want := []string{
|
||||
"/etc/pdns_admin/config.yaml",
|
||||
filepath.Join("/home/tester", "config.yaml"),
|
||||
"config.yaml",
|
||||
}
|
||||
|
||||
if len(paths) != len(want) {
|
||||
t.Fatalf("unexpected path count: %#v", paths)
|
||||
}
|
||||
for i := range want {
|
||||
if paths[i] != want[i] {
|
||||
t.Fatalf("path %d = %q, want %q", i, paths[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSearchPathsOrderOnNonUnix(t *testing.T) {
|
||||
paths := configSearchPaths(`C:\Users\tester`, "windows")
|
||||
want := []string{
|
||||
filepath.Join(`C:\Users\tester`, "config.yaml"),
|
||||
"config.yaml",
|
||||
}
|
||||
|
||||
if len(paths) != len(want) {
|
||||
t.Fatalf("unexpected path count: %#v", paths)
|
||||
}
|
||||
for i := range want {
|
||||
if paths[i] != want[i] {
|
||||
t.Fatalf("path %d = %q, want %q", i, paths[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSearchPathsWithoutHome(t *testing.T) {
|
||||
paths := configSearchPaths("", "windows")
|
||||
want := []string{"config.yaml"}
|
||||
|
||||
if len(paths) != len(want) {
|
||||
t.Fatalf("unexpected path count: %#v", paths)
|
||||
}
|
||||
if paths[0] != want[0] {
|
||||
t.Fatalf("path = %q, want %q", paths[0], want[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstExistingConfigPathUsesOrder(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
first := filepath.Join(root, "first.yaml")
|
||||
second := filepath.Join(root, "second.yaml")
|
||||
if err := os.WriteFile(second, []byte("pdns_api_key: second"), 0o600); err != nil {
|
||||
t.Fatalf("write second config: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(first, []byte("pdns_api_key: first"), 0o600); err != nil {
|
||||
t.Fatalf("write first config: %v", err)
|
||||
}
|
||||
|
||||
got := firstExistingConfigPath([]string{first, second})
|
||||
if got != first {
|
||||
t.Fatalf("got %q, want %q", got, first)
|
||||
}
|
||||
}
|
||||
|
||||
func writeConfig(t *testing.T, contents string) string {
|
||||
t.Helper()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func clearConfigEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for _, key := range []string{
|
||||
"CONFIG_FILE",
|
||||
"ADDR",
|
||||
"PDNS_API_URL",
|
||||
"PDNS_API_KEY",
|
||||
"PDNS_SERVER_ID",
|
||||
"AUTH_DISABLED",
|
||||
"AUTH_LDAP_URL",
|
||||
"AUTH_LDAP_START_TLS",
|
||||
"AUTH_LDAP_INSECURE_SKIP_VERIFY",
|
||||
"AUTH_LDAP_BIND_DN",
|
||||
"AUTH_LDAP_BIND_PASSWORD",
|
||||
"AUTH_LDAP_USER_BASE_DN",
|
||||
"AUTH_LDAP_USERNAME_ATTRIBUTE",
|
||||
"AUTH_LDAP_USER_FILTER",
|
||||
"AUTH_LDAP_GROUP_BASE_DN",
|
||||
"AUTH_LDAP_GROUP_FILTER",
|
||||
} {
|
||||
unsetEnv(t, key)
|
||||
}
|
||||
}
|
||||
|
||||
func unsetEnv(t *testing.T, key string) {
|
||||
t.Helper()
|
||||
|
||||
oldValue, hadValue := os.LookupEnv(key)
|
||||
if err := os.Unsetenv(key); err != nil {
|
||||
t.Fatalf("unset %s: %v", key, err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if hadValue {
|
||||
_ = os.Setenv(key, oldValue)
|
||||
return
|
||||
}
|
||||
_ = os.Unsetenv(key)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user