diff --git a/changelog/unreleased/nextcloud-share-manager.md b/changelog/unreleased/nextcloud-share-manager.md new file mode 100644 index 0000000000..78fe4d41c2 --- /dev/null +++ b/changelog/unreleased/nextcloud-share-manager.md @@ -0,0 +1,5 @@ +Enhancement: Nextcloud share managers + +Share manager that uses Nextcloud as a backend + +https://github.com/cs3org/reva/pull/2091 diff --git a/pkg/share/manager/loader/loader.go b/pkg/share/manager/loader/loader.go index b3ebbcae29..eada9e68e6 100644 --- a/pkg/share/manager/loader/loader.go +++ b/pkg/share/manager/loader/loader.go @@ -22,6 +22,7 @@ import ( // Load core share manager drivers. _ "github.com/cs3org/reva/pkg/share/manager/json" _ "github.com/cs3org/reva/pkg/share/manager/memory" + _ "github.com/cs3org/reva/pkg/share/manager/nextcloud" _ "github.com/cs3org/reva/pkg/share/manager/sql" // Add your own here ) diff --git a/pkg/share/manager/nextcloud/nextcloud.go b/pkg/share/manager/nextcloud/nextcloud.go new file mode 100644 index 0000000000..3b1de3d1e5 --- /dev/null +++ b/pkg/share/manager/nextcloud/nextcloud.go @@ -0,0 +1,452 @@ +// 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 nextcloud verifies a clientID and clientSecret against a Nextcloud backend. +package nextcloud + +import ( + "context" + "encoding/json" + "io" + "net/http" + "strings" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/share" + "github.com/cs3org/reva/pkg/share/manager/registry" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +func init() { + registry.Register("nextcloud", New) +} + +type mgr struct { + client *http.Client + endPoint string +} + +// ShareManagerConfig contains config for a Nextcloud-based ShareManager +type ShareManagerConfig struct { + EndPoint string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user check"` +} + +// Action describes a REST request to forward to the Nextcloud backend +type Action struct { + verb string + argS string +} + +// GranteeAltMap is an alternative map to JSON-unmarshal a Grantee +// Grantees are hard to unmarshal, so unmarshalling into a map[string]interface{} first, +// see also https://github.com/pondersource/sciencemesh-nextcloud/issues/27 +type GranteeAltMap struct { + ID *provider.Grantee_UserId `json:"id"` +} + +// ShareAltMap is an alternative map to JSON-unmarshal a Share +type ShareAltMap struct { + ID *collaboration.ShareId `json:"id"` + ResourceID *provider.ResourceId `json:"resource_id"` + Permissions *collaboration.SharePermissions `json:"permissions"` + Grantee *GranteeAltMap `json:"grantee"` + Owner *userpb.UserId `json:"owner"` + Creator *userpb.UserId `json:"creator"` + Ctime *types.Timestamp `json:"ctime"` + Mtime *types.Timestamp `json:"mtime"` +} + +// ReceivedShareAltMap is an alternative map to JSON-unmarshal a ReceivedShare +type ReceivedShareAltMap struct { + Share *ShareAltMap `json:"share"` + State collaboration.ShareState `json:"state"` +} + +func (c *ShareManagerConfig) init() { +} + +func parseConfig(m map[string]interface{}) (*ShareManagerConfig, error) { + c := &ShareManagerConfig{} + if err := mapstructure.Decode(m, c); err != nil { + err = errors.Wrap(err, "error decoding conf") + return nil, err + } + return c, nil +} + +func getUser(ctx context.Context) (*userpb.User, error) { + u, ok := ctxpkg.ContextGetUser(ctx) + if !ok { + err := errors.Wrap(errtypes.UserRequired(""), "nextcloud storage driver: error getting user from ctx") + return nil, err + } + return u, nil +} + +// New returns an share manager implementation that verifies against a Nextcloud backend. +func New(m map[string]interface{}) (share.Manager, error) { + c, err := parseConfig(m) + if err != nil { + return nil, err + } + c.init() + + return NewShareManager(c, &http.Client{}) +} + +// NewShareManager returns a new Nextcloud-based ShareManager +func NewShareManager(c *ShareManagerConfig, hc *http.Client) (share.Manager, error) { + return &mgr{ + endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/" + client: hc, + }, nil +} + +func (sm *mgr) do(ctx context.Context, a Action) (int, []byte, error) { + log := appctx.GetLogger(ctx) + user, err := getUser(ctx) + if err != nil { + return 0, nil, err + } + // url := am.endPoint + "~" + a.username + "/api/" + a.verb + url := "http://localhost/apps/sciencemesh/~" + user.Username + "/api/share/" + a.verb + log.Info().Msgf("am.do %s %s", url, a.argS) + req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS)) + if err != nil { + return 0, nil, err + } + + req.Header.Set("Content-Type", "application/json") + resp, err := sm.client.Do(req) + if err != nil { + return 0, nil, err + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, nil, err + } + + log.Info().Msgf("am.do response %d %s", resp.StatusCode, body) + return resp.StatusCode, body, nil +} + +func (sm *mgr) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) { + type paramsObj struct { + Md *provider.ResourceInfo `json:"md"` + G *collaboration.ShareGrant `json:"g"` + } + bodyObj := ¶msObj{ + Md: md, + G: g, + } + bodyStr, err := json.Marshal(bodyObj) + if err != nil { + return nil, err + } + + _, body, err := sm.do(ctx, Action{"Share", string(bodyStr)}) + + if err != nil { + return nil, err + } + + altResult := &ShareAltMap{} + err = json.Unmarshal(body, &altResult) + if altResult == nil { + return nil, err + } + return &collaboration.Share{ + Id: altResult.ID, + ResourceId: altResult.ResourceID, + Permissions: altResult.Permissions, + Grantee: &provider.Grantee{ + Id: altResult.Grantee.ID, + }, + Owner: altResult.Owner, + Creator: altResult.Creator, + Ctime: altResult.Ctime, + Mtime: altResult.Mtime, + }, err +} + +// GetShare gets the information for a share by the given ref. +func (sm *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) { + bodyStr, err := json.Marshal(ref) + if err != nil { + return nil, err + } + _, body, err := sm.do(ctx, Action{"GetShare", string(bodyStr)}) + if err != nil { + return nil, err + } + + altResult := &ShareAltMap{} + err = json.Unmarshal(body, &altResult) + if altResult == nil { + return nil, err + } + return &collaboration.Share{ + Id: altResult.ID, + ResourceId: altResult.ResourceID, + Permissions: altResult.Permissions, + Grantee: &provider.Grantee{ + Id: altResult.Grantee.ID, + }, + Owner: altResult.Owner, + Creator: altResult.Creator, + Ctime: altResult.Ctime, + Mtime: altResult.Mtime, + }, err +} + +// Unshare deletes the share pointed by ref. +func (sm *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) error { + bodyStr, err := json.Marshal(ref) + if err != nil { + return err + } + + _, _, err = sm.do(ctx, Action{"Unshare", string(bodyStr)}) + return err +} + +// UpdateShare updates the mode of the given share. +func (sm *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference, p *collaboration.SharePermissions) (*collaboration.Share, error) { + type paramsObj struct { + Ref *collaboration.ShareReference `json:"ref"` + P *collaboration.SharePermissions `json:"p"` + } + bodyObj := ¶msObj{ + Ref: ref, + P: p, + } + bodyStr, err := json.Marshal(bodyObj) + if err != nil { + return nil, err + } + + _, body, err := sm.do(ctx, Action{"UpdateShare", string(bodyStr)}) + + if err != nil { + return nil, err + } + + altResult := &ShareAltMap{} + err = json.Unmarshal(body, &altResult) + if altResult == nil { + return nil, err + } + return &collaboration.Share{ + Id: altResult.ID, + ResourceId: altResult.ResourceID, + Permissions: altResult.Permissions, + Grantee: &provider.Grantee{ + Id: altResult.Grantee.ID, + }, + Owner: altResult.Owner, + Creator: altResult.Creator, + Ctime: altResult.Ctime, + Mtime: altResult.Mtime, + }, err +} + +// ListShares returns the shares created by the user. If md is provided is not nil, +// it returns only shares attached to the given resource. +func (sm *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) { + bodyStr, err := json.Marshal(filters) + if err != nil { + return nil, err + } + + _, respBody, err := sm.do(ctx, Action{"ListShares", string(bodyStr)}) + if err != nil { + return nil, err + } + + var respArr []ShareAltMap + err = json.Unmarshal(respBody, &respArr) + if err != nil { + return nil, err + } + + var pointers = make([]*collaboration.Share, len(respArr)) + for i := 0; i < len(respArr); i++ { + altResult := respArr[i] + pointers[i] = &collaboration.Share{ + Id: altResult.ID, + ResourceId: altResult.ResourceID, + Permissions: altResult.Permissions, + Grantee: &provider.Grantee{ + Id: altResult.Grantee.ID, + }, + Owner: altResult.Owner, + Creator: altResult.Creator, + Ctime: altResult.Ctime, + Mtime: altResult.Mtime, + } + } + return pointers, err +} + +// ListReceivedShares returns the list of shares the user has access. +func (sm *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) { + bodyStr, err := json.Marshal(filters) + if err != nil { + return nil, err + } + + _, respBody, err := sm.do(ctx, Action{"ListReceivedShares", string(bodyStr)}) + if err != nil { + return nil, err + } + + var respArr []ReceivedShareAltMap + err = json.Unmarshal(respBody, &respArr) + if err != nil { + return nil, err + } + var pointers = make([]*collaboration.ReceivedShare, len(respArr)) + for i := 0; i < len(respArr); i++ { + altResultShare := respArr[i].Share + if altResultShare == nil { + pointers[i] = &collaboration.ReceivedShare{ + Share: nil, + State: respArr[i].State, + } + } else { + pointers[i] = &collaboration.ReceivedShare{ + Share: &collaboration.Share{ + Id: altResultShare.ID, + ResourceId: altResultShare.ResourceID, + Permissions: altResultShare.Permissions, + Grantee: &provider.Grantee{ + Id: altResultShare.Grantee.ID, + }, + Owner: altResultShare.Owner, + Creator: altResultShare.Creator, + Ctime: altResultShare.Ctime, + Mtime: altResultShare.Mtime, + }, + State: respArr[i].State, + } + } + } + return pointers, err + +} + +// GetReceivedShare returns the information for a received share the user has access. +func (sm *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) { + bodyStr, err := json.Marshal(ref) + if err != nil { + return nil, err + } + + _, respBody, err := sm.do(ctx, Action{"GetReceivedShare", string(bodyStr)}) + if err != nil { + return nil, err + } + + var altResult ReceivedShareAltMap + err = json.Unmarshal(respBody, &altResult) + if err != nil { + return nil, err + } + altResultShare := altResult.Share + if altResultShare == nil { + return &collaboration.ReceivedShare{ + Share: nil, + State: altResult.State, + }, err + } + return &collaboration.ReceivedShare{ + Share: &collaboration.Share{ + Id: altResultShare.ID, + ResourceId: altResultShare.ResourceID, + Permissions: altResultShare.Permissions, + Grantee: &provider.Grantee{ + Id: altResultShare.Grantee.ID, + }, + Owner: altResultShare.Owner, + Creator: altResultShare.Creator, + Ctime: altResultShare.Ctime, + Mtime: altResultShare.Mtime, + }, + State: altResult.State, + }, err +} + +// UpdateReceivedShare updates the received share with share state. +func (sm *mgr) UpdateReceivedShare(ctx context.Context, ref *collaboration.ShareReference, f *collaboration.UpdateReceivedShareRequest_UpdateField) (*collaboration.ReceivedShare, error) { + type paramsObj struct { + Ref *collaboration.ShareReference `json:"ref"` + F *collaboration.UpdateReceivedShareRequest_UpdateField `json:"f"` + } + bodyObj := ¶msObj{ + Ref: ref, + F: f, + } + bodyStr, err := json.Marshal(bodyObj) + if err != nil { + return nil, err + } + + _, respBody, err := sm.do(ctx, Action{"UpdateReceivedShare", string(bodyStr)}) + if err != nil { + return nil, err + } + + var altResult ReceivedShareAltMap + err = json.Unmarshal(respBody, &altResult) + if err != nil { + return nil, err + } + altResultShare := altResult.Share + if altResultShare == nil { + return &collaboration.ReceivedShare{ + Share: nil, + State: altResult.State, + }, err + } + return &collaboration.ReceivedShare{ + Share: &collaboration.Share{ + Id: altResultShare.ID, + ResourceId: altResultShare.ResourceID, + Permissions: altResultShare.Permissions, + Grantee: &provider.Grantee{ + Id: altResultShare.Grantee.ID, + }, + Owner: altResultShare.Owner, + Creator: altResultShare.Creator, + Ctime: altResultShare.Ctime, + Mtime: altResultShare.Mtime, + }, + State: altResult.State, + }, err +} diff --git a/pkg/share/manager/nextcloud/nextcloud_server_mock.go b/pkg/share/manager/nextcloud/nextcloud_server_mock.go new file mode 100644 index 0000000000..d72d2178c5 --- /dev/null +++ b/pkg/share/manager/nextcloud/nextcloud_server_mock.go @@ -0,0 +1,114 @@ +// 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 nextcloud + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "strings" +) + +// Response contains data for the Nextcloud mock server to respond +// and to switch to a new server state +type Response struct { + code int + body string + newServerState string +} + +const serverStateError = "ERROR" +const serverStateEmpty = "EMPTY" +const serverStateHome = "HOME" + +var serverState = serverStateEmpty + +var responses = map[string]Response{ + `POST /apps/sciencemesh/~tester/api/share/Share {"md":{"opaque":{},"type":1,"id":{"opaque_id":"fileid-/some/path"},"checksum":{},"etag":"deadbeef","mime_type":"text/plain","mtime":{"seconds":1234567890},"path":"/some/path","permission_set":{},"size":12345,"canonical_metadata":{},"arbitrary_metadata":{"metadata":{"da":"ta","some":"arbi","trary":"meta"}}},"g":{"grantee":{"Id":null},"permissions":{"permissions":{}}}}`: {200, `{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}`, serverStateHome}, + `POST /apps/sciencemesh/~tester/api/share/GetShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, `{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}`, serverStateHome}, + `POST /apps/sciencemesh/~tester/api/share/Unshare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, ``, serverStateHome}, + `POST /apps/sciencemesh/~tester/api/share/UpdateShare {"ref":{"Spec":{"Id":{"opaque_id":"some-share-id"}}},"p":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}}}`: {200, `{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}`, serverStateHome}, + `POST /apps/sciencemesh/~tester/api/share/ListShares [{"type":4,"Term":{"Creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}}]`: {200, `[{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}}]`, serverStateHome}, + `POST /apps/sciencemesh/~tester/api/share/ListReceivedShares [{"type":4,"Term":{"Creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}}]`: {200, `[{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}]`, serverStateHome}, + `POST /apps/sciencemesh/~tester/api/share/GetReceivedShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, `{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}`, serverStateHome}, + `POST /apps/sciencemesh/~tester/api/share/UpdateReceivedShare {"ref":{"Spec":{"Id":{"opaque_id":"some-share-id"}}},"f":{"Field":{"DisplayName":"some new name for this received share"}}}`: {200, `{"share":{"id":{},"resource_id":{},"permissions":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}},"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"owner":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"ctime":{"seconds":1234567890},"mtime":{"seconds":1234567890}},"state":2}`, serverStateHome}, +} + +// GetNextcloudServerMock returns a handler that pretends to be a remote Nextcloud server +func GetNextcloudServerMock(called *[]string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + _, err := io.Copy(buf, r.Body) + if err != nil { + panic("Error reading response into buffer") + } + var key = fmt.Sprintf("%s %s %s", r.Method, r.URL, buf.String()) + fmt.Printf("Nextcloud Server Mock key components %s %d %s %d %s %d\n", r.Method, len(r.Method), r.URL.String(), len(r.URL.String()), buf.String(), len(buf.String())) + fmt.Printf("Nextcloud Server Mock key %s\n", key) + *called = append(*called, key) + response := responses[key] + if (response == Response{}) { + key = fmt.Sprintf("%s %s %s %s", r.Method, r.URL, buf.String(), serverState) + fmt.Printf("Nextcloud Server Mock key with State %s\n", key) + // *called = append(*called, key) + response = responses[key] + } + if (response == Response{}) { + fmt.Println("ERROR!!") + fmt.Println("ERROR!!") + fmt.Printf("Nextcloud Server Mock key not found! %s\n", key) + fmt.Println("ERROR!!") + fmt.Println("ERROR!!") + response = Response{200, fmt.Sprintf("response not defined! %s", key), serverStateEmpty} + } + serverState = responses[key].newServerState + if serverState == `` { + serverState = serverStateError + } + w.WriteHeader(response.code) + // w.Header().Set("Etag", "mocker-etag") + _, err = w.Write([]byte(responses[key].body)) + if err != nil { + panic(err) + } + }) +} + +// TestingHTTPClient thanks to https://itnext.io/how-to-stub-requests-to-remote-hosts-with-go-6c2c1db32bf2 +// Ideally, this function would live in tests/helpers, but +// if we put it there, it gets excluded by .dockerignore, and the +// Docker build fails (see https://github.com/cs3org/reva/issues/1999) +// So putting it here for now - open to suggestions if someone knows +// a better way to inject this. +func TestingHTTPClient(handler http.Handler) (*http.Client, func()) { + s := httptest.NewServer(handler) + + cli := &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, network, _ string) (net.Conn, error) { + return net.Dial(network, s.Listener.Addr().String()) + }, + }, + } + + return cli, s.Close +} diff --git a/pkg/share/manager/nextcloud/nextcloud_suite_test.go b/pkg/share/manager/nextcloud/nextcloud_suite_test.go new file mode 100644 index 0000000000..7d75b64879 --- /dev/null +++ b/pkg/share/manager/nextcloud/nextcloud_suite_test.go @@ -0,0 +1,31 @@ +// 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 nextcloud_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestNextcloud(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Nextcloud Suite") +} diff --git a/pkg/share/manager/nextcloud/nextcloud_test.go b/pkg/share/manager/nextcloud/nextcloud_test.go new file mode 100644 index 0000000000..4d7a305242 --- /dev/null +++ b/pkg/share/manager/nextcloud/nextcloud_test.go @@ -0,0 +1,821 @@ +// 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 nextcloud_test + +import ( + "context" + "os" + + "google.golang.org/grpc/metadata" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + + "github.com/cs3org/reva/pkg/auth/scope" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/share/manager/nextcloud" + jwt "github.com/cs3org/reva/pkg/token/manager/jwt" + "github.com/cs3org/reva/tests/helpers" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Nextcloud", func() { + var ( + ctx context.Context + options map[string]interface{} + tmpRoot string + user = &userpb.User{ + Id: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Username: "tester", + } + ) + + BeforeEach(func() { + var err error + tmpRoot, err := helpers.TempDir("reva-unit-tests-*-root") + Expect(err).ToNot(HaveOccurred()) + + options = map[string]interface{}{ + "root": tmpRoot, + "enable_home": true, + "share_folder": "/Shares", + } + + ctx = context.Background() + + // Add auth token + tokenManager, err := jwt.New(map[string]interface{}{"secret": "changemeplease"}) + Expect(err).ToNot(HaveOccurred()) + scope, err := scope.AddOwnerScope(nil) + Expect(err).ToNot(HaveOccurred()) + t, err := tokenManager.MintToken(ctx, user, scope) + Expect(err).ToNot(HaveOccurred()) + ctx = ctxpkg.ContextSetToken(ctx, t) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, t) + ctx = ctxpkg.ContextSetUser(ctx, user) + }) + + AfterEach(func() { + if tmpRoot != "" { + os.RemoveAll(tmpRoot) + } + }) + + Describe("New", func() { + It("returns a new instance", func() { + _, err := nextcloud.New(options) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + // Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) + Describe("Share", func() { + It("calls the Share endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + am, _ := nextcloud.NewShareManager(&nextcloud.ShareManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + share, err := am.Share(ctx, &provider.ResourceInfo{ + Opaque: &types.Opaque{ + Map: nil, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Id: &provider.ResourceId{ + StorageId: "", + OpaqueId: "fileid-/some/path", + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + Checksum: &provider.ResourceChecksum{ + Type: 0, + Sum: "", + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + Etag: "deadbeef", + MimeType: "text/plain", + Mtime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + Path: "/some/path", + PermissionSet: &provider.ResourcePermissions{ + AddGrant: false, + CreateContainer: false, + Delete: false, + GetPath: false, + GetQuota: false, + InitiateFileDownload: false, + InitiateFileUpload: false, + ListGrants: false, + ListContainer: false, + ListFileVersions: false, + ListRecycle: false, + Move: false, + RemoveGrant: false, + PurgeRecycle: false, + RestoreFileVersion: false, + RestoreRecycleItem: false, + Stat: false, + UpdateGrant: false, + DenyGrant: false, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + Size: 12345, + Owner: nil, + Target: "", + CanonicalMetadata: &provider.CanonicalMetadata{ + Target: nil, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + ArbitraryMetadata: &provider.ArbitraryMetadata{ + Metadata: map[string]string{"some": "arbi", "trary": "meta", "da": "ta"}, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, &collaboration.ShareGrant{ + Grantee: &provider.Grantee{}, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{}, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(*share).To(Equal(collaboration.Share{ + Id: &collaboration.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Move: true, + RemoveGrant: true, + PurgeRecycle: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + DenyGrant: true, + }, + }, + Grantee: &provider.Grantee{ + Id: &provider.Grantee_UserId{ + UserId: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + }, + }, + Owner: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Creator: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Ctime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + Mtime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + })) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~tester/api/share/Share {"md":{"opaque":{},"type":1,"id":{"opaque_id":"fileid-/some/path"},"checksum":{},"etag":"deadbeef","mime_type":"text/plain","mtime":{"seconds":1234567890},"path":"/some/path","permission_set":{},"size":12345,"canonical_metadata":{},"arbitrary_metadata":{"metadata":{"da":"ta","some":"arbi","trary":"meta"}}},"g":{"grantee":{"Id":null},"permissions":{"permissions":{}}}}`)) + }) + }) + + // GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) + Describe("GetShare", func() { + It("calls the GetShare endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + am, _ := nextcloud.NewShareManager(&nextcloud.ShareManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + share, err := am.GetShare(ctx, &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: "some-share-id", + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(*share).To(Equal(collaboration.Share{ + Id: &collaboration.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Move: true, + RemoveGrant: true, + PurgeRecycle: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + DenyGrant: true, + }, + }, + Grantee: &provider.Grantee{ + Id: &provider.Grantee_UserId{ + UserId: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + }, + }, + Owner: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Creator: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Ctime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + Mtime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + })) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~tester/api/share/GetShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`)) + }) + }) + + // Unshare(ctx context.Context, ref *collaboration.ShareReference) error + Describe("Unshare", func() { + It("calls the Unshare endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + am, _ := nextcloud.NewShareManager(&nextcloud.ShareManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + err := am.Unshare(ctx, &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: "some-share-id", + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~tester/api/share/Unshare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`)) + }) + }) + + // UpdateShare(ctx context.Context, ref *collaboration.ShareReference, p *collaboration.SharePermissions) (*collaboration.Share, error) + Describe("UpdateShare", func() { + It("calls the UpdateShare endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + am, _ := nextcloud.NewShareManager(&nextcloud.ShareManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + share, err := am.UpdateShare(ctx, &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: "some-share-id", + }, + }, + }, + &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Move: true, + RemoveGrant: true, + PurgeRecycle: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + DenyGrant: true, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(*share).To(Equal(collaboration.Share{ + Id: &collaboration.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Move: true, + RemoveGrant: true, + PurgeRecycle: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + DenyGrant: true, + }, + }, + Grantee: &provider.Grantee{ + Id: &provider.Grantee_UserId{ + UserId: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + }, + }, + Owner: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Creator: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Ctime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + Mtime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + })) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~tester/api/share/UpdateShare {"ref":{"Spec":{"Id":{"opaque_id":"some-share-id"}}},"p":{"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true,"list_container":true,"list_file_versions":true,"list_recycle":true,"move":true,"remove_grant":true,"purge_recycle":true,"restore_file_version":true,"restore_recycle_item":true,"stat":true,"update_grant":true,"deny_grant":true}}}`)) + }) + }) + + // ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) + Describe("ListShares", func() { + It("calls the ListShares endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + am, _ := nextcloud.NewShareManager(&nextcloud.ShareManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + shares, err := am.ListShares(ctx, []*collaboration.Filter{ + { + Type: collaboration.Filter_TYPE_CREATOR, + Term: &collaboration.Filter_Creator{ + Creator: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(len(shares)).To(Equal(1)) + Expect(*shares[0]).To(Equal(collaboration.Share{ + Id: &collaboration.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Move: true, + RemoveGrant: true, + PurgeRecycle: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + DenyGrant: true, + }, + }, + Grantee: &provider.Grantee{ + Id: &provider.Grantee_UserId{ + UserId: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + }, + }, + Owner: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Creator: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Ctime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + Mtime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + })) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~tester/api/share/ListShares [{"type":4,"Term":{"Creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}}]`)) + }) + }) + + // ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) + Describe("ListReceivedShares", func() { + It("calls the ListReceivedShares endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + am, _ := nextcloud.NewShareManager(&nextcloud.ShareManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + receivedShares, err := am.ListReceivedShares(ctx, []*collaboration.Filter{ + { + Type: collaboration.Filter_TYPE_CREATOR, + Term: &collaboration.Filter_Creator{ + Creator: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(len(receivedShares)).To(Equal(1)) + Expect(*receivedShares[0]).To(Equal(collaboration.ReceivedShare{ + Share: &collaboration.Share{ + Id: &collaboration.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Move: true, + RemoveGrant: true, + PurgeRecycle: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + DenyGrant: true, + }, + }, + Grantee: &provider.Grantee{ + Id: &provider.Grantee_UserId{ + UserId: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + }, + }, + Owner: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Creator: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Ctime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + Mtime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + }, + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + })) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~tester/api/share/ListReceivedShares [{"type":4,"Term":{"Creator":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}}]`)) + }) + }) + + // GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) + Describe("GetReceivedShare", func() { + It("calls the GetReceivedShare endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + am, _ := nextcloud.NewShareManager(&nextcloud.ShareManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + receivedShare, err := am.GetReceivedShare(ctx, &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: "some-share-id", + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(*receivedShare).To(Equal(collaboration.ReceivedShare{ + Share: &collaboration.Share{ + Id: &collaboration.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Move: true, + RemoveGrant: true, + PurgeRecycle: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + DenyGrant: true, + }, + }, + Grantee: &provider.Grantee{ + Id: &provider.Grantee_UserId{ + UserId: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + }, + }, + Owner: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Creator: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Ctime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + Mtime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + }, + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + })) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~tester/api/share/GetReceivedShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`)) + }) + }) + + // UpdateReceivedShare(ctx context.Context, ref *collaboration.ShareReference, f *collaboration.UpdateReceivedShareRequest_UpdateField) (*collaboration.ReceivedShare, error) + Describe("UpdateReceivedShare", func() { + It("calls the UpdateReceivedShare endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + am, _ := nextcloud.NewShareManager(&nextcloud.ShareManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + receivedShare, err := am.UpdateReceivedShare(ctx, &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: "some-share-id", + }, + }, + }, + &collaboration.UpdateReceivedShareRequest_UpdateField{ + Field: &collaboration.UpdateReceivedShareRequest_UpdateField_DisplayName{ + DisplayName: "some new name for this received share", + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(*receivedShare).To(Equal(collaboration.ReceivedShare{ + Share: &collaboration.Share{ + Id: &collaboration.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Move: true, + RemoveGrant: true, + PurgeRecycle: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + DenyGrant: true, + }, + }, + Grantee: &provider.Grantee{ + Id: &provider.Grantee_UserId{ + UserId: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + }, + }, + Owner: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Creator: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Ctime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + Mtime: &types.Timestamp{ + Seconds: 1234567890, + Nanos: 0, + XXX_NoUnkeyedLiteral: struct{}{}, + XXX_unrecognized: nil, + XXX_sizecache: 0, + }, + }, + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, + })) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~tester/api/share/UpdateReceivedShare {"ref":{"Spec":{"Id":{"opaque_id":"some-share-id"}}},"f":{"Field":{"DisplayName":"some new name for this received share"}}}`)) + }) + }) + +}) diff --git a/pkg/storage/fs/nextcloud/nextcloud.go b/pkg/storage/fs/nextcloud/nextcloud.go index 9537a21b99..e53a793a25 100644 --- a/pkg/storage/fs/nextcloud/nextcloud.go +++ b/pkg/storage/fs/nextcloud/nextcloud.go @@ -620,7 +620,6 @@ func (nc *StorageDriver) ListGrants(ctx context.Context, ref *provider.Reference // }) // JSON example: // [{"grantee":{"Id":{"UserId":{"idp":"some-idp","opaque_id":"some-opaque-id","type":1}}},"permissions":{"add_grant":true,"create_container":true,"delete":true,"get_path":true,"get_quota":true,"initiate_file_download":true,"initiate_file_upload":true,"list_grants":true}}] - fmt.Println(string(respBody)) var respMapArr []map[string]interface{} err = json.Unmarshal(respBody, &respMapArr) if err != nil {