From 2ca63c840e42c0681e944c17259e2973ac99956d Mon Sep 17 00:00:00 2001 From: David Christofas Date: Wed, 10 Nov 2021 11:38:44 +0100 Subject: [PATCH] Share types for public links (#2213) --- .../unreleased/public-share-share-types.md | 5 ++ .../http/services/owncloud/ocdav/propfind.go | 51 ++++++++++++++++--- .../services/owncloud/ocdav/publicfile.go | 2 +- .../http/services/owncloud/ocdav/report.go | 2 +- .../http/services/owncloud/ocdav/versions.go | 2 +- pkg/publicshare/manager/json/json.go | 33 ++++-------- pkg/publicshare/publicshare.go | 50 ++++++++++++++++++ 7 files changed, 113 insertions(+), 32 deletions(-) create mode 100644 changelog/unreleased/public-share-share-types.md diff --git a/changelog/unreleased/public-share-share-types.md b/changelog/unreleased/public-share-share-types.md new file mode 100644 index 0000000000..6854c32e9d --- /dev/null +++ b/changelog/unreleased/public-share-share-types.md @@ -0,0 +1,5 @@ +Enhancement: Add public link share type to propfind response + +Added share type for public links to propfind responses. + +https://github.com/cs3org/reva/pull/2213 diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index 90600e393e..6fc2854f01 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -40,10 +40,12 @@ import ( "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/pkg/appctx" ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/publicshare" rtrace "github.com/cs3org/reva/pkg/trace" "github.com/cs3org/reva/pkg/utils" "github.com/rs/zerolog" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" ) const ( @@ -134,7 +136,34 @@ func (s *svc) handleSpacesPropfind(w http.ResponseWriter, r *http.Request, space } func (s *svc) propfindResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, namespace string, pf propfindXML, parentInfo *provider.ResourceInfo, resourceInfos []*provider.ResourceInfo, log zerolog.Logger) { - propRes, err := s.multistatusResponse(ctx, &pf, resourceInfos, namespace) + ctx, span := rtrace.Provider.Tracer("ocdav").Start(ctx, "propfind_response") + defer span.End() + + filters := make([]*link.ListPublicSharesRequest_Filter, 0, len(resourceInfos)) + for i := range resourceInfos { + filters = append(filters, publicshare.ResourceIDFilter(resourceInfos[i].Id)) + } + + client, err := s.getClient() + if err != nil { + log.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return + } + + var linkshares map[string]struct{} + listResp, err := client.ListPublicShares(ctx, &link.ListPublicSharesRequest{Filters: filters}) + if err == nil { + linkshares := make(map[string]struct{}) + 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()) + } + + propRes, err := s.multistatusResponse(ctx, &pf, resourceInfos, namespace, linkshares) if err != nil { log.Error().Err(err).Msg("error formatting propfind") w.WriteHeader(http.StatusInternalServerError) @@ -374,10 +403,10 @@ func readPropfind(r io.Reader) (pf propfindXML, status int, err error) { return pf, 0, nil } -func (s *svc) multistatusResponse(ctx context.Context, pf *propfindXML, mds []*provider.ResourceInfo, ns string) (string, error) { +func (s *svc) multistatusResponse(ctx context.Context, pf *propfindXML, mds []*provider.ResourceInfo, ns string, linkshares map[string]struct{}) (string, error) { responses := make([]*responseXML, 0, len(mds)) for i := range mds { - res, err := s.mdToPropResponse(ctx, pf, mds[i], ns) + res, err := s.mdToPropResponse(ctx, pf, mds[i], ns, linkshares) if err != nil { return "", err } @@ -429,7 +458,7 @@ func (s *svc) newPropRaw(key, val string) *propertyXML { // mdToPropResponse converts the CS3 metadata into a webdav PropResponse // ns is the CS3 namespace that needs to be removed from the CS3 path before // prefixing it with the baseURI -func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provider.ResourceInfo, ns string) (*responseXML, error) { +func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provider.ResourceInfo, ns string, linkshares map[string]struct{}) (*responseXML, error) { sublog := appctx.GetLogger(ctx).With().Interface("md", md).Str("ns", ns).Logger() md.Path = strings.TrimPrefix(md.Path, ns) @@ -739,11 +768,21 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:checksums", "")) } case "share-types": // desktop + var types strings.Builder k := md.GetArbitraryMetadata() amd := k.GetMetadata() if amdv, ok := amd[metadataKeyOf(&pf.Prop[i])]; ok { - st := fmt.Sprintf("%s", amdv) - propstatOK.Prop = append(propstatOK.Prop, s.newPropRaw("oc:share-types", st)) + types.WriteString("") + types.WriteString(amdv) + types.WriteString("") + } + + if _, ok := linkshares[md.Id.OpaqueId]; ok { + types.WriteString("3") + } + + if types.Len() != 0 { + propstatOK.Prop = append(propstatOK.Prop, s.newPropRaw("oc:share-types", types.String())) } else { propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:"+pf.Prop[i].Local, "")) } diff --git a/internal/http/services/owncloud/ocdav/publicfile.go b/internal/http/services/owncloud/ocdav/publicfile.go index 932a8ce996..b369f6650b 100644 --- a/internal/http/services/owncloud/ocdav/publicfile.go +++ b/internal/http/services/owncloud/ocdav/publicfile.go @@ -186,7 +186,7 @@ func (s *svc) handlePropfindOnToken(w http.ResponseWriter, r *http.Request, ns s infos := s.getPublicFileInfos(onContainer, depth == "0", tokenStatInfo) - propRes, err := s.multistatusResponse(ctx, &pf, infos, ns) + propRes, err := s.multistatusResponse(ctx, &pf, infos, ns, nil) if err != nil { sublog.Error().Err(err).Msg("error formatting propfind") w.WriteHeader(http.StatusInternalServerError) diff --git a/internal/http/services/owncloud/ocdav/report.go b/internal/http/services/owncloud/ocdav/report.go index aab2ff90bb..49df7b4f1d 100644 --- a/internal/http/services/owncloud/ocdav/report.go +++ b/internal/http/services/owncloud/ocdav/report.go @@ -119,7 +119,7 @@ func (s *svc) doFilterFiles(w http.ResponseWriter, r *http.Request, ff *reportFi infos = append(infos, statRes.Info) } - responsesXML, err := s.multistatusResponse(ctx, &propfindXML{Prop: ff.Prop}, infos, namespace) + responsesXML, err := s.multistatusResponse(ctx, &propfindXML{Prop: ff.Prop}, infos, namespace, nil) if err != nil { log.Error().Err(err).Msg("error formatting propfind") w.WriteHeader(http.StatusInternalServerError) diff --git a/internal/http/services/owncloud/ocdav/versions.go b/internal/http/services/owncloud/ocdav/versions.go index fc281fa155..ad6620ccd9 100644 --- a/internal/http/services/owncloud/ocdav/versions.go +++ b/internal/http/services/owncloud/ocdav/versions.go @@ -164,7 +164,7 @@ func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request, infos = append(infos, vi) } - propRes, err := s.multistatusResponse(ctx, &pf, infos, "") + propRes, err := s.multistatusResponse(ctx, &pf, infos, "", nil) if err != nil { sublog.Error().Err(err).Msg("error formatting propfind") w.WriteHeader(http.StatusInternalServerError) diff --git a/pkg/publicshare/manager/json/json.go b/pkg/publicshare/manager/json/json.go index d7a047a369..8f7211b687 100644 --- a/pkg/publicshare/manager/json/json.go +++ b/pkg/publicshare/manager/json/json.go @@ -333,7 +333,7 @@ func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.Pu } if ref.GetId().GetOpaqueId() == ps.Id.OpaqueId { - if !notExpired(&ps) { + if publicshare.IsExpired(&ps) { if err := m.revokeExpiredPublicShare(ctx, &ps, u); err != nil { return nil, err } @@ -383,18 +383,14 @@ func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters [] if len(filters) == 0 { shares = append(shares, &local.PublicShare) - } else { - for i := range filters { - if filters[i].Type == link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID { - if utils.ResourceIDEqual(local.ResourceId, filters[i].GetResourceId()) { - if notExpired(&local.PublicShare) { - shares = append(shares, &local.PublicShare) - } else if err := m.revokeExpiredPublicShare(ctx, &local.PublicShare, u); err != nil { - return nil, err - } - } + continue + } - } + if publicshare.MatchesFilters(&local.PublicShare, filters) { + if !publicshare.IsExpired(&local.PublicShare) { + shares = append(shares, &local.PublicShare) + } else if err := m.revokeExpiredPublicShare(ctx, &local.PublicShare, u); err != nil { + return nil, err } } } @@ -402,15 +398,6 @@ func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters [] return shares, nil } -// notExpired tests whether a public share is expired -func notExpired(s *link.PublicShare) bool { - t := time.Unix(int64(s.Expiration.GetSeconds()), int64(s.Expiration.GetNanos())) - if (s.Expiration != nil && t.After(time.Now())) || s.Expiration == nil { - return true - } - return false -} - func (m *manager) cleanupExpiredShares() { m.mutex.Lock() defer m.mutex.Unlock() @@ -423,7 +410,7 @@ func (m *manager) cleanupExpiredShares() { var ps link.PublicShare _ = utils.UnmarshalJSONToProtoV1([]byte(d.(string)), &ps) - if !notExpired(&ps) { + if publicshare.IsExpired(&ps) { _ = m.revokeExpiredPublicShare(context.Background(), &ps, nil) } } @@ -525,7 +512,7 @@ func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth } if local.Token == token { - if !notExpired(&local) { + if publicshare.IsExpired(&local) { // TODO user is not needed at all in this API. if err := m.revokeExpiredPublicShare(ctx, &local, nil); err != nil { return nil, err diff --git a/pkg/publicshare/publicshare.go b/pkg/publicshare/publicshare.go index fa00beb32c..7692779484 100644 --- a/pkg/publicshare/publicshare.go +++ b/pkg/publicshare/publicshare.go @@ -30,6 +30,7 @@ import ( 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/pkg/utils" ) // Manager manipulates public shares. @@ -93,3 +94,52 @@ func ResourceIDFilter(id *provider.ResourceId) *link.ListPublicSharesRequest_Fil }, } } + +// MatchesFilter tests if the share passes the filter. +func MatchesFilter(share *link.PublicShare, filter *link.ListPublicSharesRequest_Filter) bool { + switch filter.Type { + case link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID: + return utils.ResourceIDEqual(share.ResourceId, filter.GetResourceId()) + default: + return false + } +} + +// MatchesAnyFilter checks if the share passes at least one of the given filters. +func MatchesAnyFilter(share *link.PublicShare, filters []*link.ListPublicSharesRequest_Filter) bool { + for _, f := range filters { + if MatchesFilter(share, f) { + return true + } + } + return false +} + +// MatchesFilters checks if the share passes the given filters. +// Filters of the same type form a disjuntion, a logical OR. Filters of separate type form a conjunction, a logical AND. +// Here is an example: +// (resource_id=1 OR resource_id=2) AND (grantee_type=USER OR grantee_type=GROUP) +func MatchesFilters(share *link.PublicShare, filters []*link.ListPublicSharesRequest_Filter) bool { + grouped := GroupFiltersByType(filters) + for _, f := range grouped { + if !MatchesAnyFilter(share, f) { + return false + } + } + return true +} + +// GroupFiltersByType groups the given filters and returns a map using the filter type as the key. +func GroupFiltersByType(filters []*link.ListPublicSharesRequest_Filter) map[link.ListPublicSharesRequest_Filter_Type][]*link.ListPublicSharesRequest_Filter { + grouped := make(map[link.ListPublicSharesRequest_Filter_Type][]*link.ListPublicSharesRequest_Filter) + for _, f := range filters { + grouped[f.Type] = append(grouped[f.Type], f) + } + return grouped +} + +// IsExpired tests whether a public share is expired +func IsExpired(s *link.PublicShare) bool { + expiration := time.Unix(int64(s.Expiration.GetSeconds()), int64(s.Expiration.GetNanos())) + return s.Expiration != nil && expiration.Before(time.Now()) +}