| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- package cmd
- // Credential resolution.
- //
- // Credentials are read from a .env file in the working directory, falling back
- // to the process environment. Real environment variables always win over .env,
- // so a one-off override does not require editing the file:
- //
- // SCHLUNDTECH_CONTEXT=1 ./schlundtech-dns zones
- //
- // Nothing here prints, logs or writes a credential.
- import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "github.com/joho/godotenv"
- "schlundtech-dns/internal/api"
- )
- // envFile is the dotenv file read from the working directory.
- const envFile = ".env"
- // Credential environment variable names.
- const (
- envUser = "SCHLUNDTECH_USER"
- envPassword = "SCHLUNDTECH_PASSWORD"
- envContext = "SCHLUNDTECH_CONTEXT"
- envToken = "SCHLUNDTECH_TOKEN"
- envEndpoint = "SCHLUNDTECH_ENDPOINT"
- )
- // loadCredentials fills in any credential not already present in the
- // environment from the .env file, then returns the credentials.
- //
- // A missing .env is not an error: the environment alone is enough. A warning is
- // returned when the file exists but is readable beyond its owner, since it
- // holds a password.
- func loadCredentials() (api.Credentials, string, error) {
- warning, err := loadEnvFile(envFile)
- if err != nil {
- return api.Credentials{}, "", err
- }
- creds, err := credentialsFromEnv()
- return creds, warning, err
- }
- // loadEnvFile exports every variable from path that the environment does not
- // already define. It reports a warning if the file is too permissive.
- func loadEnvFile(path string) (string, error) {
- info, err := os.Stat(path)
- if err != nil {
- if os.IsNotExist(err) {
- return "", nil
- }
- return "", fmt.Errorf("read %s: %w", path, err)
- }
- if info.IsDir() {
- return "", fmt.Errorf("%s is a directory, expected a dotenv file", path)
- }
- raw, err := os.ReadFile(filepath.Clean(path))
- if err != nil {
- return "", fmt.Errorf("read %s: %w", path, err)
- }
- values, err := godotenv.Unmarshal(string(raw))
- if err != nil {
- return "", fmt.Errorf("parse %s: %w", path, err)
- }
- for k, v := range values {
- if _, set := os.LookupEnv(k); set {
- continue // the real environment wins
- }
- if err := os.Setenv(k, v); err != nil {
- return "", fmt.Errorf("set %s: %w", k, err)
- }
- }
- var warning string
- if perm := info.Mode().Perm(); perm&0o077 != 0 {
- warning = fmt.Sprintf(
- "%s holds your password and is readable by others (mode %#o); run: chmod 600 %s",
- path, perm, path)
- }
- return warning, nil
- }
- // credentialsFromEnv reads the credentials. It reports every missing variable
- // at once so a first run does not fail one variable at a time.
- func credentialsFromEnv() (api.Credentials, error) {
- c := api.Credentials{
- User: strings.TrimSpace(os.Getenv(envUser)),
- Password: os.Getenv(envPassword),
- Context: strings.TrimSpace(os.Getenv(envContext)),
- Token: strings.TrimSpace(os.Getenv(envToken)),
- Endpoint: strings.TrimSpace(os.Getenv(envEndpoint)),
- }
- var missing []string
- if c.User == "" {
- missing = append(missing, envUser)
- }
- if c.Password == "" {
- missing = append(missing, envPassword)
- }
- if c.Context == "" {
- missing = append(missing, envContext)
- }
- if len(missing) > 0 {
- return c, fmt.Errorf("missing %s: set them in %s or in the environment",
- strings.Join(missing, ", "), envFile)
- }
- return c, nil
- }
|