mirror of
https://github.com/photoprism/photoprism.git
synced 2025-09-26 21:01:58 +08:00
134 lines
3.9 KiB
Go
134 lines
3.9 KiB
Go
package provisioner
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/photoprism/photoprism/internal/config"
|
|
)
|
|
|
|
// Credentials contains the connection details returned when ensuring a node database.
|
|
type Credentials struct {
|
|
Driver string
|
|
Host string
|
|
Port int
|
|
Name string
|
|
User string
|
|
Password string
|
|
DSN string
|
|
RotatedAt string
|
|
}
|
|
|
|
// GetCredentials ensures a per-node database and user exist with minimal grants.
|
|
// - Requires a MySQL/MariaDB admin connection (this package maintains it).
|
|
// - Returns created=true if the database schema did not exist before.
|
|
// - If rotate is true or created, rotates the user password and includes it (and DSN) in the result.
|
|
func GetCredentials(ctx context.Context, conf *config.Config, nodeUUID, nodeName string, rotate bool) (Credentials, bool, error) {
|
|
out := Credentials{}
|
|
|
|
// Normalize the configured admin driver locally so we accept variants like "MySQL"/"MariaDB"
|
|
// without mutating the global setting (keeps config reporting consistent).
|
|
driver := strings.ToLower(DatabaseDriver)
|
|
|
|
switch driver {
|
|
case config.MySQL, config.MariaDB:
|
|
// ok
|
|
case config.SQLite3, config.Postgres:
|
|
return out, false, errors.New("database must be MySQL/MariaDB for auto-provisioning")
|
|
default:
|
|
// Driver is configured externally for the provisioner (decoupled from app config).
|
|
return out, false, fmt.Errorf("unsupported auto-provisioning database driver: %s", driver)
|
|
}
|
|
|
|
// Compute deterministic names and a candidate password.
|
|
dbName, dbUser, dbPass := GenerateCredentials(conf, nodeUUID, nodeName)
|
|
|
|
// Extra safety: enforce allowed identifier charset.
|
|
if !identRe.MatchString(dbName) || !identRe.MatchString(dbUser) {
|
|
return out, false, errors.New("invalid generated database identifiers")
|
|
}
|
|
|
|
// Get (or open) admin DB handle.
|
|
db, err := GetDB(ctx)
|
|
if err != nil {
|
|
return out, false, err
|
|
}
|
|
|
|
// 1) Determine if database already exists (values can be parameterized).
|
|
var count int
|
|
{
|
|
c, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
defer cancel()
|
|
if err := db.QueryRowContext(
|
|
c,
|
|
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?",
|
|
dbName,
|
|
).Scan(&count); err != nil {
|
|
return out, false, err
|
|
}
|
|
}
|
|
created := count == 0
|
|
|
|
// 2) Create database schema if needed (identifier must be quoted).
|
|
qDB, err := quoteIdent(dbName)
|
|
if err != nil {
|
|
return out, created, err
|
|
}
|
|
createDB := "CREATE DATABASE IF NOT EXISTS " + qDB + " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
|
|
if err := execTimeout(ctx, db, 15*time.Second, createDB); err != nil {
|
|
return out, created, err
|
|
}
|
|
|
|
// 3) Ensure user exists.
|
|
acc, err := quoteAccount("%", dbUser) // user@'%'
|
|
if err != nil {
|
|
return out, created, err
|
|
}
|
|
pass, err := quoteString(dbPass)
|
|
if err != nil {
|
|
return out, created, err
|
|
}
|
|
|
|
createUser := "CREATE USER IF NOT EXISTS " + acc + " IDENTIFIED BY " + pass
|
|
if err := execTimeout(ctx, db, 10*time.Second, createUser); err != nil {
|
|
return out, created, err
|
|
}
|
|
|
|
// 4) Rotate or set password explicitly on first creation.
|
|
if rotate || created {
|
|
alterUser := "ALTER USER " + acc + " IDENTIFIED BY " + pass
|
|
if err := execTimeout(ctx, db, 10*time.Second, alterUser); err != nil {
|
|
return out, created, err
|
|
}
|
|
out.Password = dbPass
|
|
out.RotatedAt = time.Now().UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
// 5) Grant privileges on schema.
|
|
grant := "GRANT ALL PRIVILEGES ON " + qDB + ".* TO " + acc
|
|
if err := execTimeout(ctx, db, 10*time.Second, grant); err != nil {
|
|
return out, created, err
|
|
}
|
|
|
|
// 6) Optional on modern MariaDB/MySQL; harmless if included.
|
|
if err := execTimeout(ctx, db, 5*time.Second, "FLUSH PRIVILEGES"); err != nil {
|
|
return out, created, err
|
|
}
|
|
|
|
// Compose credentials.
|
|
out.Host = DatabaseHost
|
|
out.Port = DatabasePort
|
|
out.Name = dbName
|
|
out.User = dbUser
|
|
out.Driver = driver
|
|
|
|
if out.Password != "" {
|
|
out.DSN = BuildDSN(driver, out.Host, out.Port, out.User, out.Password, out.Name)
|
|
}
|
|
|
|
return out, created, nil
|
|
}
|