From 28b7b0d0b22079077b5150910a78fd6d3bb816f0 Mon Sep 17 00:00:00 2001 From: Roman Perekhod Date: Tue, 23 Apr 2024 09:29:02 +0200 Subject: [PATCH] Replacement for TokenInfo endpoint --- internal/http/services/owncloud/ocdav/dav.go | 86 ++++++- .../owncloud/ocdav/ocdav_blackbox_test.go | 215 ++++++++++++++++++ 2 files changed, 289 insertions(+), 12 deletions(-) diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index ec3fc85a3fe..3f55d5ac7f9 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -28,14 +28,17 @@ import ( gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/config" "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors" "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net" "github.com/cs3org/reva/v2/pkg/appctx" + "github.com/cs3org/reva/v2/pkg/conversions" ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/v2/pkg/rhttp/router" + "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" "google.golang.org/grpc/metadata" ) @@ -269,12 +272,14 @@ func (h *DavHandler) Handler(s *svc) http.Handler { case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: fallthrough case res.Status.Code == rpc.Code_CODE_UNAUTHENTICATED: - w.WriteHeader(http.StatusUnauthorized) if hasValidBasicAuthHeader { + w.WriteHeader(http.StatusUnauthorized) b, err := errors.Marshal(http.StatusUnauthorized, "Username or password was incorrect", "") errors.HandleWebdavError(log, w, b, err) return } + w.Header().Add("WWW-Authenticate", "Basic") + w.WriteHeader(http.StatusUnauthorized) b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Basic' header found", "") errors.HandleWebdavError(log, w, b, err) return @@ -286,14 +291,32 @@ func (h *DavHandler) Handler(s *svc) http.Handler { return } - ctx = ctxpkg.ContextSetToken(ctx, res.Token) - ctx = ctxpkg.ContextSetUser(ctx, res.User) - ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, res.Token) + // force the internal link + var isInternal bool + sRes, err := getInternalLinkStat(ctx, s.gatewaySelector, token) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if sRes.GetStatus().GetCode() == rpc.Code_CODE_UNAUTHENTICATED { + w.Header().Add("WWW-Authenticate", "Bearer") + w.WriteHeader(http.StatusUnauthorized) + b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Bearer' header found", "") + errors.HandleWebdavError(log, w, b, err) + return + } + if sRes.GetStatus().GetCode() == rpc.Code_CODE_OK { + isInternal = true + } else { + ctx = ctxpkg.ContextSetToken(ctx, res.Token) + ctx = ctxpkg.ContextSetUser(ctx, res.User) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, res.Token) - r = r.WithContext(ctx) + r = r.WithContext(ctx) + // the public share manager knew the token, but does the referenced target still exist? + sRes, err = getTokenStatInfo(ctx, s.gatewaySelector, token) + } - // the public share manager knew the token, but does the referenced target still exist? - sRes, err := getTokenStatInfo(ctx, s.gatewaySelector, token) switch { case err != nil: log.Error().Err(err).Msg("error sending grpc stat request") @@ -316,12 +339,20 @@ func (h *DavHandler) Handler(s *svc) http.Handler { } log.Debug().Interface("statInfo", sRes.Info).Msg("Stat info from public link token path") - ctx := ContextWithTokenStatInfo(ctx, sRes.Info) - r = r.WithContext(ctx) - if sRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER { - h.PublicFileHandler.Handler(s).ServeHTTP(w, r) + if isInternal { + base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "spaces") + ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base) + r = r.WithContext(ctx) + r.URL.Path = "/" + storagespace.FormatResourceID(*sRes.GetInfo().GetId()) + h.SpacesHandler.Handler(s, nil).ServeHTTP(w, r) } else { - h.PublicFolderHandler.Handler(s).ServeHTTP(w, r) + ctx := ContextWithTokenStatInfo(ctx, sRes.Info) + r = r.WithContext(ctx) + if sRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER { + h.PublicFileHandler.Handler(s).ServeHTTP(w, r) + } else { + h.PublicFolderHandler.Handler(s).ServeHTTP(w, r) + } } default: @@ -332,6 +363,37 @@ func (h *DavHandler) Handler(s *svc) http.Handler { }) } +func getInternalLinkStat(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token string) (*provider.StatResponse, error) { + client, err := selector.Next() + if err != nil { + return nil, err + } + psRes, err := client.GetPublicShare(ctx, &link.GetPublicShareRequest{ + Ref: &link.PublicShareReference{ + Spec: &link.PublicShareReference_Token{ + Token: token, + }, + }}) + if err != nil { + if strings.Contains(err.Error(), "core access token not found") { + return nil, nil + } + return nil, err + } + if psRes.Status.Code != rpc.Code_CODE_OK { + return nil, nil + } + + role := conversions.RoleFromResourcePermissions(psRes.Share.Permissions.GetPermissions(), true) + if role.OCSPermissions() == 0 { + return client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ + ResourceId: psRes.GetShare().GetResourceId(), + }}) + + } + return nil, nil +} + func getTokenStatInfo(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token string) (*provider.StatResponse, error) { client, err := selector.Next() if err != nil { diff --git a/internal/http/services/owncloud/ocdav/ocdav_blackbox_test.go b/internal/http/services/owncloud/ocdav/ocdav_blackbox_test.go index 5c15d414465..ecc7ad2a819 100644 --- a/internal/http/services/owncloud/ocdav/ocdav_blackbox_test.go +++ b/internal/http/services/owncloud/ocdav/ocdav_blackbox_test.go @@ -341,6 +341,221 @@ var _ = Describe("ocdav", func() { }) }) + Describe("PROPFIND to public-file", func() { + + BeforeEach(func() { + // set the dav endpoint to test + basePath = "/dav/public-files" + + // setup the request + rr = httptest.NewRecorder() + req, err = http.NewRequest("PROPFIND", basePath+"/aySTemFFUcNudVD", strings.NewReader("")) + Expect(err).ToNot(HaveOccurred()) + req = req.WithContext(ctx) + }) + + When("the the link is public", func() { + It("returns a status Unauthorized", func() { + + client.On("Authenticate", mock.Anything, mock.MatchedBy(func(req *cs3gateway.AuthenticateRequest) bool { + return req.Type == "publicshares" && + strings.HasPrefix(req.ClientId, "aySTemFFUcNudVD") && + strings.HasPrefix(req.ClientSecret, "signature||") + })).Return(&cs3gateway.AuthenticateResponse{ + Status: status.NewUnauthenticated(ctx, nil, ""), + }, nil) + + handler.Handler().ServeHTTP(rr, req) + Expect(rr).To(HaveHTTPStatus(http.StatusUnauthorized)) + Expect(rr).To(HaveHTTPHeaderWithValue("WWW-Authenticate", "Basic")) + }) + It("returns a Multistatus with the file properties", func() { + req.Header.Set("Authorization", "Basic cHVibGljOmAxcWF6WHN3Mg==") + ctx = ctxpkg.ContextSetUser(context.Background(), user) + + user = &cs3user.User{Id: &cs3user.UserId{OpaqueId: "username"}, Username: "username"} + expectedPathPrefix := "/" + + userspace = &cs3storageprovider.StorageSpace{ + Opaque: &cs3types.Opaque{ + Map: map[string]*cs3types.OpaqueEntry{ + "path": { + Decoder: "plain", + Value: []byte("/public/aySTemFFUcNudVD"), + }, + }, + }, + + Id: &cs3storageprovider.StorageSpaceId{OpaqueId: storagespace.FormatResourceID(cs3storageprovider.ResourceId{StorageId: utils.PublicStorageProviderID, + SpaceId: utils.PublicStorageSpaceID, + OpaqueId: "aySTemFFUcNudVD"})}, + Root: &cs3storageprovider.ResourceId{StorageId: utils.PublicStorageProviderID, + SpaceId: utils.PublicStorageSpaceID, + OpaqueId: "aySTemFFUcNudVD"}, + Name: "", + SpaceType: "mountpoint", + } + + client.On("Authenticate", mock.Anything, mock.MatchedBy(func(req *cs3gateway.AuthenticateRequest) bool { + return req.Type == "publicshares" && + strings.HasPrefix(req.ClientId, "aySTemFFUcNudVD") && + strings.HasPrefix(req.ClientSecret, "password|`1qazXsw2") + })).Return(&cs3gateway.AuthenticateResponse{ + Status: status.NewOK(ctx), + Token: "valid-token", + User: user, + }, nil) + + client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool { + return utils.ResourceIDEqual(req.Ref.ResourceId, &cs3storageprovider.ResourceId{ + StorageId: utils.PublicStorageProviderID, + SpaceId: utils.PublicStorageSpaceID, + OpaqueId: "aySTemFFUcNudVD", + }) + })).Return(&cs3storageprovider.StatResponse{ + Status: status.NewOK(ctx), + Info: &cs3storageprovider.ResourceInfo{Id: &cs3storageprovider.ResourceId{ + StorageId: utils.PublicStorageProviderID, + SpaceId: utils.PublicStorageSpaceID, + OpaqueId: "aySTemFFUcNudVD", + }, + Type: cs3storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER}, + }, nil).Once() + + 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}, // FIXME we may need to return the /public storage provider id and mock it + }, nil) + + client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool { + return utils.ResourceIDEqual(req.Ref.ResourceId, &cs3storageprovider.ResourceId{ + StorageId: utils.PublicStorageProviderID, + SpaceId: utils.PublicStorageSpaceID, + OpaqueId: "aySTemFFUcNudVD", + }) + })).Return(&cs3storageprovider.StatResponse{ + Status: status.NewOK(ctx), + Info: &cs3storageprovider.ResourceInfo{Id: &cs3storageprovider.ResourceId{ + StorageId: utils.PublicStorageProviderID, + SpaceId: utils.PublicStorageSpaceID, + OpaqueId: "aySTemFFUcNudVD", + }, + Type: cs3storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER}, + }, nil).Once() + + client.On("ListContainer", mock.Anything, mock.Anything).Return(&cs3storageprovider.ListContainerResponse{ + Status: status.NewOK(context.Background()), + }, nil) + + handler.Handler().ServeHTTP(rr, req) + Expect(rr).To(HaveHTTPStatus(http.StatusMultiStatus)) + }) + }) + + When("the the link is internal", func() { + FIt("returns a status Unauthorized", func() { + + client.On("Authenticate", mock.Anything, mock.MatchedBy(func(req *cs3gateway.AuthenticateRequest) bool { + return req.Type == "publicshares" && + strings.HasPrefix(req.ClientId, "aySTemFFUcNudVD") && + strings.HasPrefix(req.ClientSecret, "signature||") + })).Return(&cs3gateway.AuthenticateResponse{ + Status: status.NewUnauthenticated(ctx, nil, ""), + }, nil) + + handler.Handler().ServeHTTP(rr, req) + Expect(rr).To(HaveHTTPStatus(http.StatusUnauthorized)) + Expect(rr).To(HaveHTTPHeaderWithValue("WWW-Authenticate", "Bearer")) + }) + It("returns a Multistatus with the file properties", func() { + req.Header.Set("Authorization", "Basic cHVibGljOmAxcWF6WHN3Mg==") + ctx = ctxpkg.ContextSetUser(context.Background(), user) + + user = &cs3user.User{Id: &cs3user.UserId{OpaqueId: "username"}, Username: "username"} + expectedPathPrefix := "/" + + userspace = &cs3storageprovider.StorageSpace{ + Opaque: &cs3types.Opaque{ + Map: map[string]*cs3types.OpaqueEntry{ + "path": { + Decoder: "plain", + Value: []byte("/public/aySTemFFUcNudVD"), + }, + }, + }, + + Id: &cs3storageprovider.StorageSpaceId{OpaqueId: storagespace.FormatResourceID(cs3storageprovider.ResourceId{StorageId: utils.PublicStorageProviderID, + SpaceId: utils.PublicStorageSpaceID, + OpaqueId: "aySTemFFUcNudVD"})}, + Root: &cs3storageprovider.ResourceId{StorageId: utils.PublicStorageProviderID, + SpaceId: utils.PublicStorageSpaceID, + OpaqueId: "aySTemFFUcNudVD"}, + Name: "", + SpaceType: "mountpoint", + } + + client.On("Authenticate", mock.Anything, mock.MatchedBy(func(req *cs3gateway.AuthenticateRequest) bool { + return req.Type == "publicshares" && + strings.HasPrefix(req.ClientId, "aySTemFFUcNudVD") && + strings.HasPrefix(req.ClientSecret, "password|`1qazXsw2") + })).Return(&cs3gateway.AuthenticateResponse{ + Status: status.NewOK(ctx), + Token: "valid-token", + User: user, + }, nil) + + client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool { + return utils.ResourceIDEqual(req.Ref.ResourceId, &cs3storageprovider.ResourceId{ + StorageId: utils.PublicStorageProviderID, + SpaceId: utils.PublicStorageSpaceID, + OpaqueId: "aySTemFFUcNudVD", + }) + })).Return(&cs3storageprovider.StatResponse{ + Status: status.NewOK(ctx), + Info: &cs3storageprovider.ResourceInfo{Id: &cs3storageprovider.ResourceId{ + StorageId: utils.PublicStorageProviderID, + SpaceId: utils.PublicStorageSpaceID, + OpaqueId: "aySTemFFUcNudVD", + }, + Type: cs3storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER}, + }, nil).Once() + + 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}, // FIXME we may need to return the /public storage provider id and mock it + }, nil) + + client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool { + return utils.ResourceIDEqual(req.Ref.ResourceId, &cs3storageprovider.ResourceId{ + StorageId: utils.PublicStorageProviderID, + SpaceId: utils.PublicStorageSpaceID, + OpaqueId: "aySTemFFUcNudVD", + }) + })).Return(&cs3storageprovider.StatResponse{ + Status: status.NewOK(ctx), + Info: &cs3storageprovider.ResourceInfo{Id: &cs3storageprovider.ResourceId{ + StorageId: utils.PublicStorageProviderID, + SpaceId: utils.PublicStorageSpaceID, + OpaqueId: "aySTemFFUcNudVD", + }, + Type: cs3storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER}, + }, nil).Once() + + client.On("ListContainer", mock.Anything, mock.Anything).Return(&cs3storageprovider.ListContainerResponse{ + Status: status.NewOK(context.Background()), + }, nil) + + handler.Handler().ServeHTTP(rr, req) + Expect(rr).To(HaveHTTPStatus(http.StatusMultiStatus)) + }) + }) + }) Describe("MKCOL", func() { BeforeEach(func() {