mirror of
https://github.com/photoprism/photoprism.git
synced 2025-10-16 05:41:02 +08:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -376,7 +376,7 @@ func (m *Client) WrongSecret(s string) bool {
|
||||
}
|
||||
|
||||
// Invalid?
|
||||
if pw.IsWrong(s) {
|
||||
if pw.Invalid(s) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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 {
|
||||
|
@@ -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.
|
||||
|
@@ -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}))
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -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.
|
||||
|
@@ -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")
|
||||
|
@@ -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)
|
||||
|
@@ -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 == ""
|
||||
}
|
||||
|
||||
|
@@ -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())
|
||||
})
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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
7
pkg/authn/const.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package authn
|
||||
|
||||
// Generic status messages for authentication and authorization:
|
||||
const (
|
||||
Denied = "denied"
|
||||
Granted = "granted"
|
||||
)
|
@@ -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")
|
||||
)
|
||||
|
Reference in New Issue
Block a user