diff --git a/changelog/unreleased/publicstorageprovider-rewrite.md b/changelog/unreleased/publicstorageprovider-rewrite.md new file mode 100644 index 0000000000..f6ff7f9cbe --- /dev/null +++ b/changelog/unreleased/publicstorageprovider-rewrite.md @@ -0,0 +1,6 @@ +Bugfix: replace public mountpoint fileid with grant fileid in ocdav + +We now show the same resoucre id for resources when accessing them via a public links as when using a logged in user. This allows the web ui to start a WOPI session with the correct resource id. + +https://github.com/cs3org/reva/pull/2646 +https://github.com/cs3org/reva/issues/2635 diff --git a/internal/grpc/interceptors/auth/auth.go b/internal/grpc/interceptors/auth/auth.go index 3d339290b4..a78bd37016 100644 --- a/internal/grpc/interceptors/auth/auth.go +++ b/internal/grpc/interceptors/auth/auth.go @@ -23,6 +23,7 @@ import ( "time" "github.com/bluele/gcache" + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" "github.com/cs3org/reva/v2/pkg/appctx" @@ -96,9 +97,11 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI // to decide the storage provider. tkn, ok := ctxpkg.ContextGetToken(ctx) if ok { - u, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr, false) + u, tokenScope, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr, false) if err == nil { + // store user and scopes in context ctx = ctxpkg.ContextSetUser(ctx, u) + ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) } } return handler(ctx, req) @@ -112,13 +115,15 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI } // validate the token and ensure access to the resource is allowed - u, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr, true) + u, tokenScope, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr, true) if err != nil { log.Warn().Err(err).Msg("access token is invalid") return nil, status.Errorf(codes.PermissionDenied, "auth: core access token is invalid") } + // store user and scopes in context ctx = ctxpkg.ContextSetUser(ctx, u) + ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) return handler(ctx, req) } return interceptor, nil @@ -159,9 +164,11 @@ func NewStream(m map[string]interface{}, unprotected []string) (grpc.StreamServe // to decide the storage provider. tkn, ok := ctxpkg.ContextGetToken(ctx) if ok { - u, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr, false) + u, tokenScope, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr, false) if err == nil { + // store user and scopes in context ctx = ctxpkg.ContextSetUser(ctx, u) + ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) ss = newWrappedServerStream(ctx, ss) } } @@ -177,14 +184,15 @@ func NewStream(m map[string]interface{}, unprotected []string) (grpc.StreamServe } // validate the token and ensure access to the resource is allowed - u, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr, true) + u, tokenScope, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr, true) if err != nil { log.Warn().Err(err).Msg("access token is invalid") return status.Errorf(codes.PermissionDenied, "auth: core access token is invalid") } - // store user and core access token in context. + // store user and scopes in context ctx = ctxpkg.ContextSetUser(ctx, u) + ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) wrapped := newWrappedServerStream(ctx, ss) return handler(srv, wrapped) } @@ -204,21 +212,22 @@ func (ss *wrappedServerStream) Context() context.Context { return ss.newCtx } -func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token.Manager, gatewayAddr string, fetchUserGroups bool) (*userpb.User, error) { +// dismantleToken extracts the user and scopes from the reva access token +func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token.Manager, gatewayAddr string, fetchUserGroups bool) (*userpb.User, map[string]*authpb.Scope, error) { u, tokenScope, err := mgr.DismantleToken(ctx, tkn) if err != nil { - return nil, err + return nil, nil, err } client, err := pool.GetGatewayServiceClient(gatewayAddr) if err != nil { - return nil, err + return nil, nil, err } if sharedconf.SkipUserGroupsInToken() && fetchUserGroups { groups, err := getUserGroups(ctx, u, client) if err != nil { - return nil, err + return nil, nil, err } u.Groups = groups } @@ -226,17 +235,17 @@ func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token. // Check if access to the resource is in the scope of the token ok, err := scope.VerifyScope(ctx, tokenScope, req) if err != nil { - return nil, errtypes.InternalError("error verifying scope of access token") + return nil, nil, errtypes.InternalError("error verifying scope of access token") } if ok { - return u, nil + return u, tokenScope, nil } if err = expandAndVerifyScope(ctx, req, tokenScope, gatewayAddr, mgr); err != nil { - return nil, err + return nil, nil, err } - return u, nil + return u, tokenScope, nil } func getUserGroups(ctx context.Context, u *userpb.User, client gatewayv1beta1.GatewayAPIClient) ([]string, error) { diff --git a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go index 4f496143ea..8665bdf84d 100644 --- a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go +++ b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go @@ -16,6 +16,8 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. +// Package publicstorageprovider provides a CS3 storageprovider implementation for public links. +// It will list spaces with type `grant` and `mountpoint` when a public scope is present. package publicstorageprovider import ( @@ -25,12 +27,13 @@ import ( "strings" gateway "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" typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/v2/pkg/appctx" - "github.com/cs3org/reva/v2/pkg/errtypes" + ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/rgrpc" "github.com/cs3org/reva/v2/pkg/rgrpc/status" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" @@ -44,9 +47,6 @@ import ( gstatus "google.golang.org/grpc/status" ) -// SpaceTypePublic is the public space type -var SpaceTypePublic = "public" - func init() { rgrpc.Register("publicstorageprovider", New) } @@ -81,7 +81,7 @@ func parseConfig(m map[string]interface{}) (*config, error) { return c, nil } -// New creates a new IsPublic Storage Provider service. +// New creates a new publicstorageprovider service. func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { c, err := parseConfig(m) if err != nil { @@ -163,12 +163,15 @@ func (s *service) InitiateFileDownload(ctx context.Context, req *provider.Initia func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider.Reference) (*provider.Reference, string, *link.PublicShare, *rpc.Status, error) { log := appctx.GetLogger(ctx) - tkn, opaqueid, relativePath, err := s.unwrap(ctx, ref) - if err != nil { - return nil, "", nil, nil, err + + share, _, ok := extractLinkAndInfo(ctx) + if !ok { + return nil, "", nil, nil, gstatus.Errorf(codes.NotFound, "share or token not found") } - ls, shareInfo, st, err := s.resolveToken(ctx, tkn) + // the share is minimally populated, we need more than the token + // look up complete share + ls, shareInfo, st, err := s.resolveToken(ctx, share.Token) switch { case err != nil: return nil, "", nil, nil, err @@ -180,7 +183,7 @@ func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider. switch shareInfo.Type { case provider.ResourceType_RESOURCE_TYPE_CONTAINER: // folders point to the folder -> path needs to be added - path = utils.MakeRelativePath(relativePath) + path = utils.MakeRelativePath(ref.Path) case provider.ResourceType_RESOURCE_TYPE_FILE: // files already point to the correct id path = "." @@ -189,36 +192,22 @@ func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider. // path = utils.MakeRelativePath(relativePath) } - if opaqueid == "" { - opaqueid = shareInfo.Id.OpaqueId - } - cs3Ref := &provider.Reference{ - ResourceId: &provider.ResourceId{ - StorageId: shareInfo.Id.StorageId, - OpaqueId: opaqueid, - }, - Path: path, + ResourceId: shareInfo.Id, + Path: path, } log.Debug(). Interface("sourceRef", ref). Interface("cs3Ref", cs3Ref). Interface("share", ls). - Str("tkn", tkn). + Str("tkn", share.Token). Str("originalPath", shareInfo.Path). - Str("relativePath", relativePath). + Str("relativePath", path). Msg("translatePublicRefToCS3Ref") - return cs3Ref, tkn, ls, nil, nil + return cs3Ref, share.Token, ls, nil, nil } -// Both, t.dir and tokenPath paths need to be merged: -// tokenPath = /oc/einstein/public-links -// t.dir = /public/ausGxuUePCOi/foldera/folderb/ -// res = /public-links/foldera/folderb/ -// this `res` will get then expanded taking into account the authenticated user and the storage: -// end = /einstein/files/public-links/foldera/folderb/ - func (s *service) initiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest) (*provider.InitiateFileDownloadResponse, error) { cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) switch { @@ -343,56 +332,146 @@ func (s *service) CreateStorageSpace(ctx context.Context, req *provider.CreateSt return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") } -// ListStorageSpaces returns a Storage spaces of type "public" when given a filter by id with the public link token as spaceid. -// The root node of every storag space is the real (spaceid, nodeid) of the publicly shared node -// The ocdav service has to -// 1. Authenticate / Log in at the gateway using the token and can then -// 2. look up the storage space using ListStorageSpaces. -// 3. make related requests to that (spaceid, nodeid) +// ListStorageSpaces returns storage spaces when a public scope is present +// in the context. +// +// On the one hand, it lists a `mountpoint` space that can be used by the +// registry to construct a mount path. These spaces have their root +// storageid set to 7993447f-687f-490d-875c-ac95e89a62a4 and the +// opaqueid set to the link token. +// +// On the other hand, it lists a `grant` space for the shared resource id, +// so id based requests can find the correct storage provider. These spaces +// have their root set to the shared resource. func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSpacesRequest) (*provider.ListStorageSpacesResponse, error) { + spaceTypes := map[string]struct{}{} + var exists = struct{}{} + appendTypes := []string{} + var spaceID *provider.ResourceId for _, f := range req.Filters { switch f.Type { case provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE: - switch f.GetSpaceType() { - case SpaceTypePublic: + spaceType := f.GetSpaceType() + if spaceType == "+mountpoint" || spaceType == "+grant" { + appendTypes = append(appendTypes, strings.TrimPrefix(spaceType, "+")) continue - case "mountpoint", "+mountpoint": - continue - default: - return &provider.ListStorageSpacesResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_OK}, - }, nil } + spaceTypes[spaceType] = exists case provider.ListStorageSpacesRequest_Filter_TYPE_ID: - spaceid, _, err := utils.SplitStorageSpaceID(f.GetId().OpaqueId) + spaceid, shareid, err := utils.SplitStorageSpaceID(f.GetId().OpaqueId) if err != nil { continue } if spaceid != utils.PublicStorageProviderID { return &provider.ListStorageSpacesResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_OK}, + // a specific id was requested, return not found instead of empty list + Status: &rpc.Status{Code: rpc.Code_CODE_NOT_FOUND}, }, nil } + spaceID = &provider.ResourceId{StorageId: spaceid, OpaqueId: shareid} } } - return &provider.ListStorageSpacesResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_OK}, - StorageSpaces: []*provider.StorageSpace{{ - Id: &provider.StorageSpaceId{ - OpaqueId: utils.PublicStorageProviderID, - }, - SpaceType: SpaceTypePublic, - // return the actual resource id? - Root: &provider.ResourceId{ + // if there is no public scope there are no publicstorage spaces + share, _, ok := extractLinkAndInfo(ctx) + if !ok { + return &provider.ListStorageSpacesResponse{ + Status: &rpc.Status{Code: rpc.Code_CODE_OK}, + }, nil + } + + if len(spaceTypes) == 0 { + spaceTypes["mountpoint"] = exists + } + for _, s := range appendTypes { + spaceTypes[s] = exists + } + + res := &provider.ListStorageSpacesResponse{ + Status: status.NewOK(ctx), + } + for k := range spaceTypes { + switch k { + case "grant": + // when a list storage space with the resourceid of an external + // resource is made we may have a grant for it + root := share.ResourceId + if spaceID != nil && !utils.ResourceIDEqual(spaceID, root) { + // none of our business + continue + } + // we know a grant for this resource + space := &provider.StorageSpace{ + Id: &provider.StorageSpaceId{ + OpaqueId: root.StorageId + "!" + root.OpaqueId, + }, + SpaceType: "grant", + Owner: &userv1beta1.User{Id: share.Owner}, + // the publicstorageprovider keeps track of mount points + Root: root, + } + + res.StorageSpaces = append(res.StorageSpaces, space) + case "mountpoint": + root := &provider.ResourceId{ StorageId: utils.PublicStorageProviderID, - OpaqueId: utils.PublicStorageProviderID, - }, - Name: "Public shares", - Mtime: &typesv1beta1.Timestamp{}, // do we need to update it? - }}, - }, nil + OpaqueId: share.Token, // the link share has no id, only the token + } + if spaceID != nil { + switch { + case utils.ResourceIDEqual(spaceID, root): + // we have a virtual node + case utils.ResourceIDEqual(spaceID, share.ResourceId): + // we have a mount point + root = share.ResourceId + default: + // none of our business + continue + } + } + space := &provider.StorageSpace{ + Id: &provider.StorageSpaceId{ + OpaqueId: root.StorageId + "!" + root.OpaqueId, + }, + SpaceType: "mountpoint", + Owner: &userv1beta1.User{Id: share.Owner}, // FIXME actually, the mount point belongs to no one? + // the publicstorageprovider keeps track of mount points + Root: root, + } + + res.StorageSpaces = append(res.StorageSpaces, space) + } + } + return res, nil +} +func extractLinkAndInfo(ctx context.Context) (*link.PublicShare, *provider.ResourceInfo, bool) { + scopes, ok := ctxpkg.ContextGetScopes(ctx) + if !ok { + return nil, nil, false + } + var share *link.PublicShare + var info *provider.ResourceInfo + for k, v := range scopes { + switch { + case strings.HasPrefix(k, "publicshare:") && v.Resource.Decoder == "json": + share = &link.PublicShare{} + err := utils.UnmarshalJSONToProtoV1(v.Resource.Value, share) + if err != nil { + continue + } + case strings.HasPrefix(k, "resourceinfo:") && v.Resource.Decoder == "json": + info = &provider.ResourceInfo{} + err := utils.UnmarshalJSONToProtoV1(v.Resource.Value, info) + if err != nil { + continue + } + } + } + if share == nil || info == nil || !utils.ResourceIDEqual(share.ResourceId, info.Id) { + return nil, nil, false + } + return share, info, true } func (s *service) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) { @@ -569,12 +648,16 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide Value: attribute.StringValue(req.Ref.String()), }) - tkn, opaqueid, relativePath, err := s.unwrap(ctx, req.Ref) - if err != nil { - return nil, err + share, _, ok := extractLinkAndInfo(ctx) + if !ok { + return &provider.StatResponse{ + Status: status.NewNotFound(ctx, "share or token not found"), + }, nil } - share, shareInfo, st, err := s.resolveToken(ctx, tkn) + // the share is minimally populated, we need more than the token + // look up complete share + share, shareInfo, st, err := s.resolveToken(ctx, share.Token) switch { case err != nil: return nil, err @@ -588,25 +671,18 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide }, nil } - if shareInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE || relativePath == "" { + if shareInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE || req.Ref.Path == "" { res := &provider.StatResponse{ Status: status.NewOK(ctx), Info: shareInfo, } - s.augmentStatResponse(ctx, res, shareInfo, share, tkn) + s.augmentStatResponse(ctx, res, shareInfo, share, share.Token) return res, nil } - if opaqueid == "" { - opaqueid = share.ResourceId.OpaqueId - } - ref := &provider.Reference{ - ResourceId: &provider.ResourceId{ - StorageId: share.ResourceId.StorageId, - OpaqueId: opaqueid, - }, - Path: utils.MakeRelativePath(relativePath), + ResourceId: share.ResourceId, + Path: utils.MakeRelativePath(req.Ref.Path), } statResponse, err := s.gateway.Stat(ctx, &provider.StatRequest{Ref: ref}) @@ -616,7 +692,7 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide }, nil } - s.augmentStatResponse(ctx, statResponse, shareInfo, share, tkn) + s.augmentStatResponse(ctx, statResponse, shareInfo, share, share.Token) return statResponse, nil } @@ -644,7 +720,7 @@ func (s *service) augmentStatResponse(ctx context.Context, res *provider.StatRes // setPublicStorageID encodes the actual spaceid and nodeid as an opaqueid in the publicstorageprovider space func (s *service) setPublicStorageID(info *provider.ResourceInfo, shareToken string) { info.Id.StorageId = utils.PublicStorageProviderID - info.Id.OpaqueId = shareToken + "/" + info.Id.OpaqueId + info.Id.OpaqueId = shareToken } func addShare(i *provider.ResourceInfo, ls *link.PublicShare) error { @@ -667,12 +743,16 @@ func (s *service) ListContainerStream(req *provider.ListContainerStreamRequest, } func (s *service) ListContainer(ctx context.Context, req *provider.ListContainerRequest) (*provider.ListContainerResponse, error) { - tkn, opaqueid, relativePath, err := s.unwrap(ctx, req.Ref) - if err != nil { - return nil, err - } - share, shareInfo, st, err := s.resolveToken(ctx, tkn) + share, _, ok := extractLinkAndInfo(ctx) + if !ok { + return &provider.ListContainerResponse{ + Status: status.NewNotFound(ctx, "share or token not found"), + }, nil + } + // the share is minimally populated, we need more than the token + // look up complete share + share, _, st, err := s.resolveToken(ctx, share.Token) switch { case err != nil: return nil, err @@ -687,20 +767,13 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer }, nil } - if opaqueid == "" { - opaqueid = shareInfo.Id.OpaqueId - } - listContainerR, err := s.gateway.ListContainer( ctx, &provider.ListContainerRequest{ Ref: &provider.Reference{ - ResourceId: &provider.ResourceId{ - StorageId: shareInfo.Id.StorageId, - OpaqueId: opaqueid, - }, + ResourceId: share.ResourceId, // prefix relative path with './' to make it a CS3 relative path - Path: utils.MakeRelativePath(relativePath), + Path: utils.MakeRelativePath(req.Ref.Path), }, }, ) @@ -711,8 +784,9 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer } for i := range listContainerR.Infos { + // FIXME how do we reduce permissions to what is granted by the public link? + // only a problem for id based access -> middleware filterPermissions(listContainerR.Infos[i].PermissionSet, share.GetPermissions().Permissions) - s.setPublicStorageID(listContainerR.Infos[i], tkn) if err := addShare(listContainerR.Infos[i], share); err != nil { appctx.GetLogger(ctx).Error().Err(err).Interface("share", share).Interface("info", listContainerR.Infos[i]).Msg("error when adding share") } @@ -742,43 +816,6 @@ func filterPermissions(l *provider.ResourcePermissions, r *provider.ResourcePerm l.UpdateGrant = l.UpdateGrant && r.UpdateGrant } -func (s *service) unwrap(ctx context.Context, ref *provider.Reference) (token, opaqueid, relativePath string, err error) { - isValidReference := func(r *provider.Reference) bool { - return r != nil && r.ResourceId != nil && r.ResourceId.StorageId != "" && r.ResourceId.OpaqueId != "" - } - - switch { - case !isValidReference(ref): - return "", "", "", errtypes.BadRequest("resourceid required, got " + ref.String()) - case utils.ResourceIDEqual(ref.ResourceId, &provider.ResourceId{ - StorageId: utils.PublicStorageProviderID, - OpaqueId: utils.PublicStorageProviderID, - }): - // path has the form "./{token}/relative/path/" - parts := strings.SplitN(ref.Path, "/", 3) - if len(parts) < 2 { - // FIXME ... we should expose every public link as a storage space - // but do we need to list them then? - return "", "", "", errtypes.BadRequest("need at least token in ref: got " + ref.String()) - } - token = parts[1] - if len(parts) > 2 { - relativePath = parts[2] - } - default: - // id based stat - parts := strings.SplitN(ref.ResourceId.OpaqueId, "/", 2) - if len(parts) < 2 { - return "", "", "", errtypes.BadRequest("OpaqueId needs to have form {token}/{shared node id}: got " + ref.String()) - } - token = parts[0] - opaqueid = parts[1] - relativePath = ref.Path - } - - return -} - func (s *service) ListFileVersions(ctx context.Context, req *provider.ListFileVersionsRequest) (*provider.ListFileVersionsResponse, error) { return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") } diff --git a/internal/http/interceptors/auth/auth.go b/internal/http/interceptors/auth/auth.go index 102ede554b..85fb88b86f 100644 --- a/internal/http/interceptors/auth/auth.go +++ b/internal/http/interceptors/auth/auth.go @@ -284,6 +284,9 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, r.UserAgent()) + // store scopes in context + ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) + r = r.WithContext(ctx) h.ServeHTTP(w, r) }) diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index 86f2f80a4d..7665ffdabd 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -276,9 +276,8 @@ func getTokenStatInfo(ctx context.Context, client gatewayv1beta1.GatewayAPIClien return client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ ResourceId: &provider.ResourceId{ StorageId: utils.PublicStorageProviderID, - OpaqueId: utils.PublicStorageProviderID, + OpaqueId: token, }, - Path: utils.MakeRelativePath(token), }}) } diff --git a/internal/http/services/owncloud/ocdav/propfind/propfind.go b/internal/http/services/owncloud/ocdav/propfind/propfind.go index 98c7c2773d..e3634f3bb5 100644 --- a/internal/http/services/owncloud/ocdav/propfind/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind/propfind.go @@ -274,6 +274,7 @@ func (p *Handler) propfindResponse(ctx context.Context, w http.ResponseWriter, r filters := make([]*link.ListPublicSharesRequest_Filter, 0, len(resourceInfos)) for i := range resourceInfos { + // the list of filters grows with every public link in a folder filters = append(filters, publicshare.ResourceIDFilter(resourceInfos[i].Id)) } @@ -285,15 +286,18 @@ func (p *Handler) propfindResponse(ctx context.Context, w http.ResponseWriter, r } var linkshares map[string]struct{} - listResp, err := client.ListPublicShares(ctx, &link.ListPublicSharesRequest{Filters: filters}) - if err == nil { - linkshares = make(map[string]struct{}, len(listResp.Share)) - for i := range listResp.Share { - linkshares[listResp.Share[i].ResourceId.OpaqueId] = struct{}{} + // public link access does not show share-types + if namespace != "/public" { + listResp, err := client.ListPublicShares(ctx, &link.ListPublicSharesRequest{Filters: filters}) + if err == nil { + linkshares = make(map[string]struct{}, len(listResp.Share)) + for i := range listResp.Share { + linkshares[listResp.Share[i].ResourceId.OpaqueId] = struct{}{} + } + } else { + log.Error().Err(err).Msg("propfindResponse: couldn't list public shares") + span.SetStatus(codes.Error, err.Error()) } - } else { - log.Error().Err(err).Msg("propfindResponse: couldn't list public shares") - span.SetStatus(codes.Error, err.Error()) } propRes, err := MultistatusResponse(ctx, &pf, resourceInfos, p.PublicURL, namespace, linkshares) @@ -717,6 +721,11 @@ func mdToPropResponse(ctx context.Context, pf *XML, md *provider.ResourceInfo, p sublog.Debug().Interface("role", role).Str("dav-permissions", wdp).Msg("converted PermissionSet") } + // replace fileid of /public/{token} mountpoint with grant fileid + if ls != nil && md.Id != nil && md.Id.StorageId == utils.PublicStorageProviderID && md.Id.OpaqueId == ls.Token { + md.Id = ls.ResourceId + } + propstatOK := PropstatXML{ Status: "HTTP/1.1 200 OK", Prop: []*props.PropertyXML{}, diff --git a/pkg/auth/scope/publicshare.go b/pkg/auth/scope/publicshare.go index 1a3cd44e6a..207cd7e2f9 100644 --- a/pkg/auth/scope/publicshare.go +++ b/pkg/auth/scope/publicshare.go @@ -28,6 +28,7 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" permissionsv1beta1 "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1" + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" @@ -70,6 +71,8 @@ func publicshareScope(ctx context.Context, scope *authpb.Scope, resource interfa } } return checkStorageRef(ctx, &share, ref), nil + case *provider.CreateHomeRequest: + return false, nil case *provider.GetPathRequest: return checkStorageRef(ctx, &share, &provider.Reference{ResourceId: v.GetResourceId()}), nil case *provider.StatRequest: @@ -112,14 +115,21 @@ func publicshareScope(ctx context.Context, scope *authpb.Scope, resource interfa return true, nil case *provider.ListStorageSpacesRequest: - return checkPublicListStorageSpacesFilter(v.Filters), nil + return true, nil case *link.GetPublicShareRequest: return checkPublicShareRef(&share, v.GetRef()), nil + case *link.ListPublicSharesRequest: + // public links must not leak info about other links + return false, nil + + case *collaboration.ListReceivedSharesRequest: + // public links must not leak info about collaborative shares + return false, nil case string: return checkResourcePath(v), nil } - msg := "resource type assertion failed" + msg := "public resource type assertion failed" logger.Debug().Str("scope", "publicshareScope").Interface("resource", resource).Msg(msg) return false, errtypes.InternalError(msg) } @@ -135,37 +145,20 @@ func checkStorageRef(ctx context.Context, s *link.PublicShare, r *provider.Refer return true } - // r: path:$path> + // r: path:$path> if id := r.GetResourceId(); id.GetStorageId() == PublicStorageProviderID { - if id.GetOpaqueId() == PublicStorageProviderID && strings.HasPrefix(r.Path, "./"+s.Token) { + // access to /public + if id.GetOpaqueId() == PublicStorageProviderID { return true } - // r: path:$path> - if strings.HasPrefix(id.GetOpaqueId(), s.Token+"/") { + // access relative to /public/$token + if id.GetOpaqueId() == s.Token { return true } } return false } -// public link access must send a filter with id or type -func checkPublicListStorageSpacesFilter(filters []*provider.ListStorageSpacesRequest_Filter) bool { - // return true - for _, f := range filters { - switch f.Type { - case provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE: - if f.GetSpaceType() == "public" { - return true - } - case provider.ListStorageSpacesRequest_Filter_TYPE_ID: - if f.GetId().OpaqueId != "" { - return true - } - } - } - return false -} - func checkPublicShareRef(s *link.PublicShare, ref *link.PublicShareReference) bool { // ref: return ref.GetToken() == s.Token diff --git a/pkg/ctx/scopectx.go b/pkg/ctx/scopectx.go new file mode 100644 index 0000000000..0d0dae20c4 --- /dev/null +++ b/pkg/ctx/scopectx.go @@ -0,0 +1,36 @@ +// Copyright 2018-2021 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 ctx + +import ( + "context" + + auth "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" +) + +// ContextGetScopes returns the scopes if set in the given context. +func ContextGetScopes(ctx context.Context) (map[string]*auth.Scope, bool) { + s, ok := ctx.Value(scopeKey).(map[string]*auth.Scope) + return s, ok +} + +// ContextSetScopes stores the scopes in the context. +func ContextSetScopes(ctx context.Context, s map[string]*auth.Scope) context.Context { + return context.WithValue(ctx, scopeKey, s) +} diff --git a/pkg/ctx/userctx.go b/pkg/ctx/userctx.go index 3c50df1879..4fbee7444f 100644 --- a/pkg/ctx/userctx.go +++ b/pkg/ctx/userctx.go @@ -31,6 +31,7 @@ const ( tokenKey idKey lockIDKey + scopeKey ) // ContextGetUser returns the user if set in the given context. diff --git a/tests/oc-integration-tests/drone/gateway.toml b/tests/oc-integration-tests/drone/gateway.toml index fb8e896fcc..4f20f9e7b6 100644 --- a/tests/oc-integration-tests/drone/gateway.toml +++ b/tests/oc-integration-tests/drone/gateway.toml @@ -78,7 +78,8 @@ home_template = "/users/{{.Id.OpaqueId}}" ## While public shares are mounted at /public logged in end will should never see that path because it is only created by the spaces registry when ## a public link is accessed. [grpc.services.storageregistry.drivers.spaces.providers."localhost:13000".spaces] -"public" = { "mount_point" = "/public", "path_template" = "/public" } +"grant" = { "mount_point" = "." } +"mountpoint" = { "mount_point" = "/public", "path_template" = "/public/{{.Space.Root.OpaqueId}}" } [http] address = "0.0.0.0:19001" diff --git a/tests/oc-integration-tests/local/gateway.toml b/tests/oc-integration-tests/local/gateway.toml index 0f3d8166ee..2167d1505f 100644 --- a/tests/oc-integration-tests/local/gateway.toml +++ b/tests/oc-integration-tests/local/gateway.toml @@ -84,7 +84,8 @@ home_template = "/users/{{.Id.OpaqueId}}" ## While public shares are mounted at /public logged in end will should never see that path because it is only created by the spaces registry when ## a public link is accessed. [grpc.services.storageregistry.drivers.spaces.providers."localhost:13000".spaces] -"public" = { "mount_point" = "/public" } +"grant" = { "mount_point" = "." } +"mountpoint" = { "mount_point" = "/public", "path_template" = "/public/{{.Space.Root.OpaqueId}}" } [http] address = "0.0.0.0:19001"