From addc5e8251d5b151b8ac40b8d51111c9b9a7deab Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Tue, 14 Mar 2023 21:47:14 +0100 Subject: [PATCH] Auth: Refactor users path configuration and base path default Signed-off-by: Michael Mayer --- frontend/src/model/user.js | 16 ++++++++++++++ internal/api/import.go | 1 + internal/api/users_upload.go | 1 + internal/config/client_config.go | 4 ++++ internal/config/config.go | 3 +++ internal/config/config_filepaths.go | 27 ++++++++++++++++-------- internal/config/config_filepaths_test.go | 14 ++++++------ internal/config/flags.go | 11 +++++----- internal/config/report.go | 6 ++++-- internal/entity/auth_user.go | 7 +++++- internal/server/basicauth.go | 11 +++++++++- internal/server/routes_webdav.go | 4 ++-- 12 files changed, 78 insertions(+), 27 deletions(-) diff --git a/frontend/src/model/user.js b/frontend/src/model/user.js index 94f6ff055..32926925a 100644 --- a/frontend/src/model/user.js +++ b/frontend/src/model/user.js @@ -121,6 +121,22 @@ export class User extends RestModel { 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() { if (this.DisplayName) { return this.DisplayName; diff --git a/internal/api/import.go b/internal/api/import.go index 444bcf2e0..a7ac9391e 100644 --- a/internal/api/import.go +++ b/internal/api/import.go @@ -80,6 +80,7 @@ func StartImport(router *gin.RouterGroup) { RemoveFromFolderCache(entity.RootImport) + // Get destination folder. var destFolder string if destFolder = s.User().GetUploadPath(); destFolder == "" { destFolder = conf.ImportDest() diff --git a/internal/api/users_upload.go b/internal/api/users_upload.go index 563391ea0..7dad43b6b 100644 --- a/internal/api/users_upload.go +++ b/internal/api/users_upload.go @@ -185,6 +185,7 @@ func ProcessUserUpload(router *gin.RouterGroup) { imp := get.Import() + // Get destination folder. var destFolder string if destFolder = s.User().GetUploadPath(); destFolder == "" { destFolder = conf.ImportDest() diff --git a/internal/config/client_config.go b/internal/config/client_config.go index 17d4364ef..c6180af86 100644 --- a/internal/config/client_config.go +++ b/internal/config/client_config.go @@ -60,6 +60,7 @@ type ClientConfig struct { UploadNSFW bool `json:"uploadNSFW"` Public bool `json:"public"` AuthMode string `json:"authMode"` + UsersPath string `json:"usersPath"` LoginUri string `json:"loginUri"` RegisterUri string `json:"registerUri"` PasswordLength int `json:"passwordLength"` @@ -278,6 +279,7 @@ func (c *Config) ClientPublic() ClientConfig { ReadOnly: c.ReadOnly(), Public: c.Public(), AuthMode: c.AuthMode(), + UsersPath: c.UsersPath(), LoginUri: c.LoginUri(), RegisterUri: c.RegisterUri(), PasswordResetUri: c.PasswordResetUri(), @@ -364,6 +366,7 @@ func (c *Config) ClientShare() ClientConfig { UploadNSFW: c.UploadNSFW(), Public: c.Public(), AuthMode: c.AuthMode(), + UsersPath: "", LoginUri: c.LoginUri(), RegisterUri: c.RegisterUri(), PasswordResetUri: c.PasswordResetUri(), @@ -455,6 +458,7 @@ func (c *Config) ClientUser(withSettings bool) ClientConfig { UploadNSFW: c.UploadNSFW(), Public: c.Public(), AuthMode: c.AuthMode(), + UsersPath: c.UsersPath(), LoginUri: c.LoginUri(), RegisterUri: c.RegisterUri(), PasswordLength: c.PasswordLength(), diff --git a/internal/config/config.go b/internal/config/config.go index 8efd4ca02..0c716a479 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -173,6 +173,9 @@ func (c *Config) Propagate() { // Set minimum password length. entity.PasswordLength = c.PasswordLength() + // Set path for user assets. + entity.UsersPath = c.UsersPath() + // Set API preview and download default tokens. entity.PreviewToken.Set(c.PreviewToken(), entity.TokenConfig) entity.DownloadToken.Set(c.DownloadToken(), entity.TokenConfig) diff --git a/internal/config/config_filepaths.go b/internal/config/config_filepaths.go index 2607f21e9..7343e5ac2 100644 --- a/internal/config/config_filepaths.go +++ b/internal/config/config_filepaths.go @@ -90,8 +90,8 @@ func (c *Config) CreateDirectories() error { if c.UsersPath() == "" { return notFoundError("users") - } else if err := os.MkdirAll(c.UsersPath(), fs.ModeDir); err != nil { - return createError(c.UsersPath(), err) + } else if err := os.MkdirAll(c.UsersStoragePath(), fs.ModeDir); err != nil { + return createError(c.UsersStoragePath(), err) } if c.CmdCachePath() == "" { @@ -325,19 +325,28 @@ func (c *Config) SidecarWritable() bool { return !c.ReadOnly() || c.SidecarPathIsAbs() } -// UsersPath returns the storage base path for user assets like -// avatar images and other media that should not be indexed. +// UsersPath returns the relative base path for user assets. func (c *Config) UsersPath() string { // Set default. 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. -func (c *Config) UserPath(userUid string) string { +// UsersStoragePath returns the users storage base path. +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) { return "" } @@ -357,7 +366,7 @@ func (c *Config) UserUploadPath(userUid, token string) (string, error) { 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 { return "", err diff --git a/internal/config/config_filepaths_test.go b/internal/config/config_filepaths_test.go index b88d3c519..a65d9855e 100644 --- a/internal/config/config_filepaths_test.go +++ b/internal/config/config_filepaths_test.go @@ -25,14 +25,14 @@ func TestConfig_SidecarPath(t *testing.T) { func TestConfig_UsersPath(t *testing.T) { 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()) - assert.Equal(t, "", c.UserPath("")) - assert.Equal(t, "", c.UserPath("etaetyget")) - assert.Contains(t, c.UserPath("urjult03ceelhw6k"), "testdata/users/urjult03ceelhw6k") + assert.Equal(t, "", c.UserStoragePath("")) + assert.Equal(t, "", c.UserStoragePath("etaetyget")) + assert.Contains(t, c.UserStoragePath("urjult03ceelhw6k"), "users/urjult03ceelhw6k") } func TestConfig_UserUploadPath(t *testing.T) { @@ -50,12 +50,12 @@ func TestConfig_UserUploadPath(t *testing.T) { if dir, err := c.UserUploadPath("urjult03ceelhw6k", ""); err != nil { t.Fatal(err) } 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 { t.Fatal(err) } else { - assert.Contains(t, dir, "testdata/users/urjult03ceelhw6k/upload/foo") + assert.Contains(t, dir, "users/urjult03ceelhw6k/upload/foo") } } diff --git a/internal/config/flags.go b/internal/config/flags.go index 21222a144..eb072fc9f 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -142,11 +142,6 @@ var Flags = CliFlags{ Usage: "custom relative or absolute sidecar `PATH` *optional*", EnvVar: "PHOTOPRISM_SIDECAR_PATH", }}, { - Flag: cli.StringFlag{ - Name: "users-path", - Usage: "custom users storage `PATH` *optional*", - EnvVar: "PHOTOPRISM_USERS_PATH", - }}, { Flag: cli.StringFlag{ Name: "backup-path, ba", 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*", 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{ Name: "assets-path, as", Usage: "assets `PATH` containing static resources like icons, models, and translations", diff --git a/internal/config/report.go b/internal/config/report.go index 4d5661397..84f466aac 100644 --- a/internal/config/report.go +++ b/internal/config/report.go @@ -57,10 +57,9 @@ func (c *Config) Report() (rows [][]string, cols []string) { {"originals-limit", fmt.Sprintf("%d", c.OriginalsLimit())}, {"resolution-limit", fmt.Sprintf("%d", c.ResolutionLimit())}, - // Other paths. + // Storage. {"storage-path", c.StoragePath()}, {"sidecar-path", c.SidecarPath()}, - {"users-path", c.UsersPath()}, {"albums-path", c.AlbumsPath()}, {"backup-path", c.BackupPath()}, {"cache-path", c.CachePath()}, @@ -69,6 +68,9 @@ func (c *Config) Report() (rows [][]string, cols []string) { {"thumb-cache-path", c.ThumbCachePath()}, {"import-path", c.ImportPath()}, {"import-dest", c.ImportDest()}, + {"users-path", c.UsersPath()}, + {"users-storage-path", c.UsersStoragePath()}, + {"users-originals-path", c.UsersOriginalsPath()}, {"assets-path", c.AssetsPath()}, {"static-path", c.StaticPath()}, {"build-path", c.BuildPath()}, diff --git a/internal/entity/auth_user.go b/internal/entity/auth_user.go index 2b5fcef87..e59e12b55 100644 --- a/internal/entity/auth_user.go +++ b/internal/entity/auth_user.go @@ -34,6 +34,9 @@ var UsernameLength = 1 // PasswordLength specifies the minimum length of the password in characters. var PasswordLength = 4 +// UsersPath is the relative path for user assets. +var UsersPath = "users" + // Users represents a list of users. type Users []User @@ -118,6 +121,8 @@ func FindUser(find User) *User { stmt = stmt.Where("id = ?", find.ID) } else if rnd.IsUID(find.UserUID, 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 != "" { stmt = stmt.Where("user_name = ?", find.UserName) } else if find.UserEmail != "" { @@ -432,7 +437,7 @@ func (m *User) DefaultBasePath() string { if s := m.Handle(); s == "" { return "" } else { - return fmt.Sprintf("users/%s", s) + return path.Join(UsersPath, s) } } diff --git a/internal/server/basicauth.go b/internal/server/basicauth.go index 1203abdd0..1ddfbe5e4 100644 --- a/internal/server/basicauth.go +++ b/internal/server/basicauth.go @@ -5,6 +5,8 @@ import ( "encoding/base64" "fmt" "net/http" + "os" + "path/filepath" "strings" "sync" "time" @@ -13,11 +15,13 @@ import ( gc "github.com/patrickmn/go-cache" "github.com/photoprism/photoprism/internal/api" + "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/server/limiter" "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/fs" ) // 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. -func BasicAuth() gin.HandlerFunc { +func BasicAuth(conf *config.Config) gin.HandlerFunc { var validate = func(c *gin.Context) (name, password, key string, valid bool) { name, password, key = GetCredentials(c) @@ -106,6 +110,11 @@ func BasicAuth() gin.HandlerFunc { // Sync disabled for this account. 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.LoginError(clientIp, "webdav", name, api.UserAgent(c), message) } else { diff --git a/internal/server/routes_webdav.go b/internal/server/routes_webdav.go index 681ef7c7b..c548632e5 100644 --- a/internal/server/routes_webdav.go +++ b/internal/server/routes_webdav.go @@ -31,11 +31,11 @@ func registerWebDAVRoutes(router *gin.Engine, conf *config.Config) { 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) 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) } }