diff --git a/internal/http/services/owncloud/ocdav/mkcol.go b/internal/http/services/owncloud/ocdav/mkcol.go
index 27c6736816c..e99bc61733b 100644
--- a/internal/http/services/owncloud/ocdav/mkcol.go
+++ b/internal/http/services/owncloud/ocdav/mkcol.go
@@ -20,6 +20,7 @@ package ocdav
import (
"context"
+ "errors"
"fmt"
"net/http"
"path"
@@ -117,6 +118,11 @@ func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Re
case res.Status.Code == rpc.Code_CODE_OK:
w.WriteHeader(http.StatusCreated)
return 0, nil
+ case res.Status.Code == rpc.Code_CODE_NOT_FOUND:
+ // This should never happen because if the parent collection does not exist we should
+ // get a Code_CODE_FAILED_PRECONDITION. We play stupid and return what the response gave us
+ //lint:ignore ST1005 mimic the exact oc10 error message
+ return http.StatusNotFound, errors.New("Resource not found")
case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
// check if user has access to parent
sRes, err := s.gwClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{
@@ -130,9 +136,10 @@ func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Re
// return not found error so we do not leak existence of a file
// TODO hide permission failed for users without access in every kind of request
// TODO should this be done in the driver?
- return http.StatusNotFound, fmt.Errorf("Resource not found")
+ //lint:ignore ST1005 mimic the exact oc10 error message
+ return http.StatusNotFound, errors.New("Resource not found")
}
- return http.StatusForbidden, fmt.Errorf(sRes.Status.Message)
+ return http.StatusForbidden, errors.New(sRes.Status.Message)
case res.Status.Code == rpc.Code_CODE_ABORTED:
return http.StatusPreconditionFailed, fmt.Errorf(res.Status.Message)
case res.Status.Code == rpc.Code_CODE_FAILED_PRECONDITION:
@@ -144,7 +151,8 @@ func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Re
case res.Status.Code == rpc.Code_CODE_ALREADY_EXISTS:
// https://www.rfc-editor.org/rfc/rfc4918#section-9.3.1:
// 405 (Method Not Allowed) - MKCOL can only be executed on an unmapped URL.
- return http.StatusMethodNotAllowed, fmt.Errorf("The resource you tried to create already exists")
+ //lint:ignore ST1005 mimic the exact oc10 error message
+ return http.StatusMethodNotAllowed, errors.New("The resource you tried to create already exists")
}
return rstatus.HTTPStatusFromCode(res.Status.Code), errtypes.NewErrtypeFromStatus(res.Status)
}
diff --git a/internal/http/services/owncloud/ocdav/ocdav_blackbox_test.go b/internal/http/services/owncloud/ocdav/ocdav_blackbox_test.go
index c9d19fbc799..656809f821a 100644
--- a/internal/http/services/owncloud/ocdav/ocdav_blackbox_test.go
+++ b/internal/http/services/owncloud/ocdav/ocdav_blackbox_test.go
@@ -22,6 +22,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
+ "path"
"strings"
cs3gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
@@ -203,6 +204,51 @@ var _ = Describe("ocdav", func() {
Entry("at the /dav/public-files endpoint for a folder", "/dav/public-files/tokenforfolder", "/public/tokenforfolder", http.StatusInternalServerError),
)
+ DescribeTable("HandleMkcol",
+ func(endpoint string, expectedPathPrefix string, expectedStatPath string, expectedStatus int) {
+
+ client.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.ListStorageSpacesRequest) bool {
+ p := string(req.Opaque.Map["path"].Value)
+ return p == "/" || strings.HasPrefix(p, expectedPathPrefix)
+ })).Return(nil, fmt.Errorf("unexpected io error"))
+
+ // path based requests need to check if the resource already exists
+ client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool {
+ return req.Ref.Path == expectedStatPath
+ })).Return(&cs3storageprovider.StatResponse{
+ Status: status.NewNotFound(ctx, "not found"),
+ }, nil)
+
+ // the spaces endpoint omits the list storage spaces call, it directly executes the delete call
+ client.On("CreateContainer", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.CreateContainerRequest) bool {
+ return utils.ResourceEqual(req.Ref, &cs3storageprovider.Reference{
+ ResourceId: userspace.Root,
+ Path: "./foo",
+ })
+ })).Return(&cs3storageprovider.CreateContainerResponse{
+ Status: status.NewOK(ctx),
+ }, nil)
+
+ rr := httptest.NewRecorder()
+ req, err := http.NewRequest("MKCOL", endpoint+"/foo", strings.NewReader(""))
+ Expect(err).ToNot(HaveOccurred())
+ req = req.WithContext(ctx)
+
+ handler.Handler().ServeHTTP(rr, req)
+ Expect(rr).To(HaveHTTPStatus(expectedStatus))
+ if expectedStatus == http.StatusInternalServerError {
+ Expect(rr).To(HaveHTTPBody("\nunexpected io error"), "Body must have a sabredav exception")
+ } else {
+ Expect(rr).To(HaveHTTPBody(""), "Body must be empty")
+ }
+
+ },
+ Entry("at the /webdav endpoint", "/webdav", "/users", "/users/username/foo", http.StatusInternalServerError),
+ Entry("at the /dav/files endpoint", "/dav/files/username", "/users/username", "/users/username/foo", http.StatusInternalServerError),
+ Entry("at the /dav/spaces endpoint", "/dav/spaces/provider-1$userspace!root", "/users/username", "/users/username/foo", http.StatusCreated),
+ Entry("at the /dav/public-files endpoint for a file", "/dav/public-files/tokenforfile", "", "/public/tokenforfolder/foo", http.StatusMethodNotAllowed),
+ Entry("at the /dav/public-files endpoint for a folder", "/dav/public-files/tokenforfolder", "/public/tokenforfolder", "/public/tokenforfolder/foo", http.StatusInternalServerError),
+ )
})
Context("When calls fail with an error", func() {
@@ -431,6 +477,55 @@ var _ = Describe("ocdav", func() {
Entry("at the /dav/public-files endpoint for a folder", "/dav/public-files/tokenforfolder", "/public/tokenforfolder", ".", http.StatusNotFound),
)
+ DescribeTable("HandleMkcol",
+ func(endpoint string, expectedPathPrefix string, expectedStatPath string, expectedPath string, expectedStatus int) {
+
+ client.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.ListStorageSpacesRequest) bool {
+ p := string(req.Opaque.Map["path"].Value)
+ return p == "/" || strings.HasPrefix(p, expectedPathPrefix)
+ })).Return(&cs3storageprovider.ListStorageSpacesResponse{
+ Status: status.NewOK(ctx),
+ StorageSpaces: []*cs3storageprovider.StorageSpace{userspace},
+ }, nil)
+
+ // path based requests need to check if the resource already exists
+ client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool {
+ return req.Ref.Path == expectedStatPath
+ })).Return(&cs3storageprovider.StatResponse{
+ Status: status.NewNotFound(ctx, "not found"),
+ }, nil)
+
+ ref := cs3storageprovider.Reference{
+ ResourceId: userspace.Root,
+ Path: expectedPath,
+ }
+
+ client.On("CreateContainer", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.CreateContainerRequest) bool {
+ return utils.ResourceEqual(req.Ref, &ref)
+ })).Return(&cs3storageprovider.CreateContainerResponse{
+ Status: status.NewNotFound(ctx, "not found"),
+ }, nil)
+
+ rr := httptest.NewRecorder()
+ req, err := http.NewRequest("MKCOL", endpoint+"/foo", strings.NewReader(""))
+ Expect(err).ToNot(HaveOccurred())
+ req = req.WithContext(ctx)
+
+ handler.Handler().ServeHTTP(rr, req)
+ Expect(rr).To(HaveHTTPStatus(expectedStatus))
+ if expectedStatus == http.StatusNotFound {
+ Expect(rr).To(HaveHTTPBody("\nSabre\\DAV\\Exception\\NotFoundResource not found"), "Body must have a not found sabredav exception")
+ } else {
+ Expect(rr).To(HaveHTTPBody(""), "Body must be empty")
+ }
+ },
+ Entry("at the /webdav endpoint", "/webdav", "/users", "/users/username/foo", "./foo", http.StatusNotFound),
+ Entry("at the /dav/files endpoint", "/dav/files/username", "/users/username", "/users/username/foo", "./foo", http.StatusNotFound),
+ Entry("at the /dav/spaces endpoint", "/dav/spaces/provider-1$userspace!root", "/users/username", "/users/username/foo", "./foo", http.StatusNotFound),
+ Entry("at the /dav/public-files endpoint for a file", "/dav/public-files/tokenforfile", "", "/public/tokenforfolder/foo", "", http.StatusMethodNotAllowed),
+ Entry("at the /dav/public-files endpoint for a folder", "/dav/public-files/tokenforfolder", "/public/tokenforfolder", "/public/tokenforfolder/foo", ".", http.StatusNotFound),
+ )
+
})
Context("When the operation is forbidden", func() {
@@ -525,7 +620,7 @@ var _ = Describe("ocdav", func() {
// With lock
- // when user has access he should see forbidden status
+ // when user has access he should see locked status
Entry("at the /webdav endpoint", "/webdav", "/users", "./foo", true, true, http.StatusLocked),
Entry("at the /dav/files endpoint", "/dav/files/username", "/users/username", "./foo", true, true, http.StatusLocked),
Entry("at the /dav/spaces endpoint", "/dav/spaces/provider-1$userspace!root", "/users/username", "./foo", true, true, http.StatusLocked),
@@ -539,6 +634,123 @@ var _ = Describe("ocdav", func() {
Entry("at the /dav/public-files endpoint for a folder", "/dav/public-files/tokenforfolder", "/public/tokenforfolder", ".", true, false, http.StatusNotFound),
)
+ DescribeTable("HandleMkcol",
+ func(endpoint string, expectedPathPrefix string, expectedStatPath string, expectedPath string, locked, userHasAccess bool, expectedStatus int) {
+
+ client.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.ListStorageSpacesRequest) bool {
+ p := string(req.Opaque.Map["path"].Value)
+ return p == "/" || strings.HasPrefix(p, expectedPathPrefix)
+ })).Return(&cs3storageprovider.ListStorageSpacesResponse{
+ Status: status.NewOK(ctx),
+ StorageSpaces: []*cs3storageprovider.StorageSpace{userspace},
+ }, nil)
+
+ // path based requests need to check if the resource already exists
+ client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool {
+ return req.Ref.Path == expectedStatPath
+ })).Return(&cs3storageprovider.StatResponse{
+ Status: status.NewNotFound(ctx, "not found"),
+ }, nil)
+
+ ref := cs3storageprovider.Reference{
+ ResourceId: userspace.Root,
+ Path: expectedPath,
+ }
+
+ if locked {
+ client.On("CreateContainer", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.CreateContainerRequest) bool {
+ return utils.ResourceEqual(req.Ref, &ref)
+ })).Return(&cs3storageprovider.CreateContainerResponse{
+ Opaque: &typesv1beta1.Opaque{Map: map[string]*typesv1beta1.OpaqueEntry{
+ "lockid": {Decoder: "plain", Value: []byte("somelockid")},
+ }},
+ Status: status.NewPermissionDenied(ctx, fmt.Errorf("permission denied error"), "permission denied message"),
+ }, nil)
+ } else {
+ client.On("CreateContainer", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.CreateContainerRequest) bool {
+ return utils.ResourceEqual(req.Ref, &ref)
+ })).Return(&cs3storageprovider.CreateContainerResponse{
+ Status: status.NewPermissionDenied(ctx, fmt.Errorf("permission denied error"), "permission denied message"),
+ }, nil)
+ }
+
+ parentRef := cs3storageprovider.Reference{
+ ResourceId: userspace.Root,
+ Path: utils.MakeRelativePath(path.Dir(expectedPath)),
+ }
+
+ if userHasAccess {
+ client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool {
+ return utils.ResourceEqual(req.Ref, &parentRef)
+ })).Return(&cs3storageprovider.StatResponse{
+ Status: status.NewOK(ctx),
+ Info: &cs3storageprovider.ResourceInfo{
+ Type: cs3storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER,
+ },
+ }, nil)
+ } else {
+ client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool {
+ return utils.ResourceEqual(req.Ref, &parentRef)
+ })).Return(&cs3storageprovider.StatResponse{
+ Status: status.NewPermissionDenied(ctx, fmt.Errorf("permission denied error"), "permission denied message"),
+ }, nil)
+ }
+
+ rr := httptest.NewRecorder()
+ req, err := http.NewRequest("MKCOL", endpoint+"/foo", strings.NewReader(""))
+ Expect(err).ToNot(HaveOccurred())
+ req = req.WithContext(ctx)
+
+ handler.Handler().ServeHTTP(rr, req)
+ Expect(rr).To(HaveHTTPStatus(expectedStatus))
+ if expectedStatus == http.StatusMethodNotAllowed {
+ Expect(rr).To(HaveHTTPBody(""), "Body must be empty")
+ } else {
+ if userHasAccess {
+ if locked {
+ Expect(rr).To(HaveHTTPBody("\nSabre\\DAV\\Exception\\Locked"), "Body must have a locked sabredav exception")
+ Expect(rr).To(HaveHTTPHeaderWithValue("Lock-Token", ""))
+ } else {
+ Expect(rr).To(HaveHTTPBody("\nSabre\\DAV\\Exception\\Forbidden"), "Body must have a forbidden sabredav exception")
+ }
+ } else {
+ Expect(rr).To(HaveHTTPBody("\nSabre\\DAV\\Exception\\NotFoundResource not found"), "Body must have a not found sabredav exception")
+ }
+ }
+ },
+
+ // without lock
+
+ // when user has access he should see forbidden status
+ Entry("at the /webdav endpoint", "/webdav", "/users", "/users/username/foo", "./foo", false, true, http.StatusForbidden),
+ Entry("at the /dav/files endpoint", "/dav/files/username", "/users/username", "/users/username/foo", "./foo", false, true, http.StatusForbidden),
+ Entry("at the /dav/spaces endpoint", "/dav/spaces/provider-1$userspace!root", "/users/username", "/users/username/foo", "./foo", false, true, http.StatusForbidden),
+ Entry("at the /dav/public-files endpoint for a file", "/dav/public-files/tokenforfile", "", "/public/tokenforfolder/foo", "", false, true, http.StatusMethodNotAllowed),
+ Entry("at the /dav/public-files endpoint for a folder", "/dav/public-files/tokenforfolder", "/public/tokenforfolder", "/public/tokenforfolder/foo", ".", false, true, http.StatusForbidden),
+ // when user does not have access he should get not found status
+ Entry("at the /webdav endpoint", "/webdav", "/users", "/users/username/foo", "./foo", false, false, http.StatusNotFound),
+ Entry("at the /dav/files endpoint", "/dav/files/username", "/users/username", "/users/username/foo", "./foo", false, false, http.StatusNotFound),
+ Entry("at the /dav/spaces endpoint", "/dav/spaces/provider-1$userspace!root", "/users/username", "/users/username/foo", "./foo", false, false, http.StatusNotFound),
+ Entry("at the /dav/public-files endpoint for a file", "/dav/public-files/tokenforfile", "", "/public/tokenforfolder/foo", "", false, false, http.StatusMethodNotAllowed),
+ Entry("at the /dav/public-files endpoint for a folder", "/dav/public-files/tokenforfolder", "/public/tokenforfolder", "/public/tokenforfolder/foo", ".", false, false, http.StatusNotFound),
+
+ // With lock
+
+ // when user has access he should see locked status
+ // FIXME currently the ocdav mkcol handler is not forwarding a lockid ... but decomposedfs at least cannot create locks for unmapped resources, yet
+ PEntry("at the /webdav endpoint", "/webdav", "/users", "/users/username/foo", "./foo", true, true, http.StatusLocked),
+ PEntry("at the /dav/files endpoint", "/dav/files/username", "/users/username", "/users/username/foo", "./foo", true, true, http.StatusLocked),
+ PEntry("at the /dav/spaces endpoint", "/dav/spaces/provider-1$userspace!root", "/users/username", "/users/username/foo", "./foo", true, true, http.StatusLocked),
+ Entry("at the /dav/public-files endpoint for a file", "/dav/public-files/tokenforfile", "", "/public/tokenforfolder/foo", "", true, true, http.StatusMethodNotAllowed),
+ PEntry("at the /dav/public-files endpoint for a folder", "/dav/public-files/tokenforfolder", "/public/tokenforfolder", "/public/tokenforfolder/foo", ".", true, true, http.StatusLocked),
+ // when user does not have access he should get not found status
+ Entry("at the /webdav endpoint", "/webdav", "/users", "/users/username/foo", "./foo", true, false, http.StatusNotFound),
+ Entry("at the /dav/files endpoint", "/dav/files/username", "/users/username", "/users/username/foo", "./foo", true, false, http.StatusNotFound),
+ Entry("at the /dav/spaces endpoint", "/dav/spaces/provider-1$userspace!root", "/users/username", "/users/username/foo", "./foo", true, false, http.StatusNotFound),
+ Entry("at the /dav/public-files endpoint for a file", "/dav/public-files/tokenforfile", "", "/public/tokenforfolder/foo", "", true, false, http.StatusMethodNotAllowed),
+ Entry("at the /dav/public-files endpoint for a folder", "/dav/public-files/tokenforfolder", "/public/tokenforfolder", "/public/tokenforfolder/foo", ".", true, false, http.StatusNotFound),
+ )
+
})
// listing spaces is a precondition for path based requests, what if listing spaces currently is broken?
Context("locks are forwarded", func() {
@@ -581,6 +793,54 @@ var _ = Describe("ocdav", func() {
Entry("at the /dav/public-files endpoint for a file", "/dav/public-files/tokenforfile", "", "", http.StatusMethodNotAllowed),
Entry("at the /dav/public-files endpoint for a folder", "/dav/public-files/tokenforfolder", "/public/tokenforfolder", ".", http.StatusNoContent),
)
+
+ // FIXME currently the ocdav mkcol handler is not forwarding a lockid ... but decomposedfs at least cannot create locks for unmapped resources, yet
+ PDescribeTable("HandleMkcol",
+ func(endpoint string, expectedPathPrefix string, expectedStatPath string, expectedPath string, expectedStatus int) {
+
+ client.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.ListStorageSpacesRequest) bool {
+ p := string(req.Opaque.Map["path"].Value)
+ return p == "/" || strings.HasPrefix(p, expectedPathPrefix)
+ })).Return(&cs3storageprovider.ListStorageSpacesResponse{
+ Status: status.NewOK(ctx),
+ StorageSpaces: []*cs3storageprovider.StorageSpace{userspace},
+ }, nil)
+
+ // path based requests need to check if the resource already exists
+ client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool {
+ return req.Ref.Path == expectedStatPath
+ })).Return(&cs3storageprovider.StatResponse{
+ Status: status.NewNotFound(ctx, "not found"),
+ }, nil)
+
+ ref := cs3storageprovider.Reference{
+ ResourceId: userspace.Root,
+ Path: expectedPath,
+ }
+
+ client.On("CreateContainer", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.CreateContainerRequest) bool {
+ Expect(utils.ReadPlainFromOpaque(req.Opaque, "lockid")).To(Equal("urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2"))
+ return utils.ResourceEqual(req.Ref, &ref)
+ })).Return(&cs3storageprovider.CreateContainerResponse{
+ Status: status.NewOK(ctx),
+ }, nil)
+
+ rr := httptest.NewRecorder()
+ req, err := http.NewRequest("MKCOL", endpoint+"/foo", strings.NewReader(""))
+ req.Header.Set("If", "()")
+ Expect(err).ToNot(HaveOccurred())
+ req = req.WithContext(ctx)
+
+ handler.Handler().ServeHTTP(rr, req)
+ Expect(rr).To(HaveHTTPStatus(expectedStatus))
+ },
+ Entry("at the /webdav endpoint", "/webdav", "/users", "/users/username/foo", "./foo", http.StatusNoContent),
+ Entry("at the /dav/files endpoint", "/dav/files/username", "/users/username", "/users/username/foo", "./foo", http.StatusNoContent),
+ Entry("at the /dav/spaces endpoint", "/dav/spaces/provider-1$userspace!root", "/users/username", "/users/username/foo", "./foo", http.StatusNoContent),
+ Entry("at the /dav/public-files endpoint for a file", "/dav/public-files/tokenforfile", "", "/public/tokenforfolder/foo", "", http.StatusMethodNotAllowed),
+ Entry("at the /dav/public-files endpoint for a folder", "/dav/public-files/tokenforfolder", "/public/tokenforfolder", "/public/tokenforfolder/foo", ".", http.StatusNoContent),
+ )
+
})
})