Auth: Refactor WebDAV login and increase maximum length of username
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
0643d54ffc
commit
adc91fcf6e
11 changed files with 95 additions and 41 deletions
29
.ldap.cfg
29
.ldap.cfg
|
@ -18,8 +18,11 @@ debug = true
|
|||
baseDN = "dc=localssl,dc=dev"
|
||||
[[users]]
|
||||
name = "user"
|
||||
givenname="John"
|
||||
sn="Doe"
|
||||
givenname = "John"
|
||||
objectClass = "user"
|
||||
displayName = "John Doe"
|
||||
sn = "Doe"
|
||||
userPrincipalName = "jdoe@example.com"
|
||||
mail = "jdoe@example.com"
|
||||
passsha256 = "4314c1fe282face45336b1422a3285c5ff31a39c8e24425615fa53a43b718493" # photoprism
|
||||
[[users.customattributes]]
|
||||
|
@ -29,9 +32,29 @@ debug = true
|
|||
[[users.capabilities]]
|
||||
action = "search"
|
||||
object = "*"
|
||||
[[users]]
|
||||
name = "bob"
|
||||
givenname = "Bob"
|
||||
objectClass = "user"
|
||||
displayName = "Robert Jones"
|
||||
sn = "Jones"
|
||||
userPrincipalName = "bob@example.com"
|
||||
mail = "bob@example.com"
|
||||
passsha256 = "4314c1fe282face45336b1422a3285c5ff31a39c8e24425615fa53a43b718493" # photoprism
|
||||
[[users.customattributes]]
|
||||
photoprismRoleUser = ["true"]
|
||||
photoprismNoLogin = ["false"]
|
||||
photoprismWebdav = ["true"]
|
||||
photoprismUploadPath = ["bob"]
|
||||
[[users.capabilities]]
|
||||
action = "search"
|
||||
object = "*"
|
||||
[[users]]
|
||||
name = "guest"
|
||||
givenname="Guest"
|
||||
objectClass = "user"
|
||||
givenname = "Guest"
|
||||
displayName = "Guest User"
|
||||
userPrincipalName = "guest@example.com"
|
||||
mail = "guest@example.com"
|
||||
passsha256 = "4314c1fe282face45336b1422a3285c5ff31a39c8e24425615fa53a43b718493" # photoprism
|
||||
[[users.customattributes]]
|
||||
|
|
|
@ -536,9 +536,7 @@
|
|||
</v-list-tile-avatar>
|
||||
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
{{ displayName }}
|
||||
</v-list-tile-title>
|
||||
<v-list-tile-title>{{ displayName }}</v-list-tile-title>
|
||||
<v-list-tile-sub-title>{{ accountInfo }}</v-list-tile-sub-title>
|
||||
</v-list-tile-content>
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ export default {
|
|||
}
|
||||
},
|
||||
webdavUrl() {
|
||||
let baseUrl = `${window.location.protocol}//${this.user.Name}@${window.location.host}/originals/`;
|
||||
let baseUrl = `${window.location.protocol}//${encodeURIComponent(this.user.Name)}@${window.location.host}/originals/`;
|
||||
|
||||
if (this.user.BasePath) {
|
||||
baseUrl = `${baseUrl}${this.user.BasePath}/`;
|
||||
|
|
|
@ -132,7 +132,7 @@ export class User extends RestModel {
|
|||
|
||||
getAccountInfo() {
|
||||
if (this.Name) {
|
||||
return `@${this.Name}`;
|
||||
return this.Name;
|
||||
} else if (this.Email) {
|
||||
return this.Email;
|
||||
} else if (this.Details && this.Details.JobTitle) {
|
||||
|
|
2
go.mod
2
go.mod
|
@ -42,7 +42,7 @@ require (
|
|||
github.com/tensorflow/tensorflow v1.15.2
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
|
||||
github.com/urfave/cli v1.22.11
|
||||
github.com/urfave/cli v1.22.12
|
||||
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
|
||||
golang.org/x/crypto v0.5.0
|
||||
golang.org/x/net v0.5.0
|
||||
|
|
6
go.sum
6
go.sum
|
@ -207,6 +207,7 @@ github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzS
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
|
||||
|
@ -282,7 +283,6 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
|||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpu/goacmedns v0.1.1/go.mod h1:MuaouqEhPAHxsbqjgnck5zeghuwBP1dLnPoobeGqugQ=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
|
@ -963,8 +963,8 @@ github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0
|
|||
github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6 h1:TtyC78WMafNW8QFfv3TeP3yWNDG+uxNkk9vOrnDu6JA=
|
||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6/go.mod h1:h8272+G2omSmi30fBXiZDMkmHuOgonplfKIKjQWzlfs=
|
||||
github.com/urfave/cli v1.22.11 h1:3wLoofQeDAA/zDjLA4uvtzIv73+qdxJ3QkxfAqk4UVI=
|
||||
github.com/urfave/cli v1.22.11/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
|
||||
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
|
||||
github.com/urfave/cli/v2 v2.11.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
|
||||
github.com/urfave/cli/v2 v2.14.0/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
|
||||
github.com/urfave/cli/v2 v2.19.2/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
|
||||
|
|
|
@ -35,31 +35,37 @@ func AuthPassword(user *User, f form.Login, m *Session) (err error) {
|
|||
// User found?
|
||||
if user == nil {
|
||||
message := "account not found"
|
||||
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
|
||||
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
|
||||
}
|
||||
return i18n.Error(i18n.ErrInvalidCredentials)
|
||||
}
|
||||
|
||||
// Login allowed?
|
||||
if !user.CanLogIn() {
|
||||
message := "account disabled"
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
// Password valid?
|
||||
if user.WrongPassword(f.Password) {
|
||||
message := "incorrect password"
|
||||
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
|
||||
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
|
||||
}
|
||||
return i18n.Error(i18n.ErrInvalidCredentials)
|
||||
} else {
|
||||
} else if m != nil {
|
||||
event.AuditInfo([]string{m.IP(), "session %s", "login as %s", "succeeded"}, m.RefID, clean.LogQuote(name))
|
||||
event.LoginInfo(m.IP(), "api", name, m.UserAgent)
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@ type User struct {
|
|||
UUID string `gorm:"type:VARBINARY(64);column:user_uuid;index;" json:"UUID,omitempty" yaml:"UUID,omitempty"`
|
||||
UserUID string `gorm:"type:VARBINARY(42);column:user_uid;unique_index;" json:"UID" yaml:"UID"`
|
||||
AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"AuthProvider,omitempty" yaml:"AuthProvider,omitempty"`
|
||||
AuthID string `gorm:"type:VARBINARY(128);index;default:'';" json:"AuthID,omitempty" yaml:"AuthID,omitempty"`
|
||||
UserName string `gorm:"size:64;index;" json:"Name" yaml:"Name,omitempty"`
|
||||
AuthID string `gorm:"type:VARBINARY(255);index;default:'';" json:"AuthID,omitempty" yaml:"AuthID,omitempty"`
|
||||
UserName string `gorm:"size:255;index;" json:"Name" yaml:"Name,omitempty"`
|
||||
DisplayName string `gorm:"size:200;" json:"DisplayName" yaml:"DisplayName,omitempty"`
|
||||
UserEmail string `gorm:"size:255;index;" json:"Email" yaml:"Email,omitempty"`
|
||||
BackupEmail string `gorm:"size:255;" json:"BackupEmail,omitempty" yaml:"BackupEmail,omitempty"`
|
||||
|
@ -142,7 +142,7 @@ func FirstOrCreateUser(m *User) *User {
|
|||
|
||||
// FindUserByName returns the matching user or nil if it was not found.
|
||||
func FindUserByName(name string) *User {
|
||||
name = clean.Username(name)
|
||||
name = clean.DN(name)
|
||||
|
||||
if name == "" {
|
||||
return nil
|
||||
|
@ -881,7 +881,7 @@ func (m *User) SetAvatar(thumb, thumbSrc string) error {
|
|||
|
||||
// Login returns the login name and provider.
|
||||
func (m *User) Login() string {
|
||||
if m.AuthProvider == "" {
|
||||
if m.AuthProvider == "" || strings.ContainsRune(m.UserName, '@') {
|
||||
return m.UserName
|
||||
} else {
|
||||
return fmt.Sprintf("%s@%s", m.UserName, m.AuthProvider)
|
||||
|
|
|
@ -14,7 +14,7 @@ type Login struct {
|
|||
|
||||
// Name returns the sanitized username in lowercase.
|
||||
func (f Login) Name() string {
|
||||
return clean.Username(f.UserName)
|
||||
return clean.DN(f.UserName)
|
||||
}
|
||||
|
||||
// Email returns the sanitized email in lowercase.
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/api"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
@ -84,13 +85,22 @@ func BasicAuth() gin.HandlerFunc {
|
|||
basicAuthMutex.Lock()
|
||||
defer basicAuthMutex.Unlock()
|
||||
|
||||
// Check authentication and authorization.
|
||||
if user := entity.FindUserByName(name); user == nil {
|
||||
// Username not found.
|
||||
message := "account not found"
|
||||
// User credentials.
|
||||
f := form.Login{
|
||||
UserName: name,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
// Check credentials and authorization.
|
||||
if user, _, err := entity.Auth(f, nil, c); err != nil {
|
||||
message := err.Error()
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
|
||||
event.AuditErr([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
|
||||
event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message)
|
||||
} else if user == nil {
|
||||
message := "account not found"
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditErr([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
|
||||
event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message)
|
||||
} else if !user.CanUseWebDAV() {
|
||||
// Sync disabled for this account.
|
||||
|
@ -98,13 +108,6 @@ func BasicAuth() gin.HandlerFunc {
|
|||
|
||||
event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
|
||||
event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message)
|
||||
} else if ok = user.HasPassword(password); !ok {
|
||||
// Wrong password.
|
||||
message := "incorrect password"
|
||||
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditErr([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(name))
|
||||
event.LoginError(clientIp, "webdav", name, api.UserAgent(c), message)
|
||||
} else {
|
||||
// Successfully authenticated.
|
||||
event.AuditInfo([]string{clientIp, "webdav login as %s", "succeeded"}, clean.LogQuote(name))
|
||||
|
|
|
@ -32,6 +32,30 @@ func Username(s string) string {
|
|||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
// DN returns the sanitized distinguished name (DN) with trimmed whitespace and in lowercase.
|
||||
func DN(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
// Remove unwanted characters.
|
||||
s = strings.Map(func(r rune) rune {
|
||||
if r <= 31 || r == 127 {
|
||||
return -1
|
||||
}
|
||||
switch r {
|
||||
case '"', ',', '+', '=', '`', '~', '?', '|', '*', '\\', ':', ';', '<', '>', '{', '}':
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, s)
|
||||
|
||||
// Empty or too long?
|
||||
if s == "" || reject(s, txt.ClipEmail) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
// Email returns the sanitized email with trimmed whitespace and in lowercase.
|
||||
func Email(s string) string {
|
||||
// Empty or too long?
|
||||
|
|
Loading…
Reference in a new issue