2022-10-02 09:38:30 +00:00
|
|
|
package entity
|
|
|
|
|
|
|
|
import (
|
2023-03-13 15:04:37 +00:00
|
|
|
"fmt"
|
2022-10-02 09:38:30 +00:00
|
|
|
"net/http"
|
2022-10-13 20:11:02 +00:00
|
|
|
"time"
|
|
|
|
|
2022-10-02 09:38:30 +00:00
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
|
|
"github.com/photoprism/photoprism/internal/event"
|
|
|
|
"github.com/photoprism/photoprism/internal/form"
|
|
|
|
"github.com/photoprism/photoprism/internal/i18n"
|
2022-10-11 20:44:11 +00:00
|
|
|
"github.com/photoprism/photoprism/internal/server/limiter"
|
2023-03-08 22:30:39 +00:00
|
|
|
"github.com/photoprism/photoprism/pkg/authn"
|
2022-10-02 09:38:30 +00:00
|
|
|
"github.com/photoprism/photoprism/pkg/clean"
|
2022-11-22 21:14:34 +00:00
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
2022-10-02 09:38:30 +00:00
|
|
|
)
|
|
|
|
|
2022-11-22 21:14:34 +00:00
|
|
|
// Auth checks if the credentials are valid and returns the user and authentication provider.
|
2023-03-09 14:12:10 +00:00
|
|
|
var Auth = func(f form.Login, m *Session, c *gin.Context) (user *User, provider authn.ProviderType, err error) {
|
2023-03-08 22:30:39 +00:00
|
|
|
name := f.Username()
|
2022-11-22 21:14:34 +00:00
|
|
|
|
|
|
|
user = FindUserByName(name)
|
2023-03-08 22:30:39 +00:00
|
|
|
err = AuthLocal(user, f, m)
|
2022-11-22 21:14:34 +00:00
|
|
|
|
|
|
|
if err != nil {
|
2023-03-08 22:30:39 +00:00
|
|
|
return user, authn.ProviderNone, err
|
2022-11-22 21:14:34 +00:00
|
|
|
}
|
|
|
|
|
2023-03-08 22:30:39 +00:00
|
|
|
// Update login timestamp.
|
|
|
|
user.UpdateLoginTime()
|
|
|
|
|
|
|
|
return user, authn.ProviderLocal, err
|
2022-11-22 21:14:34 +00:00
|
|
|
}
|
|
|
|
|
2023-03-08 22:30:39 +00:00
|
|
|
// AuthLocal authenticates against the local user database with the specified username and password.
|
|
|
|
func AuthLocal(user *User, f form.Login, m *Session) (err error) {
|
|
|
|
name := f.Username()
|
2022-11-22 21:14:34 +00:00
|
|
|
|
|
|
|
// User found?
|
|
|
|
if user == nil {
|
|
|
|
message := "account not found"
|
2023-01-24 05:05:31 +00:00
|
|
|
if m != nil {
|
|
|
|
limiter.Login.Reserve(m.IP())
|
|
|
|
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
|
|
|
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
|
|
|
m.Status = http.StatusUnauthorized
|
|
|
|
}
|
2022-11-22 21:14:34 +00:00
|
|
|
return i18n.Error(i18n.ErrInvalidCredentials)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Login allowed?
|
2023-03-13 15:04:37 +00:00
|
|
|
if !user.Provider().IsDefault() && !user.Provider().IsLocal() {
|
|
|
|
message := fmt.Sprintf("%s authentication disabled", authn.ProviderLocal.String())
|
|
|
|
if m != nil {
|
|
|
|
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
|
|
|
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
|
|
|
m.Status = http.StatusUnauthorized
|
|
|
|
}
|
|
|
|
return i18n.Error(i18n.ErrInvalidCredentials)
|
|
|
|
} else if !user.CanLogIn() {
|
2022-11-22 21:14:34 +00:00
|
|
|
message := "account disabled"
|
2023-01-24 05:05:31 +00:00
|
|
|
if m != nil {
|
|
|
|
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
|
|
|
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
|
|
|
m.Status = http.StatusUnauthorized
|
|
|
|
}
|
2022-11-22 21:14:34 +00:00
|
|
|
return i18n.Error(i18n.ErrInvalidCredentials)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Password valid?
|
|
|
|
if user.WrongPassword(f.Password) {
|
|
|
|
message := "incorrect password"
|
2023-01-24 05:05:31 +00:00
|
|
|
if m != nil {
|
|
|
|
limiter.Login.Reserve(m.IP())
|
|
|
|
event.AuditErr([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
|
|
|
|
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
|
|
|
|
m.Status = http.StatusUnauthorized
|
|
|
|
}
|
2022-11-22 21:14:34 +00:00
|
|
|
return i18n.Error(i18n.ErrInvalidCredentials)
|
2023-01-24 05:05:31 +00:00
|
|
|
} else if m != nil {
|
2022-11-22 21:14:34 +00:00
|
|
|
event.AuditInfo([]string{m.IP(), "session %s", "login as %s", "succeeded"}, m.RefID, clean.LogQuote(name))
|
|
|
|
event.LoginInfo(m.IP(), "api", name, m.UserAgent)
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-10-11 20:44:11 +00:00
|
|
|
// LogIn performs authentication checks against the specified login form.
|
|
|
|
func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) {
|
2022-10-02 09:38:30 +00:00
|
|
|
if c != nil {
|
|
|
|
m.SetContext(c)
|
|
|
|
}
|
|
|
|
|
2022-11-22 21:14:34 +00:00
|
|
|
var user *User
|
2023-03-09 14:12:10 +00:00
|
|
|
var provider authn.ProviderType
|
2022-11-22 21:14:34 +00:00
|
|
|
|
|
|
|
// Login credentials provided?
|
2022-10-02 09:38:30 +00:00
|
|
|
if f.HasCredentials() {
|
|
|
|
if m.IsRegistered() {
|
|
|
|
m.RegenerateID()
|
|
|
|
}
|
|
|
|
|
2022-11-22 21:14:34 +00:00
|
|
|
user, provider, err = Auth(f, m, c)
|
2022-10-02 09:38:30 +00:00
|
|
|
|
2022-11-22 21:14:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2022-10-02 09:38:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
m.SetUser(user)
|
2022-11-22 21:14:34 +00:00
|
|
|
m.SetProvider(provider)
|
2022-10-02 09:38:30 +00:00
|
|
|
}
|
|
|
|
|
2023-03-13 15:04:37 +00:00
|
|
|
// Link token provided?
|
2022-10-02 09:38:30 +00:00
|
|
|
if f.HasToken() {
|
2022-11-22 21:14:34 +00:00
|
|
|
user = m.User()
|
2022-10-02 09:38:30 +00:00
|
|
|
|
|
|
|
// Redeem token.
|
|
|
|
if user.IsRegistered() {
|
|
|
|
if shares := user.RedeemToken(f.AuthToken); shares == 0 {
|
2022-10-19 03:09:09 +00:00
|
|
|
limiter.Login.Reserve(m.IP())
|
2022-10-02 09:38:30 +00:00
|
|
|
event.AuditWarn([]string{m.IP(), "session %s", "share token %s is invalid"}, m.RefID, clean.LogQuote(f.AuthToken))
|
|
|
|
m.Status = http.StatusNotFound
|
|
|
|
return i18n.Error(i18n.ErrInvalidLink)
|
|
|
|
} else {
|
|
|
|
event.AuditInfo([]string{m.IP(), "session %s", "token redeemed for %d shares"}, m.RefID, user.RedeemToken(f.AuthToken))
|
|
|
|
}
|
|
|
|
} else if data := m.Data(); data == nil {
|
|
|
|
m.Status = http.StatusInternalServerError
|
|
|
|
return i18n.Error(i18n.ErrUnexpected)
|
|
|
|
} else if shares := data.RedeemToken(f.AuthToken); shares == 0 {
|
2022-10-19 03:09:09 +00:00
|
|
|
limiter.Login.Reserve(m.IP())
|
2022-10-02 09:38:30 +00:00
|
|
|
event.AuditWarn([]string{m.IP(), "session %s", "share token %s is invalid"}, m.RefID, clean.LogQuote(f.AuthToken))
|
2022-10-09 15:16:49 +00:00
|
|
|
event.LoginError(m.IP(), "api", "", m.UserAgent, "invalid share token")
|
2022-10-02 09:38:30 +00:00
|
|
|
m.Status = http.StatusNotFound
|
|
|
|
return i18n.Error(i18n.ErrInvalidLink)
|
|
|
|
} else {
|
|
|
|
m.SetData(data)
|
2023-03-13 15:04:37 +00:00
|
|
|
m.SetProvider(authn.ProviderLink)
|
2022-10-02 09:38:30 +00:00
|
|
|
event.AuditInfo([]string{m.IP(), "session %s", "token redeemed for %d shares"}, m.RefID, shares, data)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Upgrade session to visitor.
|
|
|
|
if user.IsUnknown() {
|
|
|
|
user = &Visitor
|
|
|
|
event.AuditDebug([]string{m.IP(), "session %s", "role upgraded to %s"}, m.RefID, user.AclRole().String())
|
2022-10-13 20:11:02 +00:00
|
|
|
expires := UTC().Add(time.Hour * 24)
|
|
|
|
m.Expires(expires)
|
|
|
|
event.AuditDebug([]string{m.IP(), "session %s", "expires at %s"}, m.RefID, txt.TimeStamp(&expires))
|
2022-10-02 09:38:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
m.SetUser(user)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unregistered visitors must use a valid share link to obtain a session.
|
|
|
|
if m.User().NotRegistered() && m.Data().NoShares() {
|
|
|
|
m.Status = http.StatusUnauthorized
|
|
|
|
return i18n.Error(i18n.ErrInvalidCredentials)
|
|
|
|
}
|
|
|
|
|
|
|
|
m.Status = http.StatusOK
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|