mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-21 09:09:04 +02:00
fix: trim values loaded from _FILE env vars in AppConfig (#1741)
This commit is contained in:
@@ -217,31 +217,28 @@ func (s *AppConfigService) loadDbConfigFromEnv() (*AppConfigModel, error) {
|
||||
return nil, fmt.Errorf("app configuration field %s is missing its environment variable name", field.Name)
|
||||
}
|
||||
|
||||
// Set the value if it's set
|
||||
// Sensitive values can also be loaded from a file, using the variable with the "_FILE" suffix (files have precedence over the env var)
|
||||
if field.Tag.Get("sensitive") == "true" {
|
||||
value, ok, err := common.LoadStringEnvVarFromFile(envVarName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
rv.Field(i).SetString(value)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Set the value from the environment variable if it's set
|
||||
value, ok := os.LookupEnv(envVarName)
|
||||
if ok {
|
||||
rv.Field(i).SetString(value)
|
||||
continue
|
||||
}
|
||||
|
||||
// If it's sensitive, we also allow reading from file
|
||||
if field.Tag.Get("sensitive") == "true" {
|
||||
fileName := os.Getenv(envVarName + "_FILE")
|
||||
if fileName != "" {
|
||||
// #nosec G703 - Value is provided by admin
|
||||
b, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read secret '%s' from file '%s': %w", envVarName, fileName, err)
|
||||
}
|
||||
|
||||
rv.Field(i).SetString(string(b))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the resolved configuration before exposing values to the rest of the application
|
||||
if err := validateEnvConfig(dest); err != nil {
|
||||
err := validateEnvConfig(dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -252,12 +249,13 @@ func (s *AppConfigService) loadDbConfigFromEnv() (*AppConfigModel, error) {
|
||||
func validateEnvConfig(config *AppConfigModel) error {
|
||||
// Map the resolved model to the canonical update DTO so both configuration paths share validation rules
|
||||
var input dto.AppConfigUpdateDto
|
||||
if err := dto.MapStruct(config, &input); err != nil {
|
||||
err := dto.MapStruct(config, &input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare environment app configuration for validation: %w", err)
|
||||
}
|
||||
|
||||
// Collect every invalid environment variable
|
||||
err := input.Validate()
|
||||
err = input.Validate()
|
||||
if err != nil {
|
||||
validationErrors, ok := errors.AsType[validator.ValidationErrors](err)
|
||||
if !ok {
|
||||
|
||||
@@ -2,6 +2,8 @@ package appconfig
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -458,3 +460,69 @@ func TestService_CIMDURLAllowlist(t *testing.T) {
|
||||
assert.Empty(t, svc.GetCIMDURLAllowlist())
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_LoadDbConfigFromEnv(t *testing.T) {
|
||||
// writeSecretFile writes content to a file in a temporary directory that is removed when the test ends, returning its path
|
||||
writeSecretFile := func(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
|
||||
fileName := filepath.Join(t.TempDir(), "secret.txt")
|
||||
err := os.WriteFile(fileName, []byte(content), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
return fileName
|
||||
}
|
||||
|
||||
t.Run("loads a sensitive value from a file", func(t *testing.T) {
|
||||
t.Setenv("LDAP_BIND_PASSWORD_FILE", writeSecretFile(t, "my-ldap-password"))
|
||||
|
||||
svc := &AppConfigService{}
|
||||
cfg, err := svc.loadDbConfigFromEnv()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, AppConfigValue("my-ldap-password"), cfg.LdapBindPassword)
|
||||
})
|
||||
|
||||
t.Run("trims whitespace from values loaded from a file", func(t *testing.T) {
|
||||
// Files containing secrets normally end with a trailing newline, which must not be part of the value
|
||||
t.Setenv("LDAP_BIND_PASSWORD_FILE", writeSecretFile(t, "my-ldap-password\n"))
|
||||
t.Setenv("SMTP_PASSWORD_FILE", writeSecretFile(t, " my-smtp-password \r\n"))
|
||||
|
||||
svc := &AppConfigService{}
|
||||
cfg, err := svc.loadDbConfigFromEnv()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, AppConfigValue("my-ldap-password"), cfg.LdapBindPassword)
|
||||
assert.Equal(t, AppConfigValue("my-smtp-password"), cfg.SmtpPassword)
|
||||
})
|
||||
|
||||
t.Run("the file takes precedence over the environment variable", func(t *testing.T) {
|
||||
t.Setenv("LDAP_BIND_PASSWORD", "from-env")
|
||||
t.Setenv("LDAP_BIND_PASSWORD_FILE", writeSecretFile(t, "from-file\n"))
|
||||
|
||||
svc := &AppConfigService{}
|
||||
cfg, err := svc.loadDbConfigFromEnv()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, AppConfigValue("from-file"), cfg.LdapBindPassword)
|
||||
})
|
||||
|
||||
t.Run("values that are not sensitive are not loaded from a file", func(t *testing.T) {
|
||||
t.Setenv("LDAP_BIND_DN_FILE", writeSecretFile(t, "cn=from-file"))
|
||||
|
||||
svc := &AppConfigService{}
|
||||
cfg, err := svc.loadDbConfigFromEnv()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Empty(t, cfg.LdapBindDn)
|
||||
})
|
||||
|
||||
t.Run("returns an error when the file cannot be read", func(t *testing.T) {
|
||||
t.Setenv("LDAP_BIND_PASSWORD_FILE", filepath.Join(t.TempDir(), "does-not-exist.txt"))
|
||||
|
||||
svc := &AppConfigService{}
|
||||
_, err := svc.loadDbConfigFromEnv()
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "LDAP_BIND_PASSWORD_FILE")
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user