From e8ca9b8db2e07e03c59d60c87e02ee829cae475d Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Thu, 11 Jan 2024 12:49:31 +0100 Subject: [PATCH] WebDAV: Check if basic auth user matches the token, if set #808 #3943 Signed-off-by: Michael Mayer --- internal/server/webdav_auth.go | 20 ++++++--- internal/server/webdav_auth_test.go | 67 +++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/internal/server/webdav_auth.go b/internal/server/webdav_auth.go index 6074a7e5b..5ffa45c5a 100644 --- a/internal/server/webdav_auth.go +++ b/internal/server/webdav_auth.go @@ -4,6 +4,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "sync" "time" @@ -93,24 +94,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 authorized user account", "denied"}, sess.RefID) + event.AuditErr([]string{clientIp, "session %s", "access webdav without user account", "denied"}, sess.RefID) WebDAVAbortUnauthorized(c) return } else if sess.IsClient() && !sess.HasScope(acl.ResourceWebDAV.String()) { // 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) + message := "denied" + event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.AuthID), 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 disabled" - event.AuditWarn([]string{clientIp, "access webdav as %s", message}, clean.LogQuote(username)) + message := "webdav access is disabled" + event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.AuthID), 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" + event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.AuthID), sess.RefID, clean.LogQuote(user.Username())) WebDAVAbortUnauthorized(c) return } else if err := os.MkdirAll(filepath.Join(conf.OriginalsPath(), user.GetUploadPath()), fs.ModeDir); err != nil { // Log warning if upload path could not be created. message := "failed to create user upload path" - event.AuditWarn([]string{clientIp, "access webdav as %s", message}, clean.LogQuote(username)) + event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.AuthID), sess.RefID, clean.LogQuote(user.Username())) WebDAVAbortServerError(c) return } else { @@ -159,7 +167,7 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc { event.LoginError(clientIp, "webdav", username, api.UserAgent(c), message) } else if !user.CanUseWebDAV() { // Abort if WebDAV is disabled for this account. - message := "webdav access disabled" + message := "webdav access is disabled" event.AuditWarn([]string{clientIp, "webdav login as %s", message}, clean.LogQuote(username)) event.LoginError(clientIp, "webdav", username, api.UserAgent(c), message) } else if err = os.MkdirAll(filepath.Join(conf.OriginalsPath(), user.GetUploadPath()), fs.ModeDir); err != nil { diff --git a/internal/server/webdav_auth_test.go b/internal/server/webdav_auth_test.go index 3ba933b5a..217c17a66 100644 --- a/internal/server/webdav_auth_test.go +++ b/internal/server/webdav_auth_test.go @@ -28,6 +28,7 @@ func TestWebDAVAuth(t *testing.T) { Header: make(http.Header), } + webdavAuthCache.Flush() webdavHandler(c) assert.Equal(t, http.StatusUnauthorized, c.Writer.Status()) @@ -40,6 +41,8 @@ func TestWebDAVAuth(t *testing.T) { Header: make(http.Header), } + webdavAuthCache.Flush() + sess := entity.SessionFixtures.Get("alice_token") header.SetAuthorization(c.Request, sess.AuthToken()) @@ -56,14 +59,32 @@ func TestWebDAVAuth(t *testing.T) { } sess := entity.SessionFixtures.Get("alice_token_webdav") - basicAuth := []byte(fmt.Sprintf("access-token:%s", sess.AuthToken())) + basicAuth := []byte(fmt.Sprintf("alice:%s", sess.AuthToken())) c.Request.Header.Add(header.Auth, fmt.Sprintf("%s %s", header.AuthBasic, base64.StdEncoding.EncodeToString(basicAuth))) + webdavAuthCache.Flush() webdavHandler(c) assert.Equal(t, http.StatusOK, c.Writer.Status()) assert.Equal(t, "", c.Writer.Header().Get("WWW-Authenticate")) }) + t.Run("AliceTokenWebdavWrongUsername", func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = &http.Request{ + Header: make(http.Header), + } + + sess := entity.SessionFixtures.Get("alice_token_webdav") + basicAuth := []byte(fmt.Sprintf("bob:%s", sess.AuthToken())) + c.Request.Header.Add(header.Auth, fmt.Sprintf("%s %s", header.AuthBasic, base64.StdEncoding.EncodeToString(basicAuth))) + + webdavAuthCache.Flush() + webdavHandler(c) + + assert.Equal(t, http.StatusUnauthorized, c.Writer.Status()) + assert.Equal(t, BasicAuthRealm, c.Writer.Header().Get("WWW-Authenticate")) + }) t.Run("AliceTokenWebdavWithoutUsername", func(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -75,6 +96,7 @@ func TestWebDAVAuth(t *testing.T) { basicAuth := []byte(fmt.Sprintf(":%s", sess.AuthToken())) c.Request.Header.Add(header.Auth, fmt.Sprintf("%s %s", header.AuthBasic, base64.StdEncoding.EncodeToString(basicAuth))) + webdavAuthCache.Flush() webdavHandler(c) assert.Equal(t, http.StatusOK, c.Writer.Status()) @@ -90,6 +112,7 @@ func TestWebDAVAuth(t *testing.T) { sess := entity.SessionFixtures.Get("alice_token_scope") header.SetAuthorization(c.Request, sess.AuthToken()) + webdavAuthCache.Flush() webdavHandler(c) assert.Equal(t, http.StatusUnauthorized, c.Writer.Status()) @@ -104,6 +127,7 @@ func TestWebDAVAuth(t *testing.T) { header.SetAuthorization(c.Request, rnd.AuthToken()) + webdavAuthCache.Flush() webdavHandler(c) assert.Equal(t, http.StatusUnauthorized, c.Writer.Status()) @@ -118,6 +142,7 @@ func TestWebDAVAuth(t *testing.T) { header.SetAuthorization(c.Request, rnd.AuthSecret()) + webdavAuthCache.Flush() webdavHandler(c) assert.Equal(t, http.StatusUnauthorized, c.Writer.Status()) @@ -136,22 +161,36 @@ func TestWebDAVAuthSession(t *testing.T) { s := entity.SessionFixtures.Get("alice_token_webdav") // Get session with authorized user and webdav scope. + webdavAuthCache.Flush() 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.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")) + + // Cache authentication. + webdavAuthCache.SetDefault(sid, user) + + // Get cached user. + sess, user, sid, cached = WebDAVAuthSession(c, s.AuthToken()) + + // Check result. + assert.Nil(t, sess) + assert.NotNil(t, user) + assert.True(t, cached) assert.Equal(t, s.ID, sid) assert.Equal(t, entity.UserFixtures.Get("alice").UserUID, user.UserUID)