Account: Add auth-related error messages to pkg/authn #808 #4114

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2024-03-29 12:16:26 +01:00
parent 9b4a162705
commit 37c3c9d624
25 changed files with 218 additions and 164 deletions

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -376,7 +376,7 @@ func (m *Client) WrongSecret(s string) bool {
}
// Invalid?
if pw.IsWrong(s) {
if pw.Invalid(s) {
return true
}

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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}))
})
}

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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.

View File

@@ -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")

View File

@@ -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)

View File

@@ -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 == ""
}

View File

@@ -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())
})
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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())

7
pkg/authn/const.go Normal file
View File

@@ -0,0 +1,7 @@
package authn
// Generic status messages for authentication and authorization:
const (
Denied = "denied"
Granted = "granted"
)

View File

@@ -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")
)