Auth: Refactor users path configuration and base path default

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2023-03-14 21:47:14 +01:00
parent 3755421945
commit addc5e8251
12 changed files with 78 additions and 27 deletions

View File

@@ -121,6 +121,22 @@ export class User extends RestModel {
return s[0].trim(); return s[0].trim();
} }
defaultBasePath() {
const handle = this.getHandle();
if (!handle) {
return "";
}
let dir = config.get("usersPath");
if (dir) {
return `${dir}/${handle}`;
} else {
return `users/${handle}`;
}
}
getDisplayName() { getDisplayName() {
if (this.DisplayName) { if (this.DisplayName) {
return this.DisplayName; return this.DisplayName;

View File

@@ -80,6 +80,7 @@ func StartImport(router *gin.RouterGroup) {
RemoveFromFolderCache(entity.RootImport) RemoveFromFolderCache(entity.RootImport)
// Get destination folder.
var destFolder string var destFolder string
if destFolder = s.User().GetUploadPath(); destFolder == "" { if destFolder = s.User().GetUploadPath(); destFolder == "" {
destFolder = conf.ImportDest() destFolder = conf.ImportDest()

View File

@@ -185,6 +185,7 @@ func ProcessUserUpload(router *gin.RouterGroup) {
imp := get.Import() imp := get.Import()
// Get destination folder.
var destFolder string var destFolder string
if destFolder = s.User().GetUploadPath(); destFolder == "" { if destFolder = s.User().GetUploadPath(); destFolder == "" {
destFolder = conf.ImportDest() destFolder = conf.ImportDest()

View File

@@ -60,6 +60,7 @@ type ClientConfig struct {
UploadNSFW bool `json:"uploadNSFW"` UploadNSFW bool `json:"uploadNSFW"`
Public bool `json:"public"` Public bool `json:"public"`
AuthMode string `json:"authMode"` AuthMode string `json:"authMode"`
UsersPath string `json:"usersPath"`
LoginUri string `json:"loginUri"` LoginUri string `json:"loginUri"`
RegisterUri string `json:"registerUri"` RegisterUri string `json:"registerUri"`
PasswordLength int `json:"passwordLength"` PasswordLength int `json:"passwordLength"`
@@ -278,6 +279,7 @@ func (c *Config) ClientPublic() ClientConfig {
ReadOnly: c.ReadOnly(), ReadOnly: c.ReadOnly(),
Public: c.Public(), Public: c.Public(),
AuthMode: c.AuthMode(), AuthMode: c.AuthMode(),
UsersPath: c.UsersPath(),
LoginUri: c.LoginUri(), LoginUri: c.LoginUri(),
RegisterUri: c.RegisterUri(), RegisterUri: c.RegisterUri(),
PasswordResetUri: c.PasswordResetUri(), PasswordResetUri: c.PasswordResetUri(),
@@ -364,6 +366,7 @@ func (c *Config) ClientShare() ClientConfig {
UploadNSFW: c.UploadNSFW(), UploadNSFW: c.UploadNSFW(),
Public: c.Public(), Public: c.Public(),
AuthMode: c.AuthMode(), AuthMode: c.AuthMode(),
UsersPath: "",
LoginUri: c.LoginUri(), LoginUri: c.LoginUri(),
RegisterUri: c.RegisterUri(), RegisterUri: c.RegisterUri(),
PasswordResetUri: c.PasswordResetUri(), PasswordResetUri: c.PasswordResetUri(),
@@ -455,6 +458,7 @@ func (c *Config) ClientUser(withSettings bool) ClientConfig {
UploadNSFW: c.UploadNSFW(), UploadNSFW: c.UploadNSFW(),
Public: c.Public(), Public: c.Public(),
AuthMode: c.AuthMode(), AuthMode: c.AuthMode(),
UsersPath: c.UsersPath(),
LoginUri: c.LoginUri(), LoginUri: c.LoginUri(),
RegisterUri: c.RegisterUri(), RegisterUri: c.RegisterUri(),
PasswordLength: c.PasswordLength(), PasswordLength: c.PasswordLength(),

View File

@@ -173,6 +173,9 @@ func (c *Config) Propagate() {
// Set minimum password length. // Set minimum password length.
entity.PasswordLength = c.PasswordLength() entity.PasswordLength = c.PasswordLength()
// Set path for user assets.
entity.UsersPath = c.UsersPath()
// Set API preview and download default tokens. // Set API preview and download default tokens.
entity.PreviewToken.Set(c.PreviewToken(), entity.TokenConfig) entity.PreviewToken.Set(c.PreviewToken(), entity.TokenConfig)
entity.DownloadToken.Set(c.DownloadToken(), entity.TokenConfig) entity.DownloadToken.Set(c.DownloadToken(), entity.TokenConfig)

View File

@@ -90,8 +90,8 @@ func (c *Config) CreateDirectories() error {
if c.UsersPath() == "" { if c.UsersPath() == "" {
return notFoundError("users") return notFoundError("users")
} else if err := os.MkdirAll(c.UsersPath(), fs.ModeDir); err != nil { } else if err := os.MkdirAll(c.UsersStoragePath(), fs.ModeDir); err != nil {
return createError(c.UsersPath(), err) return createError(c.UsersStoragePath(), err)
} }
if c.CmdCachePath() == "" { if c.CmdCachePath() == "" {
@@ -325,19 +325,28 @@ func (c *Config) SidecarWritable() bool {
return !c.ReadOnly() || c.SidecarPathIsAbs() return !c.ReadOnly() || c.SidecarPathIsAbs()
} }
// UsersPath returns the storage base path for user assets like // UsersPath returns the relative base path for user assets.
// avatar images and other media that should not be indexed.
func (c *Config) UsersPath() string { func (c *Config) UsersPath() string {
// Set default. // Set default.
if c.options.UsersPath == "" { if c.options.UsersPath == "" {
c.options.UsersPath = filepath.Join(c.StoragePath(), "users") return "users"
} }
return c.options.UsersPath return clean.UserPath(c.options.UsersPath)
} }
// UserPath returns the storage path for user assets. // UsersStoragePath returns the users storage base path.
func (c *Config) UserPath(userUid string) string { func (c *Config) UsersStoragePath() string {
return filepath.Join(c.StoragePath(), c.UsersPath())
}
// UsersOriginalsPath returns the users originals base path.
func (c *Config) UsersOriginalsPath() string {
return filepath.Join(c.OriginalsPath(), c.UsersPath())
}
// UserStoragePath returns the storage path for user assets.
func (c *Config) UserStoragePath(userUid string) string {
if !rnd.IsUID(userUid, 0) { if !rnd.IsUID(userUid, 0) {
return "" return ""
} }
@@ -357,7 +366,7 @@ func (c *Config) UserUploadPath(userUid, token string) (string, error) {
return "", fmt.Errorf("invalid uid") return "", fmt.Errorf("invalid uid")
} }
dir := filepath.Join(c.UserPath(userUid), "upload", clean.Token(token)) dir := filepath.Join(c.UserStoragePath(userUid), "upload", clean.Token(token))
if err := os.MkdirAll(dir, fs.ModeDir); err != nil { if err := os.MkdirAll(dir, fs.ModeDir); err != nil {
return "", err return "", err

View File

@@ -25,14 +25,14 @@ func TestConfig_SidecarPath(t *testing.T) {
func TestConfig_UsersPath(t *testing.T) { func TestConfig_UsersPath(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
assert.Contains(t, c.UsersPath(), "testdata/users") assert.Contains(t, c.UsersPath(), "users")
} }
func TestConfig_UserPath(t *testing.T) { func TestConfig_UserStoragePath(t *testing.T) {
c := NewConfig(CliTestContext()) c := NewConfig(CliTestContext())
assert.Equal(t, "", c.UserPath("")) assert.Equal(t, "", c.UserStoragePath(""))
assert.Equal(t, "", c.UserPath("etaetyget")) assert.Equal(t, "", c.UserStoragePath("etaetyget"))
assert.Contains(t, c.UserPath("urjult03ceelhw6k"), "testdata/users/urjult03ceelhw6k") assert.Contains(t, c.UserStoragePath("urjult03ceelhw6k"), "users/urjult03ceelhw6k")
} }
func TestConfig_UserUploadPath(t *testing.T) { func TestConfig_UserUploadPath(t *testing.T) {
@@ -50,12 +50,12 @@ func TestConfig_UserUploadPath(t *testing.T) {
if dir, err := c.UserUploadPath("urjult03ceelhw6k", ""); err != nil { if dir, err := c.UserUploadPath("urjult03ceelhw6k", ""); err != nil {
t.Fatal(err) t.Fatal(err)
} else { } else {
assert.Contains(t, dir, "testdata/users/urjult03ceelhw6k/upload") assert.Contains(t, dir, "users/urjult03ceelhw6k/upload")
} }
if dir, err := c.UserUploadPath("urjult03ceelhw6k", "foo"); err != nil { if dir, err := c.UserUploadPath("urjult03ceelhw6k", "foo"); err != nil {
t.Fatal(err) t.Fatal(err)
} else { } else {
assert.Contains(t, dir, "testdata/users/urjult03ceelhw6k/upload/foo") assert.Contains(t, dir, "users/urjult03ceelhw6k/upload/foo")
} }
} }

View File

@@ -142,11 +142,6 @@ var Flags = CliFlags{
Usage: "custom relative or absolute sidecar `PATH`*optional*", Usage: "custom relative or absolute sidecar `PATH`*optional*",
EnvVar: "PHOTOPRISM_SIDECAR_PATH", EnvVar: "PHOTOPRISM_SIDECAR_PATH",
}}, { }}, {
Flag: cli.StringFlag{
Name: "users-path",
Usage: "custom users storage `PATH`*optional*",
EnvVar: "PHOTOPRISM_USERS_PATH",
}}, {
Flag: cli.StringFlag{ Flag: cli.StringFlag{
Name: "backup-path, ba", Name: "backup-path, ba",
Usage: "custom backup `PATH` for index backup files*optional*", Usage: "custom backup `PATH` for index backup files*optional*",
@@ -167,6 +162,12 @@ var Flags = CliFlags{
Usage: "relative originals `PATH` to which the files should be imported by default*optional*", Usage: "relative originals `PATH` to which the files should be imported by default*optional*",
EnvVar: "PHOTOPRISM_IMPORT_DEST", EnvVar: "PHOTOPRISM_IMPORT_DEST",
}}, { }}, {
Flag: cli.StringFlag{
Name: "users-path",
Usage: "relative `PATH` to create base and upload subdirectories for users",
EnvVar: "PHOTOPRISM_USERS_PATH",
Value: "users",
}}, {
Flag: cli.StringFlag{ Flag: cli.StringFlag{
Name: "assets-path, as", Name: "assets-path, as",
Usage: "assets `PATH` containing static resources like icons, models, and translations", Usage: "assets `PATH` containing static resources like icons, models, and translations",

View File

@@ -57,10 +57,9 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"originals-limit", fmt.Sprintf("%d", c.OriginalsLimit())}, {"originals-limit", fmt.Sprintf("%d", c.OriginalsLimit())},
{"resolution-limit", fmt.Sprintf("%d", c.ResolutionLimit())}, {"resolution-limit", fmt.Sprintf("%d", c.ResolutionLimit())},
// Other paths. // Storage.
{"storage-path", c.StoragePath()}, {"storage-path", c.StoragePath()},
{"sidecar-path", c.SidecarPath()}, {"sidecar-path", c.SidecarPath()},
{"users-path", c.UsersPath()},
{"albums-path", c.AlbumsPath()}, {"albums-path", c.AlbumsPath()},
{"backup-path", c.BackupPath()}, {"backup-path", c.BackupPath()},
{"cache-path", c.CachePath()}, {"cache-path", c.CachePath()},
@@ -69,6 +68,9 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"thumb-cache-path", c.ThumbCachePath()}, {"thumb-cache-path", c.ThumbCachePath()},
{"import-path", c.ImportPath()}, {"import-path", c.ImportPath()},
{"import-dest", c.ImportDest()}, {"import-dest", c.ImportDest()},
{"users-path", c.UsersPath()},
{"users-storage-path", c.UsersStoragePath()},
{"users-originals-path", c.UsersOriginalsPath()},
{"assets-path", c.AssetsPath()}, {"assets-path", c.AssetsPath()},
{"static-path", c.StaticPath()}, {"static-path", c.StaticPath()},
{"build-path", c.BuildPath()}, {"build-path", c.BuildPath()},

View File

@@ -34,6 +34,9 @@ var UsernameLength = 1
// PasswordLength specifies the minimum length of the password in characters. // PasswordLength specifies the minimum length of the password in characters.
var PasswordLength = 4 var PasswordLength = 4
// UsersPath is the relative path for user assets.
var UsersPath = "users"
// Users represents a list of users. // Users represents a list of users.
type Users []User type Users []User
@@ -118,6 +121,8 @@ func FindUser(find User) *User {
stmt = stmt.Where("id = ?", find.ID) stmt = stmt.Where("id = ?", find.ID)
} else if rnd.IsUID(find.UserUID, UserUID) { } else if rnd.IsUID(find.UserUID, UserUID) {
stmt = stmt.Where("user_uid = ?", find.UserUID) stmt = stmt.Where("user_uid = ?", find.UserUID)
} else if find.AuthProvider != "" && find.AuthID != "" && find.UserName != "" {
stmt = stmt.Where("auth_provider = ? AND auth_id = ? OR user_name = ?", find.AuthProvider, find.AuthID, find.UserName)
} else if find.UserName != "" { } else if find.UserName != "" {
stmt = stmt.Where("user_name = ?", find.UserName) stmt = stmt.Where("user_name = ?", find.UserName)
} else if find.UserEmail != "" { } else if find.UserEmail != "" {
@@ -432,7 +437,7 @@ func (m *User) DefaultBasePath() string {
if s := m.Handle(); s == "" { if s := m.Handle(); s == "" {
return "" return ""
} else { } else {
return fmt.Sprintf("users/%s", s) return path.Join(UsersPath, s)
} }
} }

View File

@@ -5,6 +5,8 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"net/http" "net/http"
"os"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -13,11 +15,13 @@ import (
gc "github.com/patrickmn/go-cache" gc "github.com/patrickmn/go-cache"
"github.com/photoprism/photoprism/internal/api" "github.com/photoprism/photoprism/internal/api"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/server/limiter" "github.com/photoprism/photoprism/internal/server/limiter"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
) )
// Authentication cache with an expiration time of 5 minutes. // Authentication cache with an expiration time of 5 minutes.
@@ -38,7 +42,7 @@ func GetAuthUser(key string) *entity.User {
} }
// BasicAuth implements an HTTP request handler that adds basic authentication. // BasicAuth implements an HTTP request handler that adds basic authentication.
func BasicAuth() gin.HandlerFunc { func BasicAuth(conf *config.Config) gin.HandlerFunc {
var validate = func(c *gin.Context) (name, password, key string, valid bool) { var validate = func(c *gin.Context) (name, password, key string, valid bool) {
name, password, key = GetCredentials(c) name, password, key = GetCredentials(c)
@@ -106,6 +110,11 @@ func BasicAuth() gin.HandlerFunc {
// Sync disabled for this account. // Sync disabled for this account.
message := "sync disabled" message := "sync disabled"
event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message)
} else if err = os.MkdirAll(filepath.Join(conf.OriginalsPath(), user.GetUploadPath()), fs.ModeDir); err != nil {
message := "failed to create user upload path"
event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name)) event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message) event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message)
} else { } else {

View File

@@ -31,11 +31,11 @@ func registerWebDAVRoutes(router *gin.Engine, conf *config.Config) {
info = "" info = ""
} }
WebDAV(conf.OriginalsPath(), router.Group(conf.BaseUri(WebDAVOriginals), BasicAuth()), conf) WebDAV(conf.OriginalsPath(), router.Group(conf.BaseUri(WebDAVOriginals), BasicAuth(conf)), conf)
log.Infof("webdav: shared %s/%s", conf.BaseUri(WebDAVOriginals), info) log.Infof("webdav: shared %s/%s", conf.BaseUri(WebDAVOriginals), info)
if conf.ImportPath() != "" { if conf.ImportPath() != "" {
WebDAV(conf.ImportPath(), router.Group(conf.BaseUri(WebDAVImport), BasicAuth()), conf) WebDAV(conf.ImportPath(), router.Group(conf.BaseUri(WebDAVImport), BasicAuth(conf)), conf)
log.Infof("webdav: shared %s/%s", conf.BaseUri(WebDAVImport), info) log.Infof("webdav: shared %s/%s", conf.BaseUri(WebDAVImport), info)
} }
} }