From 37c3c9d624fa87e4edbf4d80c706bbff936110f9 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Fri, 29 Mar 2024 12:16:26 +0100 Subject: [PATCH] Account: Add auth-related error messages to pkg/authn #808 #4114 Signed-off-by: Michael Mayer --- internal/api/api_auth.go | 23 +++---- internal/api/import.go | 5 +- internal/api/session_delete.go | 5 +- internal/api/session_oauth.go | 10 +-- internal/commands/users_add.go | 14 ++-- internal/config/config.go | 2 +- internal/config/config_auth.go | 4 +- internal/entity/auth_client.go | 2 +- internal/entity/auth_session.go | 10 +-- internal/entity/auth_session_login.go | 25 ++++--- internal/entity/auth_session_login_test.go | 8 +-- internal/entity/auth_session_test.go | 76 +++++++++++----------- internal/entity/auth_tokens.go | 6 +- internal/entity/auth_user.go | 4 +- internal/entity/link.go | 2 +- internal/entity/passcode.go | 4 +- internal/entity/passcode_test.go | 16 ++--- internal/entity/password.go | 26 ++++---- internal/entity/password_test.go | 58 ++++++++--------- internal/search/photos.go | 3 +- internal/search/photos_geo.go | 3 +- internal/server/webdav_auth.go | 23 +++---- internal/server/webdav_auth_test.go | 4 +- pkg/authn/const.go | 7 ++ pkg/authn/errors.go | 42 ++++++++++-- 25 files changed, 218 insertions(+), 164 deletions(-) create mode 100644 pkg/authn/const.go diff --git a/internal/api/api_auth.go b/internal/api/api_auth.go index 03bec0de3..dab6ec7d3 100644 --- a/internal/api/api_auth.go +++ b/internal/api/api_auth.go @@ -6,6 +6,7 @@ import ( "github.com/photoprism/photoprism/internal/acl" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/event" + "github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/header" ) @@ -30,7 +31,7 @@ func AuthAny(c *gin.Context, resource acl.Resource, perms acl.Permissions) (s *e // Find active session to perform authorization check or deny if no session was found. if s = Session(clientIp, authToken); s == nil { - event.AuditWarn([]string{clientIp, "%s %s without authentication", "denied"}, perms.String(), string(resource)) + event.AuditWarn([]string{clientIp, "%s %s without authentication", authn.Denied}, perms.String(), string(resource)) return entity.SessionStatusUnauthorized() } @@ -42,32 +43,32 @@ func AuthAny(c *gin.Context, resource acl.Resource, perms acl.Permissions) (s *e // on the allowed scope, the ACL, and the user account it belongs to (if any). if s.IsClient() { // Check the resource and required permissions against the session scope. - if s.ScopeExcludes(resource, perms) { - event.AuditErr([]string{clientIp, "client %s", "session %s", "access %s", "denied"}, clean.Log(s.ClientInfo()), s.RefID, string(resource)) + if s.InsufficientScope(resource, perms) { + event.AuditErr([]string{clientIp, "client %s", "session %s", "access %s", authn.ErrInsufficientScope.Error()}, clean.Log(s.ClientInfo()), s.RefID, string(resource)) return entity.SessionStatusForbidden() } // Check request authorization against client application ACL rules. if acl.Rules.DenyAll(resource, s.ClientRole(), perms) { - event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s", "denied"}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource)) + event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s", authn.Denied}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource)) return entity.SessionStatusForbidden() } // Also check the request authorization against the user's ACL rules? if s.NoUser() { // Allow access based on the ACL defaults for client applications. - event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s", "granted"}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource)) + event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s", authn.Granted}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource)) } else if u := s.User(); !u.IsDisabled() && !u.IsUnknown() && u.IsRegistered() { if acl.Rules.DenyAll(resource, u.AclRole(), perms) { - event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as %s", "denied"}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource), u.String()) + event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as %s", authn.Denied}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource), u.String()) return entity.SessionStatusForbidden() } // Allow access based on the user role. - event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s as %s", "granted"}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource), u.String()) + event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s as %s", authn.Granted}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource), u.String()) } else { // Deny access if it is not a regular user account or the account has been disabled. - event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as unauthorized user", "denied"}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource)) + event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as unauthorized user", authn.Denied}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource)) return entity.SessionStatusForbidden() } @@ -76,13 +77,13 @@ func AuthAny(c *gin.Context, resource acl.Resource, perms acl.Permissions) (s *e // Otherwise, perform a regular ACL authorization check based on the user role. if u := s.User(); u.IsUnknown() || u.IsDisabled() { - event.AuditWarn([]string{clientIp, "session %s", "%s %s as unauthorized user", "denied"}, s.RefID, perms.String(), string(resource)) + event.AuditWarn([]string{clientIp, "session %s", "%s %s as unauthorized user", authn.Denied}, s.RefID, perms.String(), string(resource)) return entity.SessionStatusUnauthorized() } else if acl.Rules.DenyAll(resource, u.AclRole(), perms) { - event.AuditErr([]string{clientIp, "session %s", "%s %s as %s", "denied"}, s.RefID, perms.String(), string(resource), u.AclRole().String()) + event.AuditErr([]string{clientIp, "session %s", "%s %s as %s", authn.Denied}, s.RefID, perms.String(), string(resource), u.AclRole().String()) return entity.SessionStatusForbidden() } else { - event.AuditInfo([]string{clientIp, "session %s", "%s %s as %s", "granted"}, s.RefID, perms.String(), string(resource), u.AclRole().String()) + event.AuditInfo([]string{clientIp, "session %s", "%s %s as %s", authn.Granted}, s.RefID, perms.String(), string(resource), u.AclRole().String()) return s } } diff --git a/internal/api/import.go b/internal/api/import.go index 8feddb437..23c6baaa8 100644 --- a/internal/api/import.go +++ b/internal/api/import.go @@ -18,6 +18,7 @@ import ( "github.com/photoprism/photoprism/internal/get" "github.com/photoprism/photoprism/internal/photoprism" "github.com/photoprism/photoprism/internal/query" + "github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/i18n" @@ -67,9 +68,9 @@ func StartImport(router *gin.RouterGroup) { // To avoid conflicts, uploads are imported from "import_path/upload/session_ref/timestamp". if token := path.Base(srcFolder); token != "" && path.Dir(srcFolder) == UploadPath { srcFolder = path.Join(UploadPath, s.RefID+token) - event.AuditInfo([]string{ClientIP(c), "session %s", "import uploads from %s as %s", "granted"}, s.RefID, clean.Log(srcFolder), s.UserRole().String()) + event.AuditInfo([]string{ClientIP(c), "session %s", "import uploads from %s as %s", authn.Granted}, s.RefID, clean.Log(srcFolder), s.UserRole().String()) } else if acl.Rules.Deny(acl.ResourceFiles, s.UserRole(), acl.ActionManage) { - event.AuditErr([]string{ClientIP(c), "session %s", "import files from %s as %s", "denied"}, s.RefID, clean.Log(srcFolder), s.UserRole().String()) + event.AuditErr([]string{ClientIP(c), "session %s", "import files from %s as %s", authn.Denied}, s.RefID, clean.Log(srcFolder), s.UserRole().String()) AbortForbidden(c) return } diff --git a/internal/api/session_delete.go b/internal/api/session_delete.go index 037291510..7cf5e06e1 100644 --- a/internal/api/session_delete.go +++ b/internal/api/session_delete.go @@ -10,6 +10,7 @@ import ( "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/get" "github.com/photoprism/photoprism/internal/session" + "github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/header" "github.com/photoprism/photoprism/pkg/i18n" @@ -50,12 +51,12 @@ func DeleteSession(router *gin.RouterGroup) { // Only admins may delete other sessions by ref id. if rnd.IsRefID(id) { if !acl.Rules.AllowAll(acl.ResourceSessions, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) { - event.AuditErr([]string{clientIp, "session %s", "delete %s as %s", "denied"}, s.RefID, acl.ResourceSessions.String(), s.UserRole()) + event.AuditErr([]string{clientIp, "session %s", "delete %s as %s", authn.Denied}, s.RefID, acl.ResourceSessions.String(), s.UserRole()) Abort(c, http.StatusForbidden, i18n.ErrForbidden) return } - event.AuditInfo([]string{clientIp, "session %s", "delete %s as %s", "granted"}, s.RefID, acl.ResourceSessions.String(), s.UserRole()) + event.AuditInfo([]string{clientIp, "session %s", "delete %s as %s", authn.Granted}, s.RefID, acl.ResourceSessions.String(), s.UserRole()) if s = entity.FindSessionByRefID(id); s == nil { Abort(c, http.StatusNotFound, i18n.ErrNotFound) diff --git a/internal/api/session_oauth.go b/internal/api/session_oauth.go index c476c007c..3907b3f47 100644 --- a/internal/api/session_oauth.go +++ b/internal/api/session_oauth.go @@ -144,7 +144,7 @@ func RevokeOAuthToken(router *gin.RouterGroup) { // Abort if running in public mode. if get.Config().Public() { - event.AuditErr([]string{clientIp, "client", "delete session", "oauth2", "disabled in public mode"}) + event.AuditErr([]string{clientIp, "client", "delete session", "oauth2", authn.ErrDisabledInPublicMode.Error()}) Abort(c, http.StatusForbidden, i18n.ErrForbidden) return } @@ -184,18 +184,18 @@ func RevokeOAuthToken(router *gin.RouterGroup) { c.AbortWithStatusJSON(http.StatusUnauthorized, i18n.NewResponse(http.StatusUnauthorized, i18n.ErrUnauthorized)) return } else if sess == nil { - event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "oauth2", "denied"}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String()) + event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "oauth2", authn.Denied}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String()) c.AbortWithStatusJSON(http.StatusUnauthorized, i18n.NewResponse(http.StatusUnauthorized, i18n.ErrUnauthorized)) return } else if sess.Abort(c) { - event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "oauth2", "denied"}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String()) + event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "oauth2", authn.Denied}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String()) return } else if !sess.IsClient() { - event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "oauth2", "denied"}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String()) + event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "oauth2", authn.Denied}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String()) c.AbortWithStatusJSON(http.StatusForbidden, i18n.NewResponse(http.StatusForbidden, i18n.ErrForbidden)) return } else { - event.AuditInfo([]string{clientIp, "client %s", "session %s", "delete session as %s", "oauth2", "granted"}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String()) + event.AuditInfo([]string{clientIp, "client %s", "session %s", "delete session as %s", "oauth2", authn.Granted}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String()) } // Delete session cache and database record. diff --git a/internal/commands/users_add.go b/internal/commands/users_add.go index b412bb4f4..1ccf665c5 100644 --- a/internal/commands/users_add.go +++ b/internal/commands/users_add.go @@ -1,7 +1,6 @@ package commands import ( - "errors" "fmt" "github.com/manifoldco/promptui" @@ -10,6 +9,7 @@ import ( "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/txt" ) @@ -53,10 +53,10 @@ func usersAddAction(ctx *cli.Context) error { // Check if account exists but is deleted. if frm.UserName == "" { - return fmt.Errorf("username is required") + return authn.ErrUsernameRequired } else if m := entity.FindUserByName(frm.UserName); m != nil { if !m.IsDeleted() { - return fmt.Errorf("user already exists") + return authn.ErrAccountAlreadyExists } prompt := promptui.Prompt{ @@ -65,7 +65,7 @@ func usersAddAction(ctx *cli.Context) error { } if _, err := prompt.Run(); err != nil { - return fmt.Errorf("user already exists") + return authn.ErrAccountAlreadyExists } if err := m.RestoreFromCli(ctx, frm.Password); err != nil { @@ -96,7 +96,7 @@ func usersAddAction(ctx *cli.Context) error { if len([]rune(input)) < entity.PasswordLength { return fmt.Errorf("password must have at least %d characters", entity.PasswordLength) } else if len(input) > txt.ClipPassword { - return fmt.Errorf("password must have less than %d characters", txt.ClipPassword) + return authn.ErrPasswordTooLong } return nil } @@ -111,7 +111,7 @@ func usersAddAction(ctx *cli.Context) error { } validateRetype := func(input string) error { if input != resPasswd { - return errors.New("passwords do not match") + return authn.ErrPasswordsDoNotMatch } return nil } @@ -125,7 +125,7 @@ func usersAddAction(ctx *cli.Context) error { return err } if resConfirm != resPasswd { - return errors.New("password is invalid, please try again") + return authn.ErrInvalidPassword } else { frm.Password = resPasswd } diff --git a/internal/config/config.go b/internal/config/config.go index 42218dbeb..5ce9eab90 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -220,7 +220,7 @@ func (c *Config) Propagate() { // Set API preview and download default tokens. entity.PreviewToken.Set(c.PreviewToken(), entity.TokenConfig) entity.DownloadToken.Set(c.DownloadToken(), entity.TokenConfig) - entity.CheckTokens = !c.Public() + entity.ValidateTokens = !c.Public() // Set face recognition parameters. face.ScoreThreshold = c.FaceScore() diff --git a/internal/config/config_auth.go b/internal/config/config_auth.go index ab97c1362..f628fa692 100644 --- a/internal/config/config_auth.go +++ b/internal/config/config_auth.go @@ -34,11 +34,11 @@ func (c *Config) SetAuthMode(mode string) { case AuthModePublic: c.options.AuthMode = AuthModePublic c.options.Public = true - entity.CheckTokens = false + entity.ValidateTokens = false default: c.options.AuthMode = AuthModePasswd c.options.Public = false - entity.CheckTokens = true + entity.ValidateTokens = true } } diff --git a/internal/entity/auth_client.go b/internal/entity/auth_client.go index 548cf3137..768eac389 100644 --- a/internal/entity/auth_client.go +++ b/internal/entity/auth_client.go @@ -376,7 +376,7 @@ func (m *Client) WrongSecret(s string) bool { } // Invalid? - if pw.IsWrong(s) { + if pw.Invalid(s) { return true } diff --git a/internal/entity/auth_session.go b/internal/entity/auth_session.go index c272d39d1..5d7440342 100644 --- a/internal/entity/auth_session.go +++ b/internal/entity/auth_session.go @@ -518,8 +518,8 @@ func (m *Session) Scope() string { return clean.Scope(m.AuthScope) } -// ScopeAllows checks if the scope does not exclude access to specified resource. -func (m *Session) ScopeAllows(resource acl.Resource, perms acl.Permissions) bool { +// ValidateScope checks if the scope does not exclude access to specified resource. +func (m *Session) ValidateScope(resource acl.Resource, perms acl.Permissions) bool { // Get scope string. scope := m.Scope() @@ -556,9 +556,9 @@ func (m *Session) ScopeAllows(resource acl.Resource, perms acl.Permissions) bool return true } -// ScopeExcludes checks if the scope does not include access to specified resource. -func (m *Session) ScopeExcludes(resource acl.Resource, perms acl.Permissions) bool { - return !m.ScopeAllows(resource, perms) +// InsufficientScope checks if the scope does not include access to specified resource. +func (m *Session) InsufficientScope(resource acl.Resource, perms acl.Permissions) bool { + return !m.ValidateScope(resource, perms) } // SetScope sets a custom authentication scope. diff --git a/internal/entity/auth_session_login.go b/internal/entity/auth_session_login.go index 1d64ac0a2..dd971f169 100644 --- a/internal/entity/auth_session_login.go +++ b/internal/entity/auth_session_login.go @@ -83,7 +83,7 @@ func AuthLocal(user *User, f form.Login, m *Session, c *gin.Context) (provider a // Check if user account exists. if user == nil { - message := "account not found" + message := authn.ErrAccountNotFound.Error() limiter.Login.Reserve(clientIp) if m != nil { @@ -107,7 +107,7 @@ func AuthLocal(user *User, f form.Login, m *Session, c *gin.Context) (provider a return provider, method, i18n.Error(i18n.ErrInvalidCredentials) } else if !user.CanLogIn() { - message := "account disabled" + message := authn.ErrAccountDisabled.Error() if m != nil { event.AuditWarn([]string{clientIp, "session %s", "login as %s", message}, m.RefID, clean.LogQuote(userName)) @@ -121,14 +121,19 @@ func AuthLocal(user *User, f form.Login, m *Session, c *gin.Context) (provider a // Authentication with personal access token if a valid secret has been provided as password. if authSess, authUser, authErr := AuthSession(f, c); authSess != nil && authUser != nil && authErr == nil { if !authUser.IsRegistered() || authUser.UserUID != user.UserUID { - message := "incorrect user" + message := authn.ErrInvalidUsername.Error() limiter.Login.Reserve(clientIp) event.AuditErr([]string{clientIp, "session %s", "login as %s with app password", message}, m.RefID, clean.LogQuote(userName)) event.LoginError(clientIp, "api", userName, m.UserAgent, message) m.Status = http.StatusUnauthorized return provider, method, i18n.Error(i18n.ErrInvalidCredentials) - } else if !authSess.IsClient() || authSess.ScopeExcludes(acl.ResourceSessions, acl.Permissions{acl.ActionCreate}) { - message := "unauthorized" + } else if insufficientScope := authSess.InsufficientScope(acl.ResourceSessions, acl.Permissions{acl.ActionCreate}); insufficientScope || !authSess.IsClient() { + var message string + if insufficientScope { + message = authn.ErrInsufficientScope.Error() + } else { + message = authn.ErrUnauthorized.Error() + } limiter.Login.Reserve(clientIp) event.AuditErr([]string{clientIp, "session %s", "login as %s with app password", message}, m.RefID, clean.LogQuote(userName)) event.LoginError(clientIp, "api", userName, m.UserAgent, message) @@ -149,7 +154,7 @@ func AuthLocal(user *User, f form.Login, m *Session, c *gin.Context) (provider a // Otherwise, check account password. if user.WrongPassword(f.Password) { - message := "incorrect password" + message := authn.ErrInvalidPassword.Error() limiter.Login.Reserve(clientIp) if m != nil { @@ -219,8 +224,9 @@ func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) { // Redeem token. if user.IsRegistered() { if shares := user.RedeemToken(f.ShareToken); shares == 0 { + message := authn.ErrInvalidShareToken.Error() limiter.Login.Reserve(m.IP()) - event.AuditWarn([]string{m.IP(), "session %s", "share token %s is invalid"}, m.RefID, clean.LogQuote(f.ShareToken)) + event.AuditWarn([]string{m.IP(), "session %s", message}, m.RefID) m.Status = http.StatusNotFound return i18n.Error(i18n.ErrInvalidLink) } else { @@ -230,9 +236,10 @@ func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) { m.Status = http.StatusInternalServerError return i18n.Error(i18n.ErrUnexpected) } else if shares := data.RedeemToken(f.ShareToken); shares == 0 { + message := authn.ErrInvalidShareToken.Error() limiter.Login.Reserve(m.IP()) - event.AuditWarn([]string{m.IP(), "session %s", "share token %s is invalid"}, m.RefID, clean.LogQuote(f.ShareToken)) - event.LoginError(m.IP(), "api", "", m.UserAgent, "invalid share token") + event.AuditWarn([]string{m.IP(), "session %s", message}, m.RefID) + event.LoginError(m.IP(), "api", "", m.UserAgent, message) m.Status = http.StatusNotFound return i18n.Error(i18n.ErrInvalidLink) } else { diff --git a/internal/entity/auth_session_login_test.go b/internal/entity/auth_session_login_test.go index c6ab077fe..f39ac711b 100644 --- a/internal/entity/auth_session_login_test.go +++ b/internal/entity/auth_session_login_test.go @@ -110,8 +110,8 @@ func TestAuthSession(t *testing.T) { assert.True(t, authSess.IsRegistered()) assert.True(t, authSess.HasUser()) - assert.True(t, authSess.ScopeAllows(acl.ResourceWebDAV, acl.Permissions{acl.ActionCreate})) - assert.True(t, authSess.ScopeAllows(acl.ResourceSessions, acl.Permissions{acl.ActionCreate})) + assert.True(t, authSess.ValidateScope(acl.ResourceWebDAV, acl.Permissions{acl.ActionCreate})) + assert.True(t, authSess.ValidateScope(acl.ResourceSessions, acl.Permissions{acl.ActionCreate})) }) t.Run("AliceTokenWebdav", func(t *testing.T) { s := SessionFixtures.Get("alice_token_webdav") @@ -148,8 +148,8 @@ func TestAuthSession(t *testing.T) { assert.True(t, authSess.IsRegistered()) assert.True(t, authSess.HasUser()) - assert.True(t, authSess.ScopeAllows(acl.ResourceWebDAV, acl.Permissions{acl.ActionCreate})) - assert.False(t, authSess.ScopeAllows(acl.ResourceSessions, acl.Permissions{acl.ActionCreate})) + assert.True(t, authSess.ValidateScope(acl.ResourceWebDAV, acl.Permissions{acl.ActionCreate})) + assert.False(t, authSess.ValidateScope(acl.ResourceSessions, acl.Permissions{acl.ActionCreate})) }) t.Run("EmptyPassword", func(t *testing.T) { // Create test request form. diff --git a/internal/entity/auth_session_test.go b/internal/entity/auth_session_test.go index a6e9285e4..a17729625 100644 --- a/internal/entity/auth_session_test.go +++ b/internal/entity/auth_session_test.go @@ -544,7 +544,7 @@ func TestSession_SetAuthID(t *testing.T) { }) } -func TestSession_ScopeAllows(t *testing.T) { +func TestSession_ValidateScope(t *testing.T) { t.Run("AnyScope", func(t *testing.T) { s := &Session{ UserName: "test", @@ -552,7 +552,7 @@ func TestSession_ScopeAllows(t *testing.T) { AuthScope: "*", } - assert.True(t, s.ScopeAllows("", nil)) + assert.True(t, s.ValidateScope("", nil)) }) t.Run("ReadScope", func(t *testing.T) { s := &Session{ @@ -561,14 +561,14 @@ func TestSession_ScopeAllows(t *testing.T) { AuthScope: "read", } - assert.True(t, s.ScopeAllows("metrics", nil)) - assert.True(t, s.ScopeAllows("sessions", nil)) - assert.True(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionView, acl.AccessAll})) - assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionUpdate})) - assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionUpdate})) - assert.False(t, s.ScopeAllows("settings", acl.Permissions{acl.ActionUpdate})) - assert.False(t, s.ScopeAllows("settings", acl.Permissions{acl.ActionCreate})) - assert.False(t, s.ScopeAllows("sessions", acl.Permissions{acl.ActionDelete})) + assert.True(t, s.ValidateScope("metrics", nil)) + assert.True(t, s.ValidateScope("sessions", nil)) + assert.True(t, s.ValidateScope("metrics", acl.Permissions{acl.ActionView, acl.AccessAll})) + assert.False(t, s.ValidateScope("metrics", acl.Permissions{acl.ActionUpdate})) + assert.False(t, s.ValidateScope("metrics", acl.Permissions{acl.ActionUpdate})) + assert.False(t, s.ValidateScope("settings", acl.Permissions{acl.ActionUpdate})) + assert.False(t, s.ValidateScope("settings", acl.Permissions{acl.ActionCreate})) + assert.False(t, s.ValidateScope("sessions", acl.Permissions{acl.ActionDelete})) }) t.Run("ReadAny", func(t *testing.T) { s := &Session{ @@ -577,14 +577,14 @@ func TestSession_ScopeAllows(t *testing.T) { AuthScope: "read *", } - assert.True(t, s.ScopeAllows("metrics", nil)) - assert.True(t, s.ScopeAllows("sessions", nil)) - assert.True(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionView, acl.AccessAll})) - assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionUpdate})) - assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionUpdate})) - assert.False(t, s.ScopeAllows("settings", acl.Permissions{acl.ActionUpdate})) - assert.False(t, s.ScopeAllows("settings", acl.Permissions{acl.ActionCreate})) - assert.False(t, s.ScopeAllows("sessions", acl.Permissions{acl.ActionDelete})) + assert.True(t, s.ValidateScope("metrics", nil)) + assert.True(t, s.ValidateScope("sessions", nil)) + assert.True(t, s.ValidateScope("metrics", acl.Permissions{acl.ActionView, acl.AccessAll})) + assert.False(t, s.ValidateScope("metrics", acl.Permissions{acl.ActionUpdate})) + assert.False(t, s.ValidateScope("metrics", acl.Permissions{acl.ActionUpdate})) + assert.False(t, s.ValidateScope("settings", acl.Permissions{acl.ActionUpdate})) + assert.False(t, s.ValidateScope("settings", acl.Permissions{acl.ActionCreate})) + assert.False(t, s.ValidateScope("sessions", acl.Permissions{acl.ActionDelete})) }) t.Run("ReadSettings", func(t *testing.T) { s := &Session{ @@ -593,19 +593,19 @@ func TestSession_ScopeAllows(t *testing.T) { AuthScope: "read settings", } - assert.True(t, s.ScopeAllows("settings", acl.Permissions{acl.ActionView})) - assert.False(t, s.ScopeAllows("metrics", nil)) - assert.False(t, s.ScopeAllows("sessions", nil)) - assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionView, acl.AccessAll})) - assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionUpdate})) - assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionUpdate})) - assert.False(t, s.ScopeAllows("settings", acl.Permissions{acl.ActionUpdate})) - assert.False(t, s.ScopeAllows("sessions", acl.Permissions{acl.ActionDelete})) - assert.False(t, s.ScopeAllows("sessions", acl.Permissions{acl.ActionDelete})) + assert.True(t, s.ValidateScope("settings", acl.Permissions{acl.ActionView})) + assert.False(t, s.ValidateScope("metrics", nil)) + assert.False(t, s.ValidateScope("sessions", nil)) + assert.False(t, s.ValidateScope("metrics", acl.Permissions{acl.ActionView, acl.AccessAll})) + assert.False(t, s.ValidateScope("metrics", acl.Permissions{acl.ActionUpdate})) + assert.False(t, s.ValidateScope("metrics", acl.Permissions{acl.ActionUpdate})) + assert.False(t, s.ValidateScope("settings", acl.Permissions{acl.ActionUpdate})) + assert.False(t, s.ValidateScope("sessions", acl.Permissions{acl.ActionDelete})) + assert.False(t, s.ValidateScope("sessions", acl.Permissions{acl.ActionDelete})) }) } -func TestSession_ScopeExcludes(t *testing.T) { +func TestSession_InsufficientScope(t *testing.T) { t.Run("Empty", func(t *testing.T) { s := &Session{ UserName: "test", @@ -613,7 +613,7 @@ func TestSession_ScopeExcludes(t *testing.T) { AuthScope: "*", } - assert.False(t, s.ScopeExcludes("", nil)) + assert.False(t, s.InsufficientScope("", nil)) }) t.Run("ReadSettings", func(t *testing.T) { s := &Session{ @@ -622,15 +622,15 @@ func TestSession_ScopeExcludes(t *testing.T) { AuthScope: "read settings", } - assert.False(t, s.ScopeExcludes("settings", acl.Permissions{acl.ActionView})) - assert.True(t, s.ScopeExcludes("metrics", nil)) - assert.True(t, s.ScopeExcludes("sessions", nil)) - assert.True(t, s.ScopeExcludes("metrics", acl.Permissions{acl.ActionView, acl.AccessAll})) - assert.True(t, s.ScopeExcludes("metrics", acl.Permissions{acl.ActionUpdate})) - assert.True(t, s.ScopeExcludes("metrics", acl.Permissions{acl.ActionUpdate})) - assert.True(t, s.ScopeExcludes("settings", acl.Permissions{acl.ActionUpdate})) - assert.True(t, s.ScopeExcludes("sessions", acl.Permissions{acl.ActionDelete})) - assert.True(t, s.ScopeExcludes("sessions", acl.Permissions{acl.ActionDelete})) + assert.False(t, s.InsufficientScope("settings", acl.Permissions{acl.ActionView})) + assert.True(t, s.InsufficientScope("metrics", nil)) + assert.True(t, s.InsufficientScope("sessions", nil)) + assert.True(t, s.InsufficientScope("metrics", acl.Permissions{acl.ActionView, acl.AccessAll})) + assert.True(t, s.InsufficientScope("metrics", acl.Permissions{acl.ActionUpdate})) + assert.True(t, s.InsufficientScope("metrics", acl.Permissions{acl.ActionUpdate})) + assert.True(t, s.InsufficientScope("settings", acl.Permissions{acl.ActionUpdate})) + assert.True(t, s.InsufficientScope("sessions", acl.Permissions{acl.ActionDelete})) + assert.True(t, s.InsufficientScope("sessions", acl.Permissions{acl.ActionDelete})) }) } diff --git a/internal/entity/auth_tokens.go b/internal/entity/auth_tokens.go index 3c4706b9e..e46cab0dc 100644 --- a/internal/entity/auth_tokens.go +++ b/internal/entity/auth_tokens.go @@ -9,7 +9,7 @@ const TokenPublic = "public" var PreviewToken = NewStringMap(Strings{}) var DownloadToken = NewStringMap(Strings{}) -var CheckTokens = true +var ValidateTokens = true // GenerateToken returns a random string token. func GenerateToken() string { @@ -18,10 +18,10 @@ func GenerateToken() string { // InvalidDownloadToken checks if the token is unknown. func InvalidDownloadToken(t string) bool { - return CheckTokens && DownloadToken.Missing(t) + return ValidateTokens && DownloadToken.Missing(t) } // InvalidPreviewToken checks if the preview token is unknown. func InvalidPreviewToken(t string) bool { - return CheckTokens && PreviewToken.Missing(t) && DownloadToken.Missing(t) + return ValidateTokens && PreviewToken.Missing(t) && DownloadToken.Missing(t) } diff --git a/internal/entity/auth_user.go b/internal/entity/auth_user.go index fceae0f31..d04b78aa7 100644 --- a/internal/entity/auth_user.go +++ b/internal/entity/auth_user.go @@ -889,7 +889,7 @@ func (m *User) WrongPassword(s string) bool { } // Invalid? - if pw.IsWrong(s) { + if pw.Invalid(s) { return true } @@ -924,7 +924,7 @@ func (m *User) VerifyPasscode(code string) (valid bool, passcode *Passcode, err err = authn.ErrPasscodeRequired } else if l := len(code); l < 1 || l > 255 { err = authn.ErrInvalidPasscode - } else if valid, recovery, err = passcode.Verify(code); recovery { + } else if valid, recovery, err = passcode.Valid(code); recovery { // Deactivate 2FA if recovery code has been used. passcode, err = m.DeactivatePasscode() } diff --git a/internal/entity/link.go b/internal/entity/link.go index a423758e9..984d77338 100644 --- a/internal/entity/link.go +++ b/internal/entity/link.go @@ -144,7 +144,7 @@ func (m *Link) InvalidPassword(password string) bool { return password != "" } - return pw.IsWrong(password) + return pw.Invalid(password) } // Save updates the record in the database or inserts a new record if it does not already exist. diff --git a/internal/entity/passcode.go b/internal/entity/passcode.go index 4f6715adc..f8407cb78 100644 --- a/internal/entity/passcode.go +++ b/internal/entity/passcode.go @@ -236,8 +236,8 @@ func (m *Passcode) GenerateCode() (code string, err error) { return code, err } -// Verify checks if the passcode provided is valid. -func (m *Passcode) Verify(code string) (valid bool, recovery bool, err error) { +// Valid checks if the passcode provided is valid. +func (m *Passcode) Valid(code string) (valid bool, recovery bool, err error) { // Validate arguments. if m == nil { return false, false, errors.New("passcode is nil") diff --git a/internal/entity/passcode_test.go b/internal/entity/passcode_test.go index 202e64532..1e39db47f 100644 --- a/internal/entity/passcode_test.go +++ b/internal/entity/passcode_test.go @@ -318,7 +318,7 @@ func TestPasscode_Verify(t *testing.T) { t.Fatal(err) } - valid, recoveryCode, err := m.Verify(code) + valid, recoveryCode, err := m.Valid(code) if err != nil { t.Fatal(err) @@ -337,7 +337,7 @@ func TestPasscode_Verify(t *testing.T) { assert.Nil(t, m.VerifiedAt) - valid, recoveryCode, err := m.Verify("123456") + valid, recoveryCode, err := m.Valid("123456") if err != nil { t.Fatal(err) @@ -356,7 +356,7 @@ func TestPasscode_Verify(t *testing.T) { assert.Nil(t, m.VerifiedAt) - valid, recoveryCode, err := m.Verify("111") + valid, recoveryCode, err := m.Valid("111") assert.Error(t, err) assert.False(t, valid) @@ -372,7 +372,7 @@ func TestPasscode_Verify(t *testing.T) { assert.Nil(t, m.VerifiedAt) - valid, recoveryCode, err := m.Verify("123") + valid, recoveryCode, err := m.Valid("123") if err != nil { t.Fatal(err) @@ -391,7 +391,7 @@ func TestPasscode_Verify(t *testing.T) { assert.Nil(t, m.VerifiedAt) - valid, recoveryCode, err := m.Verify("") + valid, recoveryCode, err := m.Valid("") assert.Error(t, err) assert.False(t, valid) @@ -407,7 +407,7 @@ func TestPasscode_Verify(t *testing.T) { assert.Nil(t, m.VerifiedAt) - valid, recoveryCode, err := m.Verify("123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891") + valid, recoveryCode, err := m.Valid("123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891123456789112345678911234567891") assert.Error(t, err) assert.False(t, valid) @@ -424,7 +424,7 @@ func TestPasscode_Verify(t *testing.T) { assert.Nil(t, m.VerifiedAt) - valid, recoveryCode, err := m.Verify("123456") + valid, recoveryCode, err := m.Valid("123456") assert.Error(t, err) assert.False(t, valid) @@ -454,7 +454,7 @@ func TestPasscode_Activate(t *testing.T) { t.Fatal(err) } - _, _, err = m.Verify(code) + _, _, err = m.Valid(code) if err != nil { t.Fatal(err) diff --git a/internal/entity/password.go b/internal/entity/password.go index b3fc5a238..ae2c257f9 100644 --- a/internal/entity/password.go +++ b/internal/entity/password.go @@ -1,11 +1,11 @@ package entity import ( - "fmt" "time" "golang.org/x/crypto/bcrypt" + "github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/txt" ) @@ -51,9 +51,9 @@ func (m *Password) SetPassword(pw string, allowHash bool) error { // Check if password is too short or too long. if len([]rune(pw)) < 1 { - return fmt.Errorf("password is too short") + return authn.ErrPasswordTooShort } else if len(pw) > txt.ClipPassword { - return fmt.Errorf("password must have less than %d characters", txt.ClipPassword) + return authn.ErrPasswordTooLong } // Check if string already is a bcrypt hash. @@ -73,14 +73,14 @@ func (m *Password) SetPassword(pw string, allowHash bool) error { } } -// IsValid checks if the password is correct. -func (m *Password) IsValid(s string) bool { - return !m.IsWrong(s) +// Valid checks if the password is correct. +func (m *Password) Valid(s string) bool { + return !m.Invalid(s) } -// IsWrong checks if the specified password is incorrect. -func (m *Password) IsWrong(s string) bool { - if m.IsEmpty() { +// Invalid checks if the specified password is incorrect. +func (m *Password) Invalid(s string) bool { + if m.Empty() { // No password set. return true } else if s = clean.Password(s); s == "" { @@ -118,15 +118,15 @@ func FindPassword(uid string) *Password { // Cost returns the hashing cost of the currently set password. func (m *Password) Cost() (int, error) { - if m.IsEmpty() { - return 0, fmt.Errorf("password is empty") + if m.Empty() { + return 0, authn.ErrPasswordRequired } return bcrypt.Cost([]byte(m.Hash)) } -// IsEmpty returns true if no password is set. -func (m *Password) IsEmpty() bool { +// Empty checks if a password has not been set yet. +func (m *Password) Empty() bool { return m.Hash == "" } diff --git a/internal/entity/password_test.go b/internal/entity/password_test.go index 690d10a1d..7cc99a751 100644 --- a/internal/entity/password_test.go +++ b/internal/entity/password_test.go @@ -21,16 +21,16 @@ func TestPassword_SetPassword(t *testing.T) { t.Run("Text", func(t *testing.T) { p := NewPassword("urrwaxd19ldtz68x", "passwd", false) assert.Len(t, p.Hash, 60) - assert.True(t, p.IsValid("passwd")) - assert.False(t, p.IsValid("other")) + assert.True(t, p.Valid("passwd")) + assert.False(t, p.Valid("other")) if err := p.SetPassword("abcd", false); err != nil { t.Fatal(err) } assert.Len(t, p.Hash, 60) - assert.True(t, p.IsValid("abcd")) - assert.False(t, p.IsValid("other")) + assert.True(t, p.Valid("abcd")) + assert.False(t, p.Valid("other")) }) t.Run("Too long", func(t *testing.T) { p := NewPassword("urrwaxd19ldtz68x", "hgfttrgkncgdhfkbvuvygvbekdjbrtugbnljbtruhogtgbotuhblenbhoyuhntyyhngytohrpnehotyihniy", false) @@ -49,55 +49,55 @@ func TestPassword_SetPassword(t *testing.T) { t.Run("Hash", func(t *testing.T) { p := NewPassword("urrwaxd19ldtz68x", "$2a$14$qCcNjxupSJV1gjhgdYxz8e9l0e0fTZosX0s0qhMK54IkI9YOyWLt2", true) assert.Len(t, p.Hash, 60) - assert.True(t, p.IsValid("photoprism")) - assert.False(t, p.IsValid("$2a$14$qCcNjxupSJV1gjhgdYxz8e9l0e0fTZosX0s0qhMK54IkI9YOyWLt2")) - assert.False(t, p.IsValid("other")) + assert.True(t, p.Valid("photoprism")) + assert.False(t, p.Valid("$2a$14$qCcNjxupSJV1gjhgdYxz8e9l0e0fTZosX0s0qhMK54IkI9YOyWLt2")) + assert.False(t, p.Valid("other")) }) } -func TestPassword_IsValid(t *testing.T) { +func TestPassword_Valid(t *testing.T) { t.Run("EmptyHash", func(t *testing.T) { p := Password{Hash: ""} - assert.True(t, p.IsEmpty()) - assert.False(t, p.IsValid("")) + assert.True(t, p.Empty()) + assert.False(t, p.Valid("")) }) t.Run("EmptyPassword", func(t *testing.T) { p := NewPassword("urrwaxd19ldtz68x", "", false) - assert.True(t, p.IsEmpty()) - assert.False(t, p.IsValid("")) + assert.True(t, p.Empty()) + assert.False(t, p.Valid("")) }) t.Run("ShortPassword", func(t *testing.T) { p := NewPassword("urrwaxd19ldtz68x", "passwd", false) - assert.True(t, p.IsValid("passwd")) - assert.False(t, p.IsValid("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G")) + assert.True(t, p.Valid("passwd")) + assert.False(t, p.Valid("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G")) }) t.Run("LongPassword", func(t *testing.T) { p := NewPassword("urrwaxd19ldtz68x", "photoprism", false) - assert.True(t, p.IsValid("photoprism")) - assert.False(t, p.IsValid("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G")) + assert.True(t, p.Valid("photoprism")) + assert.False(t, p.Valid("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G")) }) } -func TestPassword_IsWrong(t *testing.T) { +func TestPassword_Invalid(t *testing.T) { t.Run("EmptyHash", func(t *testing.T) { p := Password{Hash: ""} - assert.True(t, p.IsEmpty()) - assert.True(t, p.IsWrong("")) + assert.True(t, p.Empty()) + assert.True(t, p.Invalid("")) }) t.Run("EmptyPassword", func(t *testing.T) { p := NewPassword("urrwaxd19ldtz68x", "", false) - assert.True(t, p.IsEmpty()) - assert.True(t, p.IsWrong("")) + assert.True(t, p.Empty()) + assert.True(t, p.Invalid("")) }) t.Run("ShortPassword", func(t *testing.T) { p := NewPassword("urrwaxd19ldtz68x", "passwd", false) - assert.True(t, p.IsWrong("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G")) - assert.False(t, p.IsWrong("passwd")) + assert.True(t, p.Invalid("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G")) + assert.False(t, p.Invalid("passwd")) }) t.Run("LongPassword", func(t *testing.T) { p := NewPassword("urrwaxd19ldtz68x", "photoprism", false) - assert.True(t, p.IsWrong("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G")) - assert.False(t, p.IsWrong("photoprism")) + assert.True(t, p.Invalid("$2a$14$p3HKuLvrTuePG/pjXLJQseUnSeAVeVO2cy4b0.34KXsLPK8lkI92G")) + assert.False(t, p.Invalid("photoprism")) }) } @@ -130,14 +130,14 @@ func TestFindPassword(t *testing.T) { if p := FindPassword("uqxetse3cy5eo9z2"); p == nil { t.Fatal("password not found") } else { - assert.False(t, p.IsWrong("Alice123!")) + assert.False(t, p.Invalid("Alice123!")) } }) t.Run("Bob", func(t *testing.T) { if p := FindPassword("uqxc08w3d0ej2283"); p == nil { t.Fatal("password not found") } else { - assert.False(t, p.IsWrong("Bobbob123!")) + assert.False(t, p.Invalid("Bobbob123!")) } }) } @@ -176,10 +176,10 @@ func TestPassword_String(t *testing.T) { func TestPassword_IsEmpty(t *testing.T) { t.Run("False", func(t *testing.T) { p := NewPassword("urrwaxd19ldtz68x", "lkjhgtyu", false) - assert.False(t, p.IsEmpty()) + assert.False(t, p.Empty()) }) t.Run("True", func(t *testing.T) { p := Password{} - assert.True(t, p.IsEmpty()) + assert.True(t, p.Empty()) }) } diff --git a/internal/search/photos.go b/internal/search/photos.go index 789587a80..ca4629b2e 100644 --- a/internal/search/photos.go +++ b/internal/search/photos.go @@ -13,6 +13,7 @@ import ( "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/geo" @@ -148,7 +149,7 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string) // Visitors and other restricted users can only access shared content. if f.Scope != "" && !sess.HasShare(f.Scope) && (sess.User().HasSharedAccessOnly(acl.ResourcePhotos) || sess.NotRegistered()) || f.Scope == "" && acl.Rules.Deny(acl.ResourcePhotos, aclRole, acl.ActionSearch) { - event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", "denied"}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePhotos), aclRole) + event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", authn.Denied}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePhotos), aclRole) return PhotoResults{}, 0, ErrForbidden } diff --git a/internal/search/photos_geo.go b/internal/search/photos_geo.go index 274ef0c06..267e3cc70 100644 --- a/internal/search/photos_geo.go +++ b/internal/search/photos_geo.go @@ -13,6 +13,7 @@ import ( "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" + "github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/geo" @@ -131,7 +132,7 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes // Visitors and other restricted users can only access shared content. if f.Scope != "" && !sess.HasShare(f.Scope) && (sess.User().HasSharedAccessOnly(acl.ResourcePlaces) || sess.NotRegistered()) || f.Scope == "" && acl.Rules.Deny(acl.ResourcePlaces, aclRole, acl.ActionSearch) { - event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", "denied"}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePlaces), aclRole) + event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", authn.Denied}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePlaces), aclRole) return GeoResults{}, ErrForbidden } diff --git a/internal/server/webdav_auth.go b/internal/server/webdav_auth.go index a1c9d36f2..f7dda9a93 100644 --- a/internal/server/webdav_auth.go +++ b/internal/server/webdav_auth.go @@ -17,6 +17,7 @@ import ( "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/server/limiter" + "github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/header" @@ -103,7 +104,7 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc { return } - event.AuditErr([]string{clientIp, "access webdav as %s with authorization granted to %s", "denied"}, clean.Log(username), clean.Log(user.Username())) + event.AuditErr([]string{clientIp, "access webdav as %s with authorization granted to %s", authn.Denied}, clean.Log(username), clean.Log(user.Username())) limiter.Auth.Reserve(clientIp) WebDAVAbortUnauthorized(c) return @@ -111,31 +112,31 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc { // Ignore and try basic auth next. } else if !sess.HasUser() || user == nil { // Log error if session does not belong to an authorized user account. - event.AuditErr([]string{clientIp, "session %s", "access webdav without user account", "denied"}, sess.RefID) + event.AuditErr([]string{clientIp, "session %s", "access webdav without user account", authn.Denied}, sess.RefID) WebDAVAbortUnauthorized(c) return - } else if sess.IsClient() && sess.ScopeExcludes(acl.ResourceWebDAV, nil) { + } else if sess.IsClient() && sess.InsufficientScope(acl.ResourceWebDAV, nil) { // Log error if the client is allowed to access webdav based on its scope. - message := "denied" + message := authn.ErrInsufficientScope.Error() event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.ClientInfo()), sess.RefID, clean.LogQuote(user.Username())) WebDAVAbortUnauthorized(c) return } else if !user.CanUseWebDAV() { // Log warning if WebDAV is disabled for this account. - message := "webdav access is disabled" + message := authn.ErrWebDAVAccessDisabled.Error() event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.ClientInfo()), sess.RefID, clean.LogQuote(user.Username())) WebDAVAbortUnauthorized(c) return } else if username != "" && !strings.EqualFold(clean.Username(username), user.Username()) { // Log warning if WebDAV is disabled for this account. - message := "basic auth username does not match" + message := authn.ErrBasicAuthDoesNotMatch.Error() event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.ClientInfo()), sess.RefID, clean.LogQuote(user.Username())) limiter.Auth.Reserve(clientIp) WebDAVAbortUnauthorized(c) return } else if err := fs.MkdirAll(filepath.Join(conf.OriginalsPath(), user.GetUploadPath())); err != nil { // Log warning if upload path could not be created. - message := "failed to create user upload path" + message := authn.ErrFailedToCreateUploadPath.Error() event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.ClientInfo()), sess.RefID, clean.LogQuote(user.Username())) WebDAVAbortServerError(c) return @@ -177,24 +178,24 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc { // Check credentials and authorization. if user, _, _, err := entity.Auth(f, nil, c); err != nil { // Abort if authentication has failed. - message := err.Error() + message := authn.ErrInvalidCredentials.Error() limiter.Login.Reserve(clientIp) event.AuditErr([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(username)) event.LoginError(clientIp, "webdav", username, api.UserAgent(c), message) } else if user == nil { // Abort if account was not found. - message := "account not found" + message := authn.ErrAccountNotFound.Error() limiter.Login.Reserve(clientIp) event.AuditErr([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(username)) event.LoginError(clientIp, "webdav", username, api.UserAgent(c), message) } else if !user.CanUseWebDAV() { // Abort if WebDAV is disabled for this account. - message := "webdav access is disabled" + message := authn.ErrWebDAVAccessDisabled.Error() event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(username)) event.LoginError(clientIp, "webdav", username, api.UserAgent(c), message) } else if err = fs.MkdirAll(filepath.Join(conf.OriginalsPath(), user.GetUploadPath())); err != nil { // Abort if upload path could not be created. - message := "failed to create user upload path" + message := authn.ErrFailedToCreateUploadPath.Error() event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(username)) event.LoginError(clientIp, "webdav", username, api.UserAgent(c), message) WebDAVAbortServerError(c) diff --git a/internal/server/webdav_auth_test.go b/internal/server/webdav_auth_test.go index 5278e2728..9d755d3bd 100644 --- a/internal/server/webdav_auth_test.go +++ b/internal/server/webdav_auth_test.go @@ -170,7 +170,7 @@ func TestWebDAVAuthSession(t *testing.T) { assert.True(t, sess.HasUser()) assert.Equal(t, user.UserUID, sess.UserUID) assert.Equal(t, entity.UserFixtures.Get("alice").UserUID, sess.UserUID) - assert.True(t, sess.ScopeAllows(acl.ResourceWebDAV, acl.Permissions{acl.ActionView})) + assert.True(t, sess.ValidateScope(acl.ResourceWebDAV, acl.Permissions{acl.ActionView})) assert.False(t, cached) assert.Equal(t, s.ID, sid) @@ -222,7 +222,7 @@ func TestWebDAVAuthSession(t *testing.T) { assert.Equal(t, entity.UserFixtures.Get("alice").UserUID, user.UserUID) assert.Equal(t, entity.UserFixtures.Get("alice").UserUID, sess.UserUID) assert.True(t, user.CanUseWebDAV()) - assert.False(t, sess.ScopeAllows(acl.ResourceWebDAV, acl.Permissions{acl.ActionView})) + assert.False(t, sess.ValidateScope(acl.ResourceWebDAV, acl.Permissions{acl.ActionView})) // WebDAVAuthSession should not set a status code or any headers. assert.Equal(t, http.StatusOK, c.Writer.Status()) diff --git a/pkg/authn/const.go b/pkg/authn/const.go new file mode 100644 index 000000000..8339dfba1 --- /dev/null +++ b/pkg/authn/const.go @@ -0,0 +1,7 @@ +package authn + +// Generic status messages for authentication and authorization: +const ( + Denied = "denied" + Granted = "granted" +) diff --git a/pkg/authn/errors.go b/pkg/authn/errors.go index 2ba04131c..5f024c479 100644 --- a/pkg/authn/errors.go +++ b/pkg/authn/errors.go @@ -2,8 +2,30 @@ package authn import ( "errors" + "fmt" + + "github.com/photoprism/photoprism/pkg/txt" ) +// Generic error messages for authentication and authorization: +var ( + ErrUnauthorized = errors.New("unauthorized") + ErrAccountAlreadyExists = errors.New("account already exists") + ErrAccountNotFound = errors.New("account not found") + ErrAccountDisabled = errors.New("account disabled") + ErrInvalidCredentials = errors.New("invalid credentials") + ErrInvalidShareToken = errors.New("invalid share token") + ErrInsufficientScope = errors.New("insufficient scope") + ErrDisabledInPublicMode = errors.New("disabled in public mode") +) + +// Username-related error messages: +var ( + ErrUsernameRequired = errors.New("username required") + ErrInvalidUsername = errors.New("invalid username") +) + +// Passcode-related error messages: var ( ErrPasscodeRequired = errors.New("passcode required") ErrPasscodeNotSetUp = errors.New("passcode required, but not set up") @@ -14,8 +36,20 @@ var ( ErrInvalidPasscodeFormat = errors.New("invalid passcode format") ErrInvalidPasscodeKey = errors.New("invalid passcode key") ErrInvalidPasscodeType = errors.New("invalid passcode type") - ErrPasswordRequired = errors.New("password required") - ErrInvalidPassword = errors.New("invalid password") - ErrInvalidPasswordFormat = errors.New("invalid password format") - ErrInvalidCredentials = errors.New("invalid credentials") +) + +// Password-related error messages: +var ( + ErrInvalidPassword = errors.New("invalid password") + ErrPasswordRequired = errors.New("password required") + ErrPasswordTooShort = errors.New("password is too short") + ErrPasswordTooLong = errors.New(fmt.Sprintf("password must have less than %d characters", txt.ClipPassword)) + ErrPasswordsDoNotMatch = errors.New("passwords do not match") +) + +// WebDAV-related error messages: +var ( + ErrWebDAVAccessDisabled = errors.New("webdav access is disabled") + ErrFailedToCreateUploadPath = errors.New("failed to create upload path") + ErrBasicAuthDoesNotMatch = errors.New("basic auth username does not match") )