Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement listing shares in spaces #2622

Merged
merged 2 commits into from
Mar 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/unreleased/sharing-in-spaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Enhancement: Allow listing shares in spaces via the OCS API

Added a `space_ref` parameter to the list shares endpoints so that one can list shares inside of spaces.

https://github.com/cs3org/reva/pull/2622
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ const (
storageIDPrefix string = "shared::"
)

var (
errParsingSpaceReference = errors.New("could not parse space reference")
)

// Handler implements the shares part of the ownCloud sharing API
type Handler struct {
gatewayAddr string
Expand Down Expand Up @@ -207,19 +211,16 @@ func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) {
}
ref, err := h.extractReference(r)
if err != nil {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "could not parse the reference", fmt.Errorf("could not parse the reference"))
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, errParsingSpaceReference.Error(), errParsingSpaceReference)
return
}

statReq := provider.StatRequest{
Ref: &ref,
}

sublog := appctx.GetLogger(ctx).With().Interface("ref", ref).Logger()

statReq := provider.StatRequest{Ref: &ref}
statRes, err := client.Stat(ctx, &statReq)
if err != nil {
sublog.Debug().Err(err).Str("createShare", "shares").Msg("error on stat call")
sublog.Debug().Err(err).Msg("CreateShare: error on stat call")
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing resource information", fmt.Errorf("error getting resource information"))
return
}
Expand All @@ -233,7 +234,7 @@ func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) {
response.WriteOCSError(w, r, http.StatusNotFound, "No share permission", nil)
w.WriteHeader(http.StatusForbidden)
default:
log.Error().Interface("status", statRes.Status).Msg("grpc request failed")
log.Error().Interface("status", statRes.Status).Msg("CreateShare: stat failed")
w.WriteHeader(http.StatusInternalServerError)
}
return
Expand Down Expand Up @@ -738,13 +739,17 @@ func (h *Handler) listSharesWithMe(w http.ResponseWriter, r *http.Request) {

var pinfo *provider.ResourceInfo
p := r.URL.Query().Get("path")
shareRef := r.URL.Query().Get("share_ref")
// we need to lookup the resource id so we can filter the list of shares later
if p != "" {
// prefix the path with the owners home, because ocs share requests are relative to the home dir
target := path.Join(h.getHomeNamespace(ctxpkg.ContextMustGetUser(ctx)), r.FormValue("path"))
if p != "" || shareRef != "" {
ref, err := h.extractReference(r)
if err != nil {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, errParsingSpaceReference.Error(), errParsingSpaceReference)
return
}

var status *rpc.Status
pinfo, status, err = h.getResourceInfoByPath(ctx, client, target)
pinfo, status, err = h.getResourceInfoByReference(ctx, client, &ref)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc stat request", err)
return
Expand Down Expand Up @@ -917,9 +922,14 @@ func (h *Handler) listSharesWithOthers(w http.ResponseWriter, r *http.Request) {

// shared with others
p := r.URL.Query().Get("path")
if p != "" {
// prefix the path with the owners home, because ocs share requests are relative to the home dir
filters, linkFilters, e = h.addFilters(w, r, h.getHomeNamespace(ctxpkg.ContextMustGetUser(r.Context())))
spaceRef := r.URL.Query().Get("space_ref")
if p != "" || spaceRef != "" {
ref, err := h.extractReference(r)
if err != nil {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, errParsingSpaceReference.Error(), errParsingSpaceReference)
return
}
filters, linkFilters, e = h.addFilters(w, r, &ref)
if e != nil {
// result has been written as part of addFilters
return
Expand Down Expand Up @@ -990,9 +1000,7 @@ func (h *Handler) logProblems(s *rpc.Status, e error, msg string) {
}
}

func (h *Handler) addFilters(w http.ResponseWriter, r *http.Request, prefix string) ([]*collaboration.Filter, []*link.ListPublicSharesRequest_Filter, error) {
collaborationFilters := []*collaboration.Filter{}
linkFilters := []*link.ListPublicSharesRequest_Filter{}
func (h *Handler) addFilters(w http.ResponseWriter, r *http.Request, ref *provider.Reference) ([]*collaboration.Filter, []*link.ListPublicSharesRequest_Filter, error) {
ctx := r.Context()

// first check if the file exists
Expand All @@ -1002,8 +1010,7 @@ func (h *Handler) addFilters(w http.ResponseWriter, r *http.Request, prefix stri
return nil, nil, err
}

target := path.Join(prefix, r.FormValue("path"))
info, status, err := h.getResourceInfoByPath(ctx, client, target)
info, status, err := h.getResourceInfoByReference(ctx, client, ref)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc stat request", err)
return nil, nil, err
Expand All @@ -1019,9 +1026,8 @@ func (h *Handler) addFilters(w http.ResponseWriter, r *http.Request, prefix stri
return nil, nil, err
}

collaborationFilters = append(collaborationFilters, share.ResourceIDFilter(info.Id))

linkFilters = append(linkFilters, publicshare.ResourceIDFilter(info.Id))
collaborationFilters := []*collaboration.Filter{share.ResourceIDFilter(info.Id)}
linkFilters := []*link.ListPublicSharesRequest_Filter{publicshare.ResourceIDFilter(info.Id)}

return collaborationFilters, linkFilters, nil
}
Expand Down Expand Up @@ -1232,11 +1238,19 @@ func (h *Handler) getAdditionalInfoAttribute(ctx context.Context, u *userIdentif
return buf.String()
}

func (h *Handler) getResourceInfoByPath(ctx context.Context, client GatewayClient, path string) (*provider.ResourceInfo, *rpc.Status, error) {
return h.getResourceInfo(ctx, client, path, &provider.Reference{
// FIXME ResourceId?
Path: path,
})
func (h *Handler) getResourceInfoByReference(ctx context.Context, client GatewayClient, ref *provider.Reference) (*provider.ResourceInfo, *rpc.Status, error) {
var key string
if ref.ResourceId == nil {
// This is a path based reference
key = ref.Path
} else {
var err error
key, err = utils.FormatStorageSpaceReference(ref)
if err != nil {
return nil, nil, err
}
}
return h.getResourceInfo(ctx, client, key, ref)
}

func (h *Handler) getResourceInfoByID(ctx context.Context, client GatewayClient, id *provider.ResourceId) (*provider.ResourceInfo, *rpc.Status, error) {
Expand Down
6 changes: 5 additions & 1 deletion internal/http/services/owncloud/ocs/response/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@ func WriteOCSData(w http.ResponseWriter, r *http.Request, m Meta, d interface{},
// WriteOCSResponse handles writing ocs responses in json and xml
func WriteOCSResponse(w http.ResponseWriter, r *http.Request, res Response, err error) {
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg(res.OCS.Meta.Message)
appctx.GetLogger(r.Context()).
Debug().
Err(err).
Str("ocs_msg", res.OCS.Meta.Message).
Msg("writing ocs error response")
}

version := APIVersion(r.Context())
Expand Down
37 changes: 36 additions & 1 deletion pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ import (
"google.golang.org/protobuf/encoding/protojson"
)

const (
spaceIDDelimiter = "!"
)

var (
matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
Expand All @@ -58,6 +62,8 @@ var (

// SpaceGrant is used to signal the storageprovider that the grant is on a space
SpaceGrant struct{}

errInvalidSpaceReference = errors.New("invalid storage space reference")
)

// Skip evaluates whether a source endpoint contains any of the prefixes.
Expand Down Expand Up @@ -326,7 +332,7 @@ func SplitStorageSpaceID(ssid string) (storageid, nodeid string, err error) {
if ssid == "" {
return "", "", errors.New("can't split empty StorageSpaceID")
}
parts := strings.SplitN(ssid, "!", 2)
parts := strings.SplitN(ssid, spaceIDDelimiter, 2)
if len(parts) == 1 {
return parts[0], parts[0], nil
}
Expand Down Expand Up @@ -357,6 +363,35 @@ func ParseStorageSpaceReference(sRef string) (provider.Reference, error) {
}, nil
}

// FormatStorageSpaceReference will format a storage space reference into a string representation.
// If ref or ref.ResourceId are nil an error will be returned.
// The function doesn't check if all values are set.
// The resulting format can be:
//
// "storage_id!opaque_id"
// "storage_id!opaque_id/path"
// "storage_id/path"
// "storage_id"
func FormatStorageSpaceReference(ref *provider.Reference) (string, error) {
if ref == nil || ref.ResourceId == nil || ref.ResourceId.StorageId == "" {
return "", errInvalidSpaceReference
}
var ssid string
if ref.ResourceId.OpaqueId == "" {

ssid = ref.ResourceId.StorageId
} else {
var sb strings.Builder
// ssid == storage_id!opaque_id
sb.Grow(len(ref.ResourceId.StorageId) + len(ref.ResourceId.OpaqueId) + 1)
sb.WriteString(ref.ResourceId.StorageId)
sb.WriteString(spaceIDDelimiter)
sb.WriteString(ref.ResourceId.OpaqueId)
ssid = sb.String()
}
return path.Join(ssid, ref.Path), nil
}

// GetViewMode converts a human-readable string to a view mode for opening a resource in an app.
func GetViewMode(viewMode string) gateway.OpenInAppRequest_ViewMode {
switch viewMode {
Expand Down
93 changes: 93 additions & 0 deletions pkg/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package utils

import (
"errors"
"testing"

provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
Expand Down Expand Up @@ -207,3 +208,95 @@ func TestParseStorageSpaceReference(t *testing.T) {
}
}
}

func TestFormatStorageSpaceReference(t *testing.T) {
tests := []struct {
ref *provider.Reference
expected string
expectedErr error
}{
{
ref: &provider.Reference{},
expected: "",
expectedErr: errInvalidSpaceReference,
},
{
ref: &provider.Reference{ResourceId: &provider.ResourceId{}},
expected: "!",
expectedErr: errInvalidSpaceReference,
},
{
ref: &provider.Reference{ResourceId: &provider.ResourceId{OpaqueId: "opaqueid"}, Path: "path"},
expectedErr: errInvalidSpaceReference,
},
{
ref: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid"}, Path: "path"},
expected: "storageid/path",
},
{
ref: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid", OpaqueId: "opaqueid"}, Path: "path"},
expected: "storageid!opaqueid/path",
},
{
ref: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid", OpaqueId: "opaqueid"}, Path: "."},
expected: "storageid!opaqueid",
},
{
ref: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid"}, Path: "."},
expected: "storageid",
},
{
ref: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid"}},
expected: "storageid",
},
}

for _, tt := range tests {
result, err := FormatStorageSpaceReference(tt.ref)
if err != nil && !errors.Is(err, tt.expectedErr) {
t.Errorf("FormateStorageSpaceRefence returned unexpected error: %v", err)
}
if err == nil && result != tt.expected {
t.Errorf("Reference %v got formatted to %s, expected %s", tt.ref, result, tt.expected)
}
}
}

func TestFormatAndParseReference(t *testing.T) {
tests := []struct {
orig *provider.Reference
expected *provider.Reference
}{
{
orig: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid"}, Path: "./path"},
expected: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid", OpaqueId: "storageid"}, Path: "./path"},
},
{
orig: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid", OpaqueId: "opaqueid"}, Path: "./path"},
expected: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid", OpaqueId: "opaqueid"}, Path: "./path"},
},
{
orig: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid", OpaqueId: "opaqueid"}, Path: "."},
expected: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid", OpaqueId: "opaqueid"}, Path: "."},
},
{
orig: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid"}, Path: "."},
expected: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid", OpaqueId: "storageid"}, Path: "."},
},
{
orig: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid"}},
expected: &provider.Reference{ResourceId: &provider.ResourceId{StorageId: "storageid", OpaqueId: "storageid"}, Path: "."},
},
}

for _, tt := range tests {
formatted, _ := FormatStorageSpaceReference(tt.orig)
parsed, err := ParseStorageSpaceReference(formatted)
if err != nil {
t.Errorf("failed to parse space reference: %s error: %s", formatted, err)
}
if !(ResourceIDEqual(parsed.ResourceId, tt.expected.ResourceId) && parsed.Path == tt.expected.Path) {
t.Errorf("Formatted then parsed references don't match the original got: %v expected %v", parsed, tt.expected)
}
}
}
6 changes: 3 additions & 3 deletions tests/oc-integration-tests/local/ldap-users.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ address = "0.0.0.0:18000"
auth_manager = "ldap"

[grpc.services.authprovider.auth_managers.ldap]
hostname="ldap"
hostname="localhost"
port=636
insecure=true
base_dn="dc=owncloud,dc=com"
Expand All @@ -34,7 +34,7 @@ cn="cn"
driver = "ldap"

[grpc.services.userprovider.drivers.ldap]
hostname="ldap"
hostname="localhost"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that one shouldn't change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not? The toml files are for local integration tests. I don't have a container 'ldap' locally.

port=636
insecure=true
base_dn="dc=owncloud,dc=com"
Expand All @@ -57,7 +57,7 @@ gid="entryuuid"
driver = "ldap"

[grpc.services.groupprovider.drivers.ldap]
hostname="ldap"
hostname="localhost"
port=636
insecure=true
base_dn="dc=owncloud,dc=com"
Expand Down