You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cloudreve/middleware/davbearer_test.go

205 lines
6.5 KiB

package middleware
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent/enttest"
"github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
"github.com/cloudreve/Cloudreve/v4/pkg/cache"
"github.com/cloudreve/Cloudreve/v4/pkg/conf"
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/cloudreve/Cloudreve/v4/pkg/util"
"github.com/gin-gonic/gin"
)
type stubSettingProvider struct {
setting.Provider
}
func (stubSettingProvider) AuditLogEnabled(context.Context, int) bool { return true }
type stubTokenAuth struct {
uid int
scopes []string
}
func (s stubTokenAuth) Issue(ctx context.Context, args *auth.IssueTokenArgs) (*auth.Token, error) {
return nil, nil
}
func (s stubTokenAuth) VerifyAndRetrieveUser(c *gin.Context) (bool, error) {
if c.GetHeader(auth.AuthorizationHeader) == "Bearer good" {
util.WithValue(c, inventory.UserIDCtx{}, s.uid)
if s.scopes != nil {
util.WithValue(c, auth.ScopeContextKey{}, s.scopes)
}
}
return false, nil
}
func (s stubTokenAuth) Refresh(ctx context.Context, refreshToken string) (*auth.Token, error) {
return nil, nil
}
func (s stubTokenAuth) Claims(ctx context.Context, tokenStr string) (*auth.Claims, error) {
return nil, nil
}
func newDavAuthDep(t *testing.T, groupPerms *boolset.BooleanSet, scopes []string) dependency.Dep {
t.Helper()
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&_fk=1")
t.Cleanup(func() { client.Close() })
group := client.Group.Create().
SetName("g").
SetPermissions(groupPerms).
SaveX(context.Background())
u := client.User.Create().
SetEmail("dav@test.com").
SetNick("dav").
SetStatus("active").
SetPassword("x").
SetGroup(group).
SaveX(context.Background())
return dependency.NewDependency(
dependency.WithKV(cache.NewMemoStore("", nil)),
dependency.WithLogger(logging.NewConsoleLogger(logging.LevelDebug)),
dependency.WithUserClient(inventory.NewUserClient(client)),
dependency.WithTokenAuth(stubTokenAuth{uid: u.ID, scopes: scopes}),
dependency.WithSettingProvider(stubSettingProvider{}),
dependency.WithActivityClient(inventory.NewActivityClient(client, conf.SQLiteDB)),
)
}
func newDavRequest(t *testing.T, dep dependency.Dep, method, authHeader string) (*gin.Context, *httptest.ResponseRecorder) {
t.Helper()
w := httptest.NewRecorder()
c := gin.CreateTestContextOnly(w, testEngine)
req := httptest.NewRequest(method, "/dav/file.txt", nil)
if authHeader != "" {
req.Header.Set("Authorization", authHeader)
}
c.Request = req.WithContext(context.WithValue(req.Context(), dependency.DepCtx{}, dep))
return c, w
}
func TestWebDAVBearerAuth(t *testing.T) {
gin.SetMode(gin.TestMode)
handler := WebDAVAuth()
davPerms := &boolset.BooleanSet{}
boolset.Set(int(types.GroupPermissionWebDAV), true, davPerms)
readOnlyPerms := &boolset.BooleanSet{}
boolset.Set(int(types.GroupPermissionWebDAV), true, readOnlyPerms)
boolset.Set(int(types.GroupPermissionWebDAVReadOnly), true, readOnlyPerms)
t.Run("valid bearer token authenticates", func(t *testing.T) {
dep := newDavAuthDep(t, davPerms, nil)
c, _ := newDavRequest(t, dep, http.MethodGet, "Bearer good")
handler(c)
if c.IsAborted() {
t.Fatal("valid bearer token was rejected")
}
u := inventory.UserFromContext(c)
if u == nil || u.Email != "dav@test.com" {
t.Fatalf("expected bearer user in ctx, got %+v", u)
}
})
t.Run("invalid bearer token rejected", func(t *testing.T) {
dep := newDavAuthDep(t, davPerms, nil)
c, _ := newDavRequest(t, dep, http.MethodGet, "Bearer bad")
handler(c)
if !c.IsAborted() || c.Writer.Status() != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", c.Writer.Status())
}
})
t.Run("no credentials still issues basic+bearer challenge", func(t *testing.T) {
dep := newDavAuthDep(t, davPerms, nil)
c, w := newDavRequest(t, dep, http.MethodGet, "")
handler(c)
if c.Writer.Status() != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", c.Writer.Status())
}
if challenges := w.Header().Values("WWW-Authenticate"); len(challenges) != 2 {
t.Fatalf("expected Basic+Bearer challenges, got %v", challenges)
}
})
t.Run("options without credentials passes", func(t *testing.T) {
dep := newDavAuthDep(t, davPerms, nil)
c, _ := newDavRequest(t, dep, http.MethodOptions, "")
handler(c)
if c.IsAborted() {
t.Fatal("unauthenticated OPTIONS was rejected")
}
})
t.Run("bearer user without group dav permission forbidden", func(t *testing.T) {
dep := newDavAuthDep(t, &boolset.BooleanSet{}, nil)
c, _ := newDavRequest(t, dep, http.MethodGet, "Bearer good")
handler(c)
if c.Writer.Status() != http.StatusForbidden {
t.Fatalf("expected 403, got %d", c.Writer.Status())
}
})
t.Run("group read-only blocks bearer writes", func(t *testing.T) {
dep := newDavAuthDep(t, readOnlyPerms, nil)
c, _ := newDavRequest(t, dep, http.MethodPut, "Bearer good")
handler(c)
if c.Writer.Status() != http.StatusForbidden {
t.Fatalf("expected 403 for PUT, got %d", c.Writer.Status())
}
c, _ = newDavRequest(t, dep, http.MethodGet, "Bearer good")
handler(c)
if c.IsAborted() {
t.Fatal("read-only bearer GET was rejected")
}
})
t.Run("scoped token without Files.Write is read-only", func(t *testing.T) {
dep := newDavAuthDep(t, davPerms, []string{types.ScopeFilesRead})
c, _ := newDavRequest(t, dep, http.MethodDelete, "Bearer good")
handler(c)
if c.Writer.Status() != http.StatusForbidden {
t.Fatalf("expected 403 for DELETE, got %d", c.Writer.Status())
}
c, _ = newDavRequest(t, dep, http.MethodGet, "Bearer good")
handler(c)
if c.IsAborted() {
t.Fatal("scoped read token GET was rejected")
}
})
t.Run("scoped token without Files.Read forbidden", func(t *testing.T) {
dep := newDavAuthDep(t, davPerms, []string{"User.Read"})
c, _ := newDavRequest(t, dep, http.MethodGet, "Bearer good")
handler(c)
if c.Writer.Status() != http.StatusForbidden {
t.Fatalf("expected 403, got %d", c.Writer.Status())
}
})
t.Run("basic auth path unchanged", func(t *testing.T) {
dep := newDavAuthDep(t, davPerms, nil)
c, _ := newDavRequest(t, dep, http.MethodGet, "Basic dGVzdDp0ZXN0")
handler(c)
// No dav account exists -> 401 via the basic credential path.
if c.Writer.Status() != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", c.Writer.Status())
}
})
}