primeiro commit

This commit is contained in:
2026-06-18 22:32:42 -03:00
commit 968f4ef5d9
25 changed files with 3222 additions and 0 deletions

153
internal/config/config.go Normal file
View 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
}

View 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)
})
}