Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
eff130cf90
commit
d481bc3d34
4 changed files with 157 additions and 38 deletions
|
@ -564,6 +564,10 @@ func (m *Session) NoUser() bool {
|
||||||
|
|
||||||
// HasUser checks if a user account is assigned to the session.
|
// HasUser checks if a user account is assigned to the session.
|
||||||
func (m *Session) HasUser() bool {
|
func (m *Session) HasUser() bool {
|
||||||
|
if m == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return m.UserUID != ""
|
return m.UserUID != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow webdav access based on the auth token or secret provided?
|
// Allow webdav access based on the auth token or secret provided?
|
||||||
if sess, user, sid, cached := WebDAVSession(c, authToken); cached && user != nil {
|
if sess, user, sid, cached := WebDAVAuthSession(c, authToken); cached && user != nil {
|
||||||
// Add user to request context and return to signal successful authentication.
|
// Add user to request context and return to signal successful authentication.
|
||||||
c.Set(gin.AuthUserKey, user)
|
c.Set(gin.AuthUserKey, user)
|
||||||
return
|
return
|
||||||
|
@ -94,17 +94,25 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
||||||
} else if !sess.HasUser() || user == nil {
|
} else if !sess.HasUser() || user == nil {
|
||||||
// Log error if session does not belong to an authorized user account.
|
// Log error if session does not belong to an authorized user account.
|
||||||
event.AuditErr([]string{clientIp, "session %s", "access webdav without authorized user account", "denied"}, sess.RefID)
|
event.AuditErr([]string{clientIp, "session %s", "access webdav without authorized user account", "denied"}, sess.RefID)
|
||||||
|
WebDAVAbortUnauthorized(c)
|
||||||
|
return
|
||||||
} else if sess.IsClient() && !sess.HasScope(acl.ResourceWebDAV.String()) {
|
} else if sess.IsClient() && !sess.HasScope(acl.ResourceWebDAV.String()) {
|
||||||
// Log error if the client is allowed to access webdav based on its scope.
|
// Log error if the client is allowed to access webdav based on its scope.
|
||||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "access webdav without scope authorization", "denied"}, clean.Log(sess.AuthID), sess.RefID)
|
event.AuditErr([]string{clientIp, "client %s", "session %s", "access webdav without scope authorization", "denied"}, clean.Log(sess.AuthID), sess.RefID)
|
||||||
|
WebDAVAbortUnauthorized(c)
|
||||||
|
return
|
||||||
} else if !user.CanUseWebDAV() {
|
} else if !user.CanUseWebDAV() {
|
||||||
// Log warning if WebDAV is disabled for this account.
|
// Log warning if WebDAV is disabled for this account.
|
||||||
message := "webdav access disabled"
|
message := "webdav access disabled"
|
||||||
event.AuditWarn([]string{clientIp, "access webdav as %s", message}, clean.LogQuote(username))
|
event.AuditWarn([]string{clientIp, "access webdav as %s", message}, clean.LogQuote(username))
|
||||||
|
WebDAVAbortUnauthorized(c)
|
||||||
|
return
|
||||||
} else if err := os.MkdirAll(filepath.Join(conf.OriginalsPath(), user.GetUploadPath()), fs.ModeDir); err != nil {
|
} else if err := os.MkdirAll(filepath.Join(conf.OriginalsPath(), user.GetUploadPath()), fs.ModeDir); err != nil {
|
||||||
// Log warning if upload path could not be created.
|
// Log warning if upload path could not be created.
|
||||||
message := "failed to create user upload path"
|
message := "failed to create user upload path"
|
||||||
event.AuditWarn([]string{clientIp, "access webdav as %s", message}, clean.LogQuote(username))
|
event.AuditWarn([]string{clientIp, "access webdav as %s", message}, clean.LogQuote(username))
|
||||||
|
WebDAVAbortServerError(c)
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
// Cache authentication to improve performance.
|
// Cache authentication to improve performance.
|
||||||
webdavAuthCache.SetDefault(sid, user)
|
webdavAuthCache.SetDefault(sid, user)
|
||||||
|
@ -159,6 +167,8 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
||||||
message := "failed to create user upload path"
|
message := "failed to create user upload path"
|
||||||
event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(username))
|
event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(username))
|
||||||
event.LoginError(clientIp, "webdav", username, api.UserAgent(c), message)
|
event.LoginError(clientIp, "webdav", username, api.UserAgent(c), message)
|
||||||
|
WebDAVAbortServerError(c)
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
// Log successful authentication.
|
// Log successful authentication.
|
||||||
event.AuditInfo([]string{clientIp, "webdav login as %s", "succeeded"}, clean.LogQuote(username))
|
event.AuditInfo([]string{clientIp, "webdav login as %s", "succeeded"}, clean.LogQuote(username))
|
||||||
|
@ -183,41 +193,7 @@ func WebDAVAbortUnauthorized(c *gin.Context) {
|
||||||
c.AbortWithStatus(http.StatusUnauthorized)
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebDAVSession returns the client session that belongs to the auth token provided, or returns nil if it was not found.
|
// WebDAVAbortServerError aborts the request with the status internal server error.
|
||||||
func WebDAVSession(c *gin.Context, authToken string) (sess *entity.Session, user *entity.User, sid string, cached bool) {
|
func WebDAVAbortServerError(c *gin.Context) {
|
||||||
if authToken == "" {
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
// Abort authentication if no token was provided.
|
|
||||||
return nil, nil, "", false
|
|
||||||
} else if !rnd.IsAuthToken(authToken) && !rnd.IsAuthSecret(authToken) {
|
|
||||||
// Abort authentication if token doesn't match expected format.
|
|
||||||
return nil, nil, "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get session ID for the auth token provided.
|
|
||||||
sid = rnd.SessionID(authToken)
|
|
||||||
|
|
||||||
// Check if client authorization has been cached to improve performance.
|
|
||||||
if cacheData, found := webdavAuthCache.Get(sid); found && cacheData != nil {
|
|
||||||
// Add cached user information to the request context.
|
|
||||||
user = cacheData.(*entity.User)
|
|
||||||
return nil, user, sid, true
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Find the session based on the hashed token used as session ID and return it.
|
|
||||||
sess, err = entity.FindSession(sid)
|
|
||||||
|
|
||||||
// Log error and return nil if no matching session was found.
|
|
||||||
if sess == nil || err != nil {
|
|
||||||
event.AuditErr([]string{header.ClientIP(c), "access webdav", "invalid auth token or secret"})
|
|
||||||
return nil, nil, sid, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the client IP and the user agent from
|
|
||||||
// the request context if they have changed.
|
|
||||||
sess.UpdateContext(c)
|
|
||||||
|
|
||||||
// Returns session and user if all checks have passed.
|
|
||||||
return sess, sess.User(), sid, false
|
|
||||||
}
|
}
|
||||||
|
|
49
internal/server/webdav_auth_session.go
Normal file
49
internal/server/webdav_auth_session.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/pkg/header"
|
||||||
|
"github.com/photoprism/photoprism/pkg/rnd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebDAVAuthSession returns the client session that belongs to the auth token provided, or returns nil if it was not found.
|
||||||
|
func WebDAVAuthSession(c *gin.Context, authToken string) (sess *entity.Session, user *entity.User, sid string, cached bool) {
|
||||||
|
if authToken == "" {
|
||||||
|
// Abort authentication if no token was provided.
|
||||||
|
return nil, nil, "", false
|
||||||
|
} else if !rnd.IsAuthToken(authToken) && !rnd.IsAuthSecret(authToken) {
|
||||||
|
// Abort authentication if token doesn't match expected format.
|
||||||
|
return nil, nil, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session ID for the auth token provided.
|
||||||
|
sid = rnd.SessionID(authToken)
|
||||||
|
|
||||||
|
// Check if client authorization has been cached to improve performance.
|
||||||
|
if cacheData, found := webdavAuthCache.Get(sid); found && cacheData != nil {
|
||||||
|
// Add cached user information to the request context.
|
||||||
|
user = cacheData.(*entity.User)
|
||||||
|
return nil, user, sid, true
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Find the session based on the hashed token used as session ID and return it.
|
||||||
|
sess, err = entity.FindSession(sid)
|
||||||
|
|
||||||
|
// Log error and return nil if no matching session was found.
|
||||||
|
if sess == nil || err != nil {
|
||||||
|
event.AuditErr([]string{header.ClientIP(c), "access webdav", "invalid auth token or secret"})
|
||||||
|
return nil, nil, sid, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the client IP and the user agent from
|
||||||
|
// the request context if they have changed.
|
||||||
|
sess.UpdateContext(c)
|
||||||
|
|
||||||
|
// Returns session and user if all checks have passed.
|
||||||
|
return sess, sess.User(), sid, false
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/acl"
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/pkg/header"
|
"github.com/photoprism/photoprism/pkg/header"
|
||||||
|
@ -123,3 +124,92 @@ func TestWebDAVAuth(t *testing.T) {
|
||||||
assert.Equal(t, BasicAuthRealm, c.Writer.Header().Get("WWW-Authenticate"))
|
assert.Equal(t, BasicAuthRealm, c.Writer.Header().Get("WWW-Authenticate"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWebDAVAuthSession(t *testing.T) {
|
||||||
|
t.Run("AliceTokenWebdav", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = &http.Request{
|
||||||
|
Header: make(http.Header),
|
||||||
|
}
|
||||||
|
|
||||||
|
s := entity.SessionFixtures.Get("alice_token_webdav")
|
||||||
|
|
||||||
|
// Get session with authorized user and webdav scope.
|
||||||
|
sess, user, sid, cached := WebDAVAuthSession(c, s.AuthToken())
|
||||||
|
|
||||||
|
// Check result.
|
||||||
|
if cached {
|
||||||
|
assert.Nil(t, sess)
|
||||||
|
assert.NotNil(t, user)
|
||||||
|
assert.True(t, cached)
|
||||||
|
} else {
|
||||||
|
assert.NotNil(t, sess)
|
||||||
|
assert.NotNil(t, user)
|
||||||
|
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.HasScope(acl.ResourceWebDAV.String()))
|
||||||
|
assert.False(t, cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, s.ID, sid)
|
||||||
|
assert.Equal(t, entity.UserFixtures.Get("alice").UserUID, user.UserUID)
|
||||||
|
assert.True(t, user.CanUseWebDAV())
|
||||||
|
|
||||||
|
// WebDAVAuthSession should not set a status code or any headers.
|
||||||
|
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||||
|
assert.Equal(t, "", c.Writer.Header().Get("WWW-Authenticate"))
|
||||||
|
})
|
||||||
|
t.Run("AliceTokenScope", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = &http.Request{
|
||||||
|
Header: make(http.Header),
|
||||||
|
}
|
||||||
|
|
||||||
|
s := entity.SessionFixtures.Get("alice_token_scope")
|
||||||
|
|
||||||
|
// Get session without sufficient authorization scope.
|
||||||
|
sess, user, sid, cached := WebDAVAuthSession(c, s.AuthToken())
|
||||||
|
|
||||||
|
// Check result.
|
||||||
|
assert.NotNil(t, sess)
|
||||||
|
assert.NotNil(t, user)
|
||||||
|
assert.Equal(t, s.ID, sid)
|
||||||
|
assert.False(t, cached)
|
||||||
|
assert.True(t, sess.HasUser())
|
||||||
|
assert.Equal(t, user.UserUID, sess.UserUID)
|
||||||
|
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.HasScope(acl.ResourceWebDAV.String()))
|
||||||
|
|
||||||
|
// WebDAVAuthSession should not set a status code or any headers.
|
||||||
|
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||||
|
assert.Equal(t, "", c.Writer.Header().Get("WWW-Authenticate"))
|
||||||
|
})
|
||||||
|
t.Run("InvalidAuthSecret", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = &http.Request{
|
||||||
|
Header: make(http.Header),
|
||||||
|
}
|
||||||
|
|
||||||
|
authToken := rnd.AuthSecret()
|
||||||
|
authId := rnd.SessionID(authToken)
|
||||||
|
|
||||||
|
// Get session with invalid auth secret.
|
||||||
|
sess, user, sid, cached := WebDAVAuthSession(c, authToken)
|
||||||
|
|
||||||
|
// Check result.
|
||||||
|
assert.Nil(t, sess)
|
||||||
|
assert.Nil(t, user)
|
||||||
|
assert.Equal(t, authId, sid)
|
||||||
|
assert.False(t, cached)
|
||||||
|
|
||||||
|
// WebDAVAuthSession should not set a status code or any headers.
|
||||||
|
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||||
|
assert.Equal(t, "", c.Writer.Header().Get("WWW-Authenticate"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue