diff --git a/changelog/unreleased/ocm-webdav-and-scope.md b/changelog/unreleased/ocm-webdav-and-scope.md new file mode 100644 index 0000000000..97d018fbd6 --- /dev/null +++ b/changelog/unreleased/ocm-webdav-and-scope.md @@ -0,0 +1,10 @@ +Enhancement: Add OCM scope and webdav endpoint + +Adds the OCM scope and the ocmshares authentication, +to authenticate the federated user to use the OCM shared +resources. +It also adds the (unprotected) webdav endpoint used to interact with +the shared resources. + +https://github.com/cs3org/reva/pull/3691 +https://github.com/cs3org/reva/issues/2739 \ No newline at end of file diff --git a/go.mod b/go.mod index 08777a79d0..77ea1cf5d9 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/cheggaaa/pb v1.0.29 github.com/coreos/go-oidc v2.2.1+incompatible github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e - github.com/cs3org/go-cs3apis v0.0.0-20230221082129-bcf2b5cf8870 + github.com/cs3org/go-cs3apis v0.0.0-20230228180528-ee4e51c97a49 github.com/dgraph-io/ristretto v0.1.1 github.com/dolthub/go-mysql-server v0.14.0 github.com/eventials/go-tus v0.0.0-20200718001131-45c7ec8f5d59 diff --git a/go.sum b/go.sum index 1b7067d6cb..72842a0bfc 100644 --- a/go.sum +++ b/go.sum @@ -305,8 +305,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJffz4pz0o1WuQxJ28+5x5JgaHD8= github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4= -github.com/cs3org/go-cs3apis v0.0.0-20230221082129-bcf2b5cf8870 h1:MUYOLg0HxBYDmrtZONje+49yanhqGKmYvishv7GaSvw= -github.com/cs3org/go-cs3apis v0.0.0-20230221082129-bcf2b5cf8870/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= +github.com/cs3org/go-cs3apis v0.0.0-20230228180528-ee4e51c97a49 h1:CG65qpsSttrPAqdK19kaZaAiRadZ5xNFVrKoIjICBBM= +github.com/cs3org/go-cs3apis v0.0.0-20230228180528-ee4e51c97a49/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -375,6 +375,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/glpatcern/go-mime v0.0.0-20221026162842-2a8d71ad17a9 h1:3um08ooi0/lyRmK2eE1XTKmRQHDzPu0IvpCPMljyMZ8= github.com/glpatcern/go-mime v0.0.0-20221026162842-2a8d71ad17a9/go.mod h1:EJaddanP+JfU3UkVvn0rYYF3b/gD7eZRejbTHqiQExA= +github.com/gmgigi96/go-cs3apis v0.0.0-20230228153318-d227be9140af h1:HmFIcBqhz0IM5NxoCN8jYZY5Ms9PQp2QXshTjGzr0us= +github.com/gmgigi96/go-cs3apis v0.0.0-20230228153318-d227be9140af/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/go-acme/lego/v4 v4.4.0/go.mod h1:l3+tFUFZb590dWcqhWZegynUthtaHJbG2fevUpoOOE0= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= diff --git a/internal/grpc/interceptors/auth/scope.go b/internal/grpc/interceptors/auth/scope.go index e78aa56b72..63775539aa 100644 --- a/internal/grpc/interceptors/auth/scope.go +++ b/internal/grpc/interceptors/auth/scope.go @@ -32,6 +32,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" + ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" "github.com/cs3org/reva/pkg/appctx" @@ -73,11 +74,14 @@ func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[s if err = resolvePublicShare(ctx, ref, tokenScope[k], client, mgr); err == nil { return nil } - case strings.HasPrefix(k, "share"): if err = resolveUserShare(ctx, ref, tokenScope[k], client, mgr); err == nil { return nil } + case strings.HasPrefix(k, "ocmshare"): + if err = resolveOCMShare(ctx, ref, tokenScope[k], client, mgr); err == nil { + return nil + } } if err != nil { log.Err(err).Msgf("error resolving reference %s under scope %+v", ref.String(), k) @@ -223,6 +227,15 @@ func resolvePublicShare(ctx context.Context, ref *provider.Reference, scope *aut return checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr) } +func resolveOCMShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error { + var share ocmv1beta1.Share + if err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share); err != nil { + return err + } + + return checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr) +} + func resolveUserShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error { var share collaboration.Share err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share) @@ -248,6 +261,13 @@ func checkCacheForNestedResource(ctx context.Context, ref *provider.Reference, r return errtypes.PermissionDenied("request is not for a nested resource") } +func isRelativePathOrEmpty(path string) bool { + if len(path) == 0 { + return true + } + return path[0] != '/' +} + func checkIfNestedResource(ctx context.Context, ref *provider.Reference, parent *provider.ResourceId, client gateway.GatewayAPIClient, mgr token.Manager) (bool, error) { // Since the resource ID is obtained from the scope, the current token // has access to it. @@ -261,7 +281,7 @@ func checkIfNestedResource(ctx context.Context, ref *provider.Reference, parent parentPath := statResponse.Info.Path childPath := ref.GetPath() - if childPath == "" { + if isRelativePathOrEmpty(childPath) { // We mint a token as the owner of the public share and try to stat the reference // TODO(ishank011): We need to find a better alternative to this @@ -308,6 +328,8 @@ func extractRefForReaderRole(req interface{}) (*provider.Reference, bool) { return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true case *gateway.OpenInAppRequest: return v.GetRef(), true + case *provider.GetLockRequest: + return v.GetRef(), true // App provider requests case *appregistry.GetAppProvidersRequest: @@ -346,6 +368,12 @@ func extractRefForEditorRole(req interface{}) (*provider.Reference, bool) { return v.GetRef(), true case *provider.UnsetArbitraryMetadataRequest: return v.GetRef(), true + case *provider.SetLockRequest: + return v.GetRef(), true + case *provider.RefreshLockRequest: + return v.GetRef(), true + case *provider.UnlockRequest: + return v.GetRef(), true } return nil, false diff --git a/internal/grpc/services/gateway/ocmshareprovider.go b/internal/grpc/services/gateway/ocmshareprovider.go index c1dbe07f97..28019e786c 100644 --- a/internal/grpc/services/gateway/ocmshareprovider.go +++ b/internal/grpc/services/gateway/ocmshareprovider.go @@ -95,6 +95,20 @@ func (s *svc) getOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*oc return res, nil } +func (s *svc) GetOCMShareByToken(ctx context.Context, req *ocm.GetOCMShareByTokenRequest) (*ocm.GetOCMShareByTokenResponse, error) { + c, err := pool.GetOCMShareProviderClient(pool.Endpoint(s.c.OCMShareProviderEndpoint)) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling GetOCMShareProviderClient") + } + + res, err := c.GetOCMShareByToken(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling GetOCMShareByToken") + } + + return res, nil +} + // TODO(labkode): read GetShare comment. func (s *svc) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest) (*ocm.ListOCMSharesResponse, error) { c, err := pool.GetOCMShareProviderClient(pool.Endpoint(s.c.OCMShareProviderEndpoint)) diff --git a/internal/grpc/services/ocminvitemanager/ocminvitemanager.go b/internal/grpc/services/ocminvitemanager/ocminvitemanager.go index 88add7619d..b56844cd08 100644 --- a/internal/grpc/services/ocminvitemanager/ocminvitemanager.go +++ b/internal/grpc/services/ocminvitemanager/ocminvitemanager.go @@ -35,6 +35,7 @@ import ( "github.com/cs3org/reva/pkg/rgrpc/status" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/sharedconf" + "github.com/cs3org/reva/pkg/utils" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "google.golang.org/grpc" @@ -132,7 +133,7 @@ func (s *service) Close() error { } func (s *service) UnprotectedEndpoints() []string { - return []string{"/cs3.ocm.invite.v1beta1.InviteAPI/AcceptInvite"} + return []string{"/cs3.ocm.invite.v1beta1.InviteAPI/AcceptInvite", "/cs3.ocm.invite.v1beta1.InviteAPI/GetAcceptedUser"} } func (s *service) GenerateInviteToken(ctx context.Context, req *invitepb.GenerateInviteTokenRequest) (*invitepb.GenerateInviteTokenResponse, error) { @@ -313,7 +314,12 @@ func isTokenValid(token *invitepb.InviteToken) bool { } func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*invitepb.GetAcceptedUserResponse, error) { - user := ctxpkg.ContextMustGetUser(ctx) + user, ok := getUserFilter(ctx, req) + if !ok { + return &invitepb.GetAcceptedUserResponse{ + Status: status.NewInvalidArg(ctx, "user not found"), + }, nil + } remoteUser, err := s.repo.GetRemoteUser(ctx, user.GetId(), req.GetRemoteUserId()) if err != nil { return &invitepb.GetAcceptedUserResponse{ @@ -327,6 +333,28 @@ func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAccepted }, nil } +func getUserFilter(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*userpb.User, bool) { + user, ok := ctxpkg.ContextGetUser(ctx) + if ok { + return user, true + } + + if req.Opaque == nil || req.Opaque.Map == nil { + return nil, false + } + + v, ok := req.Opaque.Map["user-filter"] + if !ok { + return nil, false + } + + var u userpb.UserId + if err := utils.UnmarshalJSONToProtoV1(v.Value, &u); err != nil { + return nil, false + } + return &userpb.User{Id: &u}, true +} + func (s *service) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAcceptedUsersRequest) (*invitepb.FindAcceptedUsersResponse, error) { user := ctxpkg.ContextMustGetUser(ctx) acceptedUsers, err := s.repo.FindRemoteUsers(ctx, user.GetId(), req.GetFilter()) diff --git a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go index 192d2c1e3f..87401ccecb 100644 --- a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go +++ b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go @@ -60,8 +60,8 @@ type config struct { ClientTimeout int `mapstructure:"client_timeout"` ClientInsecure bool `mapstructure:"client_insecure"` GatewaySVC string `mapstructure:"gatewaysvc"` - WebDAVPrefix string `mapstructure:"webdav_prefix"` ProviderDomain string `mapstructure:"provider_domain" docs:"The same domain registered in the provider authorizer"` + WebDAVEndpoint string `mapstructure:"webdav_endpoint"` WebappTemplate string `mapstructure:"webapp_template"` } @@ -151,7 +151,7 @@ func (s *service) Close() error { } func (s *service) UnprotectedEndpoints() []string { - return nil + return []string{"/cs3.sharing.ocm.v1beta1.OcmAPI/GetOCMShareByToken"} } func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) { @@ -177,17 +177,13 @@ func getResourceType(info *providerpb.ResourceInfo) string { return "unknown" } -func (s *service) webdavURL(ctx context.Context, path string) string { - // the url is in the form of https://cernbox.cern.ch/remote.php/dav/files/gdelmont/eos/user/g/gdelmont - user := ctxpkg.ContextMustGetUser(ctx) - p, err := url.JoinPath(s.conf.WebDAVPrefix, user.Username, path) - if err != nil { - panic(err) - } +func (s *service) webdavURL(ctx context.Context, share *ocm.Share) string { + // the url is in the form of https://cernbox.cern.ch/remote.php/dav/ocm/token + p, _ := url.JoinPath(s.conf.WebDAVEndpoint, "/remote.php/dav/ocm", share.Token) return p } -func (s *service) getWebdavProtocol(ctx context.Context, info *providerpb.ResourceInfo, m *ocm.AccessMethod_WebdavOptions) *ocmd.WebDAV { +func (s *service) getWebdavProtocol(ctx context.Context, share *ocm.Share, m *ocm.AccessMethod_WebdavOptions) *ocmd.WebDAV { var perms []string if m.WebdavOptions.Permissions.InitiateFileDownload { perms = append(perms, "read") @@ -197,9 +193,8 @@ func (s *service) getWebdavProtocol(ctx context.Context, info *providerpb.Resour } return &ocmd.WebDAV{ - SharedSecret: ctxpkg.ContextMustGetToken(ctx), // TODO: change this and use an ocm token - Permissions: perms, - URL: s.webdavURL(ctx, info.Path), // TODO: change this and use an endpoint for ocm + Permissions: perms, + URL: s.webdavURL(ctx, share), } } @@ -213,12 +208,12 @@ func (s *service) getWebappProtocol(share *ocm.Share) *ocmd.Webapp { } } -func (s *service) getProtocols(ctx context.Context, share *ocm.Share, info *providerpb.ResourceInfo, methods []*ocm.AccessMethod) ocmd.Protocols { +func (s *service) getProtocols(ctx context.Context, share *ocm.Share) ocmd.Protocols { var p ocmd.Protocols - for _, m := range methods { + for _, m := range share.AccessMethods { switch t := m.Term.(type) { case *ocm.AccessMethod_WebdavOptions: - p = append(p, s.getWebdavProtocol(ctx, info, t)) + p = append(p, s.getWebdavProtocol(ctx, share, t)) case *ocm.AccessMethod_WebappOptions: p = append(p, s.getWebappProtocol(share)) case *ocm.AccessMethod_TransferOptions: @@ -308,7 +303,7 @@ func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareReq SenderDisplayName: user.DisplayName, ShareType: "user", ResourceType: getResourceType(info), - Protocols: s.getProtocols(ctx, ocmshare, info, req.AccessMethods), + Protocols: s.getProtocols(ctx, ocmshare), } if req.Expiration != nil { @@ -362,7 +357,11 @@ func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareReq } func (s *service) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) { - user := ctxpkg.ContextMustGetUser(ctx) + // if the request is by token, the user does not need to be in the ctx + var user *userpb.User + if req.Ref.GetToken() == "" { + user = ctxpkg.ContextMustGetUser(ctx) + } ocmshare, err := s.repo.GetShare(ctx, user, req.Ref) if err != nil { if errors.Is(err, share.ErrShareNotFound) { @@ -381,6 +380,29 @@ func (s *service) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) }, nil } +func (s *service) GetOCMShareByToken(ctx context.Context, req *ocm.GetOCMShareByTokenRequest) (*ocm.GetOCMShareByTokenResponse, error) { + ocmshare, err := s.repo.GetShare(ctx, nil, &ocm.ShareReference{ + Spec: &ocm.ShareReference_Token{ + Token: req.Token, + }, + }) + if err != nil { + if errors.Is(err, share.ErrShareNotFound) { + return &ocm.GetOCMShareByTokenResponse{ + Status: status.NewNotFound(ctx, "share does not exist"), + }, nil + } + return &ocm.GetOCMShareByTokenResponse{ + Status: status.NewInternal(ctx, err, "error getting share"), + }, nil + } + + return &ocm.GetOCMShareByTokenResponse{ + Status: status.NewOK(ctx), + Share: ocmshare, + }, nil +} + func (s *service) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest) (*ocm.ListOCMSharesResponse, error) { user := ctxpkg.ContextMustGetUser(ctx) shares, err := s.repo.ListShares(ctx, user, req.Filters) diff --git a/internal/http/interceptors/auth/credential/loader/loader.go b/internal/http/interceptors/auth/credential/loader/loader.go index 452cdb8ebb..d5aa16727e 100644 --- a/internal/http/interceptors/auth/credential/loader/loader.go +++ b/internal/http/interceptors/auth/credential/loader/loader.go @@ -22,6 +22,7 @@ import ( // Load core authentication strategies. _ "github.com/cs3org/reva/internal/http/interceptors/auth/credential/strategy/basic" _ "github.com/cs3org/reva/internal/http/interceptors/auth/credential/strategy/bearer" + _ "github.com/cs3org/reva/internal/http/interceptors/auth/credential/strategy/ocmshares" _ "github.com/cs3org/reva/internal/http/interceptors/auth/credential/strategy/publicshares" // Add your own here. ) diff --git a/internal/http/interceptors/auth/credential/strategy/ocmshares/ocmshares.go b/internal/http/interceptors/auth/credential/strategy/ocmshares/ocmshares.go new file mode 100644 index 0000000000..a93f62d8d9 --- /dev/null +++ b/internal/http/interceptors/auth/credential/strategy/ocmshares/ocmshares.go @@ -0,0 +1,58 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocmshares + +import ( + "fmt" + "net/http" + + "github.com/cs3org/reva/internal/http/interceptors/auth/credential/registry" + "github.com/cs3org/reva/pkg/auth" +) + +func init() { + registry.Register("ocmshares", New) +} + +const ( + headerShareToken = "ocm-token" +) + +type strategy struct{} + +// New returns a new auth strategy that handles public share verification. +func New(m map[string]interface{}) (auth.CredentialStrategy, error) { + return &strategy{}, nil +} + +func (s *strategy) GetCredentials(w http.ResponseWriter, r *http.Request) (*auth.Credentials, error) { + token := r.Header.Get(headerShareToken) + if token == "" { + token = r.URL.Query().Get(headerShareToken) + } + if token == "" { + return nil, fmt.Errorf("no ocm token provided") + } + + return &auth.Credentials{Type: "ocmshares", ClientID: token}, nil +} + +func (s *strategy) AddWWWAuthenticate(w http.ResponseWriter, r *http.Request, realm string) { + // TODO read realm from forwarded header? +} diff --git a/internal/http/services/appprovider/appprovider.go b/internal/http/services/appprovider/appprovider.go index 075732a330..c4366dc148 100644 --- a/internal/http/services/appprovider/appprovider.go +++ b/internal/http/services/appprovider/appprovider.go @@ -338,22 +338,24 @@ func (s *svc) handleOpen(w http.ResponseWriter, r *http.Request) { fileID := r.Form.Get("file_id") + var fileRef provider.Reference if fileID == "" { - writeError(w, r, appErrorInvalidParameter, "missing file ID", nil) - return - } - - resourceID := resourceid.OwnCloudResourceIDUnwrap(fileID) - if resourceID == nil { - writeError(w, r, appErrorInvalidParameter, "invalid file ID", nil) - return - } - - fileRef := &provider.Reference{ - ResourceId: resourceID, + path := r.Form.Get("path") + if path == "" { + writeError(w, r, appErrorInvalidParameter, "missing file ID or path", nil) + return + } + fileRef.Path = path + } else { + resourceID := resourceid.OwnCloudResourceIDUnwrap(fileID) + if resourceID == nil { + writeError(w, r, appErrorInvalidParameter, "invalid file ID", nil) + return + } + fileRef.ResourceId = resourceID } - statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: fileRef}) + statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &fileRef}) if err != nil { writeError(w, r, appErrorServerError, "Internal error accessing the file, please try again later", err) return @@ -389,7 +391,7 @@ func (s *svc) handleOpen(w http.ResponseWriter, r *http.Request) { } openReq := gateway.OpenInAppRequest{ - Ref: fileRef, + Ref: &fileRef, ViewMode: viewMode, App: r.Form.Get("app_name"), Opaque: &typespb.Opaque{Map: opaqueMap}, diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index f2e412bac0..54589c322c 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -47,6 +47,7 @@ type DavHandler struct { SpacesHandler *SpacesHandler PublicFolderHandler *WebDavHandler PublicFileHandler *PublicFileHandler + OCMSharesHandler *WebDavHandler } func (h *DavHandler) init(c *Config) error { @@ -83,6 +84,11 @@ func (h *DavHandler) init(c *Config) error { return err } + h.OCMSharesHandler = new(WebDavHandler) + if err := h.OCMSharesHandler.init(c.OCMNamespace, false); err != nil { + return err + } + return h.TrashbinHandler.init(c) } @@ -171,6 +177,48 @@ func (h *DavHandler) Handler(s *svc) http.Handler { ctx := context.WithValue(ctx, ctxKeyBaseURI, base) r = r.WithContext(ctx) h.SpacesHandler.Handler(s).ServeHTTP(w, r) + case "ocm": + base := path.Join(ctx.Value(ctxKeyBaseURI).(string), "ocm") + ctx := context.WithValue(ctx, ctxKeyBaseURI, base) + + c, err := pool.GetGatewayServiceClient(pool.Endpoint(s.c.GatewaySvc)) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + token, _ := router.ShiftPath(r.URL.Path) + authRes, err := handleOCMAuth(ctx, c, token) + switch { + case err != nil: + log.Error().Err(err).Msg("error during ocm authentication") + w.WriteHeader(http.StatusInternalServerError) + return + case authRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: + log.Debug().Str("token", token).Msg("permission denied") + fallthrough + case authRes.Status.Code == rpc.Code_CODE_UNAUTHENTICATED: + log.Debug().Str("token", token).Msg("unauthorized") + w.WriteHeader(http.StatusUnauthorized) + return + case authRes.Status.Code == rpc.Code_CODE_NOT_FOUND: + log.Debug().Str("token", token).Msg("not found") + w.WriteHeader(http.StatusNotFound) + return + case authRes.Status.Code != rpc.Code_CODE_OK: + log.Error().Str("token", token).Interface("status", authRes.Status).Msg("grpc auth request failed") + w.WriteHeader(http.StatusInternalServerError) + return + } + + ctx = ctxpkg.ContextSetToken(ctx, authRes.Token) + ctx = ctxpkg.ContextSetUser(ctx, authRes.User) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, authRes.Token) + + log.Debug().Str("token", token).Interface("user", authRes.User).Msg("OCM user authenticated") + + r = r.WithContext(ctx) + h.OCMSharesHandler.Handler(s).ServeHTTP(w, r) case "public-files": base := path.Join(ctx.Value(ctxKeyBaseURI).(string), "public-files") ctx = context.WithValue(ctx, ctxKeyBaseURI, base) @@ -284,3 +332,10 @@ func handleSignatureAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, return c.Authenticate(ctx, &authenticateRequest) } + +func handleOCMAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, token string) (*gatewayv1beta1.AuthenticateResponse, error) { + return c.Authenticate(ctx, &gatewayv1beta1.AuthenticateRequest{ + Type: "ocmshares", + ClientId: token, + }) +} diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index fb1791cca0..ad608ad67b 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -96,6 +96,7 @@ type Config struct { // and received path is /docs the internal path will be: // /users///docs WebdavNamespace string `mapstructure:"webdav_namespace"` + OCMNamespace string `mapstructure:"ocm_namespace"` GatewaySvc string `mapstructure:"gatewaysvc"` Timeout int64 `mapstructure:"timeout"` Insecure bool `mapstructure:"insecure" docs:"false;Whether to skip certificate checks when sending requests."` @@ -118,6 +119,10 @@ func (c *Config) init() { if c.FavoriteStorageDriver == "" { c.FavoriteStorageDriver = "memory" } + + if c.OCMNamespace == "" { + c.OCMNamespace = "/ocm" + } } type svc struct { @@ -178,7 +183,7 @@ func (s *svc) Close() error { } func (s *svc) Unprotected() []string { - return []string{"/status.php", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/"} + return []string{"/status.php", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/", "/remote.php/dav/ocm/"} } func (s *svc) Handler() http.Handler { diff --git a/pkg/auth/manager/loader/loader.go b/pkg/auth/manager/loader/loader.go index 1f3b99df67..06f7287e0d 100644 --- a/pkg/auth/manager/loader/loader.go +++ b/pkg/auth/manager/loader/loader.go @@ -27,6 +27,7 @@ import ( _ "github.com/cs3org/reva/pkg/auth/manager/ldap" _ "github.com/cs3org/reva/pkg/auth/manager/machine" _ "github.com/cs3org/reva/pkg/auth/manager/nextcloud" + _ "github.com/cs3org/reva/pkg/auth/manager/ocmshares" _ "github.com/cs3org/reva/pkg/auth/manager/oidc" _ "github.com/cs3org/reva/pkg/auth/manager/owncloudsql" _ "github.com/cs3org/reva/pkg/auth/manager/publicshares" diff --git a/pkg/auth/manager/ocmshares/ocmshares.go b/pkg/auth/manager/ocmshares/ocmshares.go new file mode 100644 index 0000000000..17f46c592c --- /dev/null +++ b/pkg/auth/manager/ocmshares/ocmshares.go @@ -0,0 +1,190 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocmshares + +import ( + "context" + + provider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + ocminvite "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/auth" + "github.com/cs3org/reva/pkg/auth/manager/registry" + "github.com/cs3org/reva/pkg/auth/scope" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/pkg/sharedconf" + "github.com/cs3org/reva/pkg/utils" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +func init() { + registry.Register("ocmshares", New) +} + +type manager struct { + c *config + gw gateway.GatewayAPIClient +} + +type config struct { + GatewayAddr string `mapstructure:"gateway_addr"` +} + +func (c *config) init() { + c.GatewayAddr = sharedconf.GetGatewaySVC(c.GatewayAddr) +} + +// New creates a new ocmshares authentication manager. +func New(m map[string]interface{}) (auth.Manager, error) { + var mgr manager + if err := mgr.Configure(m); err != nil { + return nil, err + } + + return &mgr, nil +} + +func (m *manager) Configure(ml map[string]interface{}) error { + var c config + if err := mapstructure.Decode(ml, &c); err != nil { + return errors.Wrap(err, "error decoding config") + } + c.init() + m.c = &c + + gw, err := pool.GetGatewayServiceClient(pool.Endpoint(c.GatewayAddr)) + if err != nil { + return err + } + m.gw = gw + + return nil +} + +func (m *manager) Authenticate(ctx context.Context, token, _ string) (*userpb.User, map[string]*authpb.Scope, error) { + log := appctx.GetLogger(ctx).With().Str("token", token).Logger() + shareRes, err := m.gw.GetOCMShareByToken(ctx, &ocm.GetOCMShareByTokenRequest{ + Token: token, + }) + + switch { + case err != nil: + log.Error().Err(err).Msg("error getting ocm share by token") + return nil, nil, err + case shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND: + log.Debug().Msg("ocm share not found") + return nil, nil, errtypes.NotFound(shareRes.Status.Message) + case shareRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: + log.Debug().Msg("permission denied") + return nil, nil, errtypes.InvalidCredentials(shareRes.Status.Message) + case shareRes.Status.Code != rpc.Code_CODE_OK: + log.Error().Interface("status", shareRes.Status).Msg("got unexpected error in the grpc call to GetOCMShare") + return nil, nil, errtypes.InternalError(shareRes.Status.Message) + } + + // the user authenticated using the ocmshares authentication method + // is the recipient of the share + u := shareRes.Share.Grantee.GetUserId() + + d, err := utils.MarshalProtoV1ToJSON(shareRes.GetShare().Creator) + if err != nil { + return nil, nil, err + } + + o := &types.Opaque{ + Map: map[string]*types.OpaqueEntry{ + "user-filter": { + Decoder: "json", + Value: d, + }, + }, + } + + userRes, err := m.gw.GetAcceptedUser(ctx, &ocminvite.GetAcceptedUserRequest{ + RemoteUserId: u, + Opaque: o, + }) + + switch { + case err != nil: + return nil, nil, err + case userRes.Status.Code == rpc.Code_CODE_NOT_FOUND: + return nil, nil, errtypes.NotFound(shareRes.Status.Message) + case userRes.Status.Code != rpc.Code_CODE_OK: + return nil, nil, errtypes.InternalError(userRes.Status.Message) + } + + role, roleStr := getRole(shareRes.Share) + + scope, err := scope.AddOCMShareScope(shareRes.Share, role, nil) + if err != nil { + return nil, nil, err + } + + user := userRes.RemoteUser + user.Opaque = &types.Opaque{ + Map: map[string]*types.OpaqueEntry{ + "ocm-share-role": { + Decoder: "plain", + Value: []byte(roleStr), + }, + }, + } + + return user, scope, nil +} + +func getRole(s *ocm.Share) (authpb.Role, string) { + // TODO: consider to somehow merge the permissions from all the access methods? + // it's not clear infact which should be the role when webdav is editor role while + // webapp is only view mode for example + // this implementation considers only the simple case in which when a client creates + // a share with multiple access methods, the permissions are matching in all of them. + for _, m := range s.AccessMethods { + switch v := m.Term.(type) { + case *ocm.AccessMethod_WebdavOptions: + p := v.WebdavOptions.Permissions + if p.InitiateFileUpload { + return authpb.Role_ROLE_EDITOR, "editor" + } + if p.InitiateFileDownload { + return authpb.Role_ROLE_VIEWER, "viewer" + } + case *ocm.AccessMethod_WebappOptions: + viewMode := v.WebappOptions.ViewMode + if viewMode == provider.ViewMode_VIEW_MODE_VIEW_ONLY || + viewMode == provider.ViewMode_VIEW_MODE_READ_ONLY || + viewMode == provider.ViewMode_VIEW_MODE_PREVIEW { + return authpb.Role_ROLE_VIEWER, "viewer" + } + if viewMode == provider.ViewMode_VIEW_MODE_READ_WRITE { + return authpb.Role_ROLE_EDITOR, "editor" + } + } + } + return authpb.Role_ROLE_INVALID, "invalid" +} diff --git a/pkg/auth/scope/ocmshare.go b/pkg/auth/scope/ocmshare.go new file mode 100644 index 0000000000..19f7793b76 --- /dev/null +++ b/pkg/auth/scope/ocmshare.go @@ -0,0 +1,136 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package scope + +import ( + "context" + "path/filepath" + "strings" + + appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" + appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/pkg/utils" + "github.com/rs/zerolog" +) + +// FIXME: the namespace here is hardcoded +// find a way to pass it from the config. +const ocmNamespace = "/ocm" + +func ocmShareScope(_ context.Context, scope *authpb.Scope, resource interface{}, _ *zerolog.Logger) (bool, error) { + var share ocmv1beta1.Share + if err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share); err != nil { + return false, err + } + + switch v := resource.(type) { + // viewer role + case *registry.GetStorageProvidersRequest: + return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.StatRequest: + return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.ListContainerRequest: + return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.InitiateFileDownloadRequest: + return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *appprovider.OpenInAppRequest: + return checkStorageRefForOCMShare(&share, &provider.Reference{ResourceId: v.ResourceInfo.Id}, ocmNamespace), nil + case *gateway.OpenInAppRequest: + return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.GetLockRequest: + return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + + // editor role + case *provider.CreateContainerRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.TouchFileRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.DeleteRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.MoveRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetSource(), ocmNamespace) && checkStorageRefForOCMShare(&share, v.GetDestination(), ocmNamespace), nil + case *provider.InitiateFileUploadRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.SetArbitraryMetadataRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.UnsetArbitraryMetadataRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.SetLockRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.RefreshLockRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.UnlockRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + + // App provider requests + case *appregistry.GetDefaultAppProviderForMimeTypeRequest: + return true, nil + + case *userv1beta1.GetUserByClaimRequest: + return true, nil + + case *ocmv1beta1.GetOCMShareRequest: + return checkOCMShareRef(&share, v.GetRef()), nil + case string: + return checkResourcePath(v), nil + } + return false, nil +} + +func checkStorageRefForOCMShare(s *ocmv1beta1.Share, r *provider.Reference, ns string) bool { + if r.ResourceId != nil { + return utils.ResourceIDEqual(s.ResourceId, r.GetResourceId()) || strings.HasPrefix(r.ResourceId.OpaqueId, s.Token) + } + + // FIXME: the path here is hardcoded + return strings.HasPrefix(r.GetPath(), filepath.Join(ns, s.Token)) +} + +func checkOCMShareRef(s *ocmv1beta1.Share, ref *ocmv1beta1.ShareReference) bool { + return ref.GetToken() == s.Token +} + +// AddOCMShareScope adds the scope to allow access to an OCM share and the share resource. +func AddOCMShareScope(share *ocmv1beta1.Share, role authpb.Role, scopes map[string]*authpb.Scope) (map[string]*authpb.Scope, error) { + // Create a new "scope share" to only expose the required fields `ResourceId` and `Token` to the scope. + scopeShare := ocmv1beta1.Share{ResourceId: share.ResourceId, Token: share.Token} + val, err := utils.MarshalProtoV1ToJSON(&scopeShare) + if err != nil { + return nil, err + } + if scopes == nil { + scopes = make(map[string]*authpb.Scope) + } + + scopes["ocmshare:"+share.Id.OpaqueId] = &authpb.Scope{ + Resource: &types.OpaqueEntry{ + Decoder: "json", + Value: val, + }, + Role: role, + } + return scopes, nil +} diff --git a/pkg/auth/scope/resourceinfo.go b/pkg/auth/scope/resourceinfo.go index 446560d50b..8e908f9b4e 100644 --- a/pkg/auth/scope/resourceinfo.go +++ b/pkg/auth/scope/resourceinfo.go @@ -111,6 +111,7 @@ func checkResourcePath(path string) bool { "/archiver", "/ocs/v2.php/cloud/capabilities", "/ocs/v1.php/cloud/capabilities", + "/ocs/v1.php/cloud/user", } for _, p := range paths { if strings.HasPrefix(path, p) { diff --git a/pkg/auth/scope/scope.go b/pkg/auth/scope/scope.go index 7e5131811b..7579c9d657 100644 --- a/pkg/auth/scope/scope.go +++ b/pkg/auth/scope/scope.go @@ -37,6 +37,7 @@ var supportedScopes = map[string]Verifier{ "share": shareScope, "receivedshare": receivedShareScope, "lightweight": lightweightAccountScope, + "ocmshare": ocmShareScope, } // VerifyScope is the function to be called when dismantling tokens to check if diff --git a/pkg/ocm/share/repository/json/json.go b/pkg/ocm/share/repository/json/json.go index cd99968200..defedb3440 100644 --- a/pkg/ocm/share/repository/json/json.go +++ b/pkg/ocm/share/repository/json/json.go @@ -297,6 +297,8 @@ func (m *mgr) GetShare(ctx context.Context, user *userpb.User, ref *ocm.ShareRef s, err = m.getByID(ctx, ref.GetId()) case ref.GetKey() != nil: s, err = m.getByKey(ctx, ref.GetKey()) + case ref.GetToken() != "": + return m.getByToken(ctx, ref.GetToken()) default: err = errtypes.NotFound(ref.String()) } @@ -313,6 +315,15 @@ func (m *mgr) GetShare(ctx context.Context, user *userpb.User, ref *ocm.ShareRef return nil, share.ErrShareNotFound } +func (m *mgr) getByToken(ctx context.Context, token string) (*ocm.Share, error) { + for _, share := range m.model.Shares { + if share.Token == token { + return share, nil + } + } + return nil, errtypes.NotFound(token) +} + func (m *mgr) getByID(ctx context.Context, id *ocm.ShareId) (*ocm.Share, error) { if share, ok := m.model.Shares[id.OpaqueId]; ok { return share, nil diff --git a/pkg/ocm/share/repository/sql/sql.go b/pkg/ocm/share/repository/sql/sql.go index 957159a7d4..e27bd366c2 100644 --- a/pkg/ocm/share/repository/sql/sql.go +++ b/pkg/ocm/share/repository/sql/sql.go @@ -214,6 +214,8 @@ func (m *mgr) GetShare(ctx context.Context, user *userpb.User, ref *ocm.ShareRef s, err = m.getByID(ctx, user, ref.GetId()) case ref.GetKey() != nil: s, err = m.getByKey(ctx, user, ref.GetKey()) + case ref.GetToken() != "": + s, err = m.getByToken(ctx, ref.GetToken()) default: err = errtypes.NotFound(ref.String()) } @@ -258,6 +260,24 @@ func (m *mgr) getByKey(ctx context.Context, user *userpb.User, key *ocm.ShareKey return convertToCS3OCMShare(&s, am), nil } +func (m *mgr) getByToken(ctx context.Context, token string) (*ocm.Share, error) { + query := "SELECT id, token, fileid_prefix, item_source, name, share_with, owner, initiator, ctime, mtime, expiration, type FROM ocm_shares WHERE token=?" + + var s dbShare + if err := m.db.QueryRowContext(ctx, query, token).Scan(&s.ID, &s.Token, &s.Prefix, &s.ItemSource, &s.Name, &s.ShareWith, &s.Owner, &s.Initiator, &s.Ctime, &s.Mtime, &s.Expiration, &s.ShareType); err != nil { + if err == sql.ErrNoRows { + return nil, share.ErrShareNotFound + } + } + + am, err := m.getAccessMethods(ctx, s.ID) + if err != nil { + return nil, err + } + + return convertToCS3OCMShare(&s, am), nil +} + func (m *mgr) getAccessMethods(ctx context.Context, id int) ([]*ocm.AccessMethod, error) { query := "SELECT m.type, dav.permissions, app.view_mode FROM ocm_shares_access_methods as m LEFT JOIN ocm_access_method_webdav as dav ON m.id=dav.ocm_access_method_id LEFT JOIN ocm_access_method_webapp as app ON m.id=app.ocm_access_method_id WHERE m.ocm_share_id=?" diff --git a/pkg/ocm/share/repository/sql/sql_test.go b/pkg/ocm/share/repository/sql/sql_test.go index 6a81b2e140..66b9032431 100644 --- a/pkg/ocm/share/repository/sql/sql_test.go +++ b/pkg/ocm/share/repository/sql/sql_test.go @@ -353,6 +353,67 @@ func TestGetShare(t *testing.T) { AccessMethods: []*ocm.AccessMethod{share.NewWebDavAccessMethod(conversions.NewEditorRole().CS3ResourcePermissions())}, }, }, + { + description: "query by token", + shares: []*ocm.Share{ + { + Id: &ocm.ShareId{OpaqueId: "1"}, + ResourceId: &providerv1beta1.ResourceId{StorageId: "storage", OpaqueId: "resource-id"}, + Name: "file-name", + Token: "qwerty", + Grantee: &providerv1beta1.Grantee{Type: providerv1beta1.GranteeType_GRANTEE_TYPE_USER, Id: &providerv1beta1.Grantee_UserId{UserId: &userpb.UserId{Idp: "cesnet", OpaqueId: "marie"}}}, + Owner: &userpb.UserId{Idp: "cernbox", OpaqueId: "einstein"}, + Creator: &userpb.UserId{Idp: "cernbox", OpaqueId: "einstein"}, + Ctime: &typesv1beta1.Timestamp{Seconds: 1670859468}, + Mtime: &typesv1beta1.Timestamp{Seconds: 1670859468}, + ShareType: ocm.ShareType_SHARE_TYPE_USER, + AccessMethods: []*ocm.AccessMethod{share.NewWebDavAccessMethod(conversions.NewEditorRole().CS3ResourcePermissions())}, + }, + }, + query: &ocm.ShareReference{ + Spec: &ocm.ShareReference_Token{ + Token: "qwerty", + }, + }, + expected: &ocm.Share{ + Id: &ocm.ShareId{OpaqueId: "1"}, + ResourceId: &providerv1beta1.ResourceId{StorageId: "storage", OpaqueId: "resource-id"}, + Name: "file-name", + Token: "qwerty", + Grantee: &providerv1beta1.Grantee{Type: providerv1beta1.GranteeType_GRANTEE_TYPE_USER, Id: &providerv1beta1.Grantee_UserId{UserId: &userpb.UserId{Idp: "cesnet", OpaqueId: "marie", Type: userpb.UserType_USER_TYPE_FEDERATED}}}, + Owner: &userpb.UserId{OpaqueId: "einstein"}, + Creator: &userpb.UserId{OpaqueId: "einstein"}, + Ctime: &typesv1beta1.Timestamp{Seconds: 1670859468}, + Mtime: &typesv1beta1.Timestamp{Seconds: 1670859468}, + ShareType: ocm.ShareType_SHARE_TYPE_USER, + Expiration: &typesv1beta1.Timestamp{}, + AccessMethods: []*ocm.AccessMethod{share.NewWebDavAccessMethod(conversions.NewEditorRole().CS3ResourcePermissions())}, + }, + }, + { + description: "query by token - not found", + shares: []*ocm.Share{ + { + Id: &ocm.ShareId{OpaqueId: "1"}, + ResourceId: &providerv1beta1.ResourceId{StorageId: "storage", OpaqueId: "resource-id"}, + Name: "file-name", + Token: "qwerty", + Grantee: &providerv1beta1.Grantee{Type: providerv1beta1.GranteeType_GRANTEE_TYPE_USER, Id: &providerv1beta1.Grantee_UserId{UserId: &userpb.UserId{Idp: "cesnet", OpaqueId: "marie"}}}, + Owner: &userpb.UserId{Idp: "cernbox", OpaqueId: "einstein"}, + Creator: &userpb.UserId{Idp: "cernbox", OpaqueId: "einstein"}, + Ctime: &typesv1beta1.Timestamp{Seconds: 1670859468}, + Mtime: &typesv1beta1.Timestamp{Seconds: 1670859468}, + ShareType: ocm.ShareType_SHARE_TYPE_USER, + AccessMethods: []*ocm.AccessMethod{share.NewWebDavAccessMethod(conversions.NewEditorRole().CS3ResourcePermissions())}, + }, + }, + query: &ocm.ShareReference{ + Spec: &ocm.ShareReference_Token{ + Token: "not-existing-token", + }, + }, + err: share.ErrShareNotFound, + }, { description: "query by key", shares: []*ocm.Share{ diff --git a/pkg/ocm/storage/outcoming/ocm.go b/pkg/ocm/storage/outcoming/ocm.go new file mode 100644 index 0000000000..672dc89ec1 --- /dev/null +++ b/pkg/ocm/storage/outcoming/ocm.go @@ -0,0 +1,731 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package outcoming + +import ( + "context" + "io" + "net/http" + "net/url" + "path/filepath" + "strings" + + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/internal/http/services/datagateway" + "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/pkg/rhttp" + "github.com/cs3org/reva/pkg/rhttp/router" + "github.com/cs3org/reva/pkg/sharedconf" + "github.com/cs3org/reva/pkg/storage" + "github.com/cs3org/reva/pkg/storage/fs/registry" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + "google.golang.org/grpc/metadata" +) + +func init() { + registry.Register("ocmoutcoming", New) +} + +type driver struct { + c *config + gateway gateway.GatewayAPIClient +} + +type config struct { + GatewaySVC string `mapstructure:"gatewaysvc"` + MachineSecret string `mapstructure:"machine_secret"` +} + +func parseConfig(c map[string]interface{}) (*config, error) { + var conf config + err := mapstructure.Decode(c, &conf) + return &conf, err +} + +func (c *config) init() { + c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC) +} + +// New creates an OCM storage driver. +func New(c map[string]interface{}) (storage.FS, error) { + conf, err := parseConfig(c) + if err != nil { + return nil, errors.Wrapf(err, "error decoding config") + } + conf.init() + + gateway, err := pool.GetGatewayServiceClient(pool.Endpoint(conf.GatewaySVC)) + if err != nil { + return nil, err + } + + d := &driver{ + c: conf, + gateway: gateway, + } + + return d, nil +} + +func (d *driver) resolveToken(ctx context.Context, token string) (*ocmv1beta1.Share, error) { + shareRes, err := d.gateway.GetOCMShare(ctx, &ocmv1beta1.GetOCMShareRequest{ + Ref: &ocmv1beta1.ShareReference{ + Spec: &ocmv1beta1.ShareReference_Token{ + Token: token, + }, + }, + }) + + switch { + case err != nil: + return nil, err + case shareRes.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return nil, errtypes.NotFound(token) + case shareRes.Status.Code != rpcv1beta1.Code_CODE_OK: + return nil, errtypes.InternalError(shareRes.Status.Message) + } + + return shareRes.Share, nil +} + +func (d *driver) stat(ctx context.Context, ref *provider.Reference) (*provider.ResourceInfo, error) { + statRes, err := d.gateway.Stat(ctx, &provider.StatRequest{Ref: ref}) + switch { + case err != nil: + return nil, err + case statRes.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return nil, errtypes.NotFound(ref.String()) + case statRes.Status.Code != rpcv1beta1.Code_CODE_OK: + return nil, errtypes.InternalError(statRes.Status.Message) + } + + return statRes.Info, nil +} + +func makeRelative(path string) string { + if strings.HasPrefix(path, "/") { + return "." + path + } + return path +} + +func (d *driver) shareAndRelativePathFromRef(ctx context.Context, ref *provider.Reference) (*ocmv1beta1.Share, string, error) { + var ( + token string + path string + ) + if ref.ResourceId == nil { + // path is of type /token/ + token, path = router.ShiftPath(ref.Path) + } else { + // opaque id is of type token:rel.path + s := strings.SplitN(ref.ResourceId.OpaqueId, ":", 2) + token = s[0] + if len(s) == 2 { + path = s[1] + } + path = filepath.Join(path, ref.Path) + } + path = makeRelative(path) + + share, err := d.resolveToken(ctx, token) + if err != nil { + return nil, "", err + } + return share, path, nil +} + +func (d *driver) translateOCMShareResourceToCS3Ref(ctx context.Context, resID *provider.ResourceId, rel string) (*provider.Reference, error) { + info, err := d.stat(ctx, &provider.Reference{ResourceId: resID}) + if err != nil { + return nil, err + } + return &provider.Reference{ + Path: filepath.Join(info.Path, rel), + }, nil +} + +func (d *driver) CreateDir(ctx context.Context, ref *provider.Reference) error { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return err + } + + return d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, ref *provider.Reference) error { + res, err := d.gateway.CreateContainer(userCtx, &provider.CreateContainerRequest{Ref: ref}) + switch { + case err != nil: + return err + case res.Status.Code != rpcv1beta1.Code_CODE_OK: + // TODO: better error handling + return errtypes.InternalError(res.Status.Message) + } + return nil + }) +} + +func (d *driver) TouchFile(ctx context.Context, ref *provider.Reference) error { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return err + } + + return d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, ref *provider.Reference) error { + res, err := d.gateway.TouchFile(userCtx, &provider.TouchFileRequest{Ref: ref}) + switch { + case err != nil: + return err + case res.Status.Code != rpcv1beta1.Code_CODE_OK: + // TODO: better error handling + return errtypes.InternalError(res.Status.Message) + } + return nil + }) +} + +func (d *driver) Delete(ctx context.Context, ref *provider.Reference) error { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return err + } + + return d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, ref *provider.Reference) error { + res, err := d.gateway.Delete(userCtx, &provider.DeleteRequest{Ref: ref}) + switch { + case err != nil: + return err + case res.Status.Code != rpcv1beta1.Code_CODE_OK: + // TODO: better error handling + return errtypes.InternalError(res.Status.Message) + } + return nil + }) +} + +func (d *driver) Move(ctx context.Context, from, to *provider.Reference) error { + return errtypes.NotSupported("not yet implemented") +} + +func (d *driver) opFromUser(ctx context.Context, userID *userv1beta1.UserId, f func(ctx context.Context) error) error { + userRes, err := d.gateway.GetUser(ctx, &userv1beta1.GetUserRequest{ + UserId: userID, + }) + if err != nil { + return err + } + if userRes.Status.Code != rpcv1beta1.Code_CODE_OK { + return errors.New(userRes.Status.Message) + } + + authRes, err := d.gateway.Authenticate(context.TODO(), &gateway.AuthenticateRequest{ + Type: "machine", + ClientId: userRes.User.Username, + ClientSecret: d.c.MachineSecret, + }) + if err != nil { + return err + } + if authRes.Status.Code != rpcv1beta1.Code_CODE_OK { + return errors.New(authRes.Status.Message) + } + + ownerCtx := context.TODO() + ownerCtx = ctxpkg.ContextSetToken(ownerCtx, authRes.Token) + ownerCtx = ctxpkg.ContextSetUser(ownerCtx, authRes.User) + ownerCtx = metadata.AppendToOutgoingContext(ownerCtx, ctxpkg.TokenHeader, authRes.Token) + + return f(ownerCtx) +} + +func (d *driver) unwrappedOpFromShareCreator(ctx context.Context, share *ocmv1beta1.Share, rel string, f func(ctx context.Context, ref *provider.Reference) error) error { + return d.opFromUser(ctx, share.Creator, func(userCtx context.Context) error { + newRef, err := d.translateOCMShareResourceToCS3Ref(userCtx, share.ResourceId, rel) + if err != nil { + return err + } + return f(userCtx, newRef) + }) +} + +func (d *driver) GetMD(ctx context.Context, ref *provider.Reference, _ []string) (*provider.ResourceInfo, error) { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return nil, err + } + + var info *provider.ResourceInfo + if err := d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, newRef *provider.Reference) error { + info, err = d.stat(userCtx, newRef) + return err + }); err != nil { + return nil, err + } + + if err := d.augmentResourceInfo(ctx, info, share); err != nil { + return nil, err + } + + return info, nil +} + +func (d *driver) augmentResourceInfo(ctx context.Context, info *provider.ResourceInfo, share *ocmv1beta1.Share) error { + // prevent leaking internal paths + shareInfo, err := d.stat(ctx, &provider.Reference{ResourceId: share.ResourceId}) + if err != nil { + return err + } + fixResourceInfo(info, shareInfo, share, getPermissionsFromShare(share)) + return nil +} + +func getPermissionsFromShare(share *ocmv1beta1.Share) *provider.ResourcePermissions { + for _, m := range share.AccessMethods { + switch v := m.Term.(type) { + case *ocmv1beta1.AccessMethod_WebdavOptions: + return v.WebdavOptions.Permissions + case *ocmv1beta1.AccessMethod_WebappOptions: + mode := v.WebappOptions.ViewMode + if mode == providerv1beta1.ViewMode_VIEW_MODE_READ_WRITE { + return conversions.NewEditorRole().CS3ResourcePermissions() + } + return conversions.NewViewerRole().CS3ResourcePermissions() + } + } + return nil +} + +func fixResourceInfo(info, shareInfo *provider.ResourceInfo, share *ocmv1beta1.Share, perms *provider.ResourcePermissions) { + // fix path + relPath := makeRelative(strings.TrimPrefix(info.Path, shareInfo.Path)) + info.Path = filepath.Join("/", share.Token, relPath) + + // to enable collaborative apps, the fileid must be the same + // of the proxied storage + + // fix permissions + info.PermissionSet = perms +} + +func (d *driver) ListFolder(ctx context.Context, ref *provider.Reference, _ []string) ([]*provider.ResourceInfo, error) { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return nil, err + } + + var infos []*provider.ResourceInfo + if err := d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, newRef *provider.Reference) error { + lstRes, err := d.gateway.ListContainer(userCtx, &provider.ListContainerRequest{Ref: newRef}) + switch { + case err != nil: + return err + case lstRes.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return errtypes.NotFound(ref.String()) + case lstRes.Status.Code != rpcv1beta1.Code_CODE_OK: + return errtypes.InternalError(lstRes.Status.Message) + } + infos = lstRes.Infos + return nil + }); err != nil { + return nil, err + } + + shareInfo, err := d.stat(ctx, &provider.Reference{ResourceId: share.ResourceId}) + if err != nil { + return nil, err + } + + perms := getPermissionsFromShare(share) + for _, info := range infos { + fixResourceInfo(info, shareInfo, share, perms) + } + + return infos, nil +} + +func exposedPathFromReference(ref *provider.Reference) string { + if ref.ResourceId == nil { + return ref.Path + } + + s := strings.SplitN(ref.ResourceId.StorageId, ":", 2) + tkn := s[0] + var rel string + if len(s) == 2 { + rel = s[1] + } + return filepath.Join("/", tkn, rel, ref.Path) +} + +func (d *driver) InitiateUpload(ctx context.Context, ref *provider.Reference, _ int64, _ map[string]string) (map[string]string, error) { + p := exposedPathFromReference(ref) + return map[string]string{ + "simple": p, + }, nil +} + +func getUploadProtocol(protocols []*gateway.FileUploadProtocol, protocol string) (string, string, bool) { + for _, p := range protocols { + if p.Protocol == protocol { + return p.UploadEndpoint, p.Token, true + } + } + return "", "", false +} + +func (d *driver) Upload(ctx context.Context, ref *provider.Reference, content io.ReadCloser) error { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return err + } + + return d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, newRef *provider.Reference) error { + initRes, err := d.gateway.InitiateFileUpload(userCtx, &provider.InitiateFileUploadRequest{Ref: newRef}) + switch { + case err != nil: + return err + case initRes.Status.Code != rpcv1beta1.Code_CODE_OK: + return errtypes.InternalError(initRes.Status.Message) + } + + endpoint, token, ok := getUploadProtocol(initRes.Protocols, "simple") + if !ok { + return errtypes.InternalError("simple upload not supported") + } + + httpReq, err := rhttp.NewRequest(userCtx, http.MethodPut, endpoint, content) + if err != nil { + return errors.Wrap(err, "error creating new request") + } + + httpReq.Header.Set(datagateway.TokenTransportHeader, token) + + httpRes, err := http.DefaultClient.Do(httpReq) + if err != nil { + return errors.Wrap(err, "error doing put request") + } + defer httpRes.Body.Close() + + if httpRes.StatusCode != http.StatusOK { + return errors.Errorf("error doing put request: %s", httpRes.Status) + } + + return nil + }) +} + +func getDownloadProtocol(protocols []*gateway.FileDownloadProtocol, lst []string) (string, string, bool) { + for _, p := range protocols { + for _, prot := range lst { + if p.Protocol == prot { + return p.DownloadEndpoint, p.Token, true + } + } + } + return "", "", false +} + +func (d *driver) Download(ctx context.Context, ref *provider.Reference) (io.ReadCloser, error) { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return nil, err + } + + var r io.ReadCloser + if err := d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, newRef *provider.Reference) error { + initRes, err := d.gateway.InitiateFileDownload(userCtx, &provider.InitiateFileDownloadRequest{Ref: newRef}) + switch { + case err != nil: + return err + case initRes.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return errtypes.NotFound(ref.String()) + case initRes.Status.Code != rpcv1beta1.Code_CODE_OK: + return errtypes.InternalError(initRes.Status.Message) + } + + endpoint, token, ok := getDownloadProtocol(initRes.Protocols, []string{"simple", "spaces"}) + if !ok { + return errtypes.InternalError("simple download not supported") + } + + httpReq, err := rhttp.NewRequest(userCtx, http.MethodGet, endpoint, nil) + if err != nil { + return err + } + httpReq.Header.Set(datagateway.TokenTransportHeader, token) + + httpRes, err := http.DefaultClient.Do(httpReq) //nolint:golint,bodyclose + if err != nil { + return err + } + + if httpRes.StatusCode != http.StatusOK { + return errors.New(httpRes.Status) + } + r = httpRes.Body + return nil + }); err != nil { + return nil, err + } + + return r, nil +} + +func (d *driver) GetPathByID(ctx context.Context, id *provider.ResourceId) (string, error) { + info, err := d.GetMD(ctx, &provider.Reference{ResourceId: id}, nil) + if err != nil { + return "", err + } + return info.Path, nil +} + +func (d *driver) SetLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return err + } + + return d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, newRef *provider.Reference) error { + lockRes, err := d.gateway.SetLock(ctx, &provider.SetLockRequest{ + Ref: newRef, + Lock: lock, + }) + switch { + case err != nil: + return err + case lockRes.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return errtypes.NotFound(ref.String()) + case lockRes.Status.Code == rpcv1beta1.Code_CODE_FAILED_PRECONDITION: + return errtypes.BadRequest(lockRes.Status.Message) + case lockRes.Status.Code != rpcv1beta1.Code_CODE_OK: + return errtypes.InternalError(lockRes.Status.Message) + } + return nil + }) +} + +func (d *driver) GetLock(ctx context.Context, ref *provider.Reference) (*provider.Lock, error) { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return nil, err + } + + var lock *provider.Lock + if err := d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, newRef *provider.Reference) error { + lockRes, err := d.gateway.GetLock(userCtx, &provider.GetLockRequest{Ref: newRef}) + switch { + case err != nil: + return err + case lockRes.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return errtypes.NotFound(ref.String()) + case lockRes.Status.Code != rpcv1beta1.Code_CODE_OK: + return errtypes.InternalError(lockRes.Status.Message) + } + + lock = lockRes.Lock + return nil + }); err != nil { + return nil, err + } + return lock, nil +} + +func (d *driver) RefreshLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock, existingLockID string) error { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return err + } + + return d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, newRef *provider.Reference) error { + lockRes, err := d.gateway.RefreshLock(userCtx, &provider.RefreshLockRequest{ + Ref: newRef, + ExistingLockId: existingLockID, + Lock: lock, + }) + switch { + case err != nil: + return err + case lockRes.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return errtypes.NotFound(ref.String()) + case lockRes.Status.Code == rpcv1beta1.Code_CODE_FAILED_PRECONDITION: + return errtypes.BadRequest(lockRes.Status.Message) + case lockRes.Status.Code != rpcv1beta1.Code_CODE_OK: + return errtypes.InternalError(lockRes.Status.Message) + } + return nil + }) +} + +func (d *driver) Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return err + } + + return d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, newRef *provider.Reference) error { + lockRes, err := d.gateway.Unlock(userCtx, &provider.UnlockRequest{ + Ref: newRef, + Lock: lock, + }) + switch { + case err != nil: + return err + case lockRes.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return errtypes.NotFound(ref.String()) + case lockRes.Status.Code == rpcv1beta1.Code_CODE_FAILED_PRECONDITION: + return errtypes.BadRequest(lockRes.Status.Message) + case lockRes.Status.Code != rpcv1beta1.Code_CODE_OK: + return errtypes.InternalError(lockRes.Status.Message) + } + return nil + }) +} + +func (d *driver) SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return err + } + + return d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, newRef *provider.Reference) error { + res, err := d.gateway.SetArbitraryMetadata(userCtx, &provider.SetArbitraryMetadataRequest{ + Ref: newRef, + ArbitraryMetadata: md, + }) + switch { + case err != nil: + return err + case res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return errtypes.NotFound(ref.String()) + case res.Status.Code != rpcv1beta1.Code_CODE_OK: + return errtypes.InternalError(res.Status.Message) + } + return nil + }) +} + +func (d *driver) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error { + share, rel, err := d.shareAndRelativePathFromRef(ctx, ref) + if err != nil { + return err + } + + return d.unwrappedOpFromShareCreator(ctx, share, rel, func(userCtx context.Context, newRef *provider.Reference) error { + res, err := d.gateway.UnsetArbitraryMetadata(userCtx, &provider.UnsetArbitraryMetadataRequest{ + Ref: newRef, + ArbitraryMetadataKeys: keys, + }) + switch { + case err != nil: + return err + case res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return errtypes.NotFound(ref.String()) + case res.Status.Code != rpcv1beta1.Code_CODE_OK: + return errtypes.InternalError(res.Status.Message) + } + return nil + }) +} + +func (d *driver) Shutdown(ctx context.Context) error { + return nil +} + +func (d *driver) GetHome(ctx context.Context) (string, error) { + return "", errtypes.NotSupported("operation not supported") +} + +func (d *driver) CreateHome(ctx context.Context) error { + return errtypes.NotSupported("operation not supported") +} + +func (d *driver) ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) { + return nil, errtypes.NotSupported("operation not supported") +} + +func (d *driver) DownloadRevision(ctx context.Context, ref *provider.Reference, key string) (io.ReadCloser, error) { + return nil, errtypes.NotSupported("operation not supported") +} +func (d *driver) RestoreRevision(ctx context.Context, ref *provider.Reference, key string) error { + return errtypes.NotSupported("operation not supported") +} + +func (d *driver) ListRecycle(ctx context.Context, basePath, key, relativePath string) ([]*provider.RecycleItem, error) { + return nil, errtypes.NotSupported("operation not supported") +} + +func (d *driver) RestoreRecycleItem(ctx context.Context, basePath, key, relativePath string, restoreRef *provider.Reference) error { + return errtypes.NotSupported("operation not supported") +} + +func (d *driver) PurgeRecycleItem(ctx context.Context, basePath, key, relativePath string) error { + return errtypes.NotSupported("operation not supported") +} + +func (d *driver) EmptyRecycle(ctx context.Context) error { + return errtypes.NotSupported("operation not supported") +} + +func (d *driver) AddGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error { + return errtypes.NotSupported("operation not supported") +} + +func (d *driver) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error { + return errtypes.NotSupported("operation not supported") +} + +func (d *driver) RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error { + return errtypes.NotSupported("operation not supported") +} + +func (d *driver) UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error { + return errtypes.NotSupported("operation not supported") +} + +func (d *driver) ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error) { + return nil, errtypes.NotSupported("operation not supported") +} + +func (d *driver) GetQuota(ctx context.Context, ref *provider.Reference) ( /*TotalBytes*/ uint64 /*UsedBytes*/, uint64, error) { + return 0, 0, errtypes.NotSupported("operation not supported") +} + +func (d *driver) CreateReference(ctx context.Context, path string, targetURI *url.URL) error { + return errtypes.NotSupported("operation not supported") +} + +func (d *driver) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) { + return nil, errtypes.NotSupported("operation not supported") +} + +func (d *driver) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { + return nil, errtypes.NotSupported("operation not supported") +} + +func (d *driver) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) { + return nil, errtypes.NotSupported("operation not supported") +} diff --git a/pkg/ocm/storage/ocm.go b/pkg/ocm/storage/received/ocm.go similarity index 99% rename from pkg/ocm/storage/ocm.go rename to pkg/ocm/storage/received/ocm.go index 2db3eae14c..2a865e1508 100644 --- a/pkg/ocm/storage/ocm.go +++ b/pkg/ocm/storage/received/ocm.go @@ -46,7 +46,7 @@ import ( ) func init() { - registry.Register("ocm", New) + registry.Register("ocmreceived", New) } type driver struct { diff --git a/pkg/storage/fs/loader/loader.go b/pkg/storage/fs/loader/loader.go index 54ae50cfd0..0f4752ac85 100644 --- a/pkg/storage/fs/loader/loader.go +++ b/pkg/storage/fs/loader/loader.go @@ -20,7 +20,8 @@ package loader import ( // Load core storage filesystem backends. - _ "github.com/cs3org/reva/pkg/ocm/storage" + _ "github.com/cs3org/reva/pkg/ocm/storage/outcoming" + _ "github.com/cs3org/reva/pkg/ocm/storage/received" _ "github.com/cs3org/reva/pkg/storage/fs/cback" _ "github.com/cs3org/reva/pkg/storage/fs/cephfs" _ "github.com/cs3org/reva/pkg/storage/fs/eos" diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index 7e7237f24f..f390174b3f 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -2095,6 +2095,13 @@ func (fs *eosfs) permissionSet(ctx context.Context, eosFileInfo *eosclient.FileI return conversions.NewViewerRole().CS3ResourcePermissions() } + if role, ok := utils.HasOCMShareRole(u); ok { + if role == "editor" { + return conversions.NewEditorRole().CS3ResourcePermissions() + } + return conversions.NewViewerRole().CS3ResourcePermissions() + } + if utils.UserEqual(u.Id, owner) { return conversions.NewManagerRole().CS3ResourcePermissions() } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 008f4c2584..f2be17c4f5 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -368,6 +368,18 @@ func HasPublicShareRole(u *userpb.User) (string, bool) { return "", false } +// HasOCMShareRole return true if the user has a ocm share role. +// If yes, the string is the type of role, viewer, editor or uploader. +func HasOCMShareRole(u *userpb.User) (string, bool) { + if u.Opaque == nil { + return "", false + } + if ocmShare, ok := u.Opaque.Map["ocm-share-role"]; ok { + return string(ocmShare.Value), true + } + return "", false +} + // HasPermissions returns true if all permissions defined in the stuict toCheck // are set in the target. func HasPermissions(target, toCheck *provider.ResourcePermissions) bool { diff --git a/tests/integration/grpc/fixtures/ocm-share/cernbox-machine-authprovider.toml b/tests/integration/grpc/fixtures/ocm-share/cernbox-machine-authprovider.toml new file mode 100644 index 0000000000..03853b9ef9 --- /dev/null +++ b/tests/integration/grpc/fixtures/ocm-share/cernbox-machine-authprovider.toml @@ -0,0 +1,15 @@ +[log] +mode = "json" + +[shared] +gatewaysvc = "{{cernboxgw_address}}" + +[grpc] +address = "{{grpc_address}}" + +[grpc.services.authprovider] +auth_manager = "machine" + +[grpc.services.authprovider.auth_managers.machine] +api_key = "secret" +gateway_addr = "{{cernboxgw_address}}" diff --git a/tests/integration/grpc/fixtures/ocm-share/ocm-cernbox-outcoming-dataserver.toml b/tests/integration/grpc/fixtures/ocm-share/ocm-cernbox-outcoming-dataserver.toml new file mode 100644 index 0000000000..a27a86e82e --- /dev/null +++ b/tests/integration/grpc/fixtures/ocm-share/ocm-cernbox-outcoming-dataserver.toml @@ -0,0 +1,14 @@ +[log] +mode = "json" + +[shared] +gatewaysvc = "{{cernboxgw_address}}" + +[http] +address = "{{grpc_address}}" + +[http.services.dataprovider] +driver = "ocmoutcoming" + +[http.services.dataprovider.drivers.ocmoutcoming] +machine_secret = "secret" diff --git a/tests/integration/grpc/fixtures/ocm-share/ocm-cernbox-outcoming-shares.toml b/tests/integration/grpc/fixtures/ocm-share/ocm-cernbox-outcoming-shares.toml new file mode 100644 index 0000000000..96a3710b04 --- /dev/null +++ b/tests/integration/grpc/fixtures/ocm-share/ocm-cernbox-outcoming-shares.toml @@ -0,0 +1,22 @@ +[log] +mode = "json" + +[shared] +gatewaysvc = "{{cernboxgw_address}}" + +[grpc] +address = "{{grpc_address}}" + +[grpc.services.storageprovider] +driver = "ocmoutcoming" +mount_path = "/ocm" +mount_id = "ocm" +data_server_url = "http://{{cernboxocmdataserver_address}}/data" + +[grpc.services.storageprovider.drivers.ocmoutcoming] +machine_secret = "secret" + +[grpc.services.authprovider] +auth_manager = "ocmshares" + +[grpc.services.authprovider.auth_managers.ocmshares] \ No newline at end of file diff --git a/tests/integration/grpc/fixtures/ocm-share/ocm-server-cernbox-grpc.toml b/tests/integration/grpc/fixtures/ocm-share/ocm-server-cernbox-grpc.toml index 43bf38acca..49fc9b7bb8 100644 --- a/tests/integration/grpc/fixtures/ocm-share/ocm-server-cernbox-grpc.toml +++ b/tests/integration/grpc/fixtures/ocm-share/ocm-server-cernbox-grpc.toml @@ -22,6 +22,8 @@ home_provider = "/home" [grpc.services.storageregistry.drivers.static.rules] "/home" = {"address" = "{{grpc_address}}"} "123e4567-e89b-12d3-a456-426655440000" = {"address" = "{{grpc_address}}"} +"/ocm" = {"address" = "{{cernboxoutcomingocm_address}}"} +"ocm" = {"address" = "{{cernboxoutcomingocm_address}}"} [grpc.services.storageprovider] driver = "localhome" @@ -37,9 +39,12 @@ driver = "static" [grpc.services.authregistry.drivers.static.rules] basic = "{{grpc_address}}" +ocmshares = "{{cernboxoutcomingocm_address}}" +machine = "{{cernboxmachineauth_address}}" [grpc.services.ocminvitemanager] driver = "json" +provider_domain = "cernbox.cern.ch" [grpc.services.ocminvitemanager.drivers.json] file = "{{invite_token_file}}" @@ -52,7 +57,7 @@ providers = "{{file_providers}}" [grpc.services.ocmshareprovider] driver = "json" -webdav_prefix = "http://{{cernboxwebdav_address}}/remote.php/dav/files" +webdav_endpoint = "http://{{cernboxwebdav_address}}" [grpc.services.ocmshareprovider.drivers.json] file = "{{ocm_share_cernbox_file}}" diff --git a/tests/integration/grpc/fixtures/ocm-share/ocm-server-cesnet-grpc.toml b/tests/integration/grpc/fixtures/ocm-share/ocm-server-cesnet-grpc.toml index d55986a814..edd7e950f6 100644 --- a/tests/integration/grpc/fixtures/ocm-share/ocm-server-cesnet-grpc.toml +++ b/tests/integration/grpc/fixtures/ocm-share/ocm-server-cesnet-grpc.toml @@ -24,6 +24,7 @@ basic = "{{grpc_address}}" [grpc.services.ocminvitemanager] driver = "json" +provider_domain = "cesnet.cz" [grpc.services.ocminvitemanager.drivers.json] file = "{{invite_token_file}}" @@ -65,9 +66,9 @@ users = "fixtures/ocm-users.demo.json" "984e7351-2729-4417-99b4-ab5e6d41fa97" = {"address" = "{{grpc_address}}"} [grpc.services.storageprovider] -driver = "ocm" +driver = "ocmreceived" mount_path = "/ocm" mount_id = "984e7351-2729-4417-99b4-ab5e6d41fa97" data_server_url = "http://{{cesnethttp_address}}/data" -[grpc.services.storageprovider.drivers.ocm] +[grpc.services.storageprovider.drivers.ocmreceived] diff --git a/tests/integration/grpc/fixtures/ocm-share/ocm-server-cesnet-http.toml b/tests/integration/grpc/fixtures/ocm-share/ocm-server-cesnet-http.toml index a17d3d0183..af69055e30 100644 --- a/tests/integration/grpc/fixtures/ocm-share/ocm-server-cesnet-http.toml +++ b/tests/integration/grpc/fixtures/ocm-share/ocm-server-cesnet-http.toml @@ -22,6 +22,6 @@ providers = "fixtures/ocm-providers.demo.json" [http.services.datagateway] [http.services.dataprovider] -driver = "ocm" +driver = "ocmreceived" -[http.services.dataprovider.drivers.ocm] +[http.services.dataprovider.drivers.ocmreceived] diff --git a/tests/integration/grpc/grpc_suite_test.go b/tests/integration/grpc/grpc_suite_test.go index 1ccb128ad3..9631beff21 100644 --- a/tests/integration/grpc/grpc_suite_test.go +++ b/tests/integration/grpc/grpc_suite_test.go @@ -220,7 +220,7 @@ func startRevads(configs map[string]string, externalFiles map[string]string, new } // even the port is open the service might not be available yet - time.Sleep(1 * time.Second) + time.Sleep(2 * time.Second) revad := &Revad{ TmpRoot: tmpRoot, diff --git a/tests/integration/grpc/ocm_share_test.go b/tests/integration/grpc/ocm_share_test.go index c8be79f7bc..614d26aef4 100644 --- a/tests/integration/grpc/ocm_share_test.go +++ b/tests/integration/grpc/ocm_share_test.go @@ -24,7 +24,6 @@ import ( "net/http" "path/filepath" "strconv" - "strings" gatewaypb "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" @@ -36,7 +35,6 @@ import ( "github.com/cs3org/reva/internal/http/services/datagateway" "github.com/cs3org/reva/internal/http/services/owncloud/ocdav" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" - "github.com/cs3org/reva/pkg/ocm/client" "github.com/cs3org/reva/pkg/ocm/share" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/rhttp" @@ -125,11 +123,14 @@ var _ = Describe("ocm share", func() { ctxEinstein = ctxWithAuthToken(tokenManager, einstein) ctxMarie = ctxWithAuthToken(tokenManager, marie) revads, err = startRevads(map[string]string{ - "cernboxgw": "ocm-share/ocm-server-cernbox-grpc.toml", - "cernboxwebdav": "ocm-share/cernbox-webdav-server.toml", - "cernboxhttp": "ocm-share/ocm-server-cernbox-http.toml", - "cesnetgw": "ocm-share/ocm-server-cesnet-grpc.toml", - "cesnethttp": "ocm-share/ocm-server-cesnet-http.toml", + "cernboxgw": "ocm-share/ocm-server-cernbox-grpc.toml", + "cernboxwebdav": "ocm-share/cernbox-webdav-server.toml", + "cernboxhttp": "ocm-share/ocm-server-cernbox-http.toml", + "cesnetgw": "ocm-share/ocm-server-cesnet-grpc.toml", + "cesnethttp": "ocm-share/ocm-server-cesnet-http.toml", + "cernboxoutcomingocm": "ocm-share/ocm-cernbox-outcoming-shares.toml", + "cernboxocmdataserver": "ocm-share/ocm-cernbox-outcoming-dataserver.toml", + "cernboxmachineauth": "ocm-share/cernbox-machine-authprovider.toml", }, map[string]string{ "providers": "ocm-providers.demo.json", }, map[string]Resource{ @@ -162,14 +163,12 @@ var _ = Describe("ocm share", func() { Expect(err).ToNot(HaveOccurred()) Expect(tknRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_OK)) - _, err = client.New(&client.Config{}).InviteAccepted(ctxMarie, cernbox.Services[0].Endpoint.Path, &client.InviteAcceptedRequest{ - UserID: marie.Id.OpaqueId, - Email: marie.Mail, - RecipientProvider: "cernbox.cern.ch", - Name: marie.DisplayName, - Token: tknRes.InviteToken.Token, + invRes, err := cesnetgw.ForwardInvite(ctxMarie, &invitev1beta1.ForwardInviteRequest{ + InviteToken: tknRes.InviteToken, + OriginSystemProvider: cernbox, }) Expect(err).ToNot(HaveOccurred()) + Expect(invRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_OK)) }) Context("einstein shares a file with view permissions", func() { @@ -220,14 +219,12 @@ var _ = Describe("ocm share", func() { Expect(ok).To(BeTrue()) webdavClient := gowebdav.NewClient(webdav.WebdavOptions.Uri, "", "") - webdavClient.SetHeader("Authorization", "Bearer "+webdav.WebdavOptions.SharedSecret) d, err := webdavClient.Read(".") Expect(err).ToNot(HaveOccurred()) Expect(d).To(Equal([]byte("test"))) - // TODO: enable once we don't send anymore the owner token - // err = webdavClient.Write(".", []byte("will-never-be-written"), 0) - // Expect(err).To(HaveOccurred()) + err = webdavClient.Write(".", []byte("will-never-be-written"), 0) + Expect(err).To(HaveOccurred()) By("marie access the share using the ocm mount") ref := &provider.Reference{Path: ocmPath(share.Id, "")} @@ -250,8 +247,7 @@ var _ = Describe("ocm share", func() { Expect(err).ToNot(HaveOccurred()) Expect(data).To(Equal([]byte("test"))) - // TODO: enable once we don't send anymore the owner token - // Expect(helpers.UploadGateway(ctxMarie, cesnetgw, ref, []byte("will-never-be-written"))).ToNot(Succeed()) + Expect(helpers.UploadGateway(ctxMarie, cesnetgw, ref, []byte("will-never-be-written"))).ToNot(Succeed()) }) }) @@ -302,12 +298,10 @@ var _ = Describe("ocm share", func() { webdav, ok := protocol.Term.(*ocmv1beta1.Protocol_WebdavOptions) Expect(ok).To(BeTrue()) - u := strings.TrimSuffix(webdav.WebdavOptions.Uri, "/new-file") - webdavClient := gowebdav.NewClient(u, "", "") + webdavClient := gowebdav.NewClient(webdav.WebdavOptions.Uri, "", "") data := []byte("new-content") - webdavClient.SetHeader("Authorization", "Bearer "+webdav.WebdavOptions.SharedSecret) webdavClient.SetHeader(ocdav.HeaderUploadLength, strconv.Itoa(len(data))) - err = webdavClient.Write("new-file", data, 0) + err = webdavClient.Write(".", data, 0) Expect(err).ToNot(HaveOccurred()) By("check that the file was modified") @@ -400,14 +394,13 @@ var _ = Describe("ocm share", func() { Expect(ok).To(BeTrue()) webdavClient := gowebdav.NewClient(webdav.WebdavOptions.Uri, "", "") - webdavClient.SetHeader("Authorization", "Bearer "+webdav.WebdavOptions.SharedSecret) ok, err = helpers.SameContentWebDAV(webdavClient, fileToShare.Path, structure) Expect(err).ToNot(HaveOccurred()) Expect(ok).To(BeTrue()) - // By("check that marie does not have permissions to create files") - // Expect(webdavClient.Write("new-file", []byte("new-file"), 0)).ToNot(Succeed()) + By("check that marie does not have permissions to create files") + Expect(webdavClient.Write("new-file", []byte("new-file"), 0)).ToNot(Succeed()) By("marie access the share using the ocm mount") ref := &provider.Reference{Path: ocmPath(share.Id, "dir")} @@ -441,9 +434,8 @@ var _ = Describe("ocm share", func() { }, }) - // TODO: enable once we don't send anymore the owner token - // newFile := &provider.Reference{Path: ocmPath(share.Id, "dir/new")} - // Expect(helpers.UploadGateway(ctxMarie, cesnetgw, newFile, []byte("uploaded-from-ocm-mount"))).ToNot(Succeed()) + newFile := &provider.Reference{Path: ocmPath(share.Id, "dir/new")} + Expect(helpers.UploadGateway(ctxMarie, cesnetgw, newFile, []byte("uploaded-from-ocm-mount"))).ToNot(Succeed()) }) }) @@ -506,7 +498,6 @@ var _ = Describe("ocm share", func() { webdavClient := gowebdav.NewClient(webdav.WebdavOptions.Uri, "", "") data := []byte("new-content") - webdavClient.SetHeader("Authorization", "Bearer "+webdav.WebdavOptions.SharedSecret) webdavClient.SetHeader(ocdav.HeaderUploadLength, strconv.Itoa(len(data))) err = webdavClient.Write("new-file", data, 0) Expect(err).ToNot(HaveOccurred())