mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Config: Read admin and database password from file #2302
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -40,19 +40,21 @@ var backupFlags = []cli.Flag{
|
||||
Aliases: []string{"a"},
|
||||
Usage: "create YAML files to back up album metadata (in the standard backup path if no other path is specified)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
&cli.PathFlag{
|
||||
Name: "albums-path",
|
||||
Usage: "custom album backup `PATH`",
|
||||
TakesFile: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "database",
|
||||
Aliases: []string{"index", "i"},
|
||||
Usage: "create index database backup (in the backup path with the date as filename if no filename is passed, or sent to stdout if - is passed as filename)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
&cli.PathFlag{
|
||||
Name: "database-path",
|
||||
Aliases: []string{"index-path"},
|
||||
Usage: "custom database backup `PATH`",
|
||||
TakesFile: true,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "retain",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
@@ -80,7 +81,15 @@ func (c *Config) AdminUser() string {
|
||||
|
||||
// AdminPassword returns the initial admin password.
|
||||
func (c *Config) AdminPassword() string {
|
||||
// Read password from file if requested, otherwise return value from options.
|
||||
if fileName := FlagFilePath("ADMIN_PASSWORD"); fileName == "" {
|
||||
return clean.Password(c.options.AdminPassword)
|
||||
} else if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 {
|
||||
log.Warnf("config: failed to read admin password from %s (%s)", fileName, err)
|
||||
return ""
|
||||
} else {
|
||||
return clean.Password(string(b))
|
||||
}
|
||||
}
|
||||
|
||||
// PasswordLength returns the minimum password length in characters.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -50,6 +51,19 @@ func TestAuthMode(t *testing.T) {
|
||||
c.options.Debug = false
|
||||
}
|
||||
|
||||
func TestConfig_AdminPassword(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "photoprism", c.AdminPassword())
|
||||
|
||||
// Test setting the password via secret file.
|
||||
_ = os.Setenv(FlagFileVar("ADMIN_PASSWORD"), "testdata/secret_admin")
|
||||
assert.Equal(t, "Foo-Bar23", c.AdminPassword())
|
||||
_ = os.Setenv(FlagFileVar("ADMIN_PASSWORD"), "")
|
||||
|
||||
assert.Equal(t, "photoprism", c.AdminPassword())
|
||||
}
|
||||
|
||||
func TestPasswordLength(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
assert.Equal(t, 8, c.PasswordLength())
|
||||
|
||||
@@ -249,9 +249,16 @@ func (c *Config) DatabasePassword() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Read password from file if requested, otherwise return value from options.
|
||||
if fileName := FlagFilePath("DATABASE_PASSWORD"); fileName == "" {
|
||||
c.ParseDatabaseDsn()
|
||||
|
||||
return c.options.DatabasePassword
|
||||
return clean.Password(c.options.DatabasePassword)
|
||||
} else if b, err := os.ReadFile(fileName); err != nil || len(b) == 0 {
|
||||
log.Warnf("config: failed to read database password from %s (%s)", fileName, err)
|
||||
return ""
|
||||
} else {
|
||||
return clean.Password(string(b))
|
||||
}
|
||||
}
|
||||
|
||||
// DatabaseTimeout returns the TCP timeout in seconds for establishing a database connection:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -107,6 +108,16 @@ func TestConfig_DatabasePassword(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, "", c.DatabasePassword())
|
||||
|
||||
// Test setting the password via secret file.
|
||||
_ = os.Setenv(FlagFileVar("DATABASE_PASSWORD"), "testdata/secret_database")
|
||||
assert.Equal(t, "", c.DatabasePassword())
|
||||
c.Options().DatabaseDriver = MySQL
|
||||
assert.Equal(t, "StoryOfAmélie", c.DatabasePassword())
|
||||
c.Options().DatabaseDriver = SQLite3
|
||||
_ = os.Setenv(FlagFileVar("DATABASE_PASSWORD"), "")
|
||||
|
||||
assert.Equal(t, "", c.DatabasePassword())
|
||||
}
|
||||
|
||||
func TestConfig_DatabaseDsn(t *testing.T) {
|
||||
|
||||
@@ -208,13 +208,6 @@ func TestConfig_AdminUser(t *testing.T) {
|
||||
assert.Equal(t, "admin", c.AdminUser())
|
||||
}
|
||||
|
||||
func TestConfig_AdminPassword(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
result := c.AdminPassword()
|
||||
assert.Equal(t, "photoprism", result)
|
||||
}
|
||||
|
||||
func TestConfig_ExamplesPath(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/list"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
@@ -51,3 +52,19 @@ func Env(vars ...string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// FlagFileVar returns the name of the environment variable that can contain a filename to load a config value from.
|
||||
func FlagFileVar(flag string) string {
|
||||
return EnvVar(flag) + "_FILE"
|
||||
}
|
||||
|
||||
// FlagFilePath returns the name of the that contains the value of the specified config flag, if any.
|
||||
func FlagFilePath(flag string) string {
|
||||
if envVar := os.Getenv(FlagFileVar(flag)); envVar == "" {
|
||||
return ""
|
||||
} else if absName := fs.Abs(envVar); fs.FileExistsNotEmpty(absName) {
|
||||
return absName
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -59,3 +60,18 @@ func TestEnv(t *testing.T) {
|
||||
assert.False(t, Env("TESTENV_0"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFlagFileVar(t *testing.T) {
|
||||
t.Run("AdminPassword", func(t *testing.T) {
|
||||
assert.Equal(t, "PHOTOPRISM_ADMIN_PASSWORD_FILE", FlagFileVar("ADMIN_PASSWORD"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFlagFilePath(t *testing.T) {
|
||||
t.Run("AdminPassword", func(t *testing.T) {
|
||||
_ = os.Setenv("PHOTOPRISM_ADMIN_PASSWORD_FILE", "./testdata/secret_admin")
|
||||
actual := FlagFilePath("ADMIN_PASSWORD")
|
||||
expected := "internal/config/testdata/secret_admin"
|
||||
assert.True(t, strings.Contains(actual, expected), expected+" was expected")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -181,11 +181,12 @@ var Flags = CliFlags{
|
||||
Usage: "hosting partner id",
|
||||
EnvVars: EnvVars("PARTNER_ID"),
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Flag: &cli.PathFlag{
|
||||
Name: "config-path",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "config storage `PATH`, values in options.yml override CLI flags and environment variables if present",
|
||||
EnvVars: EnvVars("CONFIG_PATH"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Name: "defaults-yaml",
|
||||
@@ -193,12 +194,14 @@ var Flags = CliFlags{
|
||||
Usage: "load config defaults from `FILE` if exists, does not override CLI flags and environment variables",
|
||||
Value: "/etc/photoprism/defaults.yml",
|
||||
EnvVars: EnvVars("DEFAULTS_YAML"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Flag: &cli.PathFlag{
|
||||
Name: "originals-path",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "storage `PATH` of your original media files (photos and videos)",
|
||||
EnvVars: EnvVars("ORIGINALS_PATH"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.IntFlag{
|
||||
Name: "originals-limit",
|
||||
@@ -220,22 +223,25 @@ var Flags = CliFlags{
|
||||
Value: "users",
|
||||
EnvVars: EnvVars("USERS_PATH"),
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Flag: &cli.PathFlag{
|
||||
Name: "storage-path",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "writable storage `PATH` for sidecar, cache, and database files",
|
||||
EnvVars: EnvVars("STORAGE_PATH"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Flag: &cli.PathFlag{
|
||||
Name: "import-path",
|
||||
Aliases: []string{"im"},
|
||||
Usage: "base `PATH` from which files can be imported to originals *optional*",
|
||||
EnvVars: EnvVars("IMPORT_PATH"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Flag: &cli.PathFlag{
|
||||
Name: "import-dest",
|
||||
Usage: "relative originals `PATH` to which the files should be imported by default *optional*",
|
||||
EnvVars: EnvVars("IMPORT_DEST"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Name: "import-allow",
|
||||
@@ -253,25 +259,28 @@ var Flags = CliFlags{
|
||||
Usage: "allow to upload these file types (comma-separated list of `EXTENSIONS`; leave blank to allow all)",
|
||||
EnvVars: EnvVars("UPLOAD_ALLOW"),
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Flag: &cli.PathFlag{
|
||||
Name: "cache-path",
|
||||
Aliases: []string{"ca"},
|
||||
Usage: "custom cache `PATH` for sessions and thumbnail files *optional*",
|
||||
EnvVars: EnvVars("CACHE_PATH"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Flag: &cli.PathFlag{
|
||||
Name: "temp-path",
|
||||
Aliases: []string{"tmp"},
|
||||
Usage: "temporary file `PATH` *optional*",
|
||||
EnvVars: EnvVars("TEMP_PATH"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Flag: &cli.PathFlag{
|
||||
Name: "assets-path",
|
||||
Aliases: []string{"as"},
|
||||
Usage: "assets `PATH` containing static resources like icons, models, and translations",
|
||||
EnvVars: EnvVars("ASSETS_PATH"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Flag: &cli.PathFlag{
|
||||
Name: "sidecar-path",
|
||||
Aliases: []string{"sc"},
|
||||
Usage: "custom relative or absolute sidecar `PATH` *optional*",
|
||||
@@ -292,11 +301,12 @@ var Flags = CliFlags{
|
||||
Usage: "maximum aggregated size of all indexed files in `GB` (0 for unlimited)",
|
||||
EnvVars: EnvVars("FILES_QUOTA"),
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Flag: &cli.PathFlag{
|
||||
Name: "backup-path",
|
||||
Aliases: []string{"ba"},
|
||||
Usage: "custom base `PATH` for creating and restoring backups *optional*",
|
||||
EnvVars: EnvVars("BACKUP_PATH"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Name: "backup-schedule",
|
||||
@@ -826,17 +836,19 @@ var Flags = CliFlags{
|
||||
Value: "thm",
|
||||
EnvVars: EnvVars("DARKTABLE_EXCLUDE", "DARKTABLE_BLACKLIST"),
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Flag: &cli.PathFlag{
|
||||
Name: "darktable-cache-path",
|
||||
Usage: "custom Darktable cache `PATH`",
|
||||
Value: "",
|
||||
EnvVars: EnvVars("DARKTABLE_CACHE_PATH"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Flag: &cli.PathFlag{
|
||||
Name: "darktable-config-path",
|
||||
Usage: "custom Darktable config `PATH`",
|
||||
Value: "",
|
||||
EnvVars: EnvVars("DARKTABLE_CONFIG_PATH"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Name: "rawtherapee-bin",
|
||||
@@ -994,11 +1006,13 @@ var Flags = CliFlags{
|
||||
Name: "pid-filename",
|
||||
Usage: "process id `FILE` *daemon-mode only*",
|
||||
EnvVars: EnvVars("PID_FILENAME"),
|
||||
TakesFile: true,
|
||||
}}, {
|
||||
Flag: &cli.StringFlag{
|
||||
Name: "log-filename",
|
||||
Usage: "server log `FILE` *daemon-mode only*",
|
||||
Value: "",
|
||||
EnvVars: EnvVars("LOG_FILENAME"),
|
||||
TakesFile: true,
|
||||
}},
|
||||
}
|
||||
|
||||
1
internal/config/testdata/secret_admin
vendored
Normal file
1
internal/config/testdata/secret_admin
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Foo-Bar23
|
||||
2
internal/config/testdata/secret_database
vendored
Normal file
2
internal/config/testdata/secret_database
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
StoryOfAmélie
|
||||
|
||||
Reference in New Issue
Block a user