PhotoPrism's OSS edition has no way to map OIDC claims to BasePath, so
every freshly-registered OIDC user lands with BasePath="" and either
sees the whole library (admin) or nothing (guest) — never their own
subfolder.
Introduces a sidecar-driven reconciler with a single env knob the
admin sets in docker-compose / .env.photoprism:
USER_BASEPATHS="test:test, alice:family/alice, bob:bob"
(`user:originals-relative-path` pairs, comma-separated.) On boot and
every 60s thereafter the sidecar:
- mkdir -p's the target subdirectory under ORIGINALS_ROOT so
PhotoPrism's path: ACL filter has somewhere real to point;
- UPDATEs photoprism.auth_users.base_path for the matching row
where it differs (idempotent, missing users skipped — they
materialise on first OIDC login and the next pass catches them).
The reconciler uses a separate gorm connection scoped to the
`photoprism` schema with PhotoPrism's own DB user, since the existing
`sidecar` user only has grants on `mule_sidecar.*`. Connection stays
dormant when PP_DB_PASSWORD is empty — the feature is opt-in via env.
Compose changes: thread PP_DB_* + USER_BASEPATHS through to the
sidecar service. New users.go file isolates the reconciler logic;
main.go calls startUserBasepathReconciler() during boot.
93 lines
3.4 KiB
Go
93 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
)
|
|
|
|
// Config aggregates every runtime knob the sidecar reads from the
|
|
// environment. Held in one struct so the rest of the package can take a
|
|
// pointer instead of poking os.Getenv at use-sites.
|
|
type Config struct {
|
|
OriginalsRoot string // absolute path to PhotoPrism's originals dir
|
|
PhotoprismBaseURL string // e.g. http://localhost:2342
|
|
ListenAddr string // bind interface — 127.0.0.1 for host mode, 0.0.0.0 in containers
|
|
Port int // HTTP listen port
|
|
DSN string // GORM/MySQL connection string for mule_sidecar
|
|
// PpDSN is a second DB connection string pointed at PhotoPrism's own
|
|
// schema (`photoprism.*`). Sidecar code that needs to mutate
|
|
// PhotoPrism-managed rows (e.g. auth_users.base_path) opens its own
|
|
// connection with these creds rather than asking for grants on the
|
|
// mule_sidecar user. Empty if PP_DB_PASSWORD isn't provided, in
|
|
// which case PP-touching features (user-basepath reconciler) stay
|
|
// dormant.
|
|
PpDSN string
|
|
// UserBasepaths is the parsed `USER_BASEPATHS` env. Maps PhotoPrism
|
|
// usernames to originals-relative base paths so OIDC-provisioned
|
|
// users land with the right library scope without any admin
|
|
// touching `photoprism users mod`.
|
|
UserBasepaths map[string]string
|
|
}
|
|
|
|
func loadConfig() (*Config, error) {
|
|
root := envOr("ORIGINALS_ROOT", "/photoprism/originals")
|
|
abs, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
portStr := envOr("SIDECAR_PORT", "8000")
|
|
port, err := strconv.Atoi(portStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dsn := os.Getenv("SIDECAR_DSN")
|
|
if dsn == "" {
|
|
// Default matches the user that mariadb/init/01-sidecar.sql provisions
|
|
// on first boot. The literal placeholder password is intentional: the
|
|
// SQL ships with it and an env-templating step is left for whoever
|
|
// runs this in a non-local context.
|
|
user := envOr("SIDECAR_DB_USER", "sidecar")
|
|
pass := envOr("SIDECAR_DB_PASSWORD", "replace-at-m4-bringup")
|
|
host := envOr("SIDECAR_DB_HOST", "127.0.0.1")
|
|
dbPort := envOr("SIDECAR_DB_PORT", "3306")
|
|
name := envOr("SIDECAR_DB_NAME", "mule_sidecar")
|
|
dsn = user + ":" + pass + "@tcp(" + host + ":" + dbPort + ")/" + name +
|
|
"?charset=utf8mb4&parseTime=true&loc=Local"
|
|
}
|
|
|
|
// PhotoPrism schema connection — only used by the user-basepath
|
|
// reconciler. Stays empty if PP_DB_PASSWORD isn't set, and callers
|
|
// gate behaviour on that. We use PhotoPrism's own DB user rather
|
|
// than the sidecar's because `mule_sidecar` has no grants on
|
|
// `photoprism.*` (see mariadb/init/01-sidecar.sql).
|
|
ppDSN := ""
|
|
if ppPass := os.Getenv("PP_DB_PASSWORD"); ppPass != "" {
|
|
ppUser := envOr("PP_DB_USER", "photoprism")
|
|
ppHost := envOr("PP_DB_HOST", envOr("SIDECAR_DB_HOST", "mariadb"))
|
|
ppPort := envOr("PP_DB_PORT", envOr("SIDECAR_DB_PORT", "3306"))
|
|
ppName := envOr("PP_DB_NAME", "photoprism")
|
|
ppDSN = ppUser + ":" + ppPass + "@tcp(" + ppHost + ":" + ppPort + ")/" + ppName +
|
|
"?charset=utf8mb4&parseTime=true&loc=Local"
|
|
}
|
|
|
|
return &Config{
|
|
OriginalsRoot: abs,
|
|
PhotoprismBaseURL: envOr("PHOTOPRISM_BASE_URL", "http://localhost:2342"),
|
|
ListenAddr: envOr("SIDECAR_LISTEN_ADDR", "127.0.0.1"),
|
|
Port: port,
|
|
DSN: dsn,
|
|
PpDSN: ppDSN,
|
|
UserBasepaths: parseUserBasepaths(os.Getenv("USER_BASEPATHS")),
|
|
}, nil
|
|
}
|
|
|
|
func envOr(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|